在 .NET 7 中使用 Scrutor 自动依赖注入

作者:微信公众号:【架构师老卢】
3-17 14:55
80

概述:在特定场景中,手动注册注入到我们的类中所需的每个服务可能会费力且重复。例如,如果我们拥有一个具有多个实现类(如 、 、 等)的接口,则每个类都需要在 .IRepositoryCustomerRepositoryProductRepositoryOrderRepositoryServiceCollectionScrutor是一个利用框架简化依赖注入注册过程的库。它有助于加载程序集和注册满足特定条件的类型,例如实现特定接口或携带不同属性。Microsoft.Extensions.DependencyInjection在本文中,我们将探讨如何在 .NET 7 中简洁明了地简化依赖项注入。Scruto

在特定场景中,手动注册注入到我们的类中所需的每个服务可能会费力且重复。例如,如果我们拥有一个具有多个实现类(如 、 、 等)的接口,则每个类都需要在 .IRepositoryCustomerRepositoryProductRepositoryOrderRepositoryServiceCollection

Scrutor是一个利用框架简化依赖注入注册过程的库。它有助于加载程序集和注册满足特定条件的类型,例如实现特定接口或携带不同属性。Microsoft.Extensions.DependencyInjection

在本文中,我们将探讨如何在 .NET 7 中简洁明了地简化依赖项注入。Scrutor

先决条件

  • Visual Studio 2022 (.NET 7.0)
  • NuGet Packages:Scrutor

溶液

要使用 ,您应该在实例上调用该方法,并提供一个 lambda 表达式,用于定义如何识别和注册您的服务。例如,下面的代码将检查当前程序集并注册所有公共的非抽象类,这些类实现共享名称但减去初始“I”的接口:ScrutorScanIServiceCollection

public static IServiceCollection AddClassesMatchingInterfaces(this IServiceCollection services, string @namespace)  
{  
    var assemblies = DependencyContext.Default.GetDefaultAssemblyNames()  
                    .Where(assembly => assembly.FullName.StartsWith(@namespace))  
                    .Select(Assembly.Load);  
  
    services.Scan(scan => scan.FromAssemblies(assemblies)  
                              .AddClasses()  
                              .UsingRegistrationStrategy(RegistrationStrategy.Skip)  
                              .AsMatchingInterface()  
                              .WithScopedLifetime());  
  
    return services;  
}

这类似于单独注册每个服务,如下所示:

services.AddScoped<ICustomerService, CustomerService>();  
services.AddScoped<IUserService, UserService>();  
services.AddScoped<IBusinessService, BusinessService>();

这种方法使您的整洁更加整洁:Program.cs

var builder = WebApplication.CreateBuilder(args);  
  
// Additional setup...  
  
builder.Services.AddClassesMatchingInterfaces(nameof(Sample));  
  
var app = builder.Build();  
  
// Application configuration...  
  
app.Run();

Scrutor可以节省大量代码,使您的注册更加一致和可维护。

还可以使用其他方法来优化类型,例如 、 或:TakingAttributeAssignableToInNamespace

