BMC to acquire Netreo. Read theBlog

.Net Core Dependency Injection

By: jnance
  |  February 28, 2024
.Net Core Dependency Injection

What is Dependency Injection?

Dependency Injection (DI) is a pattern that can help developers decouple the different pieces of their applications. It provides a mechanism for the construction of dependency graphs independent of the class definitions. Throughout this article, I will be focusing on constructor injection where dependencies are provided to consumers through their constructors.

It’s also important to mention two other injection methods:

  • property injection, which the built-in DI framework for .NET Core doesn’t support
  • and action method injection

Consider the following classes:

class Bar : IBar { 
  // ...
}

class Foo {
  private readonly IBar _bar;
  public Foo(IBar bar) {
    _bar = bar;
  }
}

In this example, Foo depends on IBar and somewhere we’ll have to construct an instance of Foo and specify that it depends on the implementation Bar like so:

var bar = new Bar();
var foo = new Foo(bar);

The problem with this is two-fold. Firstly, it violates the Dependency Inversion Principle because the consuming class implicitly depends on the concrete types Bar and Foo. Secondly, it results in a scattered definition of the dependency graph and can make unit testing very difficult.

Why Do We Need Dependency Injection In C#?

The Composition Root pattern states that the entire dependency graph should be composed in a single location “as close as possible to the application’s entry point”. This could get pretty messy without the assistance of a framework. DI frameworks provide a mechanism, often referred to as an Inversion of Control (IoC) Container, for offloading the instantiation, injection, and lifetime management of dependencies to the framework. You invert the control of component instantiation from the consumers to the container, hence “Inversion of Control”.

To do this, you simply register services with a container, and then you can load the top level service. The framework will inject all child services for you. A simple example, based on the class definitions above, might look like:

container.Register<Bar>().As<IBar>();
container.Register<Foo>();
// per the Composition Root pattern, this _should_ be the only lookup on the container
var foo = container.Get<Foo>();

What Is .NET Core Dependency Injection?

Prior to .Net Core, the only way to get DI in your applications was through the use of a framework such as Autofac, Ninject, StructureMap and many others. However, DI is treated as a first-class citizen in ASP.Net Core. You can configure your container in your Startup.ConfigureServices method:

public class Startup {
  public void ConfigureServices(IServiceCollection services) {
    services.AddTransient<IArticleService, ArticleService>();
  }
  // ...
}

When a request gets routed to your controller, it will be resolved from the container along with all its dependencies:

public class ArticlesController : Controller {
  private readonly IArticleService _articleService;
  public ArticlesController(IArticleService articleService) {
    _articleService = articleService;
  }
 
  [HttpGet("{id}"]
  public async Task<IActionResult> GetAsync(int id) {
    var article = await _articleService.GetAsync(id);
    if(article == null)
      return NotFound();
    return Ok(article);
  }
}

What Is Singleton vs. Transient vs Scoped?

In the context of .NET Core DI, you’ll often hear or read the terms singleton, transient, and scoped. These are dependency lifetimes.

At registration time, dependencies require a lifetime definition. The service lifetime defines the conditions under which a new service instance will be created. Below are the lifetimes defined by the ASP.Net DI framework. The terminology may be different if you choose to use a different framework.

  • Transient – Created every time they are requested
  • Scoped – Created once per scope. Most of the time, scope refers to a web request. But this can also be used for any unit of work, such as the execution of an Azure Function.
  • Singleton – Created only for the first request. If a particular instance is specified at registration time, this instance will be provided to all consumers of the registration type.

Using Different Providers

If you would like to use a more mature DI framework, you can do so as long as they provide an IServiceProvider implementation. If they don’t provide one, it is a very simple interface that you should be able to implement yourself. You would just return an instance of the container in your ConfigureServices method. Here is an example using Autofac:

public class Startup { 
  public IServiceProvider ConfigureServices(IServiceCollection services) {
    // setup the Autofac container
    var builder = new ContainerBuilder();
    builder.Populate(services);
    builder.RegisterType<ArticleService>().As<IArticleService>();
    var container = builder.Build();
    // return the IServiceProvider implementation
    return new AutofacServiceProvider(container);
  }
  // ... 
}

Generics

Dependency injection can get really interesting when you start working with generics. Most DI providers allow you to register open generic types that will have their generic arguments set based on the requested generic type arguments. A great example of this is Microsoft’s new logging framework (Microsoft.Extensions.Logging). If you look under the hood  you can see how they inject the open generic ILogger<>:

services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));

This allows you to depend on the generic ILogger<> like so:

