Microsoft 依赖注入和逆变

问题描述 投票:0回答:3

我正在将一个库移植到网络核心。我们现在有内置的 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>>();
c# dependency-injection .net-core polymorphism contravariance
3个回答
1
投票

我发现自己希望服务提供商具有相同的功能,该功能将在我的图书馆的特定部分内部使用,而不是其他地方。

我的解决方案是将我的

IServiceProvider
包装在一个类中,该类可以通过在服务集合中搜索匹配的同变/逆变服务类型、基于(无可否认是任意的)策略选择一个服务类型并将其传递给底层服务来处理差异创作提供者。

我认为重要的一些注释:

  • 它是作为一个单独的方法实现的,
    GetVariantService
    ,因此调用者必须有意调用此行为。
    GetService
    方法直接传递到底层
    IServiceProvider.GetService
    ,因此,如果此类“天真”地用作常规服务提供者,则不会出现意外行为。
  • 要使用此功能,您必须控制服务提供者的创建(或者至少有权访问源
    IServiceCollection
    ,因为需要服务集合来查找可能的匹配类型)

注意: 此解决方案仅适用于解析顶级服务。它不适用于解决构造函数注入的服务(这些服务将通过“正常”行为解决,因此方差不起作用)

注2:我深入研究了框架代码,以了解

ServiceProvider
如何解决依赖关系,以及我们是否可以在任何地方挂钩来修改行为。不幸的是,答案是否定的。 查找在密封类
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory
内执行,该类维护着
Type -> (list of applicable service descriptors)
的私有字典。

这一切似乎都非常紧密地结合在一起,没有真正的方法来覆盖行为(可能是最好的......),因此为了实现注入参数的方差解析,似乎需要重新实现

ServiceProvider
及其依赖项从头开始。


代码如下:

  1. 服务提供商和接口
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;
    }

}
  1. 扩展方法,与MS内置类似。这些并不广泛,仅包含我需要的。
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));
    }

}
  1. 使用示例:
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> { }

0
投票

我可以让网络核心 DI 在这里处理正确的类型吗?

没有任何内置机制,不太可能添加。

我们了解这种情况,但添加对此的支持将使与其他容器一起工作变得困难。 https://github.com/aspnet/DependencyInjection/issues/453

但是,您可以引入另一个接口,即

IEventHandler
,解析
IEnumerable<IEventHandler>
并检查每个接口的兼容性。 缺点是这会导致所有事件处理程序被激活。


0
投票

以下代码

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>>();
© www.soinside.com 2019 - 2024. All rights reserved.