Welcome to my blog, stay tunned :
Home | Blogs | Stephane Eyskens's blog

OData CRUD in SharePoint 2013 + Authentication scheme explanation

Hi,

A while ago, I wrote a blog post about OData and recorded a video that shed some light on how to use one of the most unused built-in factory of SharePoint 2010.
This example was explaining how to leverage OData to query data stored in memory but it only demonstrated the "R" of CRUD operations. Before reading further this post, if you're not familiar with hosting WCF Data Services in SharePoint, I'd really recommend you to read my previous post since I won't go into details anymore.
In SharePoint 2010, this was quite difficult to implement because in many scenarios, the authentication providers associated with the web applications are still based on windows integrated security. When coming to data update. As described on MSDN, the article on WCF Services in SharePoint Foundation explains that the authentication scheme (anonymous/ntlm/kerberos) is added by SharePoint Foundation 2010 to the URL (and to the ID of the returned entities) to uniquely identity resources when multiple authentication protocols are enabled for a single webapp...(example anonymous/kerberos).
So far so good but what MSDN doesn't explain is that because of this, only the R of CRUD is working....To illustrate that, I've written a very basic service that is exactly the same between 2010 and 2013 but you'll see that the behavior is slightly different. This demo service allows to handle a list of courses with the support of CRUD operations. I'll share the code a bit later but first let's see how this service reacts on SharePoint 2010.
When calling it from the browser, you get this:

As you can see, the authentication scheme is indeed part of the URL... Therefore, if you try to perform an update from a server-side consumer component, say a console app for which you've added a service reference to /_vti_bin/ODataCRUD.svc, you get the following :

Because as you can see, the URL used to target the object we want to update contains NEGOCIATE...and this results in the highlighted error.
Despites of the fact that the very promising class attribute [ServiceFactoryUsingAuthSchemeInEndpointAddress(UsingAuthSchemeInEndpointAddress = false)] exists, it has, at the time of writing absolutely no effect on the WCF service behavior...meaning that the above error persists.
So, except consuming a custom OData service for update/delete via AJAX (with AJAX, you generate the URLS manually), I never managed to get it working from server-side component consuming the WCF service via a proxy. Of course, working with HttpWebRequest works :













HttpWebRequest req = HttpWebRequest.Create("http://server/_vti_bin/ODataCRUD.svc/Courses(ID)") as HttpWebRequest;
req.Method = "MERGE";
req.Credentials = System.Net.CredentialCache.DefaultCredentials;
req.ContentType = "application/json";
byte[] data = UTF8Encoding.UTF8.GetBytes("{ \"CourseTitle\" : \"Updated Course\" }");
req.ContentLength = data.Length;
Stream DataStream=req.GetRequestStream();
DataStream.Write(data, 0, data.Length);
DataStream.Close();
req.GetResponse();

but that's not really the usual way to consume a web service in .NET, whether it's a custom or an OOTB SharePoint one.

Now, in SharePoint 2013, you can forget about those problems, the authentication scheme magically disappeared from the returned entities, making this bug completely gone. Nevertheless, here is a bit of code showing how to play with CRUD operations using the built-in data factory of SharePoint 2013. For sake of simplicity, I didn't include any error handling, locking plumbing nor anything else of that kind to focus only on the most important.

Here is the code of the service :

[BasicHttpBindingServiceMetadataExchangeEndpoint]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    [System.ServiceModel.ServiceBehavior(
        IncludeExceptionDetailInFaults = true)]
    public class CRUDService : DataService<CRUDDataContext>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.All);
            config.SetServiceOperationAccessRule("*", ServiceOperationRights.AllRead);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
        }
    }

Here is the sample class I'm using as a data object:

[DataServiceKey("ID")]
    [Serializable]

    public class Course
    {
        Guid _ID;
        [DataMemberAttribute()]
        public Guid ID
        {
            get
            {
                return _ID;
            }
        }
        [DataMemberAttribute()]
        public string CourseTitle
        {
            get;
            set;
        }
        [DataMemberAttribute()]
        public string CourseCompany
        {
            get;
            set;
        }

        [DataMemberAttribute()]
        public DateTime CourseTime
        {
            get;
            set;
        }

        public Course()
        {
            _ID = Guid.NewGuid();
        }
    }

Here is the code of the context object with a partial implementation of IUpdatabbe

