Monday, July 13, 2009

Abstracting WCF Service Calls in Silverlight 3

While working with WCF in Silverlight is a blessing, it is also a challenge because of the way Silverlight manages service references. There are often extra steps required to ensure a service is Silverlight compatible, then additional considerations relative to making the service consumable on the Silverlight side and ensuring security concerns are met, etc. The purpose of this post is to provide a relatively simple and lightweight framework for abstracting the services in your applications to provide a more solid foundation for using them.

The service fundamentally begins on the web server side. Silverlight provides additional templates including a "Silverlight-enabled WCF service." Use this to add a new service reference. This is the first place that the fun can begin.

Despite the friendly sounding name, I've actually had the designer add a service and then declare custom bindings in the web.config with a mode of binary. This isn't something Silverlight knows how to consume. After receiving the ambiguous "NotFound" error and then digging deeper and finding my service was causing a "415 unsupported media type" error in the Silverlight client, I realized my service had generated incorrectly.

The first step was to dig into the web.config and find the service behaviors for my service. The "basicHttpBinding" is the one Silverlight is friendly with, something custom or binary will not. This snippet of web.config demonstrates some of the fundamentals for having the service correct on the server side:

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />    
    <behaviors>
       <serviceBehaviors>
          <behavior name="Silverlight.Service.CustomServiceBehavior">
             <serviceMetadata httpGetEnabled="true" />
             <serviceDebug includeExceptionDetailInFaults="false" />
          </behavior>
       </serviceBehaviors>
    </behaviors>
    <services>  
    <service behaviorConfiguration="Silverlight.Service.CustomServiceBehavior"
       name="Silverlight.Service.CustomService">  
       <endpoint address="" binding="basicHttpBinding"          contract="Silverlight.Service.ICustomService"/>
       <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
    </service>
  </services>
</system.serviceModel>

The basics: aspNetCompatibilityEnabled, basicHttpBinding, and the "mex" binding for meta data exchange. Now we're good and can publish this somewhere to reference from the Silverlight client.

On the client, things get more interesting. You could hard code the endpoint if you knew the location it was being published to, but I needed something much more flexible. What I did know was that the client would always be in a folder relative to the web host with the services, so by extracting my local Uri, I can recreate the service Uri on the fly. What was bothering me was doing this over and over for each service, so I finally created a generic BaseService class.

The class is based on the client and channel for the service. From this, it knows how to construct a url. Because my service named "CustomService" is actually at "Silverlight/Service/CustomService.svc", a little bit of parsing the host and changing the endpoint is all that is needed.

Here is the class:

public abstract class BaseService<TClient,TChannel> : ClientBase<TChannel> where TClient: ClientBase<TChannel>, new() where TChannel: class
{
    private const string BASE_SERVICE = "Silverlight/Service/{0}.svc";

    private static readonly string _baseUri;

    static BaseService()
    {
        _baseUri = System.Windows.Browser.HtmlPage.Document.DocumentUri.AbsoluteUri;
        int lastSlash = _baseUri.LastIndexOf("/");
        _baseUri = _baseUri.Substring(0, lastSlash+1);            
    }

    private readonly TClient _channel; 

    protected BaseService()
    {        
        if (_baseUri.Contains("http"))
        {
            Binding binding = new BasicHttpBinding();
            EndpointAddress endPoint =
                new EndpointAddress(string.Format("{0}{1}", _baseUri,
                                                  string.Format(BASE_SERVICE, typeof (TChannel).Name)));
            _channel = (TClient)Activator.CreateInstance(typeof (TClient), new object[] {binding, endPoint});                
        }    
        else
        {
            _channel = Activator.CreateInstance();
        }
    }

    protected TClient _GetClientChannel()
    {
        return _channel; 
    }
}

Basically, the service encapsulates creating the client channel for communicating with the service. The static constructor takes path to the silverlight application and then using string manipulating to find the virtual directory it is hosted in (note if you are running the client in a subfolder, you'll have to trim more slashes to get back to the application root).

Note the use of the Activator to instantiate new objects that are typed to the channel/client we need. The first passes an object[] array, which causes the activator to find the constructor that best matches the parameters, in this case we are basically doing this:

_channel = new CustomServiceClient(binding, endpoint);

When the service is constructed, it checks to see the the base URI contains "http." The reason we do this is because when you run debug, the URI is actually on the file system (file:). We assume your default set up for the endpoint is sufficient for debugging and simply instantiate the client without any parameters. If, however, you are running in a web context, the endpoint is overridden with the reconstructed path. Note that we take the name of the channel and then format it into the path so that a channel called CustomService gets mapped to Silverlight/Service/CustomService.svc.

I then created a special event argument to handle the completion of a service call. It will pass back the entity that the service references, as well as the error object, so the client can process as needed:

public class ServiceCompleteArgs<T> : EventArgs where T: class 
{
    public T Entity { get; set; }

    public Exception Error { get; set; }

    public ServiceCompleteArgs(T entity, Exception e)
    {
        Entity = entity;
        Error = e;
    }
}

Now we can inherit from the base class for our actual service calls. Let's assume CustomService returns an integer. My custom service helper will look like this:

public class CustomServiceClientHelper : BaseService<CustomServiceClient, CustomService>
{
    public event EventHandler<ServiceCompleteArgs<int>> CustomServiceCompleted;

    public CustomServiceClientHelper() 
    {
        _GetClientChannel().GetIntegerCompleted += _CustomServiceClientHelperGetIntegerCompleted;
    }        

    public void GetInteger()
    {
        _GetClientChannel().GetIntegerAsync();
    }

    void _CustomServiceClientHelperGetIntegerCompleted(object sender, GetIntegerCompletedEventArgs e)
    {
        if (CustomServiceCompleted != null)
        {
            int result = e.Error == null ? e.Result : -1; 
            CustomServiceCompleted(this, new ServiceCompleteArgs(result,e.Error));
        }
    }
}

Now it's very easy for me to reuse the service elsewhere without understanding how the endpoint or data is bound - in my code, putting the result in a TextBlock is as simple as this:

public void Init()
{
    CustomServiceClientHelper client = new CustomServiceClientHelper();
    client.CustomServiceCompleted += _ClientCustomServiceCompleted;
    client.GetInteger();
}

void _ClientCustomServiceCompleted(object sender, ServiceCompleteArgs<int> e)
{
    if (e.Error == null)
    {
        DisplayTextBlock.Text = e.Entity.ToString();
    }
    else 
    {
        HandleError(e.Error);
    }
}       

Now you've got a simple framework for plugging in services and consuming them on the client side that will update its references based on the location it is installed. Of course, there is much more you can do and I suggest the following articles for further reading:

Jeremy Likness

15 comments:

  1. Hi Jeremy, do you have a complete example with full source code that goes with this blog posting? I'm interested in implementing something like this in my own application, but there are a couple classes referenced in the posting that aren't in the post.

    ReplyDelete
  2. Hi there!

    By chance, have you tested this out of browser, and if so, have you been successful, or were there other issues involved to be able to connect to the right website?

    ReplyDelete
  3. I have not tested in out of browser ... but when I have to connect to more complicated end points in Silverlight 3, I typically build a server-side proxy and use the same method to connect from Silverlight down to the proxy.

    ReplyDelete
  4. This is very cool! It's a problem I face daily, especially when trying to build automated tests, and those tests don't work by simply mocking IService (because of the Async and Completed methods). Looking at the code, it seems to me like, for every service method, you need to hand-code a corresponding client-side helper method. If you have a handful of services, each with a dozen method calls, that's quite a bit of helper-class construction, and maintenance whenever a service-side method is added or changed. Is there a way to automate the helper-class construction?

    ReplyDelete
  5. In that case, I would use an automated mocking tool like Moq. That allows you to easily plumb out the interface ... see my more recent posts on it (Automated Testing/Moq).

    ReplyDelete
  6. Hi Jeremy,

    I am using Silverlight 3 and VS2008 .NET 3.5

    These lines are not compilling to me:

    1 - ClientBase

    2 - Binding binding = new BasicHttpBinding();

    3 - EndpointAddress endPoint =
    new EndpointAddress

    Could you please shed some light on what I am missing?

    You have to provided the code for ClientBase class right?

    Cheers

    Claudio

    ReplyDelete
  7. Those bits are in the service model namespace, and should be generated as part of the WCF proxy.

    ReplyDelete
  8. Very interesting post. I was looking for something like that last year !

    Thanks a lot Jeremy :)

    ReplyDelete
  9. Jeremy, do you have an example solution for this article. I would really like to implement this pattern in a project but need some clarification on how some of the classes interact in your example.

    Thanks
    John Maloney

    ReplyDelete
  10. Not yet but I've received plenty of requests so let me see what I can put together!

    ReplyDelete
  11. Does Silverlight 4 now supersede this article, since it now supports relative paths in the ServiceReferences.ClientConfig file?

    ReplyDelete
  12. Yes, relative paths are far less complex than trying to compute them on the fly!

    ReplyDelete
  13. Hi, should this work with SOAP web services? The Reference.cs shows that the Client implements System.ServiceModel.ClientBase however when I try to implement BaseService on my custom service I get an error:

    The type '...Client' must be convertible to 'System.ServiceModel.ClientBase<...Channel>' in order to use it as parameter 'TClient' in the generic class BaseService.

    ReplyDelete
  14. With direct SOAP, instead of using the channel factory, you'll create the client, call the Beginxxx and pass the callback as the state, then in the endxxx cast tha asyncstate to the callback and call it with the results.

    ReplyDelete
  15. As such?

    public void Authorise(string permission, string ssoToken, Action result)
    {
    var svc = new AuthorisationServiceSoapClient() as AuthorisationServiceSoap;
    var request = new AuthoriseRequest(new AuthoriseRequestBody(permission, ssoToken));

    svc.BeginAuthorise(
    request,
    iar =>
    {
    var xx = svc.EndAuthorise(iar);
    result(xx.Body.AuthoriseResult);
    },
    null);
    }


    I'm still unsure why I get the error above when trying to implement BaseService, as the Reference.cs shows that the Client implements the Channel.

    I know the constructor code will have to change to support SOAP, however the abstract signature looks like is should work.

    ReplyDelete