Calling SharePoint 2013 REST api using an AngularJS Service

Calling the SharePoint 2013’s REST api can be a repetitive task. During the last Office365 summit in Amsterdam, the developer track always pointed to the Office365 Patterns and Practices for samples on how to develop in office 365. To my greatest joy they also have an SPA sample!

AngularJS services provide a way to share code within the app. This is great for not always having to write the same code over and over again. In this case calling SharePoint REST urls using $http.

The following is a snippet from https://github.com/OfficeDev/PnP/blob/master/Samples/Core.SharePointProxyForSpaApps/SharePointProxyForSpaAppsWeb/App/common/services/sharepointproxy.service.js:

(function (angular) {
    "use strict";

    angular
        .module('sharepointproxy.service', [])
        .factory('SharepointProxyService', SharepointProxyService);

    function SharepointProxyService($http) {

        var proxyEndpointOptions = {
            url: 'api/sharepoint',
            method: 'POST',
            data: {}
        };

        var defaultProxyOptions = {

        };

        var service = {
            transformRequest: transformRequest,
            sendRequest: sendRequest,
            getWebTitle: getWebTitle
        };

        function getData(o) {
            return o.data;
        }

        function transformRequest(request) {
            var transformedRequest = proxyEndpointOptions;
            transformedRequest.data = request;

            return transformedRequest;
        }

        function sendRequest(request) {
            return $http(request)
                .then(getData)
                .then(function (data) {
                    return data;
                })
            ;
        }

        return service;
    }

    SharepointProxyService.$inject = ['$http'];

})(angular);

It’s a great example on how to start using an AngularJS service calling SharePoint. It can be easily extended with more defined methods, like getting the title of the hostWeb:

        function getWebTitle(hostWebUrl) {
            var requestUrl = hostWebUrl + '/_api/web/title';
            var request = {
                url: requestUrl,
                method: 'GET',
                headers: {
                    Accept: 'application/json;odata=verbose'
                }
            }
            var spRequest = transformRequest(request);
            return sendRequest(spRequest);
        }

All you need to add in your controller is a simple call to the service passing the hostWebUrl:

var queryParameterString = (window.location.search[0] === '?') ? window.location.search.slice(1) : window.location.search;
$scope.queryParameters = deparam(queryParameterString);

        $scope.requestServiceData = function () {
            console.log("Request service data!");
            var result = SharepointProxyService.getWebTitle($scope.queryParameters.SPHostUrl)
            .then(function (data) {
                $scope.serviceResponse = data;
            });
        };

Finish off by adding a button to your HTML to call the service:

<button ng-click="requestServiceData()">Request</button>

Variations.Current.UserAccessibleLabels can spawn lots of SPWebs

Be careful when using the OOTB SharePoint Publishing property Variations.Current.UserAccessibleLabels as it spawns SPWebs for every call.

To demo this I’ve created a web part and added 2 different code blocks to it with monitored scopes. The first one creates 3 objects with a direct reference to the UserAccessibleLabels property. The second one uses a local field which is only assigned once.

namespace TestVariations.TestAccessibleLabels
{
    [ToolboxItemAttribute(false)]
    public partial class TestAccessibleLabels : WebPart
    {
        private ReadOnlyCollection<VariationLabel> userAccessibleLables = null;

        public ReadOnlyCollection<VariationLabel> UserAccessibleLables
        {
            get
            {
                if (userAccessibleLables == null)
                    userAccessibleLables = Variations.Current.UserAccessibleLabels;

                return userAccessibleLables;
            }
        }

        public TestAccessibleLabels()
        {
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            InitializeControl();
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            try
            {
                var resultBuilder = new StringBuilder();

                using (new SPMonitoredScope("Variations.Current.UserAccessibleLabels"))
                {
                    var variationLabels = Variations.Current.UserAccessibleLabels;
                    var variationLabels2 = Variations.Current.UserAccessibleLabels;
                    var variationLabels3 = Variations.Current.UserAccessibleLabels;

                    foreach (var label in variationLabels)
                    {
                        resultBuilder.Append(label.Title);
                    }

                    foreach (var label2 in variationLabels2)
                    {
                        resultBuilder.Append(label2.Title);
                    }

                    foreach (var label3 in variationLabels3)
                    {
                        resultBuilder.Append(label3.Title);
                    }
                }

                using (new SPMonitoredScope("this.UserAccessibleLabels"))
                {
                    var variationLabels4 = this.UserAccessibleLables;
                    var variationLabels5 = this.UserAccessibleLables;
                    var variationLabels6 = this.UserAccessibleLables;

                    foreach (var label4 in variationLabels4)
                    {
                        resultBuilder.Append(label4.Title);
                    }

                    foreach (var label5 in variationLabels5)
                    {
                        resultBuilder.Append(label5.Title);
                    }

                    foreach (var label6 in variationLabels6)
                    {
                        resultBuilder.Append(label6.Title);
                    }
                }

                resultLabel.Text = resultBuilder.ToString();
            }
            catch (Exception ex)
            {
                resultLabel.Text = ex.Message;
            }
        }
    }
}

If we take a look at the developer dashboard, we can see SPWebs getting generated for each variation:

spweb spawns

Using ILSpy we can see that SharePoint opens web objects to check the current user’s access.

ilspy variations

When taking a look at the loading time, we see that the impact on the total loading time is quite high when using lots of direct references.

spweb monitored scopes

For the conclusion we can say that using the OOTB SharePoint property is the way to go when you only have to use it once. However when you’re using this property all over the place in your code it’s best to keep the “result” of the UserAccessibleLabels in a private field.

PageLayouts not updating through module

Trying to deploy an updated page layout through a module to the master page gallery was going wrong horribly. The page layout inside the gallery wasn’t updating while deployment was succesful and nothing was showing up in the logs as of why there wasn’t an update done to the file. The project as example looks like this:

pagelayoutproject

As you can see it’s a simple module which will deploy “testLayout.aspx” to the master page gallery using the following elements.xml:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="PageLayouts" Url="_catalogs/masterpage" RootWebOnly="TRUE">
    <File Path="PageLayouts\testLayout.aspx" Url="PageLayouts/testLayout.aspx" Type="GhostableInLibrary">
      <Property Name="Title" Value="My Custom Page Layout" />
      <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;" />
      <Property Name="PublishingPreviewImage" Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/CustomPageLayout.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/CustomPageLayout.png" />
      <Property Name="PublishingAssociatedContentType" Value=";#$Resources:cmscore,contenttype_articlepage_name;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D;#"/>
    </File>
  </Module>
</Elements>

The page layout was generated through SharePoint Designer and had the following source:

<%@ Page Language="C#" Inherits="Microsoft.SharePoint.Publishing.PublishingLayoutPage,Microsoft.SharePoint.Publishing,Version=14.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" meta:webpartpageexpansion="full" meta:progid="SharePoint.WebPartPage.Document" %>

<%@ Register TagPrefix="SharePointWebControls" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls" Assembly="Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingNavigation" Namespace="Microsoft.SharePoint.Publishing.Navigation" Assembly="Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<asp:content contentplaceholderid="PlaceHolderPageTitle" runat="server">
	<SharePointWebControls:FieldValue id="PageTitle" FieldName="Title" runat="server"/>
</asp:content>
<asp:content contentplaceholderid="PlaceHolderMain" runat="server">
</asp:content>

Going deeper into the page’s properties it showed that the page has been customized, which was odd since nobody touched the file through SharePoint designer which would cause ghosting.

customizedpagelayout

After some research I’ve found a lot of suggestions to go the code way of manually removing the file and then add the page layout, but in my opinion it should be done through the module without code. I knew it was possible through a simple update of the module so what was going wrong? Easy: SharePoint Designer. If you look closely, the tool adds the following meta tags to the Page tag:

meta:webpartpageexpansion="full" meta:progid="SharePoint.WebPartPage.Document"

For some reason, whenever these tags are there, SharePoint will always ghost your page layouts no matter what. Removing these tags and unghosting the page was sufficient to get back those updates through modules. Phew!

High CPU usage due to _vti_bin/cellstorage.svc/CellStorageService

Recently I faced a cpu hang by a w3wp process of one of the application pools. The process stayed on roughly 50% of cpu usage, which caused performance issues for the other processes running on the WFEs. Recycling the application pool helped at first, but after a few moments the process started spiking again, slowing everything down.

First thing to do is check the ULS logs: nothing special there. It was also pretty hard to pinpoint one specific line as a cause to the high cpu usage.

On to more specific analysis then: Process Explorer. The tool is great for checking your process threads and checking the callstack that is causing any interference.

Below is an example of how the stack window looks:

procmon

The method that was hanging during the issue was GetMetadatainternalinterfacefromplublic. Not much more info was available from the stack window. There was no custom code involved during the performance decrease, so further investigation was needed. I noted down the thread id and made a dump of the running process using task manager:

create dump file

Once the dump was created, I installed DebugDiag v1.2. Analysis of the dump was pretty straight forward using the performance analyzers:

debugdiagperfmon

The callstack of the thread with entry point mscorwks!GetMetaDataInternalInterfaceFromPublic was:

System.Data.SqlClient.TdsParserStateObject.WritePacket(Byte) 
System.Data.SqlClient.TdsParser.WriteByteArray(Byte[], Int32, Int32, System.Data.SqlClient.TdsParserStateObject) 
System.Data.SqlClient.TdsParser.WriteValue(System.Object, System.Data.SqlClient.MetaType, Byte, Int32, Int32, Int32, System.Data.SqlClient.TdsParserStateObject) 
System.Data.SqlClient.TdsParser.TdsExecuteRPC(System.Data.SqlClient._SqlRPC[], Int32, Boolean, System.Data.Sql.SqlNotificationRequest, System.Data.SqlClient.TdsParserStateObject, Boolean) 
System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(System.Data.CommandBehavior, System.Data.SqlClient.RunBehavior, Boolean, Boolean) 
System.Data.SqlClient.SqlCommand.RunExecuteReader(System.Data.CommandBehavior, System.Data.SqlClient.RunBehavior, Boolean, System.String, System.Data.Common.DbAsyncResult) 
System.Data.SqlClient.SqlCommand.RunExecuteReader(System.Data.CommandBehavior, System.Data.SqlClient.RunBehavior, Boolean, System.String) 
System.Data.SqlClient.SqlCommand.ExecuteReader(System.Data.CommandBehavior, System.String) 
System.Data.SqlClient.SqlCommand.ExecuteReader(System.Data.CommandBehavior) 
Microsoft.SharePoint.Utilities.SqlSession.ExecuteReader(System.Data.SqlClient.SqlCommand, System.Data.CommandBehavior, Microsoft.SharePoint.Utilities.SqlQueryData, Boolean) 
Microsoft.SharePoint.SPSqlClient.ExecuteQueryInternal(Boolean) 
Microsoft.SharePoint.SPSqlClient.ExecuteQuery(Boolean) 
DomainNeutralILStubClass.IL_STUB(System.String, System.String, System.Object, Int32, System.Object, Microsoft.SharePoint.Library.PutFileOpt, System.String, System.String, Int32, Int32, System.Object, System.Object, System.Object, System.String, Byte, Int64, System.String, System.String, System.String, Int32, System.String, Int32, Int32, Int32, System.Guid, UInt32 ByRef, System.String ByRef, System.String ByRef, Byte ByRef, Int32 ByRef) 
Microsoft.SharePoint.Library.SPRequest.PutFile(System.String, System.String, System.Object, Int32, System.Object, Microsoft.SharePoint.Library.PutFileOpt, System.String, System.String, Int32, Int32, System.Object, System.Object, System.Object, System.String, Byte, Int64, System.String, System.String, System.String, Int32, System.String, Int32, Int32, Int32, System.Guid, UInt32 ByRef, System.String ByRef, System.String ByRef, Byte ByRef, Int32 ByRef) 
Microsoft.SharePoint.SPFile.SaveBinaryStreamInternal(System.IO.Stream, System.String, Boolean, Boolean, Boolean, Boolean, Boolean, System.String, Microsoft.SharePoint.SPUser, System.DateTime, System.Object, Microsoft.SharePoint.SPFileFragmentPartition, Microsoft.SharePoint.SPFileFragmentId, Microsoft.SharePoint.SPFileFragmentPartition[], System.IO.Stream, System.String, Boolean, SPLockType, System.String, System.TimeSpan, Boolean, Boolean, Boolean, Boolean, System.Guid, Microsoft.SharePoint.SPVirusCheckStatus ByRef, System.String ByRef, System.String ByRef, Boolean ByRef) 
Microsoft.SharePoint.Library.SPServerStorageBridge 
Microsoft.SharePoint.SPSecurity 
Microsoft.SharePoint.Utilities.SecurityContext.RunAsProcess(CodeToRunElevated) 
Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(System.Threading.WaitCallback, System.Object) 
Microsoft.SharePoint.SPSecurity.RunWithElevatedPrivileges(CodeToRunElevated) 
Microsoft.SharePoint.Library.SPServerStorageBridge.Microsoft.SharePoint.Library.IServerStorageBridge.PutStreams(Microsoft.SharePoint.Library.IStreamBridge, Microsoft.SharePoint.Library.IStreamBridge, System.String, Boolean, UInt64, System.String, System.String, System.String, Int32, System.String, Int32, Boolean, Boolean, Boolean, System.String ByRef, Boolean ByRef, System.String ByRef, UInt32 ByRef) 
Microsoft.SharePoint.Utilities.SandboxServerBigBridge.CallServerStorageBridge_PutStreams(Microsoft.SharePoint.Utilities.SandboxMessageType, Microsoft.SharePoint.Utilities.SandboxCommunicator) 
Microsoft.SharePoint.Utilities.SandboxServerBigBridge.EventLoopFor(Microsoft.SharePoint.Utilities.SandboxMessageType, Microsoft.SharePoint.Utilities.SandboxCommunicator) 
Microsoft.SharePoint.Utilities.SPUtility.PerformSandboxOperation(Microsoft.SharePoint.Utilities.SandboxCommunicator, Microsoft.SharePoint.Utilities.ExecuteCellStorageBinaryRequestParameters) 
Microsoft.SharePoint.Utilities.SPUtility.ExecuteCellStorageBinaryRequest(Microsoft.SharePoint.SPFile, Boolean, System.IO.Stream, Boolean, System.Guid ByRef, System.String, Boolean, System.String, Boolean, System.String, System.String, System.String, Int64, System.String, Int64, Boolean, System.String ByRef, Boolean ByRef, Int32 ByRef, System.String ByRef, Boolean ByRef, Boolean ByRef, Int32 ByRef) 
Microsoft.SharePoint.SoapServer.CellStoragesImpl 
Microsoft.SharePoint.SoapServer.CellStoragesImpl.FProcessSubRequests(System.Xml.XmlReader, System.Xml.XmlWriter, Microsoft.SharePoint.SPFile, System.Collections.Generic.Dictionary`2, ResourceQuota) 
Microsoft.SharePoint.SoapServer.CellStoragesImpl.FProcessRequest(Microsoft.SharePoint.SPWeb, System.Xml.XmlReader, System.Xml.XmlWriter, System.Collections.Generic.Dictionary`2, ResourceQuota) 
Microsoft.SharePoint.SoapServer.CellStoragesImpl 
System.ServiceModel.Channels.BodyWriter.WriteBodyContents(System.Xml.XmlDictionaryWriter) 
System.ServiceModel.Channels.Message.OnWriteMessage(System.Xml.XmlDictionaryWriter) 
System.ServiceModel.Channels.MtomMessageEncoder.WriteMessage(System.ServiceModel.Channels.Message, System.IO.Stream, System.String, System.String, System.String, Boolean) 
System.ServiceModel.Channels.HttpOutput.WriteStreamedMessage(System.TimeSpan) 
System.ServiceModel.Channels.HttpOutput.Send(System.TimeSpan) 
System.ServiceModel.Channels.HttpRequestContext.OnReply(System.ServiceModel.Channels.Message, System.TimeSpan) 
System.ServiceModel.Channels.RequestContextBase.Reply(System.ServiceModel.Channels.Message, System.TimeSpan) 
System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.Reply(System.ServiceModel.Dispatcher.MessageRpc ByRef) 
System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessageCleanup(System.ServiceModel.Dispatcher.MessageRpc ByRef) 
System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(System.ServiceModel.Dispatcher.MessageRpc ByRef) 
System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(System.ServiceModel.Dispatcher.MessageRpc ByRef) 
System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean) 
System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(System.ServiceModel.Channels.RequestContext, Boolean, System.ServiceModel.OperationContext) 
System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(System.ServiceModel.Channels.RequestContext, System.ServiceModel.OperationContext) 
System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(System.IAsyncResult) 
System.ServiceModel.Diagnostics.Utility 
System.ServiceModel.AsyncResult.Complete(Boolean) 
System.ServiceModel.Channels.InputQueue`1 
System.ServiceModel.Channels.InputQueue`1[[System.__Canon, mscorlib]].EnqueueAndDispatch(Item, Boolean) 
System.ServiceModel.Channels.InputQueue`1[[System.__Canon, mscorlib]].EnqueueAndDispatch(System.__Canon, System.ServiceModel.Channels.ItemDequeuedCallback, Boolean) 
System.ServiceModel.Channels.InputQueueChannel`1[[System.__Canon, mscorlib]].EnqueueAndDispatch(System.__Canon, System.ServiceModel.Channels.ItemDequeuedCallback, Boolean) 
System.ServiceModel.Channels.SingletonChannelAcceptor`3[[System.__Canon, mscorlib],[System.__Canon, mscorlib],[System.__Canon, mscorlib]].Enqueue(System.__Canon, System.ServiceModel.Channels.ItemDequeuedCallback, Boolean) 
System.ServiceModel.Channels.SingletonChannelAcceptor`3[[System.__Canon, mscorlib],[System.__Canon, mscorlib],[System.__Canon, mscorlib]].Enqueue(System.__Canon, System.ServiceModel.Channels.ItemDequeuedCallback) 
System.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(System.ServiceModel.Channels.HttpRequestContext, System.ServiceModel.Channels.ItemDequeuedCallback) 
System.ServiceModel.Activation.HostedHttpTransportManager.HttpContextReceived(System.ServiceModel.Activation.HostedHttpRequestAsyncResult) 
System.ServiceModel.Activation.HostedHttpRequestAsyncResult.BeginRequest() 
System.ServiceModel.Activation.HostedHttpRequestAsyncResult.OnBeginRequest(System.Object) 
System.ServiceModel.PartialTrustHelpers.PartialTrustInvoke(System.Threading.ContextCallback, System.Object) 
System.ServiceModel.Activation.HostedHttpRequestAsyncResult.OnBeginRequestWithFlow(System.Object) 
System.ServiceModel.Channels.IOThreadScheduler 
System.ServiceModel.Channels.IOThreadScheduler 
System.ServiceModel.Channels.IOThreadScheduler 
System.ServiceModel.Channels.IOThreadScheduler 
System.ServiceModel.Channels.IOThreadScheduler 
System.ServiceModel.Diagnostics.Utility 
System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32, UInt32, System.Threading.NativeOverlapped*) 

