Microsoft的ServiceProvider(启用财产注入)中的自定义解析处理程序
#编程 #c #net #core

原始帖子,网址:https://siderite.dev/blog/custom-resolve-handler-in-microsofts-serviceprovid/

介绍

此博客上访问量最多的一些帖子与dependency injection in .NET有关。如您所知,几乎从一开始就已经在ASP.NET中烘烤了依赖性注入,但它以MVC框架和.NET Core Rewrite的重写达到顶峰。依赖注入已分为包装,可以从任何地方使用。但是,可能是因为他们认为这是一个核心的概念,或者可能是因为自UnityContainer时代以来就出现了代码,整个机制已被密封,内部化且没有任何添加自定义代码的挂钩。在我看来,这是疯狂的,因为依赖注入的目的是班级实例化的一种变化的目的。

现在,公平地说,我不是.NET代码中依赖性注入的设计模式的专家。可能有一些奇怪的方式可以扩展我不知道的代码。在这种情况下,请照亮我。但是,就我的代码而言,这是我发现将自己的钩子插入分辨率过程的最简单方法。如果您只想要代码,请跳至末尾。

使用AT

首先,关于如何在控制台应用中使用依赖注入(从头开始)的回顾:

// you need the nuget packages Microsoft.Extensions.DependencyInjection 
// and Microsoft.Extensions.DependencyInjection.Abstractions
using Microsoft.Extensions.DependencyInjection;
...

// create a service collection
var services = new ServiceCollection();
// add the mappings between interface and implementation
services.AddSingleton<ITest, Test>();
// build the provider
var provider = services.BuildServiceProvider();

// get the instance of a service
var test = provider.GetService<ITest>();

请注意,这是一个非常简化的方案。有关更多详细信息,请检查Creating a console app with Dependency Injection in .NET Core

建议的DI模式

第二,推荐使用依赖注入(来自微软和我自己)的建议方法是...构造仪注入。它有两个目的:

  1. 它声明构造函数中对象的所有依赖关系。您可以放心,您所有的工作都需要工作。
  2. 当构造函数开始填写页面时,您会得到一个很大的提示,您的班级可能会做太多事情,您应该将其分开。

,但再说一次,有“学习规则。掌握规则。违反规则”概念。在撰写这篇文章之前,我已经熟悉了自己,这样我就可以安全地打破第二部分,而在破解东西之前不能掌握任何东西。我现在谈论的是物业注入,这通常是有充分理由皱眉的,但是人们可能想在与班级功能相邻的场景中使用,例如记录。总是困扰着我的一件事是必须在每个构造函数中声明一个记录器,即使记录仪本身对班级的功能没有任何作用。

所以我有一个想法,除了记录以外,我将在各处使用构造函数依赖注入。我将创建一个ilogger 属性,该属性将在分辨率时自动注入正确的实现。只有一个问题:微软的依赖注入不支持财产注入或分辨率钩子(据我所知)。所以我想到了解决方案。

它是如何工作的?

第三,关于服务提供者的工作方式的小回顾。

当一个人做services.BuildServiceProvider()时,他们实际上称之为 new ServiceProvider(Services,SomeserviceProviderOptions)的扩展方法。只有那个constructor is internal,所以您不能自己使用它。然后,在提供商类中,GetService method使用服务访问者的同时介入来获取您的服务。如果不存在服务访问者,则将使用字段 _CREATESERVICEACCESSOR 的方法。因此,我的解决方案是:用包装器替换字段值,该包装器也将执行我们自己的代码。

解决方案

在向您展示代码之前,请注意这适用于.NET 7.0。我猜它在大多数.NET核心版本中都可以使用,但是它们可以更改内部字段名称或功能,在这种情况下可能会破坏。

最后,这是代码:

public static class ServiceProviderExtensions
{
    /// <summary>
    /// Adds a custom handler to be executed after service provider resolves a service
    /// </summary>
    /// <param name="provider">The service provider</param>
    /// <param name="handler">An action receiving the service provider, 
    /// the registered type of the service 
    /// and the actual instance of the service</param>
    /// <returns>the same ServiceProvider</returns>
    public static ServiceProvider AddCustomResolveHandler(this ServiceProvider provider,
                 Action<IServiceProvider, Type, object> handler)
    {
        var field = typeof(ServiceProvider).GetField("_createServiceAccessor",
                        BindingFlags.Instance | BindingFlags.NonPublic);
        var accessor = (Delegate)field.GetValue(provider);
        var newAccessor = (Type type) =>
        {
            Func<object, object> newFunc = (object scope) =>
            {
                var resolver = (Delegate)accessor.DynamicInvoke(new[] { type });
                var resolved = resolver.DynamicInvoke(new[] { scope });
                handler(provider, type, resolved);
                return resolved;
            };
            return newFunc;
        };
        field.SetValue(provider, newAccessor);
        return provider;
    }
}

如您所见,我们参加了原始的访问者委托,并将其替换为在服务实例化后立即运行自己的处理程序的版本。

填充记录器属性

我们现在可以使用它来进行财产注入:

static void Main(string[] args)
{
    var services = new ServiceCollection();
    services.AddSingleton<ITest, Test>();
    var provider = services.BuildServiceProvider();
    provider.AddCustomResolveHandler(PopulateLogger);

    var test = (Test)provider.GetService<ITest>();
    Assert.IsNotNull(test.Logger);
}

private static void PopulateLogger(IServiceProvider provider, Type type, object service)
{
    if (service is null) return;
    var propInfo = service.GetType().GetProperty("Logger",BindingFlags.Instance|BindingFlags.Public);
    if (propInfo is null) return;
    var expectedType = typeof(ILogger<>).MakeGenericType(service.GetType());
    if (propInfo.PropertyType != expectedType) return;
    var logger = provider.GetService(expectedType);
    propInfo.SetValue(service, logger);
}

查看我如何添加了我正在寻找像
的属性的Populatelogger处理程序

public ILogger<Test> Logger { get; private set; }

(iLogger的通用类型与班级相同)并填充它。

填充任何装饰的物业

当然,这有点丑陋。如果您想启用财产注入,为什么不使用使您的意图清晰且需要减少反思的属性呢?美好的。让我们这样做:

// Add handler
provider.AddCustomResolveHandler(InjectProperties);
...

// the handler populates all properties that are decorated with [Inject]
private static void InjectProperties(IServiceProvider provider, Type type, object service)
{
    if (service is null) return;
    var propInfos = service.GetType()
        .GetProperties(BindingFlags.Instance | BindingFlags.Public)
        .Where(p => p.GetCustomAttribute<InjectAttribute>() != null)
        .ToList();
    foreach (var propInfo in propInfos)
    {
        var instance = provider.GetService(propInfo.PropertyType);
        propInfo.SetValue(service, instance);
    }
}
...

// the attribute class
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class InjectAttribute : Attribute {}

结论

我已经演示了如何添加一个自定义处理程序以在默认的Microsoft ServiceProvider类解决任何服务实例之后执行的自定义处理程序,该类又可以启用属性注入,所有类的更改点等等。进入代理,将自动跟踪所有属性和方法与其参数。您可以选择上面的代码插入其中。

警告说,该解决方案正在使用反射来更改.NET 7.0 ServiceProvider类的功能,并且,如果那里的代码出于某种原因更改,则可能需要将其适应最新功能。

如果您知道这样做的更优雅的方式,请让我知道。

希望它有帮助!