Dependency Injection Example - Constructor Injection and Service Orientation

With all the talk on weblogs or technical conversations within my own organization about DI it's difficult to ignore it as little more than the latest "new black" pattern. I've given it some considerable thought and until quite recently didn't really comprehend the overall niftiness of the DI approach. As with anything else it took a moment of "a-HA" to really grasp the power of DI; I was developing a custom CruiseControl.Net build plugin and realized that the plugins are injected dynamically at construction time. If you debug  one of these plugins, you'll notice that the plugin constructors don't match a common parameter structure. The one commonality throughout all the plugins I was investigating as examples for my own education seemed to be in the parameter types - they were all interfaces, implementations of which were usually stored in the CCNet server application domain.

Services, basically. My interest in service orientation perked my interest in DI; the two concepts appeared mutually beneficial. So I opened the laptop and started coding away on my own implementation of a DI framework, with tests (since it's virtually impossible to talk about DI without talking about tests, too). This blog post consists of an investigation into my own implementation. It doesn't offer up DI as a "holier-than-thou" approach nor a dismissable coding trend. Rather, it is my first attempt to prove to myself that I am getting this DI stuff and to implement it in my own words. *cracks knuckles*

First Things First - Support Laziness

As with any framework, I anticipate that the easier it is to use the more chance I'll have of talking someone else into giving this a shot or a glance. So I knew that I wanted my approach to be very simple to use, quick to implement, and hopefully, make the approach of using DI more interesting. My first question to every pattern is "does it make my coding process easier and more flexible?" I know that:

  • I'm going to implement DI here, so my dependent classes will "just pop up" because the "stuff" they need to "live" will be provided to them by some service layer. 
  • So I know I will need to implement ServiceContainer in some fashion, and the services I put into it will implement interfaces, since the Service approach implies the use of interfaces to define contracts.

My discomfort with the ServiceContainer approach is that, someone actually has to "Add" the service implementations to the service container. Hence, they have to:

  • Know how to do that.
  • Do it the way you expect they're going to do it .
  • Write their code in such a way that they account for my service container implementation.

That point, most times, is where I get pretty aggravated when I'm using someone else's framework. That is a requirement that I must not impose on my audience, but one that I can't get started without. So a compromise is in order, and I'll use the idea of simple metadata to notify me of interfaces the developer intends need to be added to the service layer at run-time.

    1     ///<summary>

    2     ///Defines an interface as one that should be created and hosted by the

    3     ///service host during application run-time.

    4     ///</summary>

    5     [AttributeUsage(AttributeTargets.Interface, AllowMultiple=false)]

    6     public class DependencyServiceInterfaceAttribute : Attribute

    7     {

    8     }

Using the DSI attribute I can mark any interface that I intend to be added to a service container instance within the application domain. This makes it really easy to use, as the code below indicates. 

   76 [DependencyServiceInterface]

   77     public interface IMockServiceA

   78     {

   79         void DoMockServiceWork();

   80     }

   81 

   82     [DependencyServiceInterface]

   83     public interface IMockServiceB

   84     {

   85         void DoMoreWork();

   86     }

   87 

   88     public class MockServiceA : IMockServiceA

   89     {

   90         #region IMockService Members

   91 

   92         public void DoMockServiceWork()

   93         {

   94             System.Diagnostics.Debug.WriteLine("Doing Mock Service A's Job");

   95         }

   96 

   97         #endregion

   98     }

   99 

  100     public class MockServiceB : IMockServiceB

  101     {

  102         #region IMockServiceB Members

  103 

  104         public void DoMoreWork()

  105         {

  106             System.Diagnostics.Debug.WriteLine("Doing Mock Service B's Job");

  107         }

  108 

  109         #endregion

  110     }

I create a new interfaces and mark them with the DSI attribute. Then, I implement each interface with a custom class containing some basic functionality. Notice that I don't have to do anything to the classes themselves; the DI layer we'll begin to investigate next will do that work for us. 

Comprehensive - Sure, Why Not?!

If you can see where I'm going with this you're most likely scratching your head and saying "no way, not everything..." Looking through each assembly for implementations of interfaces marked with metadata isn't the most performant approach to doing type-loading but for now it will serve it's purpose. Think of it this way - I'm making exhaustively sure I'm not going to miss any service implementation that I might need later by a dependent class.  If I suspect that any interface I've got implementations of will ever be needed by any dependent class that might be required later, I can just slap the DSI attribute onto the interface and off we go.

Just to take a quick look at the code that'd perform this exhaustive search it's right here. The DependencyServiceContainer does just what you suspected - it looks through everything in the application domain to find any implementations of any interfaces that have been marked with the DSI attribute. Whenever it finds such an implementation, an instance of it is created and added via the base method ServiceContainer.AddService.

    8 public class DependencyServiceContainer : ServiceContainer

    9     {

   10         public static DependencyServiceContainer Instance

   11         {

   12             get { return _instance; }

   13         }

   14 

   15         static DependencyServiceContainer _instance;

   16         static DependencyServiceContainer()

   17         {

   18             _instance = new DependencyServiceContainer();

   19         }

   20 

   21         internal DependencyServiceContainer()

   22         {

   23             Preload();

   24         }

   25 

   26         private int _svcCount;

   27         public int ServiceCount

   28         {

   29             get { return _svcCount; }

   30         }

   31 

   32         void Preload()

   33         {

   34             foreach (Assembly assm in AppDomain.CurrentDomain.GetAssemblies())

   35             {

   36                 SearchAssemblyForDSIAttributes(assm);

   37             }

   38         }

   39 

   40         void SearchAssemblyForDSIAttributes(Assembly assm)

   41         {

   42             foreach (Type tp in assm.GetTypes())

   43             {

   44                 if (!tp.IsInterface)

   45                 {

   46                     SearchTypeForInterfaces(tp);

   47                 }

   48             }

   49         }

   50 

   51         void SearchTypeForInterfaces(Type t)

   52         {

   53             foreach (Type intrfc in t.GetInterfaces())

   54             {

   55                 if (IsInterfaceDSI(intrfc))

   56                 {

   57                     AddService(

   58                         intrfc,

   59                         Activator.CreateInstance(t)

   60                     );

   61 

   62                     _svcCount++;

   63                 }

   64             }

   65         }

   66 

   67         internal static bool IsInterfaceDependencyInjectable(Type intrfc)

   68         {

   69             return (

   70                 (intrfc.GetCustomAttributes(typeof(DependencyServiceInterfaceAttribute), false).Length > 0)

   71                 &&

   72                 (intrfc.IsInterface)

   73             );

   74         }

   75 

   76         bool IsInterfaceDSI(Type intrfc)

   77         {

   78             return DependencyServiceContainer.IsInterfaceDependencyInjectable(intrfc);

   79         }

   80     }

Activation of Dependent Objects - the Point

Now that the DI framework has a service container into which services required by dependent classes have been added the logic to create dependent objects must be created. Basically, we're going to use reflection to inspect dependent classes. During reflection each constructor signature will be observed. When a constructor is found that has parameters of interface types that are all being held within the DependencyServiceContainer, the constructor will be called and the resulting object returned. 

    8 public class DependentClassActivator

    9     {

   10         static DependentClassActivator _instance;

   11         static DependentClassActivator()

   12         {

   13             _instance = new DependentClassActivator();

   14         }

   15 

   16         public static DependentClassActivator Instance

   17         {

   18             get { return _instance; }

   19         }

   20 

   21         public T CreateInstance<T>() where T : class

   22         {

   23             Type tp = typeof(T);

   24 

   25             foreach (ConstructorInfo ctor in tp.GetConstructors())

   26             {

   27                 if (Observe(ctor))

   28                 {

   29                     return InvokeConstructor<T>(ctor);

   30                 }

   31             }

   32 

   33             return null;

   34         }

   35 

   36         #region Private Helper Methods

   37 

   38         bool Observe(ConstructorInfo ctor)

   39         {

   40             foreach (ParameterInfo prm in ctor.GetParameters())

   41             {

   42                 if (!Observe(prm)) return false;

   43             }

   44 

   45             return true;

   46         }

   47 

   48         bool Observe(ParameterInfo prm)

   49         {

   50             return (

   51                 DependencyServiceContainer.IsInterfaceDependencyInjectable(prm.ParameterType) &&

   52                 GetTypeFromServiceContainer(prm.ParameterType)

   53             );

   54         }

   55 

   56         bool GetTypeFromServiceContainer(Type intrfc)

   57         {

   58             return (DependencyServiceContainer.Instance.GetService(intrfc) !=null);

   59         }

   60 

   61         T InvokeConstructor<T>(ConstructorInfo ctor) where T : class

   62         {

   63             object[] prms = GetConstructorParametersFromServiceContainer(ctor);

   64             return ctor.Invoke(prms) as T;

   65         }

   66 

   67         object[] GetConstructorParametersFromServiceContainer(ConstructorInfo ctor)

   68         {

   69             List<object> prms =new List<object>();

   70 

   71             foreach (ParameterInfo prm in ctor.GetParameters())

   72             {

   73                 prms.Add(DependencyServiceContainer.Instance.GetService(prm.ParameterType));

   74             }

   75 

   76             return prms.ToArray();

   77         }

   78 

   79         #endregion

   80     }

Here's the basic run-down. The DependentClassActivator creates looks at all of a class's constructors. Specifically, at each constructor's parameter. When it finds a constructor that has parameters it sees in the DependencyServiceContainer, it calls that constructor. So you don't have to use your class's constructors any longer. Instead, you tell the new DependentClassActivator to give you an instance, instead. Something like this:

   27             MockServiceA a = DependentClassActivator.Instance.CreateInstance<MockServiceA>();

 

To use a metaphor, it's like walking into a kitchen in which everything you ever need to make anything is already there. You need a spatula, you got it, you need a mixer, you got it. And so on.

How Do You Test It?

Possibly the most important aspect of all this is the ability to test it. In fact, code written using this DI framework is rather simplistic to test. To explain how you'd use this approach we'll consider the ever-relevant banking scenario. Below you'll see the test code for all the functionality described earlier. You'll see some interfaces that have been marked with the DSI attribute, some implementations to create and use, and tie it all up with a mock banking execution example.

 

   10 #region Bank Service Interfaces and Implementations

   11 

   12     public class Account

   13     {

   14         private int _accountId;

   15         private decimal _bal;

   16 

   17         public decimal Balance

   18         {

   19             get { return _bal; }

   20             set { _bal = value; }

   21         }

   22 

   23         public int AccountId

   24         {

   25             get { return _accountId; }

   26             set { _accountId = value; }

   27         }

   28     }

   29 

   30     [DependencyServiceInterface]

   31     public interface IAccountLookupService

   32     {

   33         Account FindAccount(int accountId);

   34     }

   35 

   36     [DependencyServiceInterface]

   37     public interface IWithdrawalService

   38     {

   39         bool Withdraw(Account account, decimal amount);

   40     }

   41 

   42     [DependencyServiceInterface]

   43     public interface IDepositService

   44     {

   45         bool Deposit(Account account, decimal amount);

   46     }

   47 

   48     public class AccountLookup : IAccountLookupService

   49     {

   50         #region IAccountLookupService Members

   51 

   52         public Account FindAccount(int accountId)

   53         {

   54             if (accountId != 1234) return null;

   55 

   56             Account mockAccount = new Account();

   57             mockAccount.AccountId = 1234;

   58             mockAccount.Balance = 100;

   59             return mockAccount;

   60         }

   61 

   62         #endregion

   63     }

   64 

   65     public class AccountWithdrawer : IWithdrawalService

   66     {

   67         #region IWithdrawalService Members

   68 

   69         public bool Withdraw(Account account, decimal amount)

   70         {

   71             if (amount > account.Balance) return false;

   72             account.Balance -= amount;

   73             return true;

   74         }

   75 

   76         #endregion

   77     }

   78 

   79     public class AccountDepositer : IDepositService

   80     {

   81         #region IDepositService Members

   82 

   83         public bool Deposit(Account account, decimal amount)

   84         {

   85             account.Balance += amount;

   86             return true;

   87         }

   88 

   89         #endregion

   90     }

   91 

   92     public class Bank

   93     {

   94         IAccountLookupService lookupService;

   95         IWithdrawalService withdrawalService;

   96         IDepositService depositService;

   97 

   98         public Bank(IAccountLookupService lookupService,

   99             IWithdrawalService withdrawalService,

  100             IDepositService depositService)

  101         {

  102             this.lookupService = lookupService;

  103             this.withdrawalService = withdrawalService;

  104             this.depositService = depositService;

  105         }

  106 

  107         public Account GetAccount(int id)

  108         {

  109             return this.lookupService.FindAccount(id);

  110         }

  111 

  112         public bool Withdraw(Account account, decimal amount)

  113         {

  114             return this.withdrawalService.Withdraw(account, amount);

  115         }

  116 

  117         public bool Deposit(Account account, decimal amount)

  118         {

  119             return this.depositService.Deposit(account, amount);

  120         }

  121     }

  122 

  123     #endregion

  124 

  125     #region Tests

  126 

  127     [TestFixture]

  128     public class BankAccountTests

  129     {

  130         [SetUp]

  131         public void Setup()

  132         {

  133         }

  134 

  135         [TearDown]

  136         public void TearDown()

  137         {

  138         }

  139 

  140         [Test]

  141         public void CanBankClassBeCreated()

  142         {

  143             Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();

  144             Assert.IsNotNull(bank);

  145         }

  146 

  147         [Test]

  148         public void CanBankHandBackAccount()

  149         {

  150             Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();

  151             Account account  = bank.GetAccount(1234);

  152             Assert.IsNotNull(account );

  153             account = bank.GetAccount(1000);

  154             Assert.IsNull(account );

  155         }

  156 

  157         [Test]

  158         public void CanBankAccountWithdrawMoney()

  159         {

  160             Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();

  161             Account account = bank.GetAccount(1234);

  162             Assert.IsNotNull(account);

  163             decimal bal = account.Balance;

  164             decimal amt = 42;

  165 

  166             bool result = bank.Withdraw(account , amt);

  167             Assert.IsTrue(result);

  168             Assert.AreEqual((bal - amt), account .Balance);

  169 

  170             result = bank.Withdraw(account , 99999999);

  171             Assert.IsFalse(result);

  172         }

  173 

  174         [Test]

  175         public void CanBankAccountDepositMoney()

  176         {

  177             Bank bank = DependentClassActivator.Instance.CreateInstance<Bank>();

  178             Account account = bank.GetAccount(1234);

  179             Assert.IsNotNull(account );

  180             decimal bal = account .Balance;

  181             decimal amt = 42;

  182 

  183             bool result = bank.Deposit(account , amt);

  184             Assert.IsTrue(result);

  185             Assert.AreEqual((bal + amt), account .Balance);

  186         }

  187     }

  188 

  189     #endregion

I welcome any comments on this approach. Is this DI or have I completely missed the boat on this whole concept. Hopefully this look at one appraoch DI has been as enlightening as it has been for me. Happy coding!