services.Scan(scan => scan  
    .FromAssembliesOf(typeof(IUserService))  
    .AddClasses(classes => classes.HavingAttribute<CustomServiceAttribute>())  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

一个值得注意的功能是能够注册服务,这意味着服务使用自己的类型作为服务类型而不是接口或基类进行注册。当您想要按具体类型解析服务时,或者当服务没有接口或基类时,这可能很有用。ScrutorAsSelf

services.Scan(scan => scan  
    .FromAssembliesOf(typeof(IUserService))  
    .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Service")))  
    .AsSelf()  
    .WithScopedLifetime());

可以使用不同的方法和选项自定义注册依赖注入的过程,例如:

  • 指定要扫描的不同程序集或命名空间。
  • 按名称、属性或自定义谓词筛选类。
  • 将类注册为自身、特定接口或所有实现的接口。
  • 使用不同的生存期(单一实例、作用域、瞬态)或自定义函数。
  • 覆盖或修饰现有注册。

使用 Scrustor 进行服务注册的最佳实践

在 .NET 项目中使用 Scrutor 进行服务注册时,遵循最佳做法可确保依赖项注入设置可靠且可维护。以下是一些建议,用于优化您使用 Scrutor 来组织服务注册。

从明确的约定开始

为接口和实现建立命名和结构约定。一致的模式,例如后缀接口和 with 的实现,可以显著简化使用 Scrustor 的自动注册过程。ServiceServiceImpl

拥抱命名空间隔离

将类和接口组织到反映其域或功能的适当命名空间中。这种隔离简化了服务扫描,并使您的注册更具可预测性。

明智地使用作用域生存期

默认为作用域生存期,除非有令人信服的理由需要单一实例或瞬态。这种选择通常与 Web 应用程序的请求/响应性质非常吻合,并确保正确处置服务。

首选显式注册

如果可能,请对关键服务使用显式注册。这增强了清晰度,并且如果自动注册未按预期运行,则可以使调试更易于访问。

利用装配扫描

Scrutor 可以扫描整个程序集以查找和注册服务。使用此功能可以在适当的情况下批量注册服务,但要注意无意中注册不必要的服务。

使用过滤器进行自定义

Scrutor 提供了一种使用自定义过滤器注册服务的灵活方法。您可以根据特定条件筛选要注册的类型,例如类名、属性或填充在谓词中的任何其他自定义逻辑。

**谓词:**您可以提供一个谓词函数,该函数接受 a 并返回 a 以指示是否应注册该类型。这允许您仅注册与特定条件匹配的类型。Typebool

services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Service")))  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

在上面的代码中,用于过滤类。只有调用程序集中名称以“Service”结尾的那些类才会注册为实现其具有作用域生存期的接口。Where

**属性:**您还可以根据类型是否应用了特定属性来筛选类型。当您想要注册以某种方式标记的服务(例如,使用自定义属性)时,这很有用。[RegisterDependency]

假设您有一个名为 的自定义属性。您可以筛选注册,以仅包含使用此属性修饰的类:RegisterDependencyAttribute

[AttributeUsage(AttributeTargets.Class)]  
public class RegisterDependencyAttribute : Attribute  
{  
    public ServiceLifetime Lifetime { get; }  
  
    public RegisterDependencyAttribute(ServiceLifetime lifetime)  
    {  
        Lifetime = lifetime;  
    }  
}  
  
services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses(classes => classes.WithAttribute<RegisterDependencyAttribute>())  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

在此代码片段中,扩展方法将类筛选为仅应用了该类的类。然后,根据具有作用域生存期的已实现接口完成注册,但您可以进一步优化此注册,以根据需要使用属性中指定的生存期。WithAttributeRegisterDependencyAttribute

避免服务注册过载

对注册的内容要有策略。过度注册可能会导致服务集合膨胀和内存使用量增加。力求在便利性和必要的注册之间取得平衡。

保持注册可维护

作为重构过程的一部分,定期查看服务注册。确保删除过时的服务,并反映应用程序体系结构中的更改。

记录您的注册逻辑

虽然 Scrutor 减少了对样板代码的需求,但记录注册模式背后的逻辑至关重要,特别是对于可能需要更熟悉 Scrustor 扫描约定的团队。

测试 DI 配置

实施集成测试,以验证依赖项注入设置的正确性。这可以捕获服务未正确注册或具有意外生存期的问题。

谨慎更新

更新 Scrutor 和其他相关软件包时要小心。确保兼容性并进行全面测试,以避免因库中的更改而导致中断。

使用注册策略

在 Scrustor 中,注册策略定义了库如何处理相同服务类型的多个注册。以下是对 、 和 策略的解释:SkipAppendReplace

**跳:**使用注册策略时,如果服务已注册,Scrutor 将忽略服务的任何后续注册。当您希望确保仅使用服务的第一次注册而忽略在扫描过程中发现的任何其他服务时,此策略非常有用。这可以防止意外覆盖预期的服务实现。Skip

services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses(classes => classes.AssignableTo<IRepository>())  
    .UsingRegistrationStrategy(RegistrationStrategy.Skip)  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

**附加:**该策略允许对同一服务类型进行多次注册。通过这种策略,Scrutor 将每个新注册与以前的注册一起添加。解析服务时,所有注册都将可用,通常以“where is the service type”的形式提供。这在注入共享同一接口的服务集合时特别有用。AppendIEnumerable<T>T

services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses(classes => classes.AssignableTo\<IRepository>())  
    .UsingRegistrationStrategy(RegistrationStrategy.Append)  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());  
  
