SteroidsDI 1.4.2

dotnet add package SteroidsDI --version 1.4.2                
NuGet\Install-Package SteroidsDI -Version 1.4.2                
此命令用于在 Visual Studio 的包管理器控制台中,因为它使用了 NuGet 模块的版本 Install-Package.
<PackageReference Include="SteroidsDI" Version="1.4.2" />                
对于支持 PackageReference 的项目,将此 XML 节复制到项目文件中以引用包。
paket add SteroidsDI --version 1.4.2                
#r "nuget: SteroidsDI, 1.4.2"                
#r 指令可用于 F# Interactive 和 Polyglot Notebooks。将其复制到交互式工具或脚本的源代码中以引用包。
// Install SteroidsDI as a Cake Addin
#addin nuget:?package=SteroidsDI&version=1.4.2

// Install SteroidsDI as a Cake Tool
#tool nuget:?package=SteroidsDI&version=1.4.2                

SteroidsDI

<a href="https://www.buymeacoffee.com/sungam3r" target="_blank"><img src="https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/orange_img.png" alt="买我一杯咖啡" style="height: auto !important;width: auto !important;" ></a>

License

codecov Nuget Nuget

GitHub Release Date GitHub commits since latest release (by date) Size

GitHub contributors Activity Activity Activity

Run unit tests Publish preview to GitHub registry Publish release to Nuget registry CodeQL analysis

每天使用的先进依赖注入。

安装

此存储库提供以下包

下载 Nuget 最新版 描述
SteroidsDI.Core Nuget Nuget 依赖注入原语
SteroidsDI Nuget Nuget 针对 Microsoft.Extensions.DependencyInjection 的先进依赖注入:AddDefer, AddFunc, AddFactory;依赖于 SteroidsDI.Core
SteroidsDI.AspNetCore Nuget Nuget ASP.NET Core 的作用域提供者;依赖于 SteroidsDI.Core

您可以通过 NuGet 安装最新稳定版本。

> dotnet add package SteroidsDI
> dotnet add package SteroidsDI.Core
> dotnet add package SteroidsDI.AspNetCore

它是做什么的?我为什么需要它?

.NET Core 原生支持 依赖注入。它运行良好。我们可以使用三个主要生命周期的依赖项:单例、作用域、瞬态。存在规则指定了具有不同生命周期的对象中传递对象的可能组合。例如,您可能会遇到以下错误消息

验证服务描述符 '服务类型:I_XXX 生命周期:单例 实现类型:XXX' 时出错:无法从单例 'I_XXX' 消费作用域服务 'YYY'。

错误表明您不能将具有较短期限的对象传递给长期生活的对象的构造函数。这是正确的!问题很清楚。但如何解决它?显然,当使用 构造函数依赖注入(.NET Core 原生支持仅限于构造函数 DI)时,我们必须遵循这些规则。所以至少有以下 3 个选项

  1. 延长注入对象的存活期。
  2. 缩短注入依赖对象的存活期。
  3. 移除此类依赖。
  4. 更改依赖的设计以满足规则。

第一种方法远非总是可能。第二种方法更容易实现,尽管这会因为重复创建之前已创建的对象而降低性能。第三种方式...好吧,你懂的,生活不会变得容易。因此,我们需要改变设计。本项目提供了一种解决问题的方法,引入了许多辅助抽象。正如许多人所知道

任何编程问题都可以通过引入一个附加的抽象层来解决,除了过多的抽象的难题。

本项目提供了三种这样的抽象

  1. 众所周知 Func<T> 委托。
  2. Defer<T>/IDefer<T> 抽象,它们看起来像 Lazy<T>,但有一个重大的区别 - Defer<T>/IDefer<T> 不会缓存值
  3. 当运行时生成实现类型时的命名工厂接口。

所有这些抽象 都解决了相同的问题,从不同角度接近它们的API设计。挑战是通过某个中介对象X提供依赖T,其中对T的显式依赖要么不可能,要么不希望存在。重要!本包中没有任何实现缓存依赖T。

如上所述,不可能性的例子是对具有单例生命周期的对象中具有作用域生命周期的依赖。不希望存在性的例子是创建依赖很昂贵,并不总是需要。

