Tuesday, May 05, 2009

Atom feeds with the Windsor WCF Facility

This is the fourth in a series of posts featuring the WCF Facility:

Windsor WCF Integration
WCF / Windsor Integration: Using the perWebRequest lifestyle
WCF / Windsor Integration: Adding Behaviours

Today I’m going to show how easy it is to leverage the new atom support in that was introduced with 3.5 sp1 to provide feeds supplied by Windsor components.

Download the Sample Solution here:
http://static.mikehadlow.com/Suteki.Blog.zip

Let’s jump right in to the code. I’m going to use a blog example because it’s obviously a natural fit for atom, but you can use it to publish anything you want. Let’s say we have a Post class:

   1: using System;
   2:  
   3: namespace Suteki.Blog.Model
   4: {
   5:     public class Post : ModelBase
   6:     {
   7:         public string Title { get; set; }
   8:         public string Text { get; set; }
   9:         public DateTime CreatedDate { get; set; }
  10:  
  11:         public override string ToString()
  12:         {
  13:             return string.Format("Post Id: {0} Title: '{1}'", Id, Title);
  14:         }
  15:     }
  16: }

That’s pretty much what you’d expect. Now say we have an IBlogService that allows us to add and retrieve Posts:

   1: using Suteki.Blog.Model;
   2:  
   3: namespace Suteki.Blog.Service
   4: {
   5:     public interface IBlogService
   6:     {
   7:         Post[] GetPosts();
   8:         Post GetPost(string id);
   9:         void AddPost(Post post);
  10:     }
  11: }

Now let’s create a service contract for our atom feed:

   1: using System.ServiceModel;
   2: using System.ServiceModel.Syndication;
   3: using System.ServiceModel.Web;
   4:  
   5: namespace Suteki.Blog.RestService.Atom
   6: {
   7:     [ServiceContract]
   8:     public interface IAtomFeed
   9:     {
  10:         [OperationContract]
  11:         [WebGet(UriTemplate = "/")]
  12:         Atom10FeedFormatter GetPosts();
  13:     }
  14: }

The WebGet attribute was introduced with WCF 3.5 to make it easy to create RESTful web services and uses a URI templating scheme to route URLs to service methods. Here we are routing the root URL to our GetPosts method. Note that the GetPosts method returns Atom10FeedFormatter. This class knows how to format your items as an atom feed.

So far this is all straight from WCF, we haven’t brought the WCF Facility to the party yet. To tell the WCF Facility to supply the service instance, we need to create a specific AtomFeed.svc file. My one looks like this

   1: <%@ ServiceHost Service="atomFeed" 
   2: Factory="Castle.Facilities.WcfIntegration.WindsorServiceHostFactory`1[[Castle.Facilities.WcfIntegration.Rest.RestServiceModel, Castle.Facilities.WcfIntegration]], Castle.Facilities.WcfIntegration" %>

We tell WCF to use the WCF Facility’s host factory with its RestServiceModel. The bizarre square bracket syntax is simply the CLR notation for generics.  The service is specified as ‘atomFeed’, this tells the WCF Facility to resolve the component named ‘atomFeed’ from the Windsor container.

Next we have the Windsor configuration:

   1: using System.ServiceModel.Description;
   2: using Castle.Facilities.WcfIntegration;
   3: using Castle.MicroKernel.Registration;
   4: using Castle.Windsor;
   5: using Suteki.Blog.RestService.Atom;
   6: using Suteki.Blog.Service;
   7:  
   8: namespace Suteki.Blog.RestService.IoC
   9: {
  10:     public class ContainerBuilder
  11:     {
  12:         public static IWindsorContainer Build()
  13:         {
  14:             var debug = new ServiceDebugBehavior
  15:             {
  16:                 IncludeExceptionDetailInFaults = true
  17:             };
  18:  
  19:             return new WindsorContainer()
  20:                 .AddFacility<WcfFacility>()
  21:                 .Register(
  22:                     Component.For<IServiceBehavior>().Instance(debug),
  23:                     Component.For<IPostAtomFeedMapper>().ImplementedBy<DefaultPostAtomFeedMapper>(),
  24:                     Component
  25:                         .For<IAtomFeed>()
  26:                         .ImplementedBy<DefaultAtomFeed>()
  27:                         .Named("atomFeed")
  28:                         .LifeStyle.Transient,
  29:                     Component
  30:                         .For<IBlogService>()
  31:                         .ImplementedBy<DefaultBlogService>()
  32:                         .Named("blogService")
  33:                         .LifeStyle.Transient,
  34:                     Component.For<ILogger>().ImplementedBy<DefaultLogger>().LifeStyle.Transient
  35:                 );
  36:         }
  37:     }
  38: }

Note that we have a component named ‘atomFeed’ that is implemented by DefaultAtomFeed. The container will create an instance of DefaultAtomFeed to service the request.

   1: using System.ServiceModel.Syndication;
   2: using Suteki.Blog.Service;
   3:  
   4: namespace Suteki.Blog.RestService.Atom
   5: {
   6:     public class DefaultAtomFeed : IAtomFeed
   7:     {
   8:         private readonly IBlogService blogService;
   9:         private readonly IPostAtomFeedMapper postAtomFeedMapper;
  10:  
  11:         public DefaultAtomFeed(IBlogService blogService, IPostAtomFeedMapper postAtomFeedMapper)
  12:         {
  13:             this.blogService = blogService;
  14:             this.postAtomFeedMapper = postAtomFeedMapper;
  15:         }
  16:  
  17:         public Atom10FeedFormatter GetPosts()
  18:         {
  19:             var posts = blogService.GetPosts();
  20:             return new Atom10FeedFormatter(postAtomFeedMapper.Map(posts));
  21:         }
  22:     }
  23: }

Note that it has dependencies on both the IBlogService, that we saw above, and IPostAtomFeedMapper. IPostAtomFeedMapper is responsible for mapping the array of Post instances returned from IBlogService to a SyndicationFeed instance. SyndicationFeed is another WCF type to support feeds and is consumed by the Atom10FeedFormatter. Here is the IPostAtomFeedMapper implementation:

   1: using System.Collections.Generic;
   2: using System.ServiceModel.Syndication;
   3: using Suteki.Blog.Model;
   4:  
   5: namespace Suteki.Blog.RestService.Atom
   6: {
   7:     public class DefaultPostAtomFeedMapper : IPostAtomFeedMapper
   8:     {
   9:         public SyndicationFeed Map(Post[] posts)
  10:         {
  11:             var items = new List<SyndicationItem>();
  12:  
  13:             foreach (var post in posts)
  14:             {
  15:                 var item = new SyndicationItem
  16:                 {
  17:                     Id = post.Id.ToString(),
  18:                     Title = new TextSyndicationContent(post.Title),
  19:                     Content = new TextSyndicationContent(post.Text),
  20:                     PublishDate = post.CreatedDate
  21:                 };
  22:                 items.Add(item);
  23:             }
  24:  
  25:             return new SyndicationFeed(items);
  26:         }
  27:     }
  28: }

And it’s as simple as that. No need for any Web.config configuration. When I run the service and browse to its URL with Firefox I see the familiar atom feed page:

 atom_feed

No comments: