blog post image
Andrew Lock avatar

Andrew Lock

~12 min read

New dependency injection features in .NET 6

Exploring .NET 6 - Part 10

In this post I talk about some of the new features added to Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.DependencyInjection.Abstractions in .NET 6. Some of these were added to resolve existing bugs and edge cases, and others were added to support the new minimal APIs introduced in ASP.NET Core in .NET 6.

In this post I cover:

Handling IAsyncDisposable services with IServiceScope

The first feature we'll look at fixes an issue that's been around for quite a while. .NET Core 3.0 and C# 8 added support for IAsyncDisposable, which, as the name implies, is an async equivalent of the IDisposable interface. This allows you to run async code when disposing resources, which is necessary to avoid deadlocks in some cases.

The "problem" with IAsyncDisposable is that everywhere that "cleans up" IDisposable objects by calling IDisposable on them now also needs to support IAsyncDisposable objects. Due to the viral nature of async/await, this means those code paths now need to be async too, and so on.

One obvious place that would need to support IAsyncDisposable is the DI container in Microsoft.Extensions.DependencyInjection, used by ASP.NET Core. Support for this was added in the same timeframe, in .NET Core 3.0, but support was only added to IServiceProvider, it wasn't added to scopes.

As an example of when that's an issue, imagine you have a type that supports IAsyncDisposable but doesn't support IDisposable. If you register this type with a Scoped lifetime, and retrieve an instance of it from a DI scope, then when the scope is disposed, you will get an exception. This is shown in the following sample .NET 5 application:

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
					
public class Program
{
	public static async Task Main()
	{
        await using var provider = new ServiceCollection()
            .AddScoped<Foo>()
            .BuildServiceProvider();

        using (var scope = provider.CreateScope())
        {
            var foo = scope.ServiceProvider.GetRequiredService<Foo>();
        } // Throws System.InvalidOperationException
    }
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

This example program will throw an exception when the Scope variable is disposed.

Unhandled exception. System.InvalidOperationException: 'Foo' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.Dispose()
   at <Program>$.<<Main>$>d__0.MoveNext() in C:\src\tmp\asyncdisposablescope\Program.cs:line 12
--- End of stack trace from previous location ---
   at <Program>$.<<Main>$>d__0.MoveNext() in C:\src\tmp\asyncdisposablescope\Program.cs:line 12
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

The problem is that the IServiceScope type returned from CreateScope() implements IDisposable, not IAsyncDisposable, so you can't call await using on it. So that's a simple fix right, require that IServiceScope implements IAsyncDisposable?

Unfortunately, no, that would be a big breaking change. Custom containers (Autofac, Lamar, SimpleInjector etc.) need to implement the IServiceScope interface, so adding an additional interface requirement would be a big breaking change for those libraries.

Instead, the solution taken in this PR allows container implementations to opt in slowly:

  1. Add a new extension method to IServiceProvider, called CreateAsyncScope().
  2. Create an AsyncServiceScope wrapper type that implements IAsyncDisposable.

Looking at the code below, we can see how this solves the problem. First, the extension method calls CreateScope() to retrieve an implementation of IServiceScope from the container. This will be implementation-specific, depending on the container you're using. A new AsyncServiceScope is then created from the IServiceScope and returned:

public static AsyncServiceScope CreateAsyncScope(this IServiceProvider provider)
{
    return new AsyncServiceScope(provider.CreateScope());
}

The AsyncServiceScope implements both IServiceScope and IAsyncDisposable, delegating the former to the IServiceScope instance passed in the constructor. In the DisposeAsync() method, it checks to see if the IServiceScope implementation supports DisposeAsync(). If it does, DisposeAsync() is called, and the potential exception cause is avoided. If the implementation doesn't support IAsyncDisposable, then it falls back to a synchronous Dispose() call.

public readonly struct AsyncServiceScope : IServiceScope, IAsyncDisposable
{
    private readonly IServiceScope _serviceScope;
    public AsyncServiceScope(IServiceScope serviceScope)
    {
        _serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
    }

    public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider;
    public void Dispose() => _serviceScope.Dispose();

    public ValueTask DisposeAsync()
    {
        if (_serviceScope is IAsyncDisposable ad)
        {
            return ad.DisposeAsync();
        }
        _serviceScope.Dispose();

        // ValueTask.CompletedTask is only available in net5.0 and later.
        return default;
    }
}

For the built-in ServiceScope, which does support IAsyncDisposable, this solves the problem. In .NET 6, you can use await using with CreateAsyncScope() and there are no problems.

using Microsoft.Extensions.DependencyInjection;

await using var provider = new ServiceCollection()
    .AddScoped<Foo>()
    .BuildServiceProvider();

await using (var scope = provider.CreateAsyncScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
} // doesn't throw if container supports DisposeAsync()s

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

If you're using a custom container, and it doesn't support IAsyncDisposable, then this will still throw. You will either need to make Foo implement IDisposable, or update/change your custom container implementation. The good news if you're not currently running into this issue is that you can still use the CreateAsyncScope() pattern, and then when the container is updated, your code won't need to change.

In general, if you're manually creating scopes in your application (as in the above example), it seems to me that you should use CreateAsyncScope() wherever possible.

Using custom DI containers with WebApplicationBuilder

Speaking of custom containers, if you're using the new .NET 6 minimal hosting APIs with WebApplicationBuilder and WebApplication, then you might be wondering how you even register your custom container.

In .NET 5, you need to do two things:

  1. Call UseServiceProviderFactory() (or a similar extension method, like UseLamar()) on the IHostBuilder
  2. Implement an appropriate ConfigureContainer() method in your Startup class.

For example, for Autofac, your Program.cs might look something like the below example, where an AutofacServiceProviderFactory is created and passed to UseServiceProviderFactory():

public static class Program
{
    public static void Main(string[] args)
        => CreateHostBuilder(args).Build().Run();
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory(new AutofacServiceProviderFactory()) // <-- add this line
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

In Startup, you would add the ConfigureContainer() method (shown below) and do your Autofac-specific DI registration:

public class Startup
{
    public void ConfigureContainer(ContainerBuilder containerBuilder)
    {
        // Register your own things directly with Autofac here
        builder.RegisterModule(new MyApplicationModule());
    }
    // ..
}

In .NET 6's new minimal hosting, the patterns above are replaced with WebApplicationBuilder and WebApplication, so there is no Startup class. So how are you supposed to do the above configuration?

For a more in-depth look at the new minimal hosting APIs, see the earlier posts in this series.

In .NET 6, you still need to do the same 2 steps as before, you just do them on the Host property of WebApplicationBuilder. The example below shows how you would translate the Autofac example to minimal hosting:

var builder = WebApplication.CreateBuilder(args);

// Call UseServiceProviderFactory on the Host sub property 
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Call ConfigureContainer on the Host sub property 
builder.Host.ConfigureContainer<ContainerBuilder>(builder => 
{
    builder.RegisterModule(new MyApplicationModule());
});

var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

As you can see, the Host property has the same UseServiceProviderFactory() method has before, so you can add the AutofacServiceProviderFactory() instance there. There is also a ConfigureContainer<T> method that takes a lambda method, with the same shape as the method we used in Startup previously. Note that the T here is container-specific, so it's ContainerBuilder for Autofac, ServiceRegistry for Lamar etc.

This example is taken from this great "Migration to ASP.NET Core in .NET 6 " gist from David Fowler. If you're stuck trying to work out how to migrate to the new minimal hosting APIs, be sure to take a look at it for pointers and FAQs.

Detecting if a service is registered in the DI container

The next new feature we'll look at was introduced in .NET 6 to support features in the new minimal APIs. In particular, minimal APIs allow you to request services from the DI container in your route handlers without explicitly marking them with [FromService]. This is different to MVC API controllers, where you would have to use the attribute to get that feature.

For example, in .NET 6 you can do this:

var builder = WebApplication.CreateBuilder(args);

// Add a service to DI
builder.Services.AddSingleton<IGreeterService, GreeterService>();
var app = builder.Build();

// Add a route handler that uses a service from DI 
app.MapGet("/", (IGreeterService service) => service.SayHello());

app.Run();

public class GreeterService: IGreeterService
{
    public string SayHello() => "Hellow World!";
}
public interface IGreeterService
{
    string SayHello();
}

An equivalent API controller might look something like this (note the attribute).

public class GreeterApiController : ControllerBase
{
    [HttpGet("/")]
    public string SayHello([FromService] IGreeterService service)
        => service.SayHello();
}

Obviously, you'd normally use constructor injection for this, I'm just making a point!

The problem for the minimal API is that it now has no easy way of knowing the purpose of a given parameter in the lambda route handler.

  • Is it a model that should be bound to the request body?
  • Is it a service that should be retrieved from the DI container?

One (bad) solution would be for the API to always assume it's a service, and try and grab it from the DI container. However, doing this would try and create an instance of the service, which might have unintended consequences. For example, imagine if the container writes a log every time you try and retrieve a service that doesn't exist, so you can more easily detect misconfigurations. That could have performance implications if a log is written for every single request!

Instead, what ASP.NET Core really needs is a way of checking if a type is registered without creating an instance of it, as described in this issue. .NET 6 adds support for this scenario by introducing a new interface, IServiceProviderIsService to the DI abstractions library Microsoft.Extensions.DependencyInjection.Abstractions.

 public partial interface IServiceProviderIsService
{
    bool IsService(Type serviceType);
}

This service provides a single method that you can invoke to check whether a given service type is registered in the DI container. The IServiceProviderIsService service itself can also be obtained from the container. If you're using a custom container that hasn't added support for this feature, then calling GetService<IServiceProviderIsService>() would return null.

Note that this interface is meant to indicate whether calling IServiceProvider.GetService(serviceType) could return a valid service. It doesn't guarantee that doing so will actually work, as there are an infinite number of things that could go wrong when trying to construct the service!

You can see an example of how we could use the new interface below. This pretty much mirrors how ASP.NET Core uses the service behind the scenes when creating minimal API route handlers:

var collection = new ServiceCollection();
collection.AddTransient<IGreeterService, GreeterService>();
IServiceProvider provider = collection.Build();

// try and get the new interface. 
// This will return null if the feature isn't yet supported by the container
var serviceProviderIsService = provider.GetService<IServiceProviderIsService>();

// The IGreeterService is registered in the DI container
Assert.True(serviceProviderIsService.IsService(typeof(IGreeterService)));
// The GreeterService is NOT registered directly in the DI container.
Assert.False(serviceProviderIsService.IsService(typeof(GreeterService)));

I don't envisage using this service directly in my own apps, but it might be useful for libraries targeting .NET 6, as well as provide the possibility of fixing other DI related issues.

If you're cringing at the name, IServiceProviderIsService, then don't worry, so is David Fowler who originally suggested the feature and implemented it:

David Fowler saying 'IServiceProviderIsService what is this name 😄'

Additional diagnostics for the DI container

The dotnet-trace global tool is a cross-platform tool that enables collecting profiles of a running process without using a native profiler. It's based on the .NET Core EventPipe component, which is essentially a cross-platform equivalent to Event Tracing for Windows (ETW) or LTTng.

In .NET 6, two new diagnostic events were added:

  • ServiceProviderBuilt: shows when an IServiceProvider is built, and how many transient, scoped, and singleton services it contained. It also includes how many open and closed generics were registered.
  • ServiceProviderDescriptors: shows a description of all the services in the container, their lifetime, and the implementation type, as a JSON blob.

Additionally, the hash code of the IServiceProvider is included in all the events, so you can more easily correlate between different events, if required. For example the following installs the dotnet-trace global tool, and starts a simple ASP.NET Core .NET 6 app, collecting the DI events:

dotnet tool install -g dotnet-trace
dotnet trace collect --providers Microsoft-Extensions-DependencyInjection::Verbose -- name ./aspnettest.exe

The resulting trace can be viewed in PerfView (on Windows). The screenshot below shows that the Events have been recorded. You can just see the start of the descriptors for the ServiceProviderDescriptors event on the right:

PerfView showing the new DI events

This feature was part of a larger push to add more diagnostics to .NET 6, but most of the suggested diagnostics didn't make it in time for .NET 6. The DI diagnostics were the exception!

Trying to improve the performance of TryAdd* methods

The final feature in this post covers an improvement that didn't quite make it into .NET 6. I still think it's interesting nevertheless!

The issue in question came to light when performance aficionado Ben Adams was doing his thing, reducing allocations in ASP.NET Core. With a relatively minor change, he reduced the number of closure allocations for Func<ServiceDescriptor, Boolean> from 754 objects to 2 during startup of an ASP.NET MVC app.

While the fix was quickly accepted, a pertinent question was raised by Stephen Toub

How many times are these methods called in a typical startup? The fact that you said it's saving ~750 closures suggests it's called to add that many services, and this is O(N^2) (for each service looking at every other service already added)?

The problem is due to the fact that when most of the ASP.NET Core "core" framework adds services, it checks that it's not adding a duplicate service by using TryAdd* versions of the method. The logging extension method AddLogging(), for example, contains code like this:

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

services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(
    new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));

Because of the "pay-for-play" style of ASP.NET Core, the AddLogging() method might be called by many different components. You don't want to have the ILoggerFactory service descriptor registered multiple times, so TryAdd* first ensures that the service isn't already registered, and only adds the registration if this is the first call.

Unfortunately, currently, checking that a service isn't already registered required enumerating all the services that have already been registered. If we do that every time we add a new service, then you get the O(N²) behaviour described in the above issue. Further investigation by Ben showed this to be the case.

So how to resolve this issue? The approach decided on was to move the existing ServiceCollection implementation class into the Microsoft.Extensions.DependencyInjection.Abstractions package (out of the implementation package), and adding concrete implementations of TryAdd* to the type (instead of the IServiceCollection extension methods they are currently).

These additional methods would use a Dictionary<> to track which services had been registered, reducing the cost of looking up a single service to O(1), and making the startup registration of N types O(N), much better than the existing O(N²).

The ServiceCollection was moved as required in this PR, with a type-forward added to avoid the breaking change to the API, but the PR to add the dictionary to ServiceCollection was never merged 😢 But why?

The reason is summed up in this comment by Eric Erhardt:

Have we verified that the maintenance of an extra dictionary actually improves performance here?

As always with performance, you have to make sure to measure. Otherwise you can't be sure your fix has actually fixed anything! In this case, we're now storing an extra dictionary in the ServiceCollection, in addition to the list of descriptors.

public class ServiceCollection : IServiceCollection
{
    private readonly List<ServiceDescriptor> _descriptors = new();
    private readonly Dictionary<Type, List<ServiceDescriptor>> _serviceTypes = new();
    // ...
}

That's a lot of extra objects being stored, created, managed, so it's not clear-cut that it will definitely improve startup performance. Unfortunately, the performance tests weren't finished in time for .NET 6, so the improvement never made it. Hopefully it returns in a future version of .NET instead!

Summary

In this post I described some of the new features added to the DI libraries in .NET 6. Better support for IAsyncDisposable was added to IServiceScope, the ability to query whether a service is registered in DI was added, and new diagnostics were added. In addition I showed how to use a custom DI container with the new minimal hosting APIs, and talked about a performance feature that didn't make it into .NET 6.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?