Background
Recently, while building a custom service application in SharePoint, I was exploring various ways of creating a web API, hosted within SharePoint. Obviously, using WCF is the standard in SharePoint, but it certainly is not the only way. That’s when the thought of trying ServiceStack came into existence. Why ServiceStack? Because it gives you XML, JSON, JSV, CSV, SOAP 1.1 and SOAP 1.2 response types out-of-the-box! That’s pretty cool, and it's just a start. It also provides a consistent way of developing services and great performance (check out these benchmarks). Check out this interesting read by Phillip Haydon as well as he takes ServiceStack for a spin. Most of all, ServiceStack is super fun. In this post we will create the barebone essentials for building out a web service API, hosted within SharePoint 2010, using ServiceStack.Getting Started
[box style="blue"]Recent Update (Dec. 26, 2012) Some mods to the ServiceStack platform have recently made it easier to setup the App Host, and we don't need to modify the ServiceStack code any longer (as described in this post). The rest remains the same. That said, SharePoint still requires that the ServiceStack assemblies be strongly signed. I have added a new project to my skydrive folder for this blog post called "SP123", please see the readme.txt file in that *.zip file with more details.[/box] Let’s flesh out the SharePoint project. I’ll create 1 class library project, and 1 empty SharePoint project in my solution. The solution MUST be a Farm Solution, because we’re going to have to do a web.config mod, BUT, only to deploy the infrastructure. This will play really nicely with sandboxed solutions once the API is deployed, especially with jQuery or whatever other JS framework you fancy. [box style="warning"]You will need to download the zip files or clone the Git repositories if you want to deploy ServiceStack to the GAC. Oh, and I’ll be making 1 tiny little change to the ServiceStack source code (BUT, you don’t have to if you are ok with the alternative, which I’ll describe later). So all in all, you can still make it work without this unfortunate thing, by accepting the alternative web.config mods (described later in this post), and NOT deploying the dlls to the GAC (deploy to the bin instead).[/box]Project Structure
My final solution looks like this:Deploying ServiceStack
To deploy to the GAC, I strongly typed SSPLoveSS.WebApi, as well as all the ServiceStack libraries, and then I froze the versions on all the libraries for this exercise as well. To deploy the ServiceStack dlls, either use gacutil on your dev (if you strongly type them like I’m doing), or add the dlls to the SharePoint deployment package and then deploy to the GAC or the bin:API Development
We’ll create an API that - RESTfully:- Lists all the groups in a web
- Lists all the members of a group
- Lists all the users in a web
- Allows us to create a new group
- Allows us to delete a group
- Allows us to modify a group name
- /api/groups: lists all groups in a web (verb=GET), create a group (verb=PUT)
- /api/groups/{groupName}: deletes a group (verb=DELETE)
- /api/groups/{groupName}/members: lists all members of a group (verb=GET), adds a member to a group (verb=POST)
- /api/groups/{groupName}/members/{memberName}: removes a member from a group (verb=DELETE)
- /_layouts/api/groups: lists all groups in a web (verb=GET), create a group (verb=PUT)
- /_layouts/api/groups/{groupName}: deletes a group (verb=DELETE)
- /_layouts/api/groups/{groupName}/members: lists all members of a group (verb=GET), adds a member to a group (verb=POST)
- /_layouts/api/groups/{groupName}/members/{memberName}: removes a member from a group (verb=DELETE)
public static IHttpHandler GetHandlerForPathInfo(string httpMethod, string pathInfo, string requestPath, string filePath) { var pathParts = pathInfo.TrimStart(‘/’).Split(‘/’); if (pathParts.Length == 0) return NotFoundHttpHandler; // BEGIN MOD for SharePoint if (pathParts.Length > 2 && pathParts[0].Equals("_layouts", StringComparison.OrdinalIgnoreCase)) pathParts = pathParts.Skip(2).ToArray(); // END MOD for SharePoint var handler = GetHandlerForPathParts(pathParts); if (handler != null) return handler; // …. rest of method …. }The alternative to making these changes to the source code are to specify the individual Handlers in the web.config, instead of using the Handler factory class. This is the format used in the ServiceStack examples download. That would look something like this instead: As far as building services, I won’t go over the details here, it’s extremely simple and ServiceStack has a very clear tutorial on how to do this. I highly suggest you follow it to get started. If you use Nuget, you can use the "Install-Package ServiceStack" command. Avoid the "ServiceStack.Host.AspNet" package in a .NET 3.5 project because the package download has some framework dependency issues with a couple 3rd party libraries that require .NET 4.0 (at the time of this writing, i.e.: Web Activator). The services I implemented here to meet the use-case are straightforward, here’s what the “GroupService” looks like:
[CollectionDataContract(Name = "groups", ItemName = "group")] public class GroupList : List { public GroupList() { } public GroupList(IEnumerable collection) : base(collection) { } } [DataContract(Name = "group")] public class Group { public Group() { Members = new UserList(); } public Group(SPGroup group) : this() { this.Id = group.ID; this.Name = group.Name; this.Members = new UserList(group.Users.Cast().Select(u => new User(u))); this.MemberCount = Members.Count; } [DataMember(Name = "id", Order = 1)] public int Id { get; set; } [DataMember(Name = "name", Order = 2)] public string Name { get; set; } [DataMember(Name = "count", Order = 3)] public int MemberCount { get; set; } [DataMember(Name = "members", Order = 4)] public UserList Members { get; set; } } [RestService("/_layouts/api/groups", "GET,PUT")] [RestService("/_layouts/api/groups/{Ids}", "GET,POST,DELETE")] public class GroupRequest : Group { public string Ids { get; set; } } public class GroupService : BaseService { // Get 1 or more groups public override object OnGet(GroupRequest request) { if (!IsAuthenticated) return new HttpResult(HttpStatusCode.Unauthorized, "Requires authentication"); try { var ids = string.IsNullOrEmpty(request.Ids) ? null : request.Ids.Split(',').Select(i => Convert.ToInt32(i)).ToArray(); var groups = new List(); var spGroups = CurrentWeb.Groups; foreach (SPGroup spGroup in spGroups) { if (ids == null || ids.Contains(spGroup.ID)) groups.Add(new Group(spGroup)); } return new GroupResponse(CurrentWeb, groups, GetQueryStringValue("sort", "asc")); } catch { return new HttpResult(HttpStatusCode.BadRequest, "Invalid request"); } } // Create a group public override object OnPut(GroupRequest request) { if (!IsAuthenticated) return new HttpResult(HttpStatusCode.Unauthorized, "Requires authentication"); if(string.IsNullOrEmpty(request.Name)) return new HttpResult(HttpStatusCode.BadRequest, "Name is missing"); try { var web = CurrentWeb; var groupName = request.Name; var user = SPContext.Current.Web.CurrentUser; var groupArray = new string[] {groupName}; web.AllowUnsafeUpdates = true; var groupCollection = web.SiteGroups.GetCollection(groupArray); if (groupCollection.Count == 0) { web.SiteGroups.Add(groupName, user, null, "The " + groupName + " group"); } var spGroup = web.SiteGroups[groupName]; var roleDef = web.RoleDefinitions.GetByType(SPRoleType.Contributor); var roles = new SPRoleAssignment(spGroup); roles.RoleDefinitionBindings.Add(roleDef); web.RoleAssignments.Add(roles); //// Assign to site spGroup.AddUser(user); spGroup.Update(); web.AllowUnsafeUpdates = false; return new HttpResult(HttpStatusCode.OK, "SUCCESS"); } catch (UnauthorizedAccessException uex) { return new HttpResult(HttpStatusCode.Forbidden, uex.Message); } catch (Exception ex) { return new HttpResult(HttpStatusCode.BadRequest, ex.Message); } } // Update the group public override object OnPost(GroupRequest request) { if (!IsAuthenticated) return new HttpResult(HttpStatusCode.Unauthorized, "Requires authentication"); if (string.IsNullOrEmpty(request.Name)) return new HttpResult(HttpStatusCode.BadRequest, "Name is missing"); try { var ids = string.IsNullOrEmpty(request.Ids) ? null : request.Ids.Split(',').Select(i => Convert.ToInt32(i)).ToArray(); if (ids == null || ids.Length == 0) return new HttpResult(HttpStatusCode.BadRequest, "Id is missing"); CurrentWeb.AllowUnsafeUpdates = true; var spGroup = CurrentWeb.Groups.GetByID(ids[0]); spGroup.Name = request.Name; spGroup.Update(); CurrentWeb.AllowUnsafeUpdates = false; return new HttpResult(HttpStatusCode.OK, "SUCCESS"); } catch (UnauthorizedAccessException uex) { return new HttpResult(HttpStatusCode.Forbidden, uex.Message); } catch (Exception ex) { return new HttpResult(HttpStatusCode.BadRequest, ex.Message); } } // Delete 1 or more groups public override object OnDelete(GroupRequest request) { if (!IsAuthenticated) return new HttpResult(HttpStatusCode.Unauthorized, "Requires authentication"); try { var ids = string.IsNullOrEmpty(request.Ids) ? null : request.Ids.Split(',').Select(i => Convert.ToInt32(i)).ToArray(); if (ids == null || ids.Length == 0) return new HttpResult(HttpStatusCode.BadRequest, "Ids are missing"); CurrentWeb.AllowUnsafeUpdates = true; foreach (var id in ids) { CurrentWeb.Groups.RemoveByID(id); } CurrentWeb.Update(); CurrentWeb.AllowUnsafeUpdates = false; return new HttpResult(HttpStatusCode.OK, "SUCCESS"); } catch (UnauthorizedAccessException uex) { return new HttpResult(HttpStatusCode.Forbidden, uex.Message); } catch (Exception ex) { return new HttpResult(HttpStatusCode.BadRequest, ex.Message); } } }I added an event receiver to the feature (see source code download) to configure web.config and the global.asax file to enable the ServiceStack handlers. And that’s it, we’re ready to deploy.
Hi Matt,
have you ever experienced issues with ServiceStack cache? All works well for a week until consuming the web service leads me to an ArgumentException that is “magically” resolved with an iisreset (that’s why I supposed is an issue related to the cache). Any ideas?
Hey Roberto, if you’re still having this problem, I suggest you post a question on StackOverflow with some code and details surrounding the argument exception.
[…] This solution was inspired by the work of Matt Cowan “Building a Web API in SharePoint 2010 with ServiceStack“. […]
Awesome!
This is pretty awesome. Does this propagate through the SharePoint 2010 authentication or is there some other authentication in place here?
In my experience, you can just let SharePoint handle the authentication. In an intranet situation users are usually logged in with their windows creds so using SPContext in your service is all ready to go, or if you have a proxy on the front-end which is usually what I run into, authentication is pretty much already covered. That said, I’ve also used this in situations where one or more particular web applications / site collections are public with anonymous access turned on, so the services are then wide open for public consumption. In that case, you can just use the ServiceStack authentication feature and tag your services with the ServiceStack [ServiceStack.ServiceInterface.Authenticate] attribute.
If you want to use the [Authenticate] attribute to protect your services, you can pretty much handle any security paradigm you want, and you may want to optimize it for your particular environment (maybe do something that plays well with your proxy front-end, or your authentication needs). If you just want to ensure the user is properly authenticated within SharePoint, you can just add a generic auth provider feature in your AppHost.
Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] { new SharePointAuthProviderForServiceStack() }));
“SharePointAuthProviderForServiceStack” is a class you would write, that implements SharePoint logic. Just extend ServiceStack.ServiceInterface.Auth.AuthProvider.
Override the IsAuthenticated property (and make sure the SPContext.Current, SPContext.Current.Web, SPContext.Current.Web.CurrentUser are not null).
Override the IsAuthorized property (implement any logic you want there, or just -> return IsAuthenticated; and then let your services handle specific authorization scenarios).
You would also override Authenticate() and OnFailedAuthentication() (see ServiceStack samples for more info, or if you want, I can send you a basic sample class I’ve used).
Hi Matt,
I would like to use the specify the individual handlers, but it seems I don’t get it running. Do you have any help for me? I didn’t find anything in the samples for it.
I do have another question: Will ADFS work within Sharepoint?
thanks,
holger
Hey holger,
Sorry I didn’t get back sooner, holiday duties 🙂
I need to revisit this post slightly, because the code changes to the SS stack are no longer needed. The SS *.dlls do need to be signed still for SharePoint to work with them.
When you configure the AppHost, you can set the factory as so:
SetConfig(new EndpointHostConfig {
ServiceStackHandlerFactoryPath = "_layouts/api"
});
After you deploy, you then just need to make sure you changed the global.asax and web.config files to give IIS what it needs to host servicestack.
I have updated the download (which now points to a skydrive folder) with a “SP123” project which basically does the out of the box todo repository implementation from servicestack.
Read the README.txt file in that project to deploy and get further instructions on modifying the global.asax and web.config files. I have included some signed *.dlls if you just want to test.
Let me know how that works out for you.
Hmmm… apart from the code changes your making to ServiceStack, there’s also some interesting code to make the SharePoint site getting the feature to inherit from a custom SPHttpApplication by rewriting the global.asax, in order to initialize ServiceStack’s AppHostBase. Maybe another short post doing a quick discussion of the activate and deactivate is in order?
Thanks for the great post, BTW.
Right on Jacques. Thanks for the blog post suggestion.
[…] Building a Web API in SharePoint 2010 with ServiceStack (Matt Cowan) […]
Hi Matt,
First of all, Great writeup!
Also Just letting you know the ServiceStack version on NuGet (https://nuget.org/packages/ServiceStack) only contains .NET 3.5 binaries and none of the WebActivator dependencies that the NuGet Host.* packages contains.
Super. Thanks Demis for the clarification. I updated the post.