Support constrained open generic types
jbogard opened this issue · 9 comments
If I register a collection of open generics, some of the open generics may be constrained. Consider the interface and implementations:
public interface IFakeOpenGenericService<TValue>
{
TValue Value { get; }
}
public class FakeOpenGenericService<TVal> : IFakeOpenGenericService<TVal>
{
public FakeOpenGenericService(TVal value)
{
Value = value;
}
public TVal Value { get; }
}
public class ConstrainedFakeOpenGenericService<TVal> : IFakeOpenGenericService<TVal>
where TVal : PocoClass
{
public ConstrainedFakeOpenGenericService(TVal value)
{
Value = value;
}
public TVal Value { get; }
}
If I register multiple services:
collection.AddTransient(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>));
collection.AddTransient(typeof(IFakeOpenGenericService<>), typeof(ConstrainedFakeOpenGenericService<>));
And try to resolve, the version that adheres to all constraints works, but if I try to get a collection with a generic parameter that doesn't match the constraint, I get an exception:
var allServices = provider.GetServices<IFakeOpenGenericService<PocoClass>>().ToList();
var constrainedServices = provider.GetServices<IFakeOpenGenericService<IFakeSingletonService>>().ToList();
Exception:
System.ArgumentException : GenericArguments[0], 'Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeSingletonService', on 'Microsoft.Extensions.DependencyInjection.Specification.Fakes.ConstrainedFakeOpenGenericService`1[TVal]' violates the constraint of type 'TVal'.
---- System.TypeLoadException : GenericArguments[0], 'Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeSingletonService', on 'Microsoft.Extensions.DependencyInjection.Specification.Fakes.ConstrainedFakeOpenGenericService`1[TVal]' violates the constraint of type parameter 'TVal'.
Stack Trace:
at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
at System.RuntimeType.MakeGenericType(Type[] instantiation)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceLookup\GenericService.cs(26,0): at Microsoft.Extensions.DependencyInjection.ServiceLookup.GenericService.GetService(Type closedServiceType)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceLookup\ServiceTable.cs(102,0): at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceTable.TryGetEntry(Type serviceType, ServiceEntry& entry)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceLookup\OpenIEnumerableService.cs(28,0): at Microsoft.Extensions.DependencyInjection.ServiceLookup.OpenIEnumerableService.GetService(Type closedServiceType)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceLookup\ServiceTable.cs(102,0): at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceTable.TryGetEntry(Type serviceType, ServiceEntry& entry)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceProvider.cs(114,0): at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetServiceCallSite(Type serviceType, ISet`1 callSiteChain)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceProvider.cs(73,0): at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(Type serviceType, ServiceProvider serviceProvider)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\Internal\ConcurrentDictionaryExtensions.cs(24,0): at System.Collections.Concurrent.ConcurrentDictionaryExtensions.GetOrAdd[TKey,TValue,TArg](ConcurrentDictionary`2 dictionary, TKey key, Func`3 valueFactory, TArg arg)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection\ServiceProvider.cs(64,0): at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection.Abstractions\ServiceProviderServiceExtensions.cs(56,0): at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection.Abstractions\ServiceProviderServiceExtensions.cs(79,0): at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection.Abstractions\ServiceProviderServiceExtensions.cs(95,0): at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetServices[T](IServiceProvider provider)
C:\dev\aspnet-DependencyInjection\src\Microsoft.Extensions.DependencyInjection.Specification.Tests\DependencyInjectionSpecificationTests.cs(561,0): at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.ConstrainedOpenGenericServicesCanBeResolved()
----- Inner Stack Trace -----
at System.RuntimeTypeHandle.Instantiate(RuntimeTypeHandle handle, IntPtr* pInst, Int32 numGenericArgs, ObjectHandleOnStack type)
at System.RuntimeTypeHandle.Instantiate(Type[] inst)
at System.RuntimeType.MakeGenericType(Type[] instantiation)
Expected: constrained open generics are tested before attempted to instantiate. It looks like the only way to reliably test is to try-catch-swallow, see http://stackoverflow.com/a/4864565/58508
PR with failing test coming shortly.
Expected: constrained open generics are tested before attempted to instantiate. It looks like the only way to reliably test is to try-catch-swallow, see http://stackoverflow.com/a/4864565/58508
That's pretty unfortunate.
What's the scenarios for this?
I'm adding a collection of filters for my own in-memory pipes-and-filters implementation in MediatR. My real interface looks like:
public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();
public interface IPipelineBehavior<TRequest, TResponse>
{
Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next);
}
These behaviors act like filters around a core IHandler<TRequest, TResponse>
implementation. Then a common scenario is to have some sort of request pre-processor, say to supplement a request with additional information or to mutate the information. One example we had was inputs coming from an optical scanner might need leading zeroes trimmed/added. We could register a pipeline behavior:
public class TrimOrderNumberLeadingZeros<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IContainOrderNumber
{
public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next)
{
request.OrderNumber = message.OrderNumber.TrimLeadingZeros();
return next();
}
}
If I made the behavior closed instead of constrained, it conflicts with rules around "favor closed over open" (which is consistent with at least StructureMap). So if I keep it an open generic type, but merely constrain it, then I can get the correct IEnumerable<TService>
based on which implementations of TService
can successfully close the open generic type.
I assume some F# person is going to read all this and laugh one day.
See the StructureMap implementation:
We are not planning to include this feature here, in part because it doesn't seem to be broadly available in other DI systems and demand doesn't seem very high. We're also concerned about the first chance exception that gets thrown.
@Eilon Is there any way to override some defaults so that we could get the expected behavior mentioned by @jbogard?
Expected: constrained open generics are tested before attempted to instantiate.
NOTE: Same use can be solved using many other DI containers such as Autofac, DryIoC, LightInject, StructureMap, Unity and Castle Windsor. See: MediatR Container-Feature-Support, Test source code
I'm trying to implement something similar with constrained generics. Would be super helpful to have this built into the DI system.
@jbogard have you had any luck with a work around for this?
No, I had to pick one of the other containers. We were trying to get away with just using the built-in container but it's just not possible today.
In case anyone is wondering, the following containers support this feature:
- Autofac
- DryIoc
- LightInject
- Simple Injector
- StructureMap
- Lamar
- Castle Windsor
The following do not:
- This one
- Ninject
- Unity
The latter two I thought were dead but seem to be undergoing a revival of sorts.