Dependency Injection for Web Service

Standard

Dependency Injection allows developers to cleanly inject a portion of concrete code implementation into the bigger scope of the system base on certain logical condition. Dependency Injection has been a preferred way of implementing code. In fact, ASP.NET 5 (Visual Studio 2015) supports Dependency Injection as 1st class citizen. Dependency Injection removes the need of having tons of if-else statement in code implementation and keep the codes clean. Some developers implement Dependency Injection even there isn’t such need at the moment as a standard practice because they might be needed in the future. Personally I’d prefer not to implement Dependency Injection unless there is a “good reason” for implementing it.

One of the “good reasons” is while designing a web service. Web service often serve multiple clients having various needs base on certain logical condition. It is web service responsibility to handle the various logic implementation. For example a web service is serving clients from various countries on the same endpoint might need to execute different codes to produce localized result depending on who is triggering the end point.

Consider this simple scenario:

We need to design a WCF web service that serve multiple countries on an eCommerce platform. There is an endpoint to accept a Product Id and the endpoint will return the formatted local price. The local price calculation depend on various factors such as tax, shipping, marketing promotion, and other business consideration to lower or higher the price for each country through custom discount mechanism.

requirement

Here is a good scenario to create a WCF web service that implements Dependency Injection to separate different calculation formula for respective country.

