.net.net-coredependency-injection

.NET Core Dependency Injection with inherited interface


I have I1, I2 and Class Foo where I2 inherits I1 (I2:I1) and I2 is implemented in Foo. I want to register a scoped dependency such that always returns the same Foo object when any of the interface is referred within a single scope.

I tried this using the following registrations, but it yield three different instances of Foo within the same scope:

services.AddScoped<Foo>();
services.AddScoped<I1, Foo>();
services.AddScoped<I2, Foo>();

My definitions are as follows:

interface I1
{
    GetData();
}
    
interface I2 : I1
{
    string Data { set; }
}

class Foo : I2
{
    private string myData;

    public string Data { set => myData = value; }

    public string GetData() => this.myData;
}

Fuctionality use.

class Bar
{
    private I2 _i2;
    public Bar(I2 i2) => _i2 = i2;

    public void SetData() => _i2.myData = "My Data";
}

class AnotherBar
{
    private I1 _i1;
    public AnotherBar(I1 i1) => _i1 = i1;

    public string GetData() => _i1.GetData();
}

GetData() from AnotherBar will return the same data which is set at Bar-SetData() during same scope?


Solution

  • This is a common pitfall to watch out for when using MS.DI. The pitfall is called Torn Lifestyle. In the case of MS.DI, each registration gets its own cache. That means that your three registrations:

    services.AddScoped<Foo>();
    services.AddScoped<I1, Foo>();
    services.AddScoped<I2, Foo>();
    

    Each have their own cache and their own instance. So that means the following assertions hold:

    using (var scope = provider.CreateScope())
    {
       Assert.AreSame(scope.GetService<I1>(), scope.GetService<I1>());
       Assert.AreSame(scope.GetService<I2>(), scope.GetService<I2>());
       Assert.AreNotSame(scope.GetService<I1>(), scope.GetService<I2>());
    }
    

    To fix this problem, you'll have to rewrite the registrations to the following:

    services.AddScoped<Foo>();
    services.AddScoped<I1>(c => c.GetRequiredService<Foo>());
    services.AddScoped<I2>(c => c.GetRequiredService<Foo>());
    

    In this case, each registration still has its own cache, but since the second and third request the original Foo service, this ensures all three registrations return the same instance within a single scope.

    The following extension method simplifies things:

    public static void AddScoped<TService1, TService2, TImplementation>(
        this IServiceCollection services)
        where TImplementation : TService1, TService2
    {
        services.AddScoped<TImplementation>();
        services.AddScoped<TService1>(c => c.GetRequiredService<TImplementation>());
        services.AddScoped<TService2>(c => c.GetRequiredService<TImplementation>());
    }
    

    Usage:

    services.AddScoped<I1, I2, Foo>();