public class Foo {
  public Foo(ILogger<Foo> logger) {
    logger.LogInformation("Constructed!!!");
  }
}

Another common use case is the Generic Repository Pattern. Some consider this an anti-pattern when used with an ORM like Entity Framework because it already implements the Repository Pattern. But, if you’re unfamiliar with DI and generics, I think it provides an easy entry point.

Open generic injection also provides a great mechanism for libraries (such as JsonApiDotNetCore) to offer default behaviors with easy extensibility for applications. Suppose a framework provides an out-of-the-box, implementation of the generic repository pattern. It may have an interface that looks like this, implemented by a GenericRepository:

public interface IRepository<T> where T : IIdentifiable {
   T Get(int id);
}

The library would provide some IServiceCollection extension method like:

public static void AddDefaultRepositories(this IServiceCollection services) {
  services.TryAdd(ServiceDescriptor.Scoped(typeof(IRepository<>), typeof(GenericRepository<>)));
}

And the default behavior could be supplemented by the application on a per resource basis by injecting a more specific type:

services.AddScoped<IRepository<Foo>, FooRepository>();

And of course FooRepository can inherit from GenericRepository<>.

class FooRepository : GenericRepository<Foo> {
  Foo Get(int id) {
    var foo = base.Get(id);
    // ...authorization of resources or any other application concerns can go here
    return foo;
  }
}

Beyond the Web

The ASP.Net team has separated their DI framework from the ASP.Net packages into Microsoft.Extensions.DependencyInjection. What this means is that you are not limited to web apps and can leverage these new libraries in event-driven apps (such as Azure Functions and AWS Lambda) or in thread loop apps. All you need to do is:

  1. Install the framework NuGet package:
    Install-Package Microsoft.Extensions.DependencyInjection
    or
    dotnet add package Microsoft.Extensions.DependencyInjection
  2. Register your dependencies on a static container:
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddScoped<IEmailSender, AuthMessageSender>();
    serviceCollection.AddScoped<AzureFunctionEventProcessor, IEventProcessor>();
    Container = serviceCollection.BuildServiceProvider();
  3. Define the lifetime scope (if applicable) and resolve your top level dependency:
    var serviceScopeFactory = Container.GetRequiredService<IServiceScopeFactory>();
    using (var scope = serviceScopeFactory.CreateScope())
    {
      var processor = scope.ServiceProvider.GetService<IEventProcessor>();
      processor.Handle(theEvent);
    }

Under the hood, the call to .BuildServiceProvider() will inject an IServiceScopeFactory. You can load this service and define a scope so you can use properly scoped services. 

Disposable Services

If a registered service implements IDisposable it will be disposed of when the containing scope is disposed. You can see how this is done here. For this reason, it is important to always resolve services from a scope and not the root container, as described above. If you resolve IDisposables from the root container, you may create a memory leak since these services will not be disposed of until the container gets disposed. 

Dynamic Service Resolution

Some DI providers provide resolution time hooks that allow you to make runtime decisions about dependency injection. For example, Autofac provides an AttachToComponentRegistration method that can be used to make runtime decisions. At Stackify, we used this with Azure Functions to wrap the TraceWriter (before they supported the ILogger interface) behind a facade. This facade passed the logging method calls to the scoped TraceWriter instance as well as our log4net logger. To do this, we register the instance of the TraceWriter when we begin the lifetime scope:

using (var scope = ServiceProvider.BeginLifetimeScope(b => b.RegisterInstance(traceWriter)))
{
  // ...
}

I’ve created a gist here that you can reference if you’d like to see the rest of the implementation.

When Not To Use IoC Containers

In general, IoC containers are an application concern. What this means is library and framework authors should think carefully about whether or not it is really necessary to create an IoC container within the package itself. An example of one that does this is the AspNetCore.Mvc framework packages. However, this framework is intended to manage the life of the application itself. This is very different than say a logging framework.

Conclusion

Dependency Injection describes the pattern of passing dependencies to consuming services at instantiation. DI frameworks provide IoC containers that allow developers to offload control of this process to the framework. This lets us decouple our modules from their concrete dependencies, improving testability and extensibility of our applications.

Hope this article was helpful. I often reference what we do here at Stackify and wanted to share that we also use our own tools in house to continually improve our applications. Both our free dynamic code profile, Stackify Prefix, and our full lifecycle APM, Stackify Retrace, help us make sure we are providing our clients with the best value.

[x_alert heading=”Note” type=”info”]All of the source code links used in this article are permalinks to the code on the default repository branches. These links should be used as a reference and not as the current state of the underlying implementations or APIs since these are subject to change at any time.[/x_alert]

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]