User Time   01:37:04.765 
Kernel time   00:00:05.553

Notice the User Time of over 1h30! Seems that a SQL statement was staying open. When taking the creation time of the thread, I was able to check the ULS logs with more info. There were calls to a certain OneNote file and the _vti_bin/cellstorage.svc/cellstorageservice.svc was called. There were also CsiSrvExe.exe processing starting at that point.

OneNote uses the Core Storage Infrastructure service to send user edits to the SharePoint server. This server stores the master copy of the notebook. The Core Storage Infrastructure service processes file writes by storing them in a hot table and then periodically flushing those writes to the actual file. Anyway, there was something seriously going wrong with this web service.

Using the command to list the requests:

C:\Windows\System32\inetsrv\appcmd.exe list request

I was able to determine an open connection that never was closed:

REQUEST "a90000018000473e" (url:POST /_vti_bin/cellstorage.svc/CellStorageService, time:1874982 msec, client:0.0.0.0, stage:ExecuteRequestHandler, module:ManagedPipelineHandler)

Checking the web.config of the web services, everything seemed fine and it should force close after a timeout. I searched for other experiences with the CellStorageService and found the following discussion:

http://social.technet.microsoft.com/Forums/en-US/d4b10c36-2f9e-45af-aab8-9a234ccb9ce8/sharepoint-workspace-error-cellstoragesvc?forum=sharepointgeneralprevious

The post by Doc WattsMan led me to the solution: switching from Kerberos to NTLM and back, the service seemed to be “resetted” and everything runs fine since then. I’m still trying to figure out which update caused the trouble, but in the meantime you can try the solution for yourself.

Check if an item has been previously approved and approve again

Sometimes, you’ll have to check in an event receiver if an item has been previously approved and then do an action.

The following extension will return a boolean wether or not the item has been approved in the past or not.

public static bool HasBeenPreviouslyApproved(this SPListItem item)
{
    bool hasBeenPreviouslyApproved = false;
    if (item.Versions.Count > 1)
    {
        var prevPublishedVer = item.Versions.Cast<SPListItemVersion>().Where(v => v.Level == SPFileLevel.Published).LastOrDefault();
        if (prevPublishedVer != null)
        {
            hasBeenPreviouslyApproved = true;
        }
    }
    return hasBeenPreviouslyApproved;
}

As an example you can put the state back to approved if a modification was made. By default this always goes back to pending when an item gets edited, but in some cases you want to keep the state to approved.

if (properties.ListItem.HasBeenPreviouslyApproved())
{
    using (var web = properties.OpenWeb())
    {
        this.EventFiringEnabled = false;
        properties.ListItem.ModerationInformation.Status = SPModerationStatusType.Approved;
        properties.ListItem.SystemUpdate(false);
        this.EventFiringEnabled = true;
    }
}

