This is the third post in a series of writing a SharePoint Service Application. If you’ve missed the first posts, read those or this won’t make a lick of sense. (It may not make a lick of sense regardless!) Also note that creating a service application is OVERKILL for most things you’ll create for SharePoint… this post is NOT “how to write WCF code for SharePoint”, it is VERY SPECIFIC to creating a SharePoint 2010 Managed Service Application. And while this example only spits out a simple “Hello World” sample, you can use this as “starter code” for a much more complex application.
So far, we’ve created the service application library, but no WCF service. In this post, we’ll create the WCF service and a WCF client to call it using the SharePoint application proxy. The WCF client will run on the web front end and will call back to the application server’s instance of the application through your backend web service. In the next post, we’ll cover how to actually deploy the service application in the WSP project. But for now, we’ll just look at the code side of the WCF library. Because we really want to focus on CODE, we won’t cover the WCF configuration stuff quite yet. We’ll take care of that in the WSP project.
The Service Interface
As with any WCF service, start with the service interface. Ours will simply return a debug statement—which is actually ideal to include in your production WCF service for debugging support issues. Here is my service interface:
using System; using System.ServiceModel; namespace SuperCool.ServiceLibrary.Services { /// <summary> /// Defines the WCF interface to the backend service application /// </summary> /// <remarks>Referenced in WebClients\[app name]\client.config</remarks> [ServiceContract] public interface ICoolService { [OperationContract] string GetServiceDebug(); } }
The Service Class
For our service implementation, I’ll return some debug info about the call, so you can see what URL it actually gets called on and what identity is calling it. You can also include the attribute [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.NotAllowed)] on this class, since we do NOT support this interface in an ASP.NET context.
using System; using Microsoft.SharePoint.Administration.Claims; using System.ServiceModel; using System.Globalization; using Microsoft.SharePoint.Administration; namespace SuperCool.ServiceLibrary.Services { /// <summary> /// The actual WCF-callable backend service. (The rest is plumbing) /// </summary> public class CoolService : ICoolService { // this method would be called as the user. public string GetServiceDebug() { var encAccountName = SPClaimProviderManager.Local.GetUserIdentifierEncodedClaim( ServiceSecurityContext.Current.PrimaryIdentity); var claimsAccountName = SPClaimProviderManager.Local.ConvertClaimToIdentifier(encAccountName); // you can also use System.Threading.Thread.CurrentPrincipal.Identity SPIisWebServiceApplication current = SPIisWebServiceApplication.Current; return string.Format(CultureInfo.InvariantCulture, @"Hello, {0}. So glad you used ""{1}"" (a {2}, id {3}) at {4}.", claimsAccountName, current.DisplayName, current.GetType().Name, current.Id, OperationContext.Current.IncomingMessageProperties[ "OriginalHttpRequestUri"] ); } } }
This backend service will ALWAYS be authenticated by claims authentication—it is the delegation used by SharePoint, which we’ll configure in the client in the next code sample. The ServiceSecurityContext.Current.PrimaryIdentity will be a ClaimsIdentity, which you can get the current user from. To get the current User Profile from the backend service, you can use this claims identity to get the user profile from the User Profile application (it will use the User Profile Application that is in the same proxy group as your application).
private UserProfile GetDelegatedIdentityProfile() { var encAccountName = SPClaimProviderManager.Local.GetUserIdentifierEncodedClaim( ServiceSecurityContext.Current.PrimaryIdentity); var claimsAccountName = SPClaimProviderManager.Local.ConvertClaimToIdentifier(encAccountName); var um = new UserProfileManager(SPServiceContext.Current, false, false); return um.GetUserProfile(claimsAccountName, false); }
Getting the Service Application from your WCF Code
Within your service, there is a MAGIC property that you can use to get the instance of your application. SPIisWebServiceApplication.Current will return the current application that your service is created under. Note that you can create multiple instances of the service on the Central Administration “Manage Service Applications” page, each of which could have it’s own persisted data, database connections and more. This magic property is the only thing that ties your WCF class to your service application! In the smaple code above, you’ll see that we’re simply writing out the name and id of the application.
Calling the Backend through a Service Client
The last part of the plumbing is creating an actual service client. With “sane” WCF programming, you usually let Visual Studio do this type of thing for you. Not so in this case however, you’ve got to do a lot to the channel with credentials and dynamic, load-balanced endpoint URLs.
Most of this is “boilerplate” code that uses the SharePoint load balancer to call the application servers. To scale out your application, be sure the service instance is running on multiple app servers. You’ll need to start it on the “Services on Server” page (well, after you create the WCF project, which we’ll do in the next post!). The client will use the ConfigurationChannelFactory and the proxy’s configuration which we’ll deploy in the WSP project in order to configure itself. Specifically, the channel factory will call SuperCoolServiceApplicationProxy.Configuration to read the configuration for the client. We previously defined this as:
internal Configuration Configuration { get { return OpenClientConfiguration(SPUtility.GetGenericSetupPath( @"WebClients\SuperCoolServiceApplication")); } }
Following is a client class that contains the infrastructure to call the backend service. You could make this into a common base class, but I’ll keep it simple to get the concept across. The main methods to look at are “ExecuteOnChannel” and GetChannel. After that, just implement the interface of your service, and use ExecuteOnChannel to call the backend service.
using SuperCool.ServiceLibrary.Services; using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Configuration; using System; namespace SuperCool.ServiceLibrary.Client { /// <summary> /// A WCF client for talking through the WCF channel to the backend. /// </summary> public class SuperCoolClient : ICoolService { private SPServiceContext serviceContext; public SuperCoolClient() : this(null) { } public SuperCoolClient(SPServiceContext serviceContext) { if (serviceContext == null) { if (SPServiceContext.Current != null) serviceContext = SPServiceContext.Current; else this.serviceContext = SPServiceContext.GetContext( SPServiceApplicationProxyGroup.Default, new SPSiteSubscriptionIdentifier(Guid.Empty)); } else this.serviceContext = serviceContext; if (this.serviceContext == null) throw new ArgumentNullException("serviceContext"); } private delegate void CodeToExecuteOnChannel(ICoolService channel); private void ExecuteOnChannel(CodeToExecuteOnChannel codeBlock, bool asProcess) { if (codeBlock == null) throw new ArgumentNullException("codeBlock"); SuperCoolServiceApplicationProxy proxy = (SuperCoolServiceApplicationProxy) this.serviceContext.GetDefaultProxy( typeof(SuperCoolServiceApplicationProxy)); if (proxy == null) throw new InvalidOperationException("service proxy not found."); SPServiceLoadBalancer loadBalancer = proxy.LoadBalancer; if (loadBalancer == null) throw new InvalidOperationException("load balancer not found."); SPServiceLoadBalancerContext loadBalancerContext = loadBalancer.BeginOperation(); try { using (new SPServiceContextScope(this.serviceContext)) { // Get a channel to the service application endpoint IChannel channel = (IChannel)GetChannel(proxy, loadBalancerContext.EndpointAddress, asProcess); try { // Execute the delegate codeBlock((ICoolService)channel); // Close the channel channel.Close(); } finally { if (channel.State != CommunicationState.Closed) channel.Abort(); } } } catch (EndpointNotFoundException) { loadBalancerContext.Status = SPServiceLoadBalancerStatus.Failed; throw; } finally { loadBalancerContext.EndOperation(); } } // Used to cache the client channel factory private static ChannelFactory<ICoolService> s_ChannelFactory; private static object s_ChannelFactoryLock = new object(); private ICoolService GetChannel(SuperCoolServiceApplicationProxy proxy, Uri address, bool asProcess) { if (proxy == null) throw new ArgumentNullException("proxy"); if (address == null) throw new ArgumentNullException("address"); // Check for a cached channel factory if (s_ChannelFactory == null) { lock (s_ChannelFactoryLock) { if (s_ChannelFactory == null) { s_ChannelFactory = new ConfigurationChannelFactory<ICoolService>( "https", proxy.Configuration, null); // Configure the channel factory for claims-based auth s_ChannelFactory.ConfigureCredentials( SPServiceAuthenticationMode.Claims); } } } if (asProcess) return s_ChannelFactory.CreateChannelAsProcess( new EndpointAddress(address)); else return s_ChannelFactory.CreateChannelActingAsLoggedOnUser( new EndpointAddress(address)); } public string GetServiceDebug() { string result = null; this.ExecuteOnChannel( channel => result = channel.GetServiceDebug(), asProcess: false); return result; } } }
Also note that ExecuteOnChannel takes an “asProcess” parameter—we will either create the channel as the logged in user and configure the SharePoint claim to delegate, or we’ll delegate the current process identity.
The last piece is a ServiceHostFactory. There’s not too much to this, but you’ve got to use a custom host factory to configure claims authentication. We’ll reference this class in the .SVC file. Not sure why there isn’t one of these that SharePoint ships, or maybe there is one that I’m missing… but for some reason you have to do this to wire up the backend WCF endpoint. There’s nothing special about this class for our application.
using System; using System.ServiceModel.Activation; using System.ServiceModel; using Microsoft.SharePoint; namespace SuperCool.ServiceLibrary.Services { internal sealed class SuperCoolHostFactory: ServiceHostFactory { public override ServiceHostBase CreateServiceHost( string constructorString, Uri[] baseAddresses) { ServiceHostBase serviceHost = base.CreateServiceHost( constructorString, baseAddresses); serviceHost.Configure(SPServiceAuthenticationMode.Claims); return serviceHost; } } }
With the WCF interface, service and client in place, we’re now ready to deploy the service application in a WSP project. We’ll do that next… stay tuned! (And I promise, I’ll post the whole source code for this… )
What about the case when I want to consume the service application from a REST interface. Should I create a WCF RESTful service that acts like the client SuperCoolClient (create something like nested services?)?
yeah, you’d have to write a REST web service that calls or wraps the service client.
Nice series. I hope you get around to finishing it and posting the code
Daniel,
Thanks for some great info.
What I’m wondering is can you “safely” use the SharePoint OM in your service application code? The reason I ask is that one Service can be reused from multiple farms so that implies to me that you can ONLY use the client .NET object model since it is remotable. If your service is built to support multiple farms you can’t use the server side object model.
Thanks
/Jonas
That’s true: backend service code SHOULD NOT talk to any site content. It can talk to other service apps in the same proxy group, such as the user profile application or search: but you should avoid talking to site content.
Hey there,
Well done!
When can we get the next and final post of the series?
Cheers.
Hi Daniel,
thanks for this great article. But with Service Pack 1 for Windows 2008 R2 installed ServiceSecurityContext.Current.PrimaryIdentity in GetServiceDebug() and only works with classic authentication and not with claims, because it is NULL. With Service Pack 1 the new parameter aspnet:AllowAnonymousImpersonation shows up in Application Settings and I think this could be the problem. It’s not important if the parameter is set to true or false. In both scenarios the ServiceSecurityContext.Current.PrimaryIdentity is null and so no delegation of the current user possible. Any suggestion?
Thanks
Harry
On the backend service call, there will ALWAYS be a PrimaryIdentity, if you delegate it along correctly (using the code samples I’ve provided).
Hi Daniel,
Good Posts – when are you going to file the next instalment – I am keen to see how it completes?
Very interested to code up a Sharepoint Service App
Any update on when the last part will be posted? Thanks.
Sorry for the long delay but it is up.
Hi Dan
Great series and then suddenly it stops! For that reasons I’m now trying to understand the process by following another example here: http://blogs.msdn.com/b/dtaylor/archive/2010/04/06/sharepoint-calculator-service-application-part-1.aspx but would still be interested in knowing how to package the WSP and deploy the service. I guess I also need to add a web.config.
Cheers, Marco
Hey Marco
thanks, I finally did finish and post the code too.
Pingback: Step by Step SharePoint Service Applications : Part 4 (Deploying the Solution) | Daniel Larson's Developer Blog