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
orout
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 thatT
can be specificallyT
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 typeT
. This is what we need in OPs case to allow types that inherit fromIDetail
to implementIBaseApi<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.