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      ///

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

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

    4      ///

    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 () 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 (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 (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!

Brady Gaster
Hi! I'm a christian dad who lives near Seattle, where I work with the talented folks at Microsoft to create compelling demonstrations for conferences that instruct and inspire developers who want to party in the cloud.