次の方法で共有


WCF Extensibility – IInstanceProvider

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

Moving down to the list of the extensibility points, the IInstanceProvider is actually quite useful when you have a service class which needs some initialization data. In most of the WCF service examples I’ve seen in the web, they’re simple classes, almost self-contained, which didn’t have any external dependencies. The default instancing behavior for WCF (simply call the parameter-less constructor for the service class) works perfectly in those cases. However, there are quite a few scenarios where this is not the case – the service depends on some database, the service class needs to listen to some external events, or we want to initialize the service class with a different parameter depending on the situation where it’s being used (or even on the incoming message which is being sent to it) – and the instance provider interface is the hook that the WCF extensibility provides to allow us total control over the instantiation of the service class. Another scenario where this may be useful is that if the service class only has a default constructor, but it does some heavy initialization, so you may want to create a pool of service instances – the instance provider can help in this case as well.

Public implementations in WCF

None. As with most runtime extensibility points, there are no public implementations in WCF. There are many internal implementations, such as the default System.ServiceModel.Dispatcher.InstanceProvider, which uses the default constructor for the service type to create the instances.

Interface declaration

  1. public interface IInstanceProvider
  2. {
  3.     object GetInstance(InstanceContext instanceContext);
  4.     object GetInstance(InstanceContext instanceContext, Message message);
  5.     void ReleaseInstance(InstanceContext instanceContext, object instance);
  6. }

The IInstanceProvider has two overloads of the GetInstance method used to create a new instance of the service class. The first one receives only an InstanceContext object, while the second one also receives an instance of the Message object which represents the request which is going to be sent to the service. In practice, the first one is seldom invoked – I could find in Reflector some code paths which called that one, but I couldn’t actually make a service in which that method was invoked after trying for about 30 minutes. It’s possible that it is invoked by some obscure code path, so if you’re implementing a custom one, and on the GetInstance(InstanceContext) you simply delegate the call to the other overload passing a null message, it’s nice to have the code consider that the message may in fact be null.

The need for a new instance of the service class depends on the instancing mode of the service. For services with the PerCall mode, a new service instance (with a call to GetInstance) will be created for every new request coming from the client; for PerSession services, whenever a request which creates a session new session arrives, a new service instance is created, but subsequent requests for the same session will continue using the same service instance (if the binding does not support sessions, then a PerSession instance context mode becomes PerCall). Services with InstanceContextMode.Single actually do not use the instance provider – the service instances must be passed to the service host constructor, or the service class needs to have a parameter-less constructor which will be used to create the singleton instance.

When the WCF runtime is done with the service object, it will call ReleaseInstance on the provider, which can be useful if the service class has some dependencies which it needs to dispose of, or in a pooling scenario, where the code can then return that service instance to the pool of available instances.

How to add instances providers

Instance providers only apply at the server side, and they’re bound to the endpoint dispatcher, and available at the DispatchRuntime object. They’re typically defined as either service or endpoint behaviors, as in the example below.

  1. public class InstanceProviderBehaviorAttribute : Attribute, IServiceBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  4.     {
  5.         foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
  6.         {
  7.             foreach (EndpointDispatcher ed in cd.Endpoints)
  8.             {
  9.                 if (!ed.IsSystemEndpoint)
  10.                 {
  11.                     ed.DispatchRuntime.InstanceProvider = new ServiceInstanceProvider();
  12.                 }
  13.             }
  14.         }
  15.     }
  16. }

 

Real world scenario: testing WCF services with external dependencies

Among the usage examples for instance providers, one which is quite interesting is to be able to test WCF service classes which have dependencies to external components, such as databases. Outside of the WCF world, the way this is usually done is by using some sort of IoC / factory framework which manages the dependency based on some creation rules. That dependency is often represented as an interface which is passed to the service class constructor, so that it can be mocked with a test implementation during test time. In the example below I’ll implement a very simple factory method for such dependency, so that the service can be tested both inside the WCF pipeline and outside of it.

And before starting with the code, the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). Also, it does take many shortcuts for simplicity sake, such as using a file-based “database”, a very simple IoC container (don’t use it in production), it doesn’t add a lot of error checking, and it definitely does not have even a minimum number of tests to make us claim that the service is “tested”.

The service is a simple pricing service. You add items to a “shopping cart”, and at the end you can “checkout”, where you get the price (and in this simplified example, the cart is emptied). Here are the data contracts used in this service:

  1. [DataContract]
  2. public class OrderItem
  3. {
  4.     [DataMember]
  5.     public int ItemId { get; set; }
  6.     [DataMember]
  7.     public string Name { get; set; }
  8.     [DataMember]
  9.     public double Amount;
  10. }
  11.  
  12. [DataContract]
  13. public class Product
  14. {
  15.     [DataMember]
  16.     public int Id { get; set; }
  17.     [DataMember]
  18.     public string Name { get; set; }
  19.     [DataMember]
  20.     public string Unit { get; set; }
  21.     [DataMember]
  22.     public double UnitPrice { get; set; }
  23. }

The service interface will use a session to store the items in the cart, so the service contract will require a binding which supports such sessions.

  1. [ServiceContract(SessionMode = SessionMode.Required)]
  2. public interface IPricingService
  3. {
  4.     [OperationContract(IsTerminating = true)]
  5.     double PriceOrder();
  6.     [OperationContract(IsInitiating = true)]
  7.     void AddToCart(OrderItem item);
  8. }

The product repository interface I’ll use in this example is a very simple one, with a single operation which returns product instance via its ID.

  1. public interface IProductRepository
  2. {
  3.     Product GetProduct(int productId);
  4. }

Now the service class. As I mentioned before, it has a dependency on a product repository interface, which needs to be passed at construction time.  The service is decorated with the [InstanceProviderBehavior] interface, which is the service behavior which we’ll use to define the instance provider for this service. Since we’re using a session, we can store the cart as an instance variable for the service class.

  1. [ServiceBehavior(
  2.     InstanceContextMode = InstanceContextMode.PerSession,
  3.     IncludeExceptionDetailInFaults = true)]
  4. [InstanceProviderBehavior]
  5. public class PricingService : IPricingService
  6. {
  7.     List<OrderItem> cart = new List<OrderItem>();
  8.     IProductRepository productRepository;
  9.  
  10.     public PricingService(IProductRepository productRepository)
  11.     {
  12.         this.productRepository = productRepository;
  13.     }
  14.  
  15.     public void AddToCart(OrderItem item)
  16.     {
  17.         this.cart.Add(item);
  18.     }
  19.  
  20.     public double PriceOrder()
  21.     {
  22.         double result = 0;
  23.         foreach (var item in this.cart)
  24.         {
  25.             Product product = this.productRepository.GetProduct(item.ItemId);
  26.             result += product.UnitPrice * item.Amount;
  27.         }
  28.  
  29.         this.cart.Clear();
  30.         return result;
  31.     }
  32. }

With this definition we actually can start writing some unit tests for the pricing service class, completely outside of WCF:

  1. [TestMethod]
  2. public void EmptyCart()
  3. {
  4.     PricingService service = new PricingService(new MockProductRepository(x => new Product()));
  5.     Assert.AreEqual(0, service.PriceOrder());
  6. }
  7.  
  8. [TestMethod]
  9. public void OneItem()
  10. {
  11.     double price = AnyInstance.AnyDouble;
  12.     PricingService service = new PricingService(new MockProductRepository(x => new Product { Id = x, UnitPrice = price }));
  13.     service.AddToCart(new OrderItem { Amount = 1, ItemId = 1 });
  14.     Assert.AreEqual(price, service.PriceOrder());
  15. }

Back to the service code. Since the service does not have a parameter-less constructor, setting the instance provider is required – otherwise there would be a validation error when opening the service. The provider is implemented using a product repository factory (a very, very simple IoC container).

  1. class ServiceInstanceProvider : IInstanceProvider
  2. {
  3.     public object GetInstance(InstanceContext instanceContext, Message message)
  4.     {
  5.         IProductRepository repository = ProductRepositoryFactory.Instance.CreateProductRepository();
  6.         return new PricingService(repository);
  7.     }
  8.  
  9.     public object GetInstance(InstanceContext instanceContext)
  10.     {
  11.         return this.GetInstance(instanceContext, null);
  12.     }
  13.  
  14.     public void ReleaseInstance(InstanceContext instanceContext, object instance)
  15.     {
  16.     }
  17. }