Please note that when you use this code in an event receiver, it only works in the ItemUpdated event.

As a result, an announcement which was once approved, will always be approved, until a content editor decides to change the approval status back to pending or decline.

announcement approved

Prevent items from being deleted using an event receiver

One way to prevent item deletion is creating a custom permission level, where the item deletion permission is unchecked. When you use this method to prevent item deletion, you’ll have to break the permission inheritance of your list. Plus, you’ll create the permission level for the whole site collection. If that’s a problem you can also use an event receiver.

The following code snippet will prevent item deletion and also notify the user of what happened. If you simply use the properties.Cancel = true; property, then the item is prevented from deletion, but the user isn’t notified.

By setting the properties.Status = SPEventReceiverStatus.CancelWithError; property, we tell SharePoint to generate an error message. The properties.ErrorMessage = “This item cannot be deleted; Contact your administrator”; property defines the message the user sees.

/// <summary>
/// An item is being deleted.
/// </summary>
public override void ItemDeleting(SPItemEventProperties properties)
{
    base.ItemDeleting(properties);
    properties.Status = SPEventReceiverStatus.CancelWithError;
    properties.ErrorMessage = "This item cannot be deleted; Contact your administrator";
    properties.Cancel = true;
}

The above snippet generates the following error when a user tries to delete an item in a list where the event receiver is active:

error screenshot from event receiver

Find out wether a filled in user field is a group or a user

The following method allows you to find out wether a filled in user field is an actual user, or might be an active directory group. Here I use the principalInfo variable to see if there’s an actual windows principal behind the value of “user”. I use the user’s e-mail in the first part, since there might be more persons with the same user.LookupValue value. In other words: some people have the same displayname.

For the same reason, I use the user.User.LoginName in the web.EnsureUser method.

protected static SPPrincipal GetUserOrGroup(SPWeb web, SPFieldUserValue user)
{
	if (user.User != null)
		var principalInfo = SPUtility.ResolveWindowsPrincipal(
			web.Site.WebApplication, user.User.Email, SPPrincipalType.All, true);

	// if the user has no e-mail, check the user's name. This is not failsafe when 2 persons have the same name
	if (principalInfo == null)
		principalInfo = SPUtility.ResolveWindowsPrincipal(
			web.Site.WebApplication, user.LookupValue, SPPrincipalType.All, false);

	SPPrincipal principal;
	if (principalInfo != null)
		principal = web.EnsureUser(user.User.LoginName);
	else
		principal = web.SiteGroups[user.LookupValue];
	return principal;
}