ASP.NET Core Dependency Injection


codesoupI’ve been building some ASP.NET Core apps as of late and had to dig into how Dependency Injection works there. After talking with Julie Lerman a bit on Twitter about it, I realized that there might be some confusing things about how it works in ASP.NET Core, so I’m hoping I can add some clarity in this post.

One thing I like about ASP.NET Core is that since it is a new platform, I’m learning something new all the time. When I suggested to Julie to use DI in her example database seeder, but of course there were things I was missing and my suggestion would actually just leak a context object. Lets look at some of the default dependency injection in ASP.NET Core to see how it is supposed to work.

Which DI Layer?

First question is whether the built-in dependency injection should be used at all. Lots of developers and companies have had long-winded meetings and discussions how a specific DI layer is marginally better than all the rest. Some third-party DI layers have additional features that you really want, so it’s up to you. Most of the major players (e.g. StructureMap, Ninject, etc.) have integration with ASP.NET Core’s DI so you can switch out the built-in provider. I find the built-in provider to be fast, but not exactly feature rich but for smaller projects it’s easy and simple so that’s what I’ll talk about.

Using Dependency Injection in ASP.NET Core

In these examples, I’m using what I’m doing with my WilderBlog project on GitHub if you want to see it in action. Let’s start with some of the basics. In the startup class, the ConfigureServices method is where you set up your depenedency injection container (e.g. service container). For example:

public void ConfigureServices(IServiceCollection svcs)
{
  svcs.AddSingleton(_config);

  if (_env.IsDevelopment())
  {
    svcs.AddTransient<IMailService, LoggingMailService>();
  }
  else
  {
    svcs.AddTransient<IMailService, MailService>();
  }

  svcs.AddDbContext<WilderContext>(ServiceLifetime.Scoped);

  // ...
}

The IServiceCollection parameter lets you set up different kinds of services either by object creation or matching to a specific interface. This isn’t that different. The default service collection supports three kinds of lifetimes:

ServiceLifetime.Singleton: A single shared instance throughout your application’s lifetime. Only created once.

ServiceLifetime.Scoped: Shared within a single request (or Service Scope).

ServiceLifetime.Transient: Created on every request for the service.

You can see that the IServiceCollection supports AddXXX() methods for these three different lifetimes, but when you’re using extension methods to add other service types (e.g. AddMvc, AddDbContext) you can often specify the lifetime (like I’ve done with the AddDbContext call).

Once you have registered these services, you can use constructor and parameter injection (Property Injection was dropped before RTM as it was deemed too confusing). For example:

[Route("[controller]")]
public class VideosController : Controller
{
  private VideosProvider _videos;

  public VideosController(VideosProvider videos)
  {
    _videos = videos;
  }

But you can do parameter injection, most obviously in actions on controllers because the FromServices attribute is part of ModelBinding:

[Route("changepwd")]
public async Task<IActionResult> ChangePwd(
  [FromServices] UserManager<WilderUser> userManager, 
  string username, 
  string oldPwd, 
  string newPwd)
{
  var user = await userManager.FindByEmailAsync(username);
  if (user == null) return BadRequest(new { success = false });
  var result = await userManager.ChangePasswordAsync(user, oldPwd, newPwd);
  if (result.Succeeded) return Ok(new { success = true });
  else return BadRequest(new { success = false, errors = result.Errors });
}

If you’re going to use a service more than once in a controller, might as well do constructor injection but this is very useful for one-offs or non-lightweight objects.

Understanding Scope Lifetime

The Transient and Singleton scopes are very obvious, but the Scope lifetime is more confusing. Many people (yours included) assumed that this meant once-per-request or transient if outside a request. But that’s not what it means. In fact, the DI layer specific has an interface called IServiceScopeFactory to allow you to create a scope for DI injection. Calling CreateScope returns an IServiceScope which implements IDisposable. Therefore you’d commonly use a ‘using’ statement to protect it’s destruction (and ending the scope):

using (var scope = scopeFactory.CreateScope())
{
  // ...
}

Under the covers ASP.NET MVC does this around an entire request. The hosting middleware wraps the entire request (so it affects any request, not just MVC). (Thanks @davidfowl for correcting me!) This way every request inside the scope returns the same instance of services that are of Scoped lifetime. That’s great, but inside of the Startup.cs, we aren’t in a scope so if we create instances of scoped classes, we effectively get a singleton (thanks @davidfowl!). To fix this, Julie Lerman has a post on it, but I have an alternative solution. No idea which one is ‘best’, but I like the classes to not know about DI in general. Here is what makes the most sense to me. First I’m asking for the IServiceScopeFactory in the parameter list of Configure (in Startup.cs) so I can create a scope if necessary):

public void Configure(IApplicationBuilder app,
                      ILoggerFactory loggerFactory,
                      IMailService mailService,
                      IServiceScopeFactory scopeFactory)

Then I’m creating a scope around the call to seed the database and use the scope to create the instance of my seeder (so that all the DI inside is inside a scope):

using (var scope = scopeFactory.CreateScope())
{
  var initializer = scope.ServiceProvider.GetService<WilderInitializer>();
  initializer.SeedAsync().Wait();
}

This way the initializer doesn’t need to do the actual execution of the scope and can be DI ignorant (in case of testing or moving the seeding to inside a request later).

Here is a link to Julie’s post of the twitter conversation if you want to see how it transpired!

http://thedatafarm.com/dotnet/twitter-education-re-aspnet-core-scope/ 

Opinions?




Application Name WilderBlog Environment Name Production
Application Ver 1.1.0.0 Runtime Framework .NETCoreApp,Version=v1.1
App Path D:\home\site\wwwroot Runtime Version .NET Core 4.6.25009.03
Operating System Microsoft Windows 6.2.9200 Runtime Arch X86