次の方法で共有


SharePoint Calculator Service Part 10 – Service Client

So far, we’ve been covering the service side of our Calculator service. We have a working service application that can be deployed and managed in a SharePoint server farm.

In this article, we’ll switch focus to the client side. Remember that SharePoint service applications live in the middle-tier, so our “client” is actually another server—typically, a Web Front End server.

For example, our client may be a web page or a web part that renders a calculator user interface for presentation in a browser.

If you need a refresher on the various moving parts of a service application, you can refer to my previous post on Service Application Topologies.

Service Proxy and Service Application Proxy

The Service Proxy (SPServiceProxy) and Service Application Proxy (SPServiceApplicationProxy) objects represent the client components that communicate with a service.

They are logically the client-side equivalents of the SPService and SPServiceApplication.

In other words, in the same way that the SPService represents a service that has been registered in a SharePoint server farm, an SPServiceProxy represents a service client that has been registered in a SharePoint server farm. The SPServiceProxy represents the capability to connect to some SPService.

Similarly, an SPServiceApplication represents a logical endpoint of a service in a SharePoint server farm, and a SPServiceApplicationProxy represents a connection to that logical endpoint.

And, just as we have specialized SPIisWebService and SPIisWebServiceApplication classes for web services, we have specialized classes for web service clients:  SPIisWebServiceProxy and SPIisWebServiceApplicationProxy.

We will derive classes from the abstract SPIisWebServiceProxy and SPIisWebServiceApplicationProxy classes and register them in the SharePoint configuration database in order to deeply integrate the client with the SharePoint administration experience and runtime.

So let’s do it!

CalculatorServiceProxy

First, we’ll create our service proxy class:

  1. internal sealed class CalculatorServiceProxy : SPIisWebServiceProxy
  2. {
  3.     internal CalculatorServiceProxy(
  4.         SPFarm farm)
  5.         : base(farm)
  6.     {
  7.     }
  8. }

This is the class that we’ll register in the configuration database in order to tell SharePoint that our client classes are installed and ready to use.

CalculatorServiceApplicationProxy

Next, we’ll create our service application proxy class:

  1. internal sealed class CalculatorServiceApplicationProxy : SPIisWebServiceApplicationProxy
  2. {
  3.     [Persisted]
  4.     private SPServiceLoadBalancer loadBalancer;
  5.  
  6.     internal CalculatorServiceApplicationProxy(
  7.         string name,
  8.         CalculatorServiceProxy serviceProxy,
  9.         Uri serviceApplicationAddress)
  10.         : base(name, serviceProxy, serviceApplicationAddress)
  11.     {
  12.         // Create a new round-robin load balancer
  13.         this.loadBalancer = new SPRoundRobinServiceLoadBalancer(serviceApplicationAddress);
  14.     }
  15.  
  16.     public override void Provision()
  17.     {
  18.         // Provision the load balancer
  19.         this.loadBalancer.Provision();
  20.  
  21.         base.Provision();
  22.     }
  23.  
  24.     public override void Unprovision(
  25.         bool deleteData)
  26.     {
  27.         // Unprovision the load balancer
  28.         this.loadBalancer.Unprovision();
  29.  
  30.         base.Unprovision(deleteData);
  31.     }
  32.  
  33.     internal SPServiceLoadBalancer LoadBalancer
  34.     {
  35.         get
  36.         {
  37.             return this.loadBalancer;
  38.         }
  39.     }
  40.  
  41.     internal Configuration Configuration
  42.     {
  43.         get
  44.         {
  45.             return OpenClientConfiguration(SPUtility.GetGenericSetupPath(
  46.                 @"WebClients\Sample\Calculator"));
  47.         }
  48.     }
  49. }

This is the class that will be created on demand when an administrator chooses to create a connection to a new service application.

The class is constructed like any other configuration object, with one additional parameter:  serviceApplicationAddress (lines 9-10).

Service Application Address and Load Balancer

The serviceApplicationAddress property identifies the service application to which the service application proxy will connect. Specifically, it must match the value of the CalculatorServiceApplication.Uri property.

If you’re really observant, you may have noted that a service application actually has many addresses—one per service instance. Remember that a service application may be hosted on any number of servers, and the host on each server is called the service instance.

For example, if our service application is hosted on two machines, server1 and server2, it will have two addresses: server1:32843/…/calculator.svc and server2:32843/…/calculator.svc.

So how can a service application have a single address in the object model?

The answer is that the service application address is a logical service address that can be resolved into many physical addresses at runtime—one physical address per service instance.

The Service Application Framework takes care of this detail for us by providing the SPRoundRobinServiceLoadBalancer class and the supporting Topology service. (I’ll save the details of how this works for a future post.)

For now, suffice it to say that we can simply construct (line 13) and provision/unprovision (lines 16-31) the load balancer component with our service application proxy and get this functionality.

We’ll also expose the load balancer object as an internal property for later use by our client runtime (lines 33-39).

Client Configuration

We’ll be using WCF to connect to our service application, and as a best practice we’ll keep the client settings in a configuration file.

This is the client-side equivalent of having service settings in a web.config file. However, unlike the service side, our client assembly dll can be loaded by any arbitrary executable.

This is a little tricky for the .NET configuration system, which supports configuration only at the exe scope and doesn’t support per-dll configuration. There simply isn’t a place we can drop a config file where it will be loaded by the .NET configuration system and recognized by WCF regardless of what exe (w3wp.exe, powershell.exe, someapp.exe, etc) loads our client dll.

To solve this problem, we use the SharePoint OpenClientConfiguration helper method to reference a file named “client.config” in a well-known location.

By convention, our deployment solution will drop the client.config file under the “WebClients” folder of the SharePoint installation. We separate our client.config file from all other config files by creating a subfolder (“Sample\Calculator”).

Calculator Client (Runtime)

Next, we’ll implement our calculator client. This is the public-facing class that clients, such as web parts, will use to interact with our service.

  1. public sealed class CalculatorClient
  2. {
  3.     private SPServiceContext serviceContext;
  4.  
  5.     // Used to cache the client channel factory
  6.     private static ChannelFactory<ICalculatorServiceContract> s_ChannelFactory;
  7.     private static object s_ChannelFactoryLock = new object();
  8.  
  9.     public CalculatorClient(
  10.         SPServiceContext serviceContext)
  11.     {
  12.         if (null == serviceContext)
  13.         {
  14.             throw new ArgumentNullException("serviceContext");
  15.         }
  16.  
  17.         this.serviceContext = serviceContext;
  18.     }
  19.  
  20.     /// <summary>
  21.     /// Client method executed on WFE (front-end), for example, by a web part.
  22.     /// </summary>
  23.     public int Add(int a, int b)
  24.     {
  25.         int result = 0;
  26.  
  27.         // Execute the service application method
  28.         this.ExecuteOnChannel(
  29.             channel => result = channel.Add(a, b));
  30.  
  31.         return result;
  32.     }
  33.  
  34.     private delegate void CodeToExecuteOnChannel(
  35.         ICalculatorServiceContract channel);
  36.  
  37.     private void ExecuteOnChannel(
  38.         CodeToExecuteOnChannel codeBlock)
  39.     {
  40.         if (null == codeBlock)
  41.         {
  42.             throw new ArgumentNullException("codeBlock");
  43.         }
  44.  
  45.         // Get the default service application proxy from the service context
  46.         CalculatorServiceApplicationProxy proxy = (CalculatorServiceApplicationProxy)this.serviceContext.GetDefaultProxy(
  47.             typeof(CalculatorServiceApplicationProxy));
  48.         if (null == proxy)
  49.         {
  50.             throw new InvalidOperationException("Calculator service application proxy not found.");
  51.         }
  52.  
  53.         SPServiceLoadBalancer loadBalancer = proxy.LoadBalancer;
  54.         if (null == loadBalancer)
  55.         {
  56.             throw new InvalidOperationException("Calculator load balancer not found.");
  57.         }
  58.  
  59.         SPServiceLoadBalancerContext loadBalancerContext = loadBalancer.BeginOperation();
  60.         try
  61.         {
  62.             using (new SPServiceContextScope(this.serviceContext))
  63.             {
  64.                 // Get a channel to the service application endpoint
  65.                 IChannel channel = (IChannel)GetChannel(proxy, loadBalancerContext.EndpointAddress);
  66.                 try
  67.                 {
  68.                     // Execute the delegate
  69.                     codeBlock((ICalculatorServiceContract)channel);
  70.  
  71.                     // Close the channel
  72.                     channel.Close();
  73.                 }
  74.                 finally
  75.                 {
  76.                     if (channel.State != CommunicationState.Closed)
  77.                     {
  78.                         channel.Abort();
  79.                     }
  80.                 }
  81.             }
  82.         }
  83.         catch (EndpointNotFoundException)
  84.         {
  85.             loadBalancerContext.Status = SPServiceLoadBalancerStatus.Failed;
  86.             throw;
  87.         }
  88.         finally
  89.         {
  90.             loadBalancerContext.EndOperation();
  91.         }
  92.     }
  93.  
  94.     private ICalculatorServiceContract GetChannel(
  95.         CalculatorServiceApplicationProxy proxy,
  96.         Uri address)
  97.     {
  98.         // Check for a cached channel factory
  99.         if (null == s_ChannelFactory)
  100.         {
  101.             lock (s_ChannelFactoryLock)
  102.             {
  103.                 if (null == s_ChannelFactory)
  104.                 {
  105.                     // Get the endpoint configuration name
  106.                     string endpointConfigurationName = "http";
  107.  
  108.                     // Create a channel factory without specifying an endpoint address
  109.                     // so it can be cached and used for multiple endpoint addresses
  110.                     s_ChannelFactory = new ConfigurationChannelFactory<ICalculatorServiceContract>(
  111.                         endpointConfigurationName, proxy.Configuration, null);
  112.  
  113.                     // Configure the channel factory for claims-based authentication
  114.                     s_ChannelFactory.ConfigureCredentials(SPServiceAuthenticationMode.Claims);
  115.                 }
  116.             }
  117.         }
  118.  
  119.         return s_ChannelFactory.CreateChannelActingAsLoggedOnUser(new EndpointAddress(address));
  120.     }
  121. }

The first thing to note about this class is that it doesn’t subclass any framework classes. This is an intentional decision on my part, because I want this public class to have longevity. If I choose to use some alternate framework down the road or maybe move from a web service to some other type of service, I want to protect my client code from these changes.

So, I have intentionally made all of my framework subclasses internal to my implementation and have only exposed the CalculatorClient class to my consumers.

Note that if my goal was to have the shortest sample code possible I could have merged some of these classes, but in this case I am prioritizing what I consider to be a best practice over code brevity.

SPServiceContext

The one part of the framework that is necessarily exposed by the client is SPServiceContext. SharePoint does require some context in order to be able to invoke a service method, and the SPServiceContext object wraps up all of that context into a a single object.

Technically, this context could be acquired automatically in some cases. For example, the SPServiceContext.Current method is a static method that can acquire the necessary context in certain cases, for example, when executed by a web part.

However, SPServiceContext.Current will return null in other situations, for example, when it cannot automatically detect the necessary context from the PowerShell console or from a timer job.

So, it is not appropriate to use SPServiceContext.Current from general-purpose object model code such as CalculatorClient.

Instead, the caller must provide this context to the object model using whatever SPServiceContext method is appropriate for the execution context, i.e., SPServiceContext.Current for a SharePoint web part or page context, or the SPServiceContextPipeBind object for a PowerShell cmdlet.

I plan to cover more about SPServiceContext in depth in a future article. For now, you can just consider it a “black box” of context that gives SharePoint the runtime context for executing a service request, such as the context necessary for finding the appropriate service application proxy configured by the SharePoint farm administrator.

ExecuteOnChannel

ExecuteOnChannel is a private helper method that I wrote to wrap up all of the steps that are common to every service call. You can pass a delegate as a parameter and the delegate function will be executed on a WCF channel that has authenticated with the service application.

private void ExecuteOnChannel(
    CodeToExecuteOnChannel codeBlock)

With some nice C# lambda expression syntax (lines 28-29), the code for calling a service method is fairly readable, and you don’t have the maintenance problems associated with copy-paste.

this.ExecuteOnChannel(channel =>
    result = channel.Add(a, b));

The first thing we do in ExecuteOnChannel is lookup our service application proxy (lines 45-51). We use the GetDefaultProxy method on our SPServiceContext object, which knows how to find the proxy that the farm administrator has configured for the given context. More on how this works in a future article.

CalculatorServiceApplicationProxy proxy = (CalculatorServiceApplicationProxy)this.serviceContext.GetDefaultProxy(
    typeof(CalculatorServiceApplicationProxy));

From our service application proxy, we get our load balancer object (lines 53-57).

SPServiceLoadBalancer loadBalancer = proxy.LoadBalancer;

Then, we tell the load balancer that we want to begin an operation using the BeginOperation method (line 59).

SPServiceLoadBalancerContext loadBalancerContext = loadBalancer.BeginOperation();

This returns an SPServiceLoadBalancerContext object that we can use to get a service endpoint address using the loadBalancerContext.EndpointAddress property (line 65). Note that this address is a physical endpoint address, in that it identifies a URI to which we can directly send a WCF message.

We also use the load balancer context to report failures to the load balancer (lines 83-87).

catch (EndpointNotFoundException)
{
    loadBalancerContext.Status = SPServiceLoadBalancerStatus.Failed;
    throw;
}

It is important to note that the consequence of reporting a failure to the load balancer is that the endpoint will temporarily be removed from rotation. In other words, the load balancer will not direct requests to that endpoint for some time.

