Compartilhar via


Adding vcard support and bookmarked URIs for specific representations with WCF Web Apis

REST is primarily about 2 things, Resources and Representations. If you’ve seen any of my recent talks you’ve probably seen me open Fiddler and how our ContactManager sample supports multiple representations including Form Url Encoded, Xml, Json and yes an Image. I use the image example continually not necessarily because it is exactly what you would do in the real world, but instead to drive home the point that a representation can be anything. Http Resources / services can return anything and are not limited to xml, json or even html though those are the most common media type formats many people are used to.

One advantage of this is your applications can return domain specific representations that domain specific clients (not browsers) can understand. What do I mean? Let’s take the ContactManager as an example. The domain here is contact management. Now there are applications like Outlook, ACT, etc that actually manage contacts. Wouldn’t it be nice if I could point my desktop contact manager application at my contact resources and actually integrate the two allowing me to import in contacts? Turns out this is where media types come to the rescue. There actually is a format called vcard that encapsulates the semantics for specifying an electronic business card which includes contact information. rfc2425 then defines a “text/directory” media type which clients can use to transfer vcards over HTTP.

A sample of how a vcard looks is below (taken form the rfc)

begin:VCARD
source:ldap://cn=bjorn%20Jensen, o=university%20of%20Michigan, c=US
name:Bjorn Jensen
fn:Bj=F8rn Jensen
n:Jensen;Bj=F8rn
email;type=internet:bjorn@umich.edu
tel;type=work,voice,msg:+1 313 747-4454
key;type=x509;encoding=B:dGhpcyBjb3VsZCBiZSAKbXkgY2VydGlmaWNhdGUK
end:VCARD

Notice, it is not xml, not json and not an image :-) It is an arbitrary format thus driving the point I was making about the flexibility of HTTP.

Creating a vcard processor

So putting two and two together that means if we create a vcard processor for our ContactManager that supports “text/directory” then Outlook can import contacts from the ContactManager right?

OK, here is the processor for VCARD.

 public class VCardProcessor : MediaTypeProcessor
{
    public VCardProcessor(HttpOperationDescription operation)
        :base(operation, MediaTypeProcessorMode.Response)
    {
    }
    public override IEnumerable<string> SupportedMediaTypes
    {
        get
        {
            yield return "text/directory";
        }
    }
    public override void WriteToStream(object instance, 
      Strea stream, HttpRequestMessage request)
    {
        var contact = instance as Contact;
        if (contact != null)
        {
            var writer = new StreamWriter(stream);
            writer.WriteLine("BEGIN:VCARD");
            writer.WriteLine(string.Format(
                "FN:{0}", contact.Name));
            writer.WriteLine(string.Format(
                 "ADR;TYPE=HOME;{0};{1};{2}", 
                 contact.Address, contact.City, 
                 contact.Zip));
            writer.WriteLine(string.Format(
                 "EMAIL;TYPE=PREF,INTERNET:{0}", 
                 contact.Email));
            writer.WriteLine("END:VCARD");
            writer.Flush();
        }
    }
    public override object ReadFromStream(Stream stream, 
        HttpRequestMessage request)
    {
        throw new NotImplementedException();
    }
}

The processor above does not supporting posting vcard, but it actually could.

Right. However there is one caveat, Outlook won’t send Accept headers Winking smile, all it has a “File-Open” dialog.  There is hope though. It turns out that the dialog supports uris, thus as long as I can give it uri which is a bookmark to a vcard representation we’re golden.

This is where in the past it gets a bit hairy with WCF. Today to do this means I need to ensure that my UriTemplate has a variable i.e. {id} is fine, but then I have to parse that ID to pull out the extension. It’s ugly code point blank. Jon Galloway expressed his distaste for this approach (which I suggested as a shortcut) in his post here (see the section Un-bonus: anticlimactic filename extension filtering epilogue).

In that post, I showed parsing the ID inline. See the ugly code in bold?

 [WebGet(UriTemplate = "{id}")] 
public Contact Get(string id, HttpResponseMessage response) 
{ 
    int contactID = !id.Contains(".")   
         ? int.Parse(id, CultureInfo.InvariantCulture)   
         : int.Parse(id.Substring(0, id.IndexOf(".")),  
        CultureInfo.InvariantCulture);  
  
    var contact = this.repository.Get(contactID); 
    if (contact == null) 
    { 
        response.StatusCode = HttpStatusCode.NotFound; 
        response.Content = HttpContent.Create("Contact not found"); 
    } 
    return contact; 
}

Actually that only solves part of the problem as I still need the Accept header to contain the media type or our content negotiation will never invoke the VCardProcessor!

