Home > Blockchain >  How do I register a service which implements a generic interface which also implements a generic int
How do I register a service which implements a generic interface which also implements a generic int

Time:11-18

I have a scenario such as follows and cannot get my SpecificApi service to register.

    public interface IDetail
    {
        string Name { get; set;}
    }

    public class SpecificDetail : IDetail
    {
        public string Name { get; set; }
    }

    public interface IBaseApi<TDetail> where TDetail: IDetail
    {
        TDetail Method1();
    }

    public interface ISpecificApi<TDetail> : IBaseApi<TDetail> where TDetail : IDetail
    {

    }

    public class SpecificApi : ISpecificApi<SpecificDetail>
    {
        public SpecificDetail Method1()
        {
            return new SpecificDetail();
        }
    }

    public class Consumer
    {
        public  Consumer(ISpecificApi<IDetail> api) // Generic must be of IDetail, not SpecificDetail
        {

        }
    }

I've tried the following to register the service, but with no luck.

// Fails at runtime with System.ArgumentException: 'Open generic service type 'DiGenericsTest.ISpecificApi`1[TDetail]' requires registering an open generic implementation type. (Parameter 'descriptors')'
builder.Services.AddSingleton(typeof(ISpecificApi<>), typeof(SpecificApi));


// Fails at build time with "there is no implicit reference conversion"
builder.Services.AddSingleton<ISpecificApi<IDetail>, SpecificApi>();

// This runs, but then I have to inject ISpecificApi<SpecificDetail> into Consumer instead of ISpecificApi<IDetail>.
builder.Services.AddSingleton<ISpecificApi<SpecificDetail>, SpecificApi>();

builder.Services.AddSingleton<Consumer>();

CodePudding user response:

Just modify your interfaces to be co-variant:

public interface IBaseApi<out TDetail> where TDetail : IDetail
{
    TDetail Method1();
}

public interface ISpecificApi<out TDetail> : IBaseApi<TDetail> where TDetail : IDetail
{

}

After that builder.Services.AddSingleton<ISpecificApi<IDetail>, SpecificApi>(); won't fail.

Example on dotnetfiddle.

See more about covariance and contravariance

CodePudding user response:

It's not necessarily the registration that is the problem, it is the type inference that doesn't work, so this comes up in a lot of different contexts.

Even though SpecificDetail implements IDetail, SpecificApi does not implement ISpecificApi<IDetail> specifically, it only implements the very specific interface: ISpecificApi<SpecificDetail>

  • Your observations already confirm this behavior

That is because you have used the default Invariant interface specification.

  • In generic type declarations, Invariant (default behaviour, no in or out modifier) declarations require the implementation to match exactly. Any implementation detail is internal and cannot be guaranteed.

The traditional work around is to simplify your API by removing the dependency to SpecificDetail from the specification, this has a less desirable side effect of requiring your Method1 return type to be in the form that specifically matches the ISpecificApi<IDetail> definition, returning IDetail :

public class SpecificApi : ISpecificApi<IDetail>
{
    public IDetail Method1()
    {
        return new SpecificDetail();
    }
}

This makes the consumption of SpecificApi ambiguous, it will work, but to access any of the non-IDetail members of SpecificDetail we are forced to cast it to SpecificDetail first or risk a runtime exception.

Even though we know that SpecificApi.Method1 returns SpecificDetail now, the compiler and future developers may decide to return a different implementation of IDetail and our consuming code will not know until runtime that the implementation has changed.

Variance in generics does allow us to work around this: Define variant generic interfaces and delegates and importantly allows us to define constraints on the types of implementations that we want to support on the interfaces that we have defined.

  • Contravariant parameters, declared with the in modifier: IComparer<in T> mean that T can be specifically T or a descendent type that is less derived.

  • Covariant parameters use the out modifier, this allows us to use a more derived type in place of the generic type T. This is what we need in OPs case to allow types that inherit from IDetail to implement IBaseApi<out T>.

Image the following declarations in a base library:

public interface IBaseApi<out TDetail> where TDetail : IDetail
{
    TDetail Method1();
}

public interface ISpecificApi<out TDetail> : IBaseApi<TDetail> where TDetail : IDetail
{

}

You can then use this middleware implementation:

public class SpecificApi : ISpecificApi<SpecificDetail>
{
    public SpecificDetail Method1()
    {
        return new SpecificDetail();
    }
}

You can then register this specific implementation:

builder.Services.AddSingleton<ISpecificApi<IDetail>, SpecificApi>();

And the implementation can now be injected:

public class Consumer
{
    public Consumer(ISpecificApi<IDetail> api)
    {
        this.Api = api;
    }

    ISpecificApi<IDetail> Api { get; }
    IDetail Detail { get => Api.Method1(); }
}

As you can see there is still some ambiguity when we are using injection, although the resolved instance of the api will be SpecificApi it still can't be implicitly typed to SpecificApi and the response from Method() is still at this level constrained to IDetail and can't be implicitly typed to SpecificDetail, but that shouldn't matter.

That is why from an IoC point of view, to the consuming logic, there is little or no difference between using covariance over a fixed implementation of an invariant interface in this scenario.

  • It is also why many developers are not familiar with variance in generics and may not even be aware of the generics they use today that support variance. There is less value to consumers, but can be an invaluable mechanism to control and class library authors.

It is in the implementation logic that variance can give us the additional compiler and intellisense support that will help keep the code cleaner and make your intent more explicit.

  •  Tags:  
  • c#
  • Related