需要指出的一点是 - 注入依赖与使用依赖不同。事实上,在构造函数注入的情况下,构造函数中的依赖注入(在大多数情况下)导致将传递值的引用存储在某些字段中。依赖将在稍后调用“父”对象的方法时使用。

Func<T>

此方法是最简单的,提供注入 Func<T> 而不是 T

之前

class MyObject
{
    private IRepository _repo;

    public MyObject(IRepository repo) { _repo = repo; }
    
    public void DoSomething() { _repo.DoMagic(); }
}

之后

class MyObject
{
    private Func<IRepository> _repo;

    public MyObject(Func<IRepository> repo) { _repo = repo; }
    
    public void DoSomething() { _repo().DoMagic(); }
}

在DI中如何配置

public void ConfigureServices(IServiceCollection services)
{
    // First register your IRepository and then call
    services.AddFunc<IRepository>();
}

请注意,您应该对每个想要作为 Func<T> 注入的依赖 T 调用 AddFunc

IDefer<T> 和 Defer<T>

这种方法建议更明确的API - 注入 IDefer<T>Defer<T> 而不是 T

之前

class MyObject
{
    private IRepository _repo;

    public MyObject(IRepository repo) { _repo = repo; }
    
    public void DoSomething() { _repo.DoMagic(); }
}

之后

class MyObject
{
    private Defer<IRepository> _repo;

    public MyObject(Defer<IRepository> repo) { _repo = repo; }
    
    public void DoSomething() { _repo.Value.DoMagic(); }
}

在DI中如何配置

public void ConfigureServices(IServiceCollection services)
{
    // First register your IRepository and then call
    services.AddDefer();
}

请注意,与 AddFunc<T> 不同,AddDefer 方法只需要调用一次。如果需要协变,请使用 IDefer<T> 接口。

命名工厂

此方法最难实现,但从公共API的角度来看,它和前两种方法一样简单。它假设你声明了一个带有一个或多个无参方法的工厂接口。方法名无关紧要。每个工厂方法应返回DI容器中配置的某些依赖类型。

public interface IRepositoryFactory
{
    IRepository GetPersonsRepo();
}

并将这个工厂注入到你的“父”类型中

class MyObject
{
    private IRepositoryFactory _factory;

    public MyObject(IRepositoryFactory factory) { _factory = factory; }
    
    public void DoSomething() { _factory.GetPersonsRepo().DoMagic(); }
}

在DI中如何配置

public void ConfigureServices(IServiceCollection services)
{
    // First register your IRepository and then call
    services.AddFactory<IRepositoryFactory>();
}

IRepositoryFactory 的实现将在运行时生成。

实际上,每个工厂方法都可以接受任意类型的一个参数 - 字符串、枚举、自定义类等。在这种情况下,应指定一个 命名绑定。然后你可以在工厂方法中通过传递绑定的名称来解析所需的服务。如果你想要提供默认实现,则可以配置 默认绑定。默认绑定是在没有命名绑定时使用的绑定。用户应明确设置默认绑定,以便能够解析未注册的名称的服务。

public interface IRepositoryFactory
{
    IRepository GetPersonsRepo(string mode);
}

public interface IRepository
{
    void Save(Person person); 
}

public class DemoRepository : IRepository
{
...
}

public class ProductionRepository : IRepository
{
...
}

public class RandomRepository : IRepository
{
...
}

public class DefaultRepository : IRepository
{
...
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IRepository, DemoRepository>()
            .AddTransient<IRepository, ProductionRepository>()
            .AddTransient<IRepository, RandomRepository>()
            .AddFactory<IRepositoryFactory>()
                .For<IRepository>()
                    .Named<DemoRepository>("demo")
                    .Named<ProductionRepository>("prod")
                    .Named<RandomRepository>("rnd")
                    .Default<DefaultRepository>();
}

public class Person
{
    public string Name { get; set; }
}

public class SomeClassWithDependency
{
    private readonly IRepositoryFactory _factory;

    public SomeClassWithDependency(IRepositoryFactory factory)
    {
        _factory = factory;
    }

