blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

How to register a service with multiple interfaces in ASP.NET Core DI

In this post I describe how to register a concrete class with multiple public interfaces in the Microsoft.Extensions.DependencyInjection container used in ASP.NET Core. With this approach you'll be able to retrieve the concrete class using any of the interfaces it implements. For example, if you have the following class:

public class MyTestClass: ISomeInterface, ISomethingElse { }

then you'll be able to inject either ISomeInterface or ISomethingElse and you will receive the same MyTestClass instance.

Its important that you register the MyTestClass in a specific way to avoid unexpected lifetime issues, such as having two instances of a singleton!

In this post I give a brief overview of the DI container in ASP.NET Core and some of its limitations compared to third party containers. I'll then describe the concept of "forwarding" requests for multiple interfaces to a concrete type, and how you can achieve this with the ASP.NET Core DI container.

TL;DR The ASP.NET Core DI container doesn't natively support registering an implementation as multiple services (sometimes called "forwarding"). Instead, you have to manually delegate resolution of the service to a factory function, e.g services.AddSingleton<IFoo>(x=> x.GetRequiredService<Foo>())

Dependency Injection in ASP.NET Core

One of the key features of ASP.NET Core is its use of dependency injection (DI). The framework is designed around a "conforming container" abstraction that allows the framework itself to use a simple container, while also allowing you to plug in more feature-rich third-party containers.

The "conforming container" idea is not without controversy - I suggest reading this post by Mark Seemann about conforming containers as an anti pattern, or this from the the SimpleInjector team about the ASP.NET Core DI container specifically.

To make the conforming container as simple as possible for third-party containers to implement, it exposes a very limited number of APIs. For a given service (e.g. IFoo), you can define the concrete class that implements it (e.g. Foo), and the lifetime it should have (e.g. Singleton). There are variations on this where you can directly provide an instance of the service, or you can provide a factory method, but that's about as complex as you can get.

In contrast, third-party DI containers in .NET often provide more advanced registration APIs. For example, many DI containers expose a "scan" API for configuration, in which you can search through all types in an assembly, and add them to your DI container. The following is an Autofac example:

var dataAccess = Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(dataAccess) // find all types in the assembly
       .Where(t => t.Name.EndsWith("Repository")) // filter the types
       .AsImplementedInterfaces()  // register the service with all its public interfaces
       .SingleInstance(); // register the services as singletons

In this example, Autofac will find all concrete classes in the assembly who's name ends with "Repository", and register them in the container against any public interfaces they implement. So for example, given the following classes and interfaces:

public interface IStubRepository {}
public interface ICachingRepository {}

public class StubRepository : IStubRepository {}
public class MyRepository : ICachingRepository {}

The previous Autofac code is equivalent to manually registering both classes with their respective interfaces in the ASP.NET Core container in Startup.ConfigureServices:

services.AddSingleton<IStubRepository, StubRepository>();
services.AddSingleton<ICachingRepository, MyRepository>();

But what happens if a class implements multiple interfaces?

Registering a single implementation as multiple services

It's pretty common to see classes that implement multiple interfaces, for example:


public interface IBar {}
public interface IFoo {}

public class Foo : IFoo, IBar {}

Lets write a quick test to see what happens if we register the class against both interfaces using the ASP.NET Core DI container:

[Fact]
public void WhenRegisteredAsSeparateSingleton_InstancesAreNotTheSame()
{
    var services = new ServiceCollection();

    services.AddSingleton<IFoo, Foo>();
    services.AddSingleton<IBar, Foo>();

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<IFoo>(); // An instance of Foo
    var foo2 = provider.GetService<IBar>(); // An instance of Foo

    Assert.Same(foo1, foo2); // FAILS
}

We registered Foo as a singleton for both IFoo and IBar, but the result might not be what you expect. We actually have two instances of our Foo "Singleton", one for each service it was registered as.

Forwarding requests for a service

The general pattern of having an implementation registered against multiple services is a common one. Most third-party DI containers have this concept built in. For example:

  • Autofac uses this behaviour by default - the previous test would have passed
  • Windsor has the concept of "forwarded types" which allows you to "forward" multiple services to a single implementation
  • StructureMap (now sunsetted) had a similar concept of "forwarded" types. As far as I can tell, it's successor, Lamar, doesn't yet, but I could be wrong on that one.

Given this requirement is quite common, it might seem odd that it's not possible with the ASP.NET Core DI container. The issue was raised (by David Fowler) over 2 years ago, but it was closed. Luckily there's a couple of conceptually simple, if somewhat inelegant, solutions.

1. Provide an instance of the service (Singleton only)

The simplest approach is to provide an instance of Foo when you're registering your services. Each registered service will return the exact instance you provided when requested, ensuring there is only ever a single instance.

[Fact]
public void WhenRegisteredAsInstance_InstancesAreTheSame()
{
    var foo = new Foo(); // The singleton instance
    var services = new ServiceCollection();

    services.AddSingleton<IFoo>(foo);
    services.AddSingleton<IBar>(foo);

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<IFoo>();
    var foo2 = provider.GetService<IBar>();

    Assert.Same(foo1, foo); // PASSES;
    Assert.Same(foo2, foo); // PASSES;
}

There's one big caveat with this - you have to be able to instantiate Foo at configuration time, and you have to know and provide all of it's dependencies. This may work for you in some cases, but it's not very flexible.

Additionally, you can only use this approach for registering Singletons. If you want Foo to be a single instance per-request-scope (Scoped), then you're out of luck. Instead, you'll need to use the following technique.

2. Implement forwarding using factory methods

If we break down our requirements, then an alternative solution pops out:

  • We want our registered service (Foo) to have a specific lifetime (e.g. Singleton or Scoped)
  • When IFoo is requested, return the instance of Foo
  • When IBar is requested, also return the instance of Foo

From those three rules, we can write another test:

[Fact]
public void WhenRegisteredAsForwardedSingleton_InstancesAreTheSame()
{
    var services = new ServiceCollection();

    services.AddSingleton<Foo>(); // We must explicitly register Foo
    services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>()); // Forward requests to Foo
    services.AddSingleton<IBar>(x => x.GetRequiredService<Foo>()); // Forward requests to Foo

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<Foo>(); // An instance of Foo
    var foo2 = provider.GetService<IFoo>(); // An instance of Foo
    var foo3 = provider.GetService<IBar>(); // An instance of Foo

    Assert.Same(foo1, foo2); // PASSES
    Assert.Same(foo1, foo3); // PASSES
}

In order to "forward" requests for an interface to the concrete type you must do two things:

  • Explicitly register the concrete type using services.AddSingleton<Foo>()
  • Delegate requests for the interfaces to the concrete type by providing a factory function: services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>())

With this approach, you will have a true singleton instance of Foo, no matter which implemented service you request.

This approach to providing "forwarded" types was noted in the original issue, along with a caveat - it's not very efficient. The "service-locator style" GetService() invocation is generally best avoided where possible. However, I feel it's definitely the preferable course of action in this case.

Summary

In this post I described what happens if you register a concrete type as multiple services with the ASP.NET Core DI service. In particular, I showed how you could end up with multiple copies of Singleton objects, which could lead to subtle bugs. To get around this, you can either provide an instance of the service at registration time, or you can use factory methods to delegate resolution of the service. Using factory methods is not very efficient, but is generally the best approach.

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