The provider is plugged in to the WCF pipeline by using the InstanceProviderBehaviorAttribute class, first shown in the service definition

  1. public class InstanceProviderBehaviorAttribute : Attribute, IServiceBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  4.     {
  5.         foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
  6.         {
  7.             foreach (EndpointDispatcher ed in cd.Endpoints)
  8.             {
  9.                 if (!ed.IsSystemEndpoint)
  10.                 {
  11.                     ed.DispatchRuntime.InstanceProvider = new ServiceInstanceProvider();
  12.                 }
  13.             }
  14.         }
  15.     }
  16. }

And with this behavior, we can start testing the service inside the WCF stack (I wouldn’t dare to call it unit test, WCF is well-known for being unit-test-unfriendly).

  1. [TestClass]
  2. public class PricingServiceInWCFTest
  3. {
  4.     static readonly string ServiceBaseAddress = "https://" + Environment.MachineName + ":8000/Service";
  5.     static ServiceHost host;
  6.     static Dictionary<int, Product> products;
  7.  
  8.     IPricingService proxy;
  9.  
  10.     [ClassInitialize]
  11.     public static void ClassInitialize(TestContext context)
  12.     {
  13.         products = new Dictionary<int, Product>();
  14.         products.Add(1, new Product { Id = 1, UnitPrice = AnyInstance.AnyDouble });
  15.         products.Add(2, new Product { Id = 2, UnitPrice = AnyInstance.AnyDouble + 1.23 });
  16.         ProductRepositoryFactory.Instance.SetProductRepository(
  17.             new MockProductRepository(x => products[x]));
  18.  
  19.         host = new ServiceHost(typeof(PricingService), new Uri(ServiceBaseAddress));
  20.         host.AddServiceEndpoint(typeof(IPricingService), new WSHttpBinding(), "");
  21.         host.Open();
  22.     }
  23.  
  24.     [ClassCleanup]
  25.     public static void ClassCleanup()
  26.     {
  27.         host.Close();
  28.     }
  29.  
  30.     [TestInitialize]
  31.     public void Initialize()
  32.     {
  33.         ChannelFactory<IPricingService> factory = new ChannelFactory<IPricingService>(new WSHttpBinding(), new EndpointAddress(ServiceBaseAddress));
  34.         this.proxy = factory.CreateChannel();
  35.         ((IClientChannel)proxy).Closed += delegate { factory.Close(); };
  36.     }
  37.  
  38.     [TestCleanup]
  39.     public void Cleanup()
  40.     {
  41.         ((IClientChannel)this.proxy).Close();
  42.     }
  43.  
  44.     [TestMethod]
  45.     public void EmptyCart()
  46.     {
  47.         Assert.AreEqual(0, proxy.PriceOrder());
  48.     }
  49.  
  50.     [TestMethod]
  51.     public void OneItem()
  52.     {
  53.         double price = products[1].UnitPrice;
  54.         proxy.AddToCart(new OrderItem { Amount = 1, ItemId = 1 });
  55.         Assert.AreEqual(price, proxy.PriceOrder());
  56.     }
  57.  
  58.     [TestMethod]
  59.     public void MultipleItems()
  60.     {
  61.         double price1 = products[1].UnitPrice;
  62.         double price2 = products[2].UnitPrice;
  63.         int amount1 = AnyInstance.AnyInt;
  64.         int amount2 = AnyInstance.AnyInt + 3;
  65.         proxy.AddToCart(new OrderItem { ItemId = 1, Amount = amount1 });
  66.         proxy.AddToCart(new OrderItem { ItemId = 2, Amount = amount2 });
  67.         double expectedPrice = price1 * amount1 + price2 * amount2;
  68.         Assert.AreEqual(expectedPrice, proxy.PriceOrder());
  69.     }
  70. }

