有什么方法可以验证 DI 中的生命周期吗?

Is there any way to validate the lifetime in DI?

我正在寻找一些方法来在 .Net Core 或更高版本中强制检查(当然是运行时)依赖注入服务的正确生命周期注册。

假设我有这样一个有状态服务:

public class MyStatefulService
{
    private object _state;
}

但是,我错误地注册了错误的生命周期:

services.AddTransient<MyStatefulService>();

所以,我不会收到警报,但实际行为并不是我所期望的:服务是在每次请求时创建的,并且不会保留状态。

我想知道是否有办法加强这种模式。例如,如果我可以用这样的属性装饰 class 就好了:

[Singleton]
public class MyStatefulService
{
    private object _state;
}

此时,可能在启动时或在第一次请求时,如果注册不同于 AddSingleton(及其重载),框架应该抛出异常。

同样,主体可以申请临时服务,不应注册为单身人士。

我想到的唯一解决方案比较幼稚,我不太喜欢:

//line of principle code
public static class MySingletonChecker
{
    private static HashSet<Type> _set = new HashSet<Type>();

    public static void Validate(Type type)
    {
        if (_set.Contains(type))
        {
            throw new Exception();
        }
        else
        {
            _set.Add(type);
        }
    }
}


public class MyStatefulService
{
    public MyStatefulService()
    {
        MySingletonChecker.Validate(this.GetType());
    }

    private object _state;
}

是否有更好的解决方案、黑客或任何有助于防止错误的方法?

要将 DI 组合移动到 class 的声明中,您可以使用一些自制的标记界面,例如 IRegisterTransientServiceAs<T> 并将其添加到您的 class 中,如下所示:

public class MyStatefulService : IMyStatefulService, IRegisterSingletonServiceAs<IMyStatefulService>

在您的组合中,您拥有自己的扩展方法,该方法遍历应用程序域中的所有加载类型,搜索给定的通用接口并使用给定接口的声明生命周期注册它们。

也许这不是真正的 DI,但正如您已经提到的,大部分服务的生命周期都包含在服务本身的代码中,并且很少发生特定服务将在不同项目中使用不同的情况一生。所以恕我直言,可以将所需的生命周期融入 class 本身,而不是在外部做出决定。

另一种验证生命周期的方法是在 ConfigureServices 子句中检查整个 DI 容器。

IServiceCollection 的每个成员都有一个 LifeTime 属性,它为您提供您正在寻找的信息:https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor.lifetime?view=dotnet-plat-ext-5.0#Microsoft_Extensions_DependencyInjection_ServiceDescriptor_Lifetime

如果您想防止您的代码出现白痴问题,您可以向 IServiceCollection 提供一个扩展方法,以完全按照应有的方式注册您的服务。

public static void AddMyStatefulService(this IServiceCollection services) 
{
    services.AddSingleton<MyStatefulService>();
}

然后在您的服务配置部分,开发人员将键入:

services.AddMyStatefulService();

这是我在评论中指出的方法

让我们有一些空类型:

public class ScopedService {}
public class TransientService {}

让我们有一个派生自空类型之一的真实服务:

public class RealService: ScopedService, IRealService {
  //impl
}

还有一个通用的注册方法助手:

static void MyAddScoped<TService, TImplementation>(IServiceCollection services)
    where TService : class
    where TImplementation : ScopedService, TService 
{
    services.AddScoped<TService, TImplementation>();
}

让我们使用帮助程序注册我们的真实服务:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        MyAddScoped<IRealService, RealService>(services);
    }

RealService 是一个 ScopedService - 将其视为“您如何装饰服务以坚持将其添加为作用域”

假设开发人员将服务更改为瞬态服务:

class RealService: TransientService, IRealService {

现在您从辅助方法中得到一个编译器错误:

The type 'YourApplication.RealService' cannot be used as type parameter 'TImplementation' in the generic type or method 'Startup.MyAddScoped<TService, TImplementation>(IServiceCollection)'. There is no implicit reference conversion from 'WebApplication1.RealService' to 'WebApplication1.ScopedService'.

你的“一个负责注册的人”可以知道开发人员表明该服务不再可注册为 Scoped 并且可以更改它(并且构建将被破坏直到它被更改,这是一个好方法防止意外发布错误代码)