我正在将一个库移植到网络核心。我们现在有内置的 DI,我想尽可能地使用它。
我的库将事件从后端代理到信号客户端,它包含一个框架,用于在将事件发送到特定客户端之前验证事件。我的旧库支持逆变,因此如果需要,一个事件处理程序可以支持所有事件。考虑一下这个
public class ContravariantHandler : IEventHandler<BaseEvent>
{
public void Handle(BaseEvent message)
{
}
}
public abstract class BaseEvent { }
public class MyEvent : BaseEvent { }
public class MyEvent2 : BaseEvent { }
public interface IEventHandler<in T>
{
void Handle(T message);
}
在这里做
new ContravariantHandler().Handle(new MyEvent());
和 new ContravariantHandler().Handle(new MyEvent2());
都很好。
我可以让网络核心 DI 在这里处理正确的类型吗?
这行不通,
var provider = new ServiceCollection()
.AddTransient<IEventHandler<BaseEvent>, ContravariantHandler>()
.BuildServiceProvider();
var contravariance = provider.GetService<IEventHandler<MyEvent>>();
我发现自己希望服务提供商具有相同的功能,该功能将在我的图书馆的特定部分内部使用,而不是其他地方。
我的解决方案是将我的
IServiceProvider
包装在一个类中,该类可以通过在服务集合中搜索匹配的同变/逆变服务类型、基于(无可否认是任意的)策略选择一个服务类型并将其传递给底层服务来处理差异创作提供者。
我认为重要的一些注释:
GetVariantService
,因此调用者必须有意调用此行为。 GetService
方法直接传递到底层 IServiceProvider.GetService
,因此,如果此类“天真”地用作常规服务提供者,则不会出现意外行为。IServiceCollection
,因为需要服务集合来查找可能的匹配类型)注意: 此解决方案仅适用于解析顶级服务。它不适用于解决构造函数注入的服务(这些服务将通过“正常”行为解决,因此方差不起作用)
注2:我深入研究了框架代码,以了解
ServiceProvider
如何解决依赖关系,以及我们是否可以在任何地方挂钩来修改行为。不幸的是,答案是否定的。
查找在密封类 Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory
内执行,该类维护着 Type -> (list of applicable service descriptors)
的私有字典。
这一切似乎都非常紧密地结合在一起,没有真正的方法来覆盖行为(可能是最好的......),因此为了实现注入参数的方差解析,似乎需要重新实现
ServiceProvider
及其依赖项从头开始。
代码如下:
public interface IVariantServiceProvider : IServiceProvider {
object? GetVariantService(Type serviceType);
}
public class VariantServiceProvider : IVariantServiceProvider {
private IServiceProvider _serviceProvider;
private IServiceCollection _services;
public VariantServiceProvider(IServiceProvider serviceProvider, IServiceCollection services) {
this._serviceProvider = serviceProvider;
this._services = services;
}
public object? GetService(Type serviceType) {
return this._serviceProvider.GetService(serviceType);
}
public object? GetVariantService(Type serviceType) {
// Variance only applies to interfaces..
if (!serviceType.IsInterface) {
return this.GetService(serviceType);
}
// .. with generics
if (!serviceType.IsConstructedGenericType) {
return this.GetService(serviceType);
}
//
// 1. If serviceType has variant generic parameters,
// list all service descriptors that have compatible type and gen. params.
//
// Are any of our generic params variant?
var genericDef = serviceType.GetGenericTypeDefinition();
var genericParams = genericDef.GetGenericArguments();
if (!genericParams.Any(gp => GetGenericParamVariance(gp) != GenericParameterAttributes.None)) {
// No params have variance
return this.GetService(serviceType);
}
// Find descriptors that match our serviceType
var candidates = new List<ServiceDescriptor>();
foreach (var service in this._services) {
var candidateServiceType = service.ServiceType;
if (!candidateServiceType.IsInterface) {
continue;
}
if (!candidateServiceType.IsGenericType) {
continue;
}
// If this is a catch-all generic definition (not a defined type),
// we don't count it. If no other matches are found, the
// underlying IServiceProvider should pick this up.
if (candidateServiceType.IsGenericTypeDefinition) {
continue;
}
// Check they have the same generic definition
// --
// To remain consistent with Microsoft's ServiceProvider, candidates must have the same
// generic definition as our serviceType (i.e. be the same exact interface, not a derived one)
if (candidateServiceType.GetGenericTypeDefinition() != genericDef) {
continue;
}
// Check that our co/contra-variance matches
if (!serviceType.IsAssignableFrom(candidateServiceType)) {
continue;
}
candidates.Add(service);
}
// If no candidates, fall back on underlying provider
if (!candidates.Any()) {
return this.GetService(serviceType);
}
// If only one candidate, we don't need to try to reduce the
// list
if (candidates.Count == 1) {
return this.GetService(candidates[0].ServiceType);
}
//
// 2. We have multiple candidates. Prioritise them according to the following strategy:
// - Choose candidate whose 1st type arg is closest in the heirarchy to the serviceType's 1st arg
// - If more than one candidate, order by 2nd type arg, and so on.
// - If still more than one candidate after reaching end of type args, use the last service added
//
var serviceTypeParams = serviceType.GetGenericArguments();
var genericParameterCount = genericDef.GenericTypeArguments.Length;
var genericParamIdx = 0;
while (genericParamIdx < genericParameterCount && candidates.Count > 1) {
var serviceTypeParam = serviceTypeParams[genericParamIdx];
var shortlist = new List<ServiceDescriptor>();
var shortlistDistance = 0;
foreach (var candidate in candidates) {
var candidateType = candidate.ServiceType;
var candidateTypeParam = candidateType.GetGenericArguments()[genericParamIdx];
var distance = TypeDistance(serviceTypeParam, candidateTypeParam);
if (distance == -1) {
// This shouldn't happen, because we already ensured that
// one gen. param is assignable to the corresponding other when we selected candidates.
throw new Exception("Failed to get distance between types: " + candidateTypeParam.Name + " and " + serviceTypeParam.Name);
}
if (distance < shortlistDistance) {
shortlistDistance = distance;
shortlist.Clear();
shortlist.Add(candidate);
} else if (distance == shortlistDistance) {
shortlist.Add(candidate);
}
}
// Have we reduced the list?
if (shortlist.Any()) {
candidates = shortlist;
}
genericParamIdx += 1;
}
// If there is still more than one candidate, use the one that was
// added to _services most recently
ServiceDescriptor match;
if (candidates.Count > 1) {
match = candidates.OrderBy(c => this._services.IndexOf(c)).Last();
} else {
match = candidates[0];
}
return this.GetService(match.ServiceType);
}
private static GenericParameterAttributes GetGenericParamVariance(Type genericParam) {
var attributes = genericParam.GenericParameterAttributes;
return attributes & GenericParameterAttributes.VarianceMask;
}
private static int TypeDistance(Type t1, Type t2) {
Type ancestor;
Type derived;
if (t1.IsAssignableTo(t2)) {
ancestor = t2;
derived = t1;
} else if (t2.IsAssignableTo(t1)) {
ancestor = t1;
derived = t2;
} else {
return -1;
}
var distance = 0;
var current = derived;
while (current != ancestor) {
if (current == null) {
return -1;
}
distance += 1;
current = current.BaseType;
}
return distance;
}
}
public static class VariantServiceExtensions {
public static VariantServiceProvider BuildVariantServiceProvider(this IServiceCollection services) {
return new VariantServiceProvider(services.BuildServiceProvider(), services);
}
public static T? GetVariantService<T>(this IVariantServiceProvider provider) {
return (T?) provider.GetVariantService(typeof(T));
}
}
var services = new ServiceCollection();
services.AddTransient<ITest<TypeParamB>, Test>();
var serviceProvider = services.BuildVariantServiceProvider();
// `Test` can be assigned to `ITest<TypeParamA>` via covariance
ITest<TypeParamA> test = new Test();
// Retrieve `Test` via the regular service provider
var regularResult = serviceProvider.GetService<ITest<TypeParamA>>();
Console.WriteLine(regularResult is null); // Output: True
// Retrieve `Test` via the variant service provider
var variantResult = serviceProvider.GetVariantService<ITest<TypeParamA>>();
Console.WriteLine(variantResult is null); // Output: False
//
// CLASS DEFINITIONS
//
public class TypeParamA { }
public class TypeParamB : TypeParamA { }
public interface ITest<out T> { }
public class Test : ITest<TypeParamB> { }
我可以让网络核心 DI 在这里处理正确的类型吗?
没有任何内置机制,不太可能添加。
我们了解这种情况,但添加对此的支持将使与其他容器一起工作变得困难。 https://github.com/aspnet/DependencyInjection/issues/453
但是,您可以引入另一个接口,即
IEventHandler
,解析 IEnumerable<IEventHandler>
并检查每个接口的兼容性。 缺点是这会导致所有事件处理程序被激活。
以下代码
using ContravarianceDependencyInjection; // nuget
var provider = new ServiceCollection()
.AddTransient<IEventHandler<BaseEvent>, ContravariantHandler>()
// REQUIRED: Adds contravariance injection for IEventHandler
.AddContravariance(typeof(IEventHandler<>))
.BuildServiceProvider();
var contravariance = provider.GetService<IEventHandler<MyEvent>>();