In the sample code, the Dependency Injection library that we are using is WCF (C#.NET) with Autofac.

Create a BaseService class. In BaseService class, we define a static container to register and store a list of logic. In this example, IProduct is an interface that get registered with different logic classes (UsProduct, UkProduct, MyProduct) depending on the logical condition.

We create a BaseService class for this purpose so that all the services in WCF could inherit BaseService class and access to BuildContainer method, which will be common among all services. In the following example, all public API method would be required to build the container to initialize the logic classes.

Base service class

using Autofac;
using Ecommerce.Logic;

namespace Ecommerce.Api
{
  public abstract class BaseService
  {
    public IProduct ProductLogic { get; set; }

    private static IContainer Container { get; set; }

    public void BuildContainer(string country)
    {
      var builder = new ContainerBuilder();

      switch (country)
      {
        case "Us":
          builder.RegisterType<UsProduct>().As<IProduct>();
          break;

        case "Uk":
          builder.RegisterType<UkProduct>().As<IProduct>();
          break;

        case "My":
          builder.RegisterType<MyProduct>().As<IProduct>();
          break;
      }
      
      Container = builder.Build();
      var scope = Container.BeginLifetimeScope();

      ProductLogic = scope.Resolve<IProduct>();
    }
  }
}

Catalog service class

  1. Create a new Catalog service in WCF.
  2. Add a method GetProductPrice. It has UriTemplate as following which means Country and ProductId are parameters to be constructed in the Url endpoint.
    "{Country}/Product/{ProductId}/Price"
  3. Inherits BaseService. Implements IProduct.
  4. BuildContainer is a method defined in parent class.
using System.ServiceModel.Web;

namespace Ecommerce.Api
{
   public class Catalog : BaseService, ICatalog
   {
      [WebInvoke(Method = "GET",
          ResponseFormat = WebMessageFormat.Json,
          BodyStyle = WebMessageBodyStyle.Bare,
          UriTemplate = "{Country}/Product/{ProductId}/Price",
          RequestFormat = WebMessageFormat.Json)]
      public string GetProductPrice(string country, string productId)
      {
         BuildContainer(country);
         string price = ProductLogic.CalculateLocalPrice(productId);
         return price;
      }
   }
}

Define the respective logic class for each country…

United State product logic class

UsProduct class is also the parent class for rest of the country class. For example, the method GetProductBasePriceById is applicable for all countries implementation, hence this method stays at the parent class (UsProduct) so that it could be accessed by the child classes (UkProduct & MyProduct).

using System;

namespace Ecommerce.Logic
{
  public class UsProduct : IProduct
  {
    private const string CurrencyPrefix = "$ ";

    public string CalculateLocalPrice(string productId)
    {
      decimal basePrice = GetProductBasePriceById(productId);
      decimal priceAfterTax = CalculateTax(basePrice);
      decimal shippingCost = CalculateShipping(basePrice);
      decimal priceWithShipping = priceAfterTax + shippingCost;
      decimal roundedPrice = Math.Round(priceWithShipping, 2);

      string formattedLocalPrice = string.Format("{0}{1}", CurrencyPrefix, roundedPrice);
      return formattedLocalPrice;
    }

    protected decimal GetProductBasePriceById(string productId)
    {
      return 200.00m;
    }

    protected decimal CalculateShipping(decimal basePrice)
    {
      return basePrice * 0.1m;
    }

    private decimal CalculateTax(decimal basePrice)
    {
      return basePrice * GetUserTaxRateByState();
    }

    private decimal GetUserTaxRateByState()
    {
      return 1.075m;
    }          
  }
}

United Kingdom product logic class

UkProduct class inherits UsProduct and implements IProduct. GetProductBasePriceById could be accessed by UkProduct as UsProduct is the parent class.

using System;

namespace Ecommerce.Logic
{
  public class UkProduct : UsProduct, IProduct
  {
    private const string CurrencyPrefix = "£ ";

    public new string CalculateLocalPrice(string productId)
    {
      decimal basePrice = GetProductBasePriceById(productId);
      decimal localPrice = ConvertToLocalPrice(basePrice);
      decimal priceAfterTax = CalculateTax(localPrice);
      decimal priceAfterSpecialDiscount = CalculateSpecialDiscount(priceAfterTax);
      decimal roundedPrice = Math.Round(priceAfterSpecialDiscount, 2);

      string formattedLocalPrice = string.Format("{0}{1}", CurrencyPrefix, roundedPrice);
      return formattedLocalPrice;
    }

    private decimal ConvertToLocalPrice(decimal basePrice)
    {
      return basePrice * 0.64m;
    }

    private decimal CalculateSpecialDiscount(decimal priceWithShipping)
    {
      decimal dicountedPrice = priceWithShipping;

      if (priceWithShipping > 500)
      {
        dicountedPrice = dicountedPrice * 0.8m;
      }
      else if (priceWithShipping > 200)
      {
        dicountedPrice = dicountedPrice * 0.95m;
      }

      return dicountedPrice;
    }

    private decimal CalculateTax(decimal basePrice)
    {
      return basePrice * 1.2m;
    }
  }
}

Malaysia product logic class

MyProduct class inherits UsProduct and implements IProduct. GetProductBasePriceById could be accessed by UkProduct as UsProduct is the parent class.

using System;

namespace Ecommerce.Logic
{
  public class MyProduct : UsProduct, IProduct
  {
    private const string CurrencyPrefix = "RM ";

    public new string CalculateLocalPrice(string productId)
    {
      decimal basePrice = GetProductBasePriceById(productId);
      decimal localPrice = ConvertToLocalPrice(basePrice);
      decimal priceAfterTax = CalculateTax(localPrice);
      decimal shippingCost = CalculateShipping(localPrice);
      decimal priceWithShipping = priceAfterTax + shippingCost;
      decimal priceAfterSpecialDiscount = CalculateSpecialDiscount(priceWithShipping);
      decimal roundedPrice = Math.Round(priceAfterSpecialDiscount, 2);

      string formattedLocalPrice = string.Format("{0}{1}", CurrencyPrefix, roundedPrice);
      return formattedLocalPrice;
    }

    private decimal ConvertToLocalPrice(decimal basePrice)
    {
      return basePrice * 3.8m;
    }

    private decimal CalculateSpecialDiscount(decimal priceWithShipping)
    {
      decimal dicountedPrice = priceWithShipping;

      if (priceWithShipping > 500)
      {
        dicountedPrice = dicountedPrice * 0.9m;
      }

      return dicountedPrice;
    }

    private decimal CalculateTax(decimal basePrice)
    {
      return basePrice * 1.06m;
    }
  }
}

All the product logic classes implement IProduct interface so that they all could be registered into Autofac container builder. (The following code is part of BaseService class shown earlier)

      var builder = new ContainerBuilder();

      switch (country)
      {
        case "Us":
          builder.RegisterType<UsProduct>().As<IProduct>();
          break;

        case "Uk":
          builder.RegisterType<UkProduct>().As<IProduct>();
          break;

        case "My":
          builder.RegisterType<MyProduct>().As<IProduct>();
          break;
      }
      
      Container = builder.Build();
      var scope = Container.BeginLifetimeScope();

      ProductLogic = scope.Resolve<IProduct>();

By registering the interface with the appropriate concrete logic class (base on the logical condition of country), Autofac would build a static container that allows the whole application know which concrete logic implementation to call.

If we put this into test using Postman, we would get the following result

Product price for United State

Country is specified by replacing {Country} parameter to “Us”. Result is returned base on UsProduct logic class implementation.

Us-test

Product price for United Kingdom

Country is specified by replacing {Country} parameter to “Uk”. Result is returned base on UkProduct logic class implementation.

Uk-test

Product price for Malaysia

Country is specified by replacing {Country} parameter to “My”. Result is returned base on MyProduct logic class implementation.

My-test

Some consideration…

  1. Performance for building a container and inject dependency on runtime instead of direct initialization of concrete class. From the above 3 examples, we could see each request completed within 15-16ms. I ran a few more test switching between countries, most of the request completed in less than 20ms, which shows there isn’t any major overhead for builder the container.
  2. What are some other reasons to implement Dependency Injection? Answer is unit test. With Dependency Injection in place, the codes would be much testable. (If you currently have codes that is not testable, consider using Shim under Microsoft.Fakes. However this reason is arguable as we do not necessary need Dependency Injection to test our code. All we need is appropriate interfaces in the codes.

One last thought…

Dependency Injection is a clean way of separating codes implementation which conforms to a standard set of Interface. With Dependency Injection developer no longer need to separate which line to execute using complex if-else statement. It makes the code base much cleaner and easier to implement different codes base on logical condition. The drawback is Dependency Injection makes debugging more complicated as the developer need to first figure out which class has been injected during runtime. It is a recommended approach if you have a standard set of interface but requires different codes to be executed base on logical condition.