Using the Silverlight HttpClient in WCF, and still passing cookies.

In addition to using the browser’s own HTTP stack, Silverlight 3 introduces the new HttpClient. For me, it shines mostly because of these reasons:

  • It supports SOAP faults that are passed with an HTTP 500 status code, and does not pretend everything that was not utterly right was a 404. The only way to work around it was to let the server return a 200 (OK) HTTP status code, which is a hack, a lot of code, and wrong.
  • It executes requests in parallel. When using the browser HTTP stack, WCF sends requests sequentially on the same thread. This is more than a small nuisance when you’re building a composite application that requests resources in parallel because its different parts do not know of each other. With the ClientHttp stack, the requests are executed in parallel, just as you would expect them to.

However, there are two problems (that I know of). First of all, flaky SSL certificates cause problems. You might have a development environment that uses SSL on build servers and developer machines (good) but does not use generally trusted certificates, but some that you’ve just created for yourself (good enough). When you use Firefox to access your app, you just add a permanent security exception and off you go… not. Because since the ClientHttp stack does not use the browser’s SSL negotiation, but WinINET, the certificate is not accepted, and you won’t be able to connect. But once you’ve added the certificate to the machine’s certificate store, you’re fine (I don’t know how things are on the Mac, but I suspect something similar there – got to check tomorrow. I don’t have one at home).

The more severe limitation is that cookies are not automatically passed back to the server. You might well do without cookies – if you don’t need to pass them to the server in WCF calls, there’s no worries here. But in case you use Forms Authentication or have a load balancer that relies on cookies, you’re going to miss them.

Like things are with WCF, there is of course a way to get around that, and it’s totally illogical and sparsely documented. There is a special BindingElement System.ServiceModel.Channels.HttpCookieContainerBindingElement that can be added to a Binding in order to enable cookies on channels. A very interesting fact about that class is that it only exists on Silverlight, not in the normal .NET library.

Here’s a simple factory class that creates WCF service proxies that uses that code.

using System;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Windows;
using System.Collections.Generic;
using System.Windows.Browser;

namespace GreenIcicle.WcfHttpClientWithCookies
{
  /// <summary>
  /// Creates WCF service clients that use SOAP 1.2 via the ClientHttp stack, and passes cookies
  /// to the server.
  /// </summary>
  public static class ServiceClientFactory
  {
    /// <summary>
    /// Backing store for cookies.
    /// </summary>
    private static IDictionary<string, string> m_Cookies;

    /// <summary>
    /// Return the lazily initialized dictionary of cookies.
    /// </summary>
    public static IDictionary<string, string> Cookies
    {
      get
      {
        if( m_Cookies == null )
        {
          throw new InvalidOperationException( "The cookie jar has not been initialized: Call ServiceClientFactory.ReadCookies when the application starts" );
        }
        return m_Cookies;
      }
    }

    /// <summary>
    /// Creates a service proxy for a given service contract.
    /// </summary>
    public static T CreateServiceClient<T>( Uri serviceUrl )
    {
      // Determine whether to use transport layer security (HTTP over SSL).
      // Depending on whether HTTPS is used, create a different BindingElement for
      // the transport layer of the Binding.
      bool useTransportSecurity = serviceUrl.Scheme.Equals( "https",  StringComparison.InvariantCultureIgnoreCase );
      BindingElement transportBinding = useTransportSecurity ?
        new HttpsTransportBindingElement() :
        new HttpTransportBindingElement();

      // Since the ClientHttp stack is used, it is necessary to define cookies manually.
      // We read the cookies from the browser window and attach them to the WCF binding
      // via an additional BindingElement that lets us inject cookies.
      BindingElement cookieBinding = new HttpCookieContainerBindingElement();

      // Create a custom binding that uses the specified elements for transport and 
      // accepting cookies. The order of the bindings is relevant; the transport binding 
      // needs to come last it's the lowest in the stack.
      Binding binding = new CustomBinding( cookieBinding, transportBinding );

      // Set the address for the service endpoint.
      EndpointAddress address = new EndpointAddress( serviceUrl );

      // Let the ChannelFactory create the service client.
      ChannelFactory<T> factory = new ChannelFactory<T>( binding, address );
      T channel = factory.CreateChannel();

      // Because we've injected the HttpCookieContainerBindingElement, the channel supports cookies:
      // it exposes a CookieContainerManager. 
      IHttpCookieContainerManager cookieManager =
        ( (IChannel)channel ).GetProperty<IHttpCookieContainerManager>();

      // The CookieContainerManager can, but does not have to, provide a Cookie Container;
      // in the case it has not yet been created instantiate one now.
      if( cookieManager.CookieContainer == null )
      {
        cookieManager.CookieContainer = new CookieContainer();
      }

      // Walk through the cookies of the browser window the application lives in and 
      // add the cookies to the channel. The URL for the cookie is set to the application root.
      // We get to that URL by quering the application URL - that's the URL of the XAP - 
      // and go one level up.
      Uri applicationUri = new Uri( Application.Current.Host.Source, "../" );
      foreach( var cookieContent in Cookies )
      {
        Cookie cookie = new Cookie( cookieContent.Key, cookieContent.Value );
        cookieManager.CookieContainer.Add( applicationUri, cookie );
      }
      return channel;
    }

    /// <summary>
    /// Read cookies from the browser into a local store.
    /// </summary>
    public static void ReadCookies()
    {
      Deployment.Current.Dispatcher.BeginInvoke( () =>
      {
        // Initialize dictionary
        m_Cookies = new Dictionary<string, string>();

        // Get the cookies from the browser. It returns it in a single, semicolon-delimited string.
        string cookies = HtmlPage.Document.Cookies;
        string[] cookieSplit = cookies.Split( new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries );
        foreach( string cookie in cookieSplit )
        {
          string[] keyAndValue = cookie.Split( new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries );
          if( keyAndValue.Length == 2 )
          {
            string cookieKey = keyAndValue[ 0 ].Trim();
            string cookieValue = keyAndValue[ 1 ].Trim();

            // add cookie to dictionary
            m_Cookies.Add( cookieKey, cookieValue );
          }
        }
      } );
    }  
  }
}

In order to tell Silverlight to use the ClientHttp stack instead of the browser’s, it needs to be registered. This can best be done in the application startup event handler. In addition, the ServiceClientFactory needs to read the cookies from the browser. That gives us this code block in the App.xaml.cs file:

    private void Application_Startup( object sender, StartupEventArgs args )
    {
      this.RootVisual = new MainPage();

      // Register the client http stacks for all web requests
      WebRequest.RegisterPrefix( "https://", System.Net.Browser.WebRequestCreator.ClientHttp );
      WebRequest.RegisterPrefix( "http://", System.Net.Browser.WebRequestCreator.ClientHttp );

      // Read the cookies so they can be used for WCF requests
      ServiceClientFactory.ReadCookies();
    }
About

Christian is a software architect/developer. He lives in Germany, reads a lot, and likes cycling.

Tagged with: , ,
Posted in Coding
11 comments on “Using the Silverlight HttpClient in WCF, and still passing cookies.
  1. Panos says:

    I tried using this solution with Forms Authentication but it seems that HtmlPage.Document.Cookies does not contain my session cookie! I can see (in Firebug) that it gets trasmitted correctly but SL can’t see it! Any ideas?

    • Christian says:

      The cookies should be available via HtmlPage.Document.Cookies (which returns a string that you need to parse) – I’m not sure why this should not include the ASP.NET authentication cookie; in fact, the production version of the code example in the article does exactly that. The only difference is that in that project, the ASP.NET authentication mechanism is partly replaced and a custom cookie is used.
      I’ll try to repro this tomorrow (for today, I’d rather not look at a screen any more. Code reviews today).

    • Panos says:

      Currently I’m sending the value of ASP.NET_SessionId cookie (via Session.SessionID) to the SL app through the InitParams collection – then manually add this to m_Cookies. Is there a better way? Thanks a lot for your help!

    • john says:

      The reason why Silverlight (or JavaScript etc) won’t be able to see the .ASPXAUTH and ASP.NET_SessionId cookies in HtmlPage.Document.Cookies is because these cookies have the HttpOnly flag set. This mean that client side script can’t read (and for instance steal via XSS) the cookie.

      If you want to use Forms Authentication in Silverlight you might be interested in the AuthenticationService. see http://msdn.microsoft.com/en-us/library/system.web.applicationservices.authenticationservice.aspx
      We have successfully used it in combination with the HttpClient, CookieContainer and WCF services through http.

  2. rob263 says:

    Do you know if it’s possible to use HttpClient for a single WCF request (instead of setting it for all requests using RegisterPrefix)?

    • Christian says:

      I don’t think so – somewhere belieben all the layers of abstraction, WCF has got to know what to instatiate, but I don’t know of a hook to influence that. Although, with WCF you never quite know – this API must hold the world Reid for hard-to-find extensibility points.

  3. Bungle says:

    Sorry, I think I’m losing it. How do I use the channel object that the factory creates? can I replace the underlying channel of an existing ClientBase object, or do I need to use it directly?

    Thanks!

  4. Tim says:

    Can you show an example of when /how you call CreateServiceClient()?

  5. Very good information. Lucky me I came across your blog by accident (stumbleupon).
    I have saved it for later!

  6. paolo says:

    Excuse me can you please provide a demo for silverlight? I’m stucked at all…not cookie returned…Thanks

  7. advapi says:

    hello, can you please provide a working sample on SL? a downloadable one I mean…I’m stucked… I don’t understand how to set the cookie after the authentication has been done… Thanks

Leave a reply to john Cancel reply