    private bool SomeInterestingCondition => ...

    public void DoSomething(Person person)
    {
        if (person.Name == "demoUser")
            _factory.GetPersonsRepo("demo").Save(person); // DemoRepository
        else if (person.Name.StartsWith("tester"))
            _factory.GetPersonsRepo("rnd").Save(person); // RandomRepository
        else if (SomeInterestingCondition)
            _factory.GetPersonsRepo("prod").Save(person); // ProductionRepository
        else
            _factory.GetPersonsRepo(person.Name).Save(person); // DefaultRepository
    }
}

在上面的例子中,GetPersonsRepo 方法将返回为提供的名称配置的相应的 IRepository 接口的实现。对于所有未注册的名称(包括空值),它将返回 DefaultRepository

它是如何工作的?

这里的一切都很简单。三种方法都归结为将依赖项解析委派给恰当的 IServiceProvider。这里的“恰当”是什么意思?通常情况下,在 ASP.NET Core 应用程序中,每个人都习惯于使用一个(作用域)从 IHttpContextAccessor 获取的提供者 —— HttpContext.RequestServices。但在一般情况下,可能有多个这样的提供者。此外,消耗依赖项的代码并不知道它们的存在。这段代码可能是一个通用库,与应用程序特定的环境没有紧密耦合。因此,引入了对获取恰当的 IServiceProvider 的抽象。是的,又是一种抽象!

本项目提供了两个内置提供程序

  1. AspNetCoreHttpScopeProvider,用于 ASP.NET Core 应用程序。
  2. GenericScopeProvider{T},用于通用库。

在DI中如何配置

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpScope();
    services.AddGenericScope<SomeClass>();
}

当然,你也可以始终编写自己的提供程序

public class MyScopeProvider : IScopeProvider
{
    public IServiceProvider? GetScopedServiceProvider(IServiceProvider rootProvider) => rootProvider.ReturnSomeMagic();
}

并为其在 DI 中提供注册

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyScope(this IServiceCollection services)
    {
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IScopeProvider, MyScopeProvider>());
        return services;
    }
}

高级行为

你可以通过 ServiceProviderAdvancedOptions 来自定义 AddFunc/AddDefer/AddFactory APIs 的行为:只需使用来自 Microsoft.Extensions.Options/Microsoft.Extensions.Options.ConfigurationExtensions 包的标准扩展方法。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<ServiceProviderAdvancedOptions>(options => options.AllowRootProviderResolve = true)
    services.Configure<ServiceProviderAdvancedOptions>(Configuration.GetSection("Steroids"));
}

示例

您可以在 示例项目 中看到如何使用上述所有 API。

常见问题解答

问题。等等。难道 Microsoft.Extensions.DependencyInjection 没有提供开箱即用的支持吗?

答案。很遗憾没有。我宁愿使用现有的功能,也不愿自己编写软件包。

<br/>

问题。您提供的是否是一种 ServiceLocator?我听说 ServiceLocator 是一种反模式。

答案。是的,ServiceLocator 是一种已知的反模式。这里可以找到关于 ServiceLocator 的描述。与所提出解决方案的本质区别在于,ServiceLocator 允许您在运行时解决任何依赖项,而 Func<T>Defer<T> 和命名工厂仅设计用于解决在编译时指定的已知依赖项。因此,主要区别是类的所有依赖项都显式声明并注入其中。该类本身 不会 在其实现中秘密地获取这些依赖项。这就是所谓的 显式依赖原理

<br/>

问题。这是一种新的依赖注入方法吗?

答案。实际上不是。这种方法的描述可以在多年前的文章/博客中找到,例如 这里

<br/>

问题。我应该选择 Func<> 还是 [I]Defer<>

答案。最重要的是,它们在底层都工作得同样好。区别在于您打算在什么环境中使用这些 API。

主要有两个区别

  1. Func<> 的优势在于,注入 Func<> 的代码不需要任何新的依赖项,它是一个众所周知 的 .NET 委托类型。相反,[I]Defer<> 需要引用 SteroidsDI.Core 包。

  2. 对于每个您希望以 Func<T> 形式注入的依赖项 T,您应该调用 AddFunc。相反,只需要调用一次 AddDefer 方法。