And when we’re happy with the tests, we can finally hook it up with the “real” product repository. For this example I’m using a file-based “database”, but in a more realistic scenario we’d retrieve the products from the inventory database.

  1. static void Main(string[] args)
  2. {
  3.     // Populate the "database"
  4.     string fileName = "Products.json";
  5.     Product[] allProducts = new Product[]
  6.     {
  7.         new Product { Id = 1, Name = "Bread", Unit = "un", UnitPrice = 0.34 },
  8.         new Product { Id = 2, Name = "Milk", Unit = "gal", UnitPrice = 2.99 },
  9.         new Product { Id = 3, Name = "Eggs", Unit = "doz", UnitPrice = 2.25 },
  10.         new Product { Id = 4, Name = "Butter", Unit = "lb", UnitPrice = 3.99 },
  11.         new Product { Id = 5, Name = "Flour", Unit = "lb", UnitPrice = 1.25 },
  12.         new Product { Id = 6, Name = "Sugar", Unit = "lb", UnitPrice = 3.22 },
  13.         new Product { Id = 7, Name = "Vanilla", Unit = "oz", UnitPrice = 5.49 },
  14.     };
  15.     using (FileStream fs = File.Create(fileName))
  16.     {
  17.         new DataContractJsonSerializer(typeof(Product[])).WriteObject(fs, allProducts);
  18.     }
  19.  
  20.     ProductRepositoryFactory.Instance.SetProductRepository(new FileBasedProductRepository(fileName));
  21.  
  22.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  23.     ServiceHost host = new ServiceHost(typeof(PricingService), new Uri(baseAddress));
  24.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IPricingService), new WSHttpBinding(), "");
  25.     host.Open();
  26.     Console.WriteLine("Host opened");
  27.  
  28.     ChannelFactory<IPricingService> factory = new ChannelFactory<IPricingService>(new WSHttpBinding(), new EndpointAddress(baseAddress));
  29.     IPricingService proxy = factory.CreateChannel();
  30.     proxy.AddToCart(new OrderItem { ItemId = 1, Name = "2 breads", Amount = 2 });
  31.     proxy.AddToCart(new OrderItem { ItemId = 2, Name = "1 galon of milk", Amount = 1 });
  32.     proxy.AddToCart(new OrderItem { ItemId = 3, Name = "1 dozen eggs", Amount = 1 });
  33.     proxy.AddToCart(new OrderItem { ItemId = 4, Name = "2 lbs. butter", Amount = 2 });
  34.     proxy.AddToCart(new OrderItem { ItemId = 5, Name = "1.2 lbs. flour", Amount = 1.2 });
  35.     Console.WriteLine(proxy.PriceOrder());
  36.  
  37.     Console.WriteLine("Press ENTER to close");
  38.     Console.ReadLine();
  39.     host.Close();
  40. }

And that’s it. WCF has a (well-deserved, unfortunately) reputation for not being very unit-testable, but there are some improvements we can make, and the instance provider is one way of improving the testability of WCF services.

Coming up

I’ll stop following the order listed in the table of contents, and start jumping around to cover the extensibility points which are used more often. Next: error handlers.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    September 19, 2011
    Thank you very much, this is what I need!
  • Anonymous
    April 17, 2012
    This really looks like what I need, but I'm not using VS2010. Is there someplace I can get an example of this using VS2008?  
  • Anonymous
    April 23, 2012
    Hi souldeep, the solution is built on 2010, but the code also works fine in 2008 as well. You can create a new project (console application), add references to System.ServiceModel (and System.Runtime.Serialization), then add the files from the sample.
  • Anonymous
    November 13, 2012
    Very good, but that is only simple way to call parametrized  wcf services constructor class?Please, no dependence inject.Thanks!
  • Anonymous
    July 26, 2013
    Where do you define ProductRepositoryFactory? I'm getting error saying definition does not exist.
  • Anonymous
    July 26, 2013
    In the code (from the MSDN Samples), you can find it at code.msdn.microsoft.com/.../sourcecode
  • Anonymous
    July 27, 2013
    Thank You!