Another processor to the rescue

Processors are one of the swiss army knives in our Web Api. We can use processors to do practically whatever we want to an HTTP request or response before it hits our operation. That means we can create a processor that automatically rips the extension out of the uri so that the operation doesn’t have to handle it as in above, and we can make it automatically set the accept header based on mapping the extension to the appropriate accept header.

And here’s how it is done, enter UriExtensionProcessor.

 public class UriExtensionProcessor : 
   Processor<HttpRequestMessage, Uri>
{
    private IEnumerable<Tuple<string, string>> extensionMappings;
    public UriExtensionProcessor(
      IEnumerable<Tuple<string, string>> extensionMappings)
    {
        this.extensionMappings = extensionMappings;
        this.OutArguments[0].Name = HttpPipelineFormatter.ArgumentUri;
    }
    public override ProcessorResult<Uri> OnExecute(
      HttpRequestMessage httpRequestMessage)
    {
        var requestUri = httpRequestMessage.RequestUri.OriginalString;
        var extensionPosition = requestUri.IndexOf(".");
        if (extensionPosition > -1)
        {
            var extension = requestUri.Substring(extensionPosition + 1);
            var query = httpRequestMessage.RequestUri.Query;
            requestUri = string.Format("{0}?{1}", 
                requestUri.Substring(0, extensionPosition), query);;
            var mediaType = extensionMappings.Single(
                map => extension.StartsWith(map.Item1)).Item2;
            var uri = new Uri(requestUri);
            httpRequestMessage.Headers.Accept.Clear();
            httpRequestMessage.Headers.Accept.Add(
                new MediaTypeWithQualityHeaderValue(mediaType));
            var result = new ProcessorResult<Uri>();
            result.Output = uri;
            return result;
        }
        return new ProcessorResult<Uri>();
    }
}

Here is how it works (note how this is done will change in future bits but the concept/appoach will be the same).

  • First UrlExtensionProcessor takes a collection of Tuples with the first value being the extension and the second being the media type.
  • The output argument is set to the key “Uri”. This is because in the current bits the UriTemplateProcessor grabs the Uri to parse it. This processor will replace it.
  • In OnExecute the first thing we do is look to see if the uri contains a “.”. Note: This is a simplistic implementation which assumes the first “.” is the one that refers to the extension. A more robust implementation would look after the last uri segment at the first dot. I am lazy, sue me.
  • Next strip the extension and create a new uri. Notice the query string is getting tacked back on.
  • Then do a match against the mappings passed in to see if there is an extension match.
  • If there is a match, set the accept header to use the associated media type for the extension.
  • Return the new uri.

With our new processors in place, we can now register them in the ContactManagerConfiguration class first for the request.

 public void RegisterRequestProcessorsForOperation(
   HttpOperationDescription operation, IList<Processor> processors,
   MediaTypeProcessorMode mode)
{
    var map = new List<Tuple<string, string>>();
    map.Add(new Tuple<string, string>("vcf", "text/directory"));
    processors.Insert(0, new UriExtensionProcessor(map));
}

Notice above that I am inserting the UriExtensionProcessor first. This is to ensure that the parsing happens BEFORE the UriTemplatePrcoessor executes.

And then we can register the new VCardProcessor for the response.

 public void RegisterResponseProcessorsForOperation(
   HttpOperationDescription operation, 
   IList<Processor> processors, 
   MediaTypeProcessorMode mode)
{
    processors.Add(new PngProcessor(operation, mode));
    processors.Add(new VCardProcessor(operation)); 
}

Moment of truth – testing Outlook

Now with everything in place we “should” be able to import a contact into Outlook. First we have Outlook before the contact has been imported. I’ll use Jeff Handley as the guinea pig. Notice below when I search through my contacts he is NOT there.

 

image

Now after launching the ContactManager, I will go to the File->Open->Import dialog, and choose to import .vcf.

image

image

Click ok, and then refresh the search. Here is what we get.

image

What we’ve learned

  • Applying a RESTful style allows us evolve our application to support a new vcard representation.
  • Using representations allows us to integrate with a richer set of clients such as Outlook and ACT.
  • WCF Web APIs allows us to add support for new representations without modifying the resource handler code (ContactResource)
  • We can use processors for a host of HTTP related concerns including mapping uri extensions as bookmarks to variant representations.
  • WCF Web APIs is pretty cool Smile

I will post the code. For now you can copy paste the code and follow my direction using the ContactManager. It will work Smile

What’s next.

In the next post I will show you how to use processors to do a redirect or set the content-location header.