// Later, you might resolve all implementations like this:  
// IEnumerable\<IRepository> repositories = serviceProvider.GetServices\<IRepository>();

**取代:**该策略将用最新的注册覆盖以前的注册。如果服务类型已注册,并且与策略重新相遇,则新注册将取代现有注册。当您想要确保最近的注册是应用程序使用的注册时,此策略非常方便,当您将默认实现替换为自定义实现时,可能会出现这种情况。ReplaceReplace

services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses(classes => classes.AssignableTo<IRepository>())  
    .UsingRegistrationStrategy(RegistrationStrategy.Replace)  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

在这种情况下,如果它已经注册,它将在扫描期间被最新找到的实现所取代。IRepository

选择适当的注册策略对于在服务解析过程中实现所需的行为至关重要。它允许更好地控制和自定义依赖注入框架行为,以满足应用程序的特定需求。

考虑装配加载的影响

加载程序集进行扫描时,请注意潜在的性能影响,并确保仅加载必要的程序集,以避免不必要的开销。

对 Scrutor 的常见问题进行故障排除

在 .NET 项目中集成 Scrutor 以进行依赖注入时,您可能会遇到几个常见问题。以下是一些常见的挑战及其解决方案。

服务未注册

一个常见的问题是,预计由 Scrutor 注册的服务不可用于注入。

**溶液:**确保正在扫描包含该服务的程序集。检查服务的可访问性级别是否为公共级别,以及它是否实现了您尝试解析的接口。

services.Scan(scan => scan  
    .FromAssemblyOf<SomeKnownType>()  
    .AddClasses() // Make sure classes are public  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

不正确的服务寿命

有时,服务可能会注册为意外的生存期,从而导致意外出现单例保持状态等问题。

**溶液:**验证 Scrutor 链中指定的生存期。如果属性或谓词不同,请使用属性或谓词来控制每个服务的生存期。

services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses()  
    .AsImplementedInterfaces()  
    .WithLifetime(ServiceLifetime.Singleton)); // Be explicit about the desired lifetime

多种实现导致混淆

如果注册了一个接口的多个实现,并且您希望注入一个特定的实现,这可能会导致注入错误的实现。

**溶液:**使用策略或控制注册订单使用或适当。或者,解析服务以获取所有实现。ReplaceSkipAppendIEnumerable

services.Scan(scan => scan  
    .FromCallingAssembly()  
    .AddClasses(classes => classes.Where(type => type.Name.Contains("Preferred")))  
    .UsingRegistrationStrategy(RegistrationStrategy.Replace)  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

装配扫描性能

扫描大量程序集可能会影响启动性能。

**溶液:**通过更具选择性来限制程序集的扫描,或者考虑在适当的情况下对服务使用延迟加载。

services.Scan(scan => scan  
    .FromAssemblies(AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.StartsWith("MyApp")))  
    .AddClasses()  
    .AsImplementedInterfaces()  
    .WithScopedLifetime());

装饰器未按预期应用

尝试将修饰器应用于服务时,如果注册顺序不正确或服务类型未正确注册,则修饰器可能无法按预期工作。

**溶液:**在应用装饰器之前,请确保已注册装饰服务。使用 Scruder 提供的扩展方法。Decorate

services.AddTransient<IService, MyService>();  
services.Decorate<IService, MyServiceDecorator>();

未反映在运行时的更改

更新服务注册逻辑后,有时在应用程序运行时似乎不会反映更改。

**溶液:**清除生成输出并重新生成项目。依赖项注入是在应用程序启动时设置的,过时的生成项目可能会导致旧配置持续存在。

最后的思考

Scrutor能够以最少的代码扫描和注册依赖注入,并支持各种自定义选项以满足您的需求。如果要在 .NET 应用程序中使用依赖项注入,建议尝试一下它如何提高代码的质量和生产力。

相关留言评论
昵称:
邮箱:
阅读排行