Tuesday, May 12, 2009

Services as Classes and WCF Services

Taking a break from my "results oriented theory"...

I like services, I like the ability to call the same API’s remotely as I would call inside a website that I develop. The issue I have with this is overhead. And not just service call overhead, there is configuration overhead, debugging overhead and a slew of little things that all have to be aligned in order for things to work right.

Add on top of that I end up doing double duty. I hit the database to load up a context with items I expect to work with, then pass those items to a service which has to requery the same objects back out to do their work. Considering my web server is only 1 hop away from my database this seems pointless.

So what I want, is a way to construct a class that can be used like a the service would when it is constructed as a local heap object, and also use that same class to run as a service. Depending on how the class is hosted it changes some of its functionality. Take this simple service interface.

   1: [ServiceContract]
   2: public interface ISampleService
   3: {    
   4:     [OperationContract]    
   5:     User FindUserByUserId(int userId);
   6: }
   7:  

And the implementation

   1: public partial class SampleService
   2:     : ServiceBase
   3: {
   4:     protected SampleModelContainer Context { get; private set; }
   5:  
   6:     protected SampleService()
   7:     {
   8:     }
   9:  
  10:     public SampleService(SampleModelContainer context)
  11:     {
  12:         if (context == null)
  13:             throw new ArgumentNullException("context");
  14:          Context = context;
  15:     }
  16:  
  17:     public User FindUserByUserId(int userId)
  18:     {
  19:         return
  20:             (from u in Context.Users
  21:              where u.UserId == userId
  22:              select u).FirstOrDefault();
  23:     }
  24:  
  25:     protected override IDisposable CreateServiceContext()
  26:     {
  27:         Context = new SampleModelContainer();
  28:  
  29:         return new DisposableContainer(
  30:             Context);
  31:     }
  32: }

Notice the empty constructor is “protected”, WCF can use the protected constructor when instantiating this as a service, however our web code will use the constructor with the “context” parameter passing in our database context.

And now we see the problem. The service based instance won’t have a database context to run against. FindUserByUserId will fail with a NullReferenceException.

We could just construct the Context in the constructor, but what fun is that. So what we need is a way to wrap our service calls. For starters I have the CreateServiceContext() call, this is where you put your creation logic for a service call. Its wrapped in an IDisposable so all your cleanup will occur at the end of the method call. So for my service calls I implement an Invoke on the base using a delegate to call the original method on the class.

   1: public abstract class ServiceBase
   2: {
   3:     protected virtual IDisposable CreateServiceContext()
   4:     {
   5:         return null;
   6:     }
   7:  
   8:     protected virtual void HandleServiceException(Exception ex)
   9:     {
  10:         throw ex;
  11:     }
  12:  
  13:     protected TResult Invoke<TResult>(Func<TResult> func)
  14:     {
  15:         try
  16:         {
  17:             using (CreateServiceContext())
  18:             {
  19:                 return func();
  20:             }
  21:         }
  22:         catch (Exception ex)
  23:         {
  24:             HandleServiceException(ex);
  25:         }
  26:  
  27:         return default(TResult);
  28:     }
  29: }

I have a HandleServiceException virtual method in case I want to custom wrap exceptions. Lets say I want to throw an ArgumentException when the method is invoked in web code, but I want that wrapped into an FaultException<ArgumentFault> in the service. I would put that code in the HandleServiceException.

Now, to implement the service interface explicitly and forward the calls through the invoke.

   1: partial class SampleService
   2:     : ISampleService
   3: {
   4:     #region ISampleService Members
   5:  
   6:     User ISampleService.FindUseryByUserId(int userId)
   7:     {
   8:         return Invoke<int, User>(FindUserByUserId, userId);
   9:     }
  10:  
  11:     #endregion
  12: }

And there we have it, a service class that is context bound when running from the heap, yet single-call when running in a service host.

Now to move a step beyond, we may need to change the way the service works based on parameters. Inside the service hosted environment we will expect the user to provide scope parameters. While being called as a local heap object we can provide those parameters as part of the construction of the object.

The class implementation

   1: public partial class SampleService2
   2:     : ServiceBase
   3: {
   4:     protected SampleModelContainer Context { get; private set; }
   5:     protected int PlatformId { get; private set; }
   6:     protected Platform Platform { get; private set; }
   7:  
   8:     protected SampleService2()
   9:     {
  10:  
  11:     }
  12:  
  13:     public SampleService2(SampleModelContainer context, int platformId)
  14:     {
  15:         if (context == null)
  16:             throw new ArgumentNullException("context");
  17:  
  18:         Context = context;
  19:         PlatformId = platformId;
  20:     }
  21:  
  22:     public SampleService2(SampleModelContainer context, Platform platform)
  23:     {
  24:         if (context == null)
  25:             throw new ArgumentNullException("context");
  26:  
  27:         if (platform == null)
  28:             throw new ArgumentNullException("platform");
  29:  
  30:         Context = context;
  31:         Platform = platform;
  32:         PlatformId = platform.PlatformId;
  33:     }
  34:  
  35:     protected override IDisposable CreateServiceContext()
  36:     {
  37:         Context = new SampleModelContainer();
  38:  
  39:         return new DisposableContainer(
  40:             Context);
  41:     }
  42:  
  43:     public IEnumerable<User> FindUsers()
  44:     {
  45:         ValidatePlatform();
  46:  
  47:         return
  48:             (from u in Context.Users
  49:              where u.Platforms.Contains(Platform)
  50:              select u);
  51:     }
  52:  
  53:     private void ValidatePlatform()
  54:     {
  55:         if (Platform != null)
  56:         {
  57:             Platform =
  58:                 (from p in Context.Platforms
  59:                  where p.PlatformId == PlatformId
  60:                  select p).FirstOrDefault();
  61:         }
  62:  
  63:         if (Platform == null)
  64:             throw new Exception("Invalid Platform");
  65:     }
  66: }

In this service we expect to only operate on a single platform level. From a service call we must determine that context from the method call, from the web code we know that earlier and sending it in via the constructor is more prudent.

The ValidatePlatform() is invoked in methods that we want to ensure we have a platform bound before we begin our work.

Now for the service interface

   1: [ServiceContract]
   2: public interface ISampleService2
   3: {
   4:     [OperationContract]
   5:     IEnumerable<User> FindUsers(int platformId);
   6: }

And the service interface implementation

   1: partial class SampleService2
   2:     : ISampleService2
   3: {
   4:     #region ISampleService2 Members
   5:  
   6:     IEnumerable<User> ISampleService2.FindUsers(int platformId)
   7:     {
   8:         PlatformId = platformId;
   9:  
  10:         return Invoke<IEnumerable<User>>(FindUsers);
  11:     }
  12:  
  13:     #endregion
  14: }

Notice the platformId is sent in via the method call. This scoping parameter gets converted to the service property in the explicit interface implementation.

I’ve been using this technique for a few months now on my services. And it has been pretty slick. The composition capabilities of my services is insane, but I’ll save that for a future article.

Another bonus of this technique is testing. Since the core of the service gets exercised both through the testing of our app via WCF and through direct invocation from the websites.

Check out my codeplex project coderjoe.codeplex.com to browse through the full solution.

Find it in Source Code –> Browse –> trunk/Samples/ServiceObjects

No comments: