Registering a Type as an Interface and as Self With ASP.NET Core Dependency Injection
In this post, we go over how to use the built-in dependency injection that comes with the ASP.NET Core web development framework. Read on to get started!
Join the DZone community and get the full member experience.
Join For FreeWhile I am a big fan of using Autofac to serve as the framework for making Inversion of Control (IoC) and Dependency Injection (DI) work in an application, it is quite convenient in simple projects to use the built-in dependency injection in ASP.NET Core.
While it's simple to replace the default one with Autofac, the default one is often sufficient. Unless it's not!
Consider the following component registration:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddTransient<ICustomerService, DefaultCustomerService>();
// ...
}
}
With the default Microsoft.Extensions.DependencyInjection package in ASP.NET Core, we can now consume an ICustomerService
in, for example, our controllers:
public class SupportController
{
// DefaultCustomerService will be injected here:
public SupportController(ICustomerService customerService)
{
// ...
}
}
The above code will work fine, and where we expect a ICustomerService
, we'll receive a DefaultCustomerService
because that is what we registered.
Now, what will happen if at some point we want to be more specific? Let's say we have another controller where we really want to get a concrete DefaultCustomerService
injected?
public class AnotherController
{
public AnotherController(DefaultCustomerService customerService)
{
// ...
}
}
This will blow up with a nice exception!
An unhandled exception occurred while processing the request. InvalidOperationException: Unable to resolve service for type 'DefaultCustomerService' while attempting to activate 'AnotherController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired)
The reason for the InvalidOperationException
we get is that the service collection only contains a registration for ICustomerService
, and not for DefaultCustomerService
.
How to Register a Type as an Interface and as Self
So how can we solve this and register a type as an interface and as self? Googling/Binging/DuckDuckGoing reveals many developers who ran into this issue, and recommend registering our service twice, in any of the following forms:
// Using a factory to fetch the previously registered type
services.AddTransient<ICustomerService, DefaultCustomerService>();
services.AddTransient<DefaultCustomerService>(
provider => provider.GetService<ICustomerService>());
// Using a concrete instance during registration,
// with the downside that this will always be registered as a singleton
// (and that we lose dependency injection in DefaultCustomerService itself)
var customerService = new DefaultCustomerService();
services.AddTransient<ICustomerService>(customerService);
services.AddTransient<DefaultCustomerService>(customerService);
All of the above will work, but it looks... ugly. Especially when a few instances need to be registered like this. Wouldn't it be nice if ASP.NET Core's built-in service collection supported registering types as a specific interface, and AsSelf()
, much like Autofac supports? Something like this:
services.AddTransient<ICustomerService, DefaultCustomerService>().AsSelf();
The idea behind the ASP.NET Core dependency injection was that it should be sufficient for most scenarios, and that for more fine-grained control of dependencies we can always plug in another framework. But this is one of those cases where we'd need an overhaul of service registrations... Why not "make it work"?
Building an AsSelf()
Extension Method
We can easily write an AsSelf()
extension method on IServiceCollection
, so that we can register a type "as self" as well. Our goal will be to write an extension method that pretty much resembles this solution from above:
// Using a factory to fetch the previously registered type
services.AddTransient<ICustomerService, DefaultCustomerService>();
services.AddTransient<DefaultCustomerService>(
provider => provider.GetService<ICustomerService>());
Disclaimer: this is not a perfect solution in all cases and can easily go wrong with more registrations. Plug another framework when needed!
Boiler-Plate Extension Method
With that out of the way, let's start. We'll extend IServiceCollection
, and since we want the code to flow a bit nicer where AsSelf()
will follow the previous registration, we are interested in that one:
public static class ServiceCollectionHostedServiceExtensions
{
/// <summary>
/// Register the last registration as its own type.
/// </summary>
/// <returns>The original <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" />.</returns>
public static IServiceCollection AsSelf(this IServiceCollection services)
{
var lastRegistration = services.LastOrDefault();
if (lastRegistration != null)
{
// TODO
}
return services;
}
}
Hardening
Let's also add some hardening: if the previous registration is already registered and the service type and implementation type are the same, we have no more work to do:
var implementationType = GetImplementationType(lastRegistration);
// When the last registration service type was already registered
// as its implementation type, bail out.
if (lastRegistration.ServiceType == implementationType)
{
return services;
}
Note that we use a GetImplementationType()
helper method which is in the full extension method code below — it helps us find the implementation type regardless of whether the previous registration was a concrete instance registration, a type registration, or a factory registration.
Registering a Single Instance "as Self"
Next up, let's cover the easiest case first: instance registration. Concrete instances are always registered as a singleton in ASP.NET Core's dependency injection framework, so we can mimic this behavior:
if (lastRegistration.ImplementationInstance != null)
{
// Register "self" registration as the same instance
services.Add(new ServiceDescriptor(
implementationType,
lastRegistration.ImplementationInstance));
}
That's it, really. We repeat the last registration, just with the first argument passed to ServiceDescriptor
as the implementation type instead of the service type. Our instance is now registered twice: once as the original registration, and once as its own type.
Side-Step to Registering a Type or a Type Factory "as Self"
Next up: type registration and factory registration. This is a little bit more complex because there is an edge case we must cover... let's side-step for a bit. The code we GoogleBingDucked previously has an issue waiting to happen. Look at this registration:
services.AddTransient<ICustomerService, DefaultCustomerService>();
services.AddTransient<DefaultCustomerService>(
provider => provider.GetService<ICustomerService>());
Now what happens when we have multiple ICustomerService
registered?
services.AddTransient<ICustomerService, OtherCustomerService>();
services.AddTransient<ICustomerService, DefaultCustomerService>();
services.AddTransient<DefaultCustomerService>(
provider => provider.GetService<ICustomerService>());
Exactly: our concrete type registration may return OtherCustomerService
in this case (I haven't actually tried to run this, but it looks suspicious, right?)
So we want to rewrite our registration for this case, and come up with this instead:
services.AddTransient<ICustomerService, OtherCustomerService>();
services.AddTransient<DefaultCustomerService>();
services.AddTransient<ICustomerService>(
provider => provider.GetService<DefaultCustomerService>());
This would leave our original intent intact (multiple ICustomerService
, but at least DefaultCustomerService
would resolve the correct type.
Registering a Type or a Type Factory "as Self"
Back to our extension method! We want to start by removing the previous service registration. That's easy, right?
// Remove last registration
services.Remove(lastRegistration);
Next, we want to register our implementation type first, either using a factory method or as a type registration:
// Register "self" registration first
if (lastRegistration.ImplementationFactory != null)
{
// Factory-based
services.Add(new ServiceDescriptor(
implementationType,
lastRegistration.ImplementationFactory,
lastRegistration.Lifetime));
}
else
{
// Type-based
services.Add(new ServiceDescriptor(
implementationType,
implementationType,
lastRegistration.Lifetime));
}
Since we removed the original registration, let's re-add it with a small modification: instead of being a full copy of the original registration, we will use a factory method to resolve the implementation type we just registered:
// Re-register last registration, proxying our specific registration
services.Add(new ServiceDescriptor(
lastRegistration.ServiceType,
provider => provider.GetService(implementationType),
lastRegistration.Lifetime));
This method now helps us to register a type with the service collection twice, once as a service type and once as its own type, as intended:
services.AddTransient<ICustomerService, DefaultCustomerService>().AsSelf();
Completed Extension Method
Here's the complete extension method:
public static class ServiceCollectionHostedServiceExtensions
{
/// <summary>
/// Register the last registration as its own type.
/// </summary>
/// <returns>The original <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" />.</returns>
public static IServiceCollection AsSelf(this IServiceCollection services)
{
var lastRegistration = services.LastOrDefault();
if (lastRegistration != null)
{
var implementationType = GetImplementationType(lastRegistration);
// When the last registration service type was already registered
// as its implementation type, bail out.
if (lastRegistration.ServiceType == implementationType)
{
return services;
}
if (lastRegistration.ImplementationInstance != null)
{
// Register "self" registration as the same instance
services.Add(new ServiceDescriptor(
implementationType,
lastRegistration.ImplementationInstance));
}
else
{
// Remove last registration
services.Remove(lastRegistration);
// Register "self" registration first
if (lastRegistration.ImplementationFactory != null)
{
// Factory-based
services.Add(new ServiceDescriptor(
lastRegistration.ImplementationType,
lastRegistration.ImplementationFactory,
lastRegistration.Lifetime));
}
else
{
// Type-based
services.Add(new ServiceDescriptor(
lastRegistration.ImplementationType,
lastRegistration.ImplementationType,
lastRegistration.Lifetime));
}
// Re-register last registration, proxying our specific registration
services.Add(new ServiceDescriptor(
lastRegistration.ServiceType,
provider => provider.GetService(implementationType),
lastRegistration.Lifetime));
}
}
return services;
}
private static Type GetImplementationType(ServiceDescriptor descriptor)
{
if (descriptor.ImplementationType != null)
{
return descriptor.ImplementationType;
}
if (descriptor.ImplementationInstance != null)
{
return descriptor.ImplementationInstance.GetType();
}
if (descriptor.ImplementationFactory != null)
{
return descriptor.ImplementationFactory.GetType().GenericTypeArguments[1];
}
return null;
}
}
Again, this is not a perfect solution in all cases and can easily go wrong with more registrations. Plug another framework when needed! But for the simple cases where double registration is needed, this solution will work.
Enjoy!
Published at DZone with permission of Maarten Balliauw, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments