Inversion of Control / Dependency Injection

IOC | Scoped Service

Artifacts: Download free LinqPad.net to run the following references scripts: 

For our sample application we are going to follow basic architectural guidelines, these guidelines lend themselves to IOC / DI.   We'll focus on the business and data layers with a CustomerFacade, CustomerBe (business entity), and CustomerDl (data layer).


Figure 1.  Web application architecture

Conventional Programming

Below we have a simple application where Main() retrieves the customer list and displays it to the output window; this uses the LinqPad's Customer data which has 59 records. 

Figure 2 demonstrates conventional programming that most developers will be comfortable with.  It may be argued that it would have been simpler to bypass the CustomerBe and CustomerDl and move that code into the CustomerFacade, that these layers simply add unnecessary complexity.  However, this argument would violate architectural principals such as separation of concerns, single responsibility principle, principle of least knowledge, and others.   All of which simplify decoupled architectures using IOC / DI (which will quickly become apparent).

Note that our classes below are using Primary Constructors (introduced with C# 12) which allows me to add parameters to the class, versus adding parameters to constructors.  Primary constructor parameters are automatically available to all members of the class.


Figure 2. Conventional programming

Simulated requirement - filtering

In figure 3, we add // NEW code in five locations.  This simulates a new client requirement where they want to be able to filter the list by country and state; if missing then it defaults to all [as applicable].  It makes sense that we would put the filtering in the data access component thus reducing the amount of data being retrieved.

Note that because we have a clear separation of concerns and apply the single responsibility principal, our sample program just required us to update the CustomerDl class; the CustomerFacade and CustomerBe classes are not touched.


Figure 3. CustomerDl updated to support filtering

Inversion of Control / Dependency Injection 

Below we converted the sample application into one that uses IOC / DI.  Note as we switch between the tabs of legacy and new code (below), that outside of changing CustomerDl to CustomerDac (data access component) none of the classes have been changed; we only had to add a new IocHelper class, which we'll dissect below, and update Main() to transition away from conventional programming.  This is a testimony to the power of using sound architectural processes during development.


Figure 4.  Converted our conventional application to IOC / DI

Below we just perform a sanity check to ensure the logic works as expected.


Figure 5. Converted IOC / DI running

Difference between Legacy and IOC/DI 

With conventional programming, which I'll refer to as legacy, we have to instantiate a class with 'new' and provide it parameters, e.g.,  var foo = new Foo( new Bar("doh!") ); 

With Inversion Of Control, as the name implies, the framework has the responsibility of instantiating the classes as well as providing any parameters.  The provider on line 20 in figure 6 (bottom frame) is often referred to as a container; the container holds all of the "registered" components required for injection.


Figure 6.  Main() applications for both legacy (conventional) and IOC/DI

Registering dependencies in the IOC container

The following registrations (in figure 7, lines 65 thru 69) are what permit the container to instantiate the classes and their parameters.  Each constructor parameter could have its own dependencies, e.g. the CustomerBe is dependent on CustomerDac (reference figure 5, line 34), with these registrations the framework will know how to resolve the dependency chain.

You'll see many examples on the internet that have the registrations being done during application startup.  This is because once registered, and built (line 70 below), you cannot add to the services, at least not without having to rebuild and take a performance hit.  

Note how I pass in the UserQuery context (LinqPad's data context) as well as the country and state as optional parameters.


Figure 7.  IocHelper - registering all dependencies

Propagating the dependency chain

The provider, aka container, will be used by the framework to instantiate classes.  It drills down into each of a classes constructor parameters, to instantiate them recursively, until the entire hierarchy is populated; this form of injection is called constructor injection.  If anywhere in the chain you use "new" to instantiate a class - the propagation is broken for that class, as well as  its child classes.

As long as your dependency chain is properly propagated you can add any registered interface or type to your constructor at any depth of the logic hierarchy.

How is this helpful? Let's use an example for a rule that states that if no state is provided, that it should default to "CA".  This would be a business rule that we would want in our CustomerBe (business entity) - this requires CustomerBe to have access to the config.State value (reference pointer in image below).  For a real world app, the CustomerBe might be reused by numerous classes; each with different paths to our business entity.   
Figure 8. CustomerBe is three layers deep - we'll need config.State instance.

To meet this requirement in our app, all we would have to do is add IConfig config to our CustomerBe primary constructor (line 34 below) and then apply the applicable logic (line 36).  Note we only provide the country "USA" on line 19 of app, so it will default to "CA".   

Note: if you find that you have to include a large number of constructor parameters, you are probably violating the single responsibility principal and should revisit your design.


Figure 9.  Updating [only] our CustomerBe to support new business rule.

Comments are closed