<br/>

问题。如果我想创建自己的作用域来使用,也就是说,不仅要消耗它,还要提供它怎么办?

A. 首先,您需要以某种方式获取根 IServiceProvider 的实例。然后通过调用其在 CreateScope() 方法上创建作用域并将它设置为 GenericScope

var rootProvider = ...;
using var scope = rootProvider.CreateScope();
GenericScope<SomeClass>.CurrentScope = scope;
...
... Some code here that works with scopes.
... All registered Func<T>, [I]Defer<T> and
... factories use created scope.
...
GenericScope<T>.CurrentScope = null;

或者,您可以使用更简单的 Scoped<T> 结构。

IScopeFactory scopeFactory = ...; // can be obtained from DI, see AddMicrosoftScopeFactory extension method
using (new Scoped<SomeClass>(scopeFactory))
or
await using (new Scoped<SomeClass>(scopeFactory)) // Scoped class supports IAsyncDisposable as well
{
...
... Some code here that works with scopes.
... All registered Func<T>, [I]Defer<T> and
... factories use created scope.
...
} 

有关更多信息,请参见 ScopedTestBaseScopedTestDerived。这个示例展示了如何将作用域支持添加到所有单元测试中。

基准测试

结果可在 此处查看

产品 兼容的和额外的计算目标框架版本。
.NET net5.0 已计算。 net5.0-windows 已计算。 net6.0 已计算。 net6.0-android 已计算。 net6.0-ios 已计算。 net6.0-maccatalyst 已计算。 net6.0-macos 已计算。 net6.0-tvos 已计算。 net6.0-windows 已计算。 net7.0 已计算。 net7.0-android 已计算。 net7.0-ios 已计算。 net7.0-maccatalyst 已计算。 net7.0-macos 已计算。 net7.0-tvos 已计算。 net7.0-windows 已计算。 net8.0 已计算。 net8.0-android 已计算。 net8.0-browser 已计算。 net8.0-ios 已计算。 net8.0-maccatalyst 已计算。 net8.0-macos 已计算。 net8.0-tvos 已计算。 net8.0-window 已计算。
.NET Core netcoreapp2.0 已计算。 netcoreapp2.1 已计算。 netcoreapp2.2 已计算。 netcoreapp3.0 已计算。 netcoreapp3.1 已计算。
.NET Standard netstandard2.0 兼容。 netstandard2.1 已计算。
.NET Framework net461 已计算。 net462 已计算。 net463 已计算。 net47 已计算。 net471 已计算。 net472 已计算。 net48 已计算。 net481 已计算。
MonoAndroid monoandroid 已计算。
MonoMac monomac 已计算。
MonoTouch monotouch 已计算。
Tizen tizen40 已计算。 tizen60 已计算。
Xamarin.iOS xamarinios 已计算。
Xamarin.Mac xamarinmac 已计算。
Xamarin.TVOS xamarintvos 已计算。
Xamarin.WatchOS xamarinwatchos 已计算。
兼容目标框架
包含的目标框架(在包中)
有关 目标框架 以及 .NET Standard 的更多信息。

NuGet 包

此包未由任何 NuGet 包使用。

GitHub 仓库

此包未由任何热门 GitHub 仓库使用。

版本 下载 最后更新
1.4.2 2,376 3/18/2024
1.4.1 1,935 1/20/2024
1.4.0 437 1/4/2024
1.3.0 743 11/18/2023
1.2.0 9,987 12/29/2022
1.1.1 2,332 12/1/2022
1.1.0 6,752 5/2/2022
1.0.3 889 4/15/2022
1.0.2 1,325 3/31/2022
1.0.1 7,833 10/16/2021
1.0.0 16,171 2/26/2021
0.0.7 4,214 11/24/2020
0.0.6 706 11/24/2020
0.0.5 1,731 10/26/2020
0.0.4 18,868 6/25/2020
0.0.3 950 3/10/2020
0.0.2 770 3/9/2020
0.0.1 850 3/8/2020