public class CRUDDataContext : IUpdatable
    {
        static readonly List<Course> objects = new List<Course>()
        {
            new Course
            {
                CourseTitle="Windows Communication Foundation",
                CourseCompany="Microsoft",
                CourseTime=DateTime.Now.AddDays(10)
            },
            new Course
            {
                CourseTitle="SharePoint 2013 Development Essentials",
                CourseCompany="U2U",
                CourseTime=DateTime.Now.AddDays(5)
            }
        };
        Course CurrentEntry;
        bool NewEntry = false;
        public IQueryable<Course> Courses
        {
            get
            {
                return objects.AsQueryable();
            }
        }

        #region implemented methods
        object IUpdatable.CreateResource(string containerName, string fullTypeName)
        {
            var objType = Type.GetType(fullTypeName);
            var resourceToAdd = Activator.CreateInstance(objType);
            NewEntry = true;
            return resourceToAdd;
        }

        void IUpdatable.DeleteResource(object targetResource)
        {
            objects.Remove(targetResource as Course);
        }

        object IUpdatable.GetResource(IQueryable query, string fullTypeName)
        {
            object result = null;
            var enumerator = query.GetEnumerator();
            while (enumerator.MoveNext())
            {
                if (enumerator.Current != null)
                {
                    result = enumerator.Current;
                    break;
                }
            }
            if (fullTypeName != null && !fullTypeName.Equals(result.GetType().FullName))
            {
                throw new DataServiceException();
            }

            return result;
        }

        object IUpdatable.GetValue(object targetResource, string propertyName)
        {
            return targetResource.GetType().GetProperty(propertyName).GetValue(targetResource, null);
        }

        object IUpdatable.ResolveResource(object resource)
        {
            return resource;
        }

        void IUpdatable.SaveChanges()
        {
            if (NewEntry)
                objects.Add(CurrentEntry);
        }

        void IUpdatable.SetValue(object targetResource, string propertyName, object propertyValue)
        {
            if (propertyName != "ID")
            {
                Type TargetType = targetResource.GetType();
                PropertyInfo property = TargetType.GetProperty(propertyName);
                property.SetValue(targetResource, propertyValue, null);
                CurrentEntry = targetResource as Course;
            }
        }
        #endregion

        #region not implemented methods

        void IUpdatable.RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
        {
            throw new NotImplementedException();
        }

        void IUpdatable.ClearChanges()
        {
            throw new NotImplementedException();
        }
        object IUpdatable.ResetResource(object resource)
        {
            throw new NotImplementedException();
        }
        void IUpdatable.SetReference(object targetResource, string propertyName, object propertyValue)
        {
            throw new NotImplementedException();
        }
        #endregion

    }

When deploying the solution to SharePoint 2013, you get this from the browser because the default authentication mechanism is Claims, so no authentication scheme is added to the ID:

You can see that the authentication scheme is not there in the identifier. Therefore, when adding a service reference in Visual Studio, you can now easily use the generated proxy and this won't result in the resource not found error anymore. So, this will work just fine:

ODataCRUDService.CRUDDataContext ctx =
                new ODataCRUDService.CRUDDataContext(
                    new Uri("http://server/_vti_bin/ODataCRUD.svc"));
            ctx.Credentials = System.Net.CredentialCache.DefaultCredentials;
            var target = ctx.Courses.FirstOrDefault();
            if (target != null)
            {
                target.CourseTitle = "Updated from console";
                ctx.UpdateObject(target);
                ctx.SaveChanges();
            }

Of course, all the other operations (Add, Delete) will also work fine.

If you'd like to download the samples, you can get them here:

Service for SharePoint 2010
Service for SharePoint 2013


Happy Coding!

Comments

Extending MultipleBaseAddressDataServiceHostFactory

First of all, thanks for your blog post, it was very helpful.

The ServiceFactoryUsingAuthSchemeInEndpointAddress attribute must be applied to a custom host factory class in SP2010, in order to be effective.

[ServiceFactoryUsingAuthSchemeInEndpointAddress(UsingAuthSchemeInEndpointAddress = false)]
public class ECSMultipleBaseAddressDataServiceHostFactory : MultipleBaseAddressDataServiceHostFactory
{
}


<%@ ServiceHost Language="C#" Debug="true" CodeBehind="ODataService.svc.cs"
Service="ODataServiceSP2010.ODataService, $SharePoint.Project.AssemblyFullName$"
Factory="ODataServiceSP2010.ECSMultipleBaseAddressDataServiceHostFactory, $SharePoint.Project.AssemblyFullName$" %>

Jürgen

UsingAuthSchemeInEndpointAddress

Hi Jürgen,

Thanks for your reply. I had mentioned the usage of this attribute in 2010 but it didn't work at the time of writing the blog post. I've never retried since :) but it's definitely worth checking it.

Thanks
Best Regards