OdeToCode IC Logo

.NET Core Opinion 9 - Embrace Dependency Injection

Tuesday, February 26, 2019

Someone asked me why dependency injection is popular in .NET Core. They told me DI makes code harder to follow because you never know what classes and objects the app will use unless you run with a debugger.

The argument that DI makes software harder to understand has been around for a long time, because there is some truth to the argument. However, if you want to build flexible, testable, decoupled classes in C#, then using a container and constructor injection is still the simplest solution.

The alternative is to write code like the ASP.NET MVC AccountController (not the .NET Core controller, but the MVC 4 and 5 controllers). If you've worked with the framework over the years, you might remember a time when the project scaffolding gave us the following code.

public AccountController()
  : this(new UserManager<ApplicationUser>(
      new UserStore<ApplicationUser>(
       new ApplicationDbContext())))
{
}
then 
public AccountController(UserManager<ApplicationUser> userManager)
{
   UserManager = userManager;
}

public UserManager<ApplicationUser> UserManager { get; private set; }

The two different constructors do provide some flexibility. In a unit test, you can pass in a test double as a UserManager, but when the application is live the default constructor combines a DbContext with a UserStore to provide a production implementation.

The problem is, the production implementation becomes hard-coded into the default constructor. What if you want to wrap the UserStore with a caching or logging component? What if you wanted to use a non-default connection string for the DbContext? Then you need to scour the entire code base to find all the dependencies hardcoded with new.

Later versions of the scaffolding tried to improve the situation by centralizing dependency registration. The following is code from today's Startup.Auth.cs. Notice how the method is similar to ConfigureServices in ASP.NET Core.

public void ConfigureAuth(IAppBuilder app)
{
    // Configure the db context, user manager and 
    // signin manager to use a single instance per request
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

    // ...

}

While the central registration code is an improvement, the non-Core ASP.NET framework does not offer DI as a native service. The application needs to manually resolve a dependencies via an OwinContext reference. Now, the AccountController looks like:

public AccountController()
{
}

public AccountController(ApplicationUserManager userManager)
{
   UserManager = userManager;
}

public ApplicationUserManager UserManager
{
   get
   {
   return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
   }

   private set
   {
       _userManager = value;
   }
}

The problem is, every dependency requires a developer to write a property and follow the service locator anti-pattern. So, while the indirection of DI in ASP.NET Core does have some downsides, at least DI doesn’t add more code to a project. In fact, an AccountController in ASP.NET has a simpler setup.

public class AccountController
{
    public AccountController(ApplicationUserManager userManager)
    {
        UserManager = userManager;
    }

    // …

}

As always, software is about tradeoffs. If you want the flexibility of testable classes, go all in with dependency injection. The alternative is to still face uncertainties from indirection, but in a code base that is larger and harder to maintain.


Comments
Gravatar Yibeltal Beyabel Wednesday, February 27, 2019
I am starting with DI, and I decided to rewrite my personal project to use .NET core DI. I am learning it still, but as technique, I kind of sketch a Visio diagram which really helps me to visualize the architecture and dependencies. Lower tier components (like Repository) are injected to upper tier component (like My Service Layer). With the aid of my diagram and standard architecture, DI seems to be easy to use and to maintain.
Gravatar Joe Conrey Wednesday, March 6, 2019
Hey, Scott, Perhaps I can provide some clarity for finding the actual method implementation for an interface method in Visual Studio. If you place your cursor anywhere on the method name that is called on the interface instance, and hit Ctrl + F12, it will take you to the implementation method, similar to how Shift + F12 will usually take you to the method definition, which on an interface, would simply take you to the method signature on the interface. Hope that helps make someone's life a little easier. I remember spending hours trying to wrap my head around finding method implementations early in my dev career. Not fun. Thanks, joe
Gravatar Xiaoguo Ge Thursday, March 14, 2019
I've been programming for more than 20 years, and most of the time following OO practices. For the past year, I started trying to use pure static functions whenever I can. To my pleasant surprise, it is easy with C# especially with the new language features. There is no DI needed and no mocking needed for unittest.
Gravatar Saad Monday, March 25, 2019
@Joe Conrey Thanks a lot for the tip. I have wasted a lot of time, in the last couple of years while using DI, to find the method definitions and was the biggest downside for me to using DI. This will certainly help me save a lot of time. :)
Comments are closed.