So, if the service method throws some exception that is expected given the inputs it would not be appropriate to report that exception as a failure to the load balancer, because the endpoint is working fine.

It is only appropriate to report failures that indicate that the endpoint is unavailable or not functioning properly. In our example, if the endpoint cannot be found we report the failure and ensure that future requests will not be sent to this endpoint for some time.

I plan to cover the details of the SPRoundRobinServiceLoadBalancer implementation in a future article, but for now your service client will function correctly by simply following this example.

When the operation completes, we report this to the load balancer in a finally block using the EndOperation method so that, for example, it can track statistics about in-flight operations for its internal load balancing algorithm.

finally
{
    loadBalancerContext.EndOperation();
}

I’m going to postpone discussion of the SPServiceContextScope (line 62) for now. It’s not strictly necessary for this example, but it will come into play if we implement a partitioning scheme for hosted deployments in the future. I’m including it for now in case the code gets copy-pasted, since it makes the ExecuteOnChannel method more widely applicable.

That leaves the code to actually acquire the WCF channel, execute the specified code on it, and clean up (lines 64-80). This is pretty standard WCF code, so I won’t go into detail.

GetChannel

The private GetChannel helper method is used by ExecuteOnChannel to acquire a WCF channel.

It is important to note that we cache the WCF channel factory, which is a requirement to achieve great performance. We use a standard double-check lock (lines 99-103) and cache the channel factory in a static variable (lines 6-7).

In order to enable the channel factory to be cached, we do not specify an endpoint address when creating the channel factory. If you recall the earlier discussion on load balancing, this is because each service request may be addressed to a different physical service instance (aka service host).

Instead, we specify the endpoint address at the time the channel is created. So a given WCF channel is effectively bound to a single physical service instance, but each channel may be bound to a different service instance.

As noted earlier in the Client Configuration section of this article, we want the channel factory to get its configuration from our client.config file. So we use the WCF ConfigurationChannelFactory class to create our channel factory using the Configuration object specified by the service application proxy (lines 110-111).

s_ChannelFactory = new ConfigurationChannelFactory<ICalculatorServiceContract>(
    endpointConfigurationName, proxy.Configuration, null);

We also specify the endpoint configuration name from our client.config file, which I’ve currently hard-coded to “http” (line 106). More on supporting multiple endpoint addresses in a future article.

Since our service application is configured for claims-based authentication, we use the ConfigureCredentials extension method from the SAF SPChannelFactoryOperations class.

s_ChannelFactory.ConfigureCredentials(SPServiceAuthenticationMode.Claims);

And finally, we create the WCF channel using the CreateChannelActingAsLoggedOnUser extension method from the SAF SPChannelFactoryOperations class, specifying the endpoint address from the load balancer context.

return s_ChannelFactory.CreateChannelActingAsLoggedOnUser(new EndpointAddress(address));

This gives us a WCF channel that authenticates both the executing process and the signed-in user (i.e., using a SAML token that represents the process “acting as” the current user) to the service.

Client.config

Nothing really special here, but I’m including it for completeness. It’s a standard WCF config file with the binding copy-pasted from the service application web.config:

  1. <configuration>
  2.   <system.serviceModel>
  3.     <client>
  4.       <endpoint
  5.         name="http"
  6.         contract="Sample.Calculator.Client.ICalculatorServiceContract"
  7.         binding="customBinding"
  8.         bindingConfiguration="CalculatorServiceHttpBinding" />
  9.     </client>
  10.     <bindings>
  11.       <customBinding>
  12.         <binding
  13.           name="CalculatorServiceHttpBinding">
  14.           <security
  15.             authenticationMode="IssuedTokenOverTransport"
  16.             allowInsecureTransport="true" />
  17.           <binaryMessageEncoding>
  18.             <readerQuotas
  19.               maxStringContentLength="1048576"
  20.               maxArrayLength="2097152" />
  21.           </binaryMessageEncoding>
  22.           <httpTransport
  23.             maxReceivedMessageSize="2162688"
  24.             authenticationScheme="Anonymous"
  25.             useDefaultWebProxy="false" />
  26.         </binding>
  27.       </customBinding>
  28.     </bindings>
  29.   </system.serviceModel>
  30. </configuration>

That’s all for now

At this point we have a completely functional service application client, however, we have a little more to do to integrate with the administrator user experience. Next time.

Comments

  • Anonymous
    March 11, 2011
    Great example.  I'd like to have multiple endpoint addresses in a single service.  I'm not quite sure how this fits in with the example.  You mentioned a future article.  Any info on that?  Thanks.

  • Anonymous
    March 11, 2011
    The comment has been removed