【问题标题】:Is there any way to validate the lifetime in DI?有什么方法可以验证 DI 的寿命吗?
【发布时间】:2021-07-20 11:08:51
【问题描述】:

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

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

public class MyStatefulService
{
    private object _state;
}

但是,我可能会错误地将其注册到错误的生命周期:

services.AddTransient<MyStatefulService>();

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

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

[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;
}

有没有更好的解决方案、hack 或任何有助于防止错误的方法?

【问题讨论】:

  • 所以你是说注册类型时会出错,而用属性装饰类型时不会出错?
  • 我写服务,我知道它是否需要单例。这是你要求的吗?
  • 我在问“如果你都写,为什么你放属性时可能不会出错?为什么注册类型时容易出错?为什么你需要一个属性(你永远不会出错)来检查你以后没有犯过你在注册时会犯的错误?如果你做这两件事,为什么你在创建课程时会做对,但在注册课程时 5 分钟后会出错?”
  • @CaiusJard 如果我不面对这个问题,我会像你一样思考。首先,我们正在进行的项目使用了近百种服务。其次,有几个人编写/维护这些服务,但通常只有一个人处理注册。第三,许多服务被设计为在其他应用程序中重复使用,在这些应用程序中,注册不能简单地“链接”。我什至可以告诉你第四个原因:服务可能从无状态开始,然后转为有状态。数据库表适配器就是这种情况,在某个时候您决定添加缓存。
  • 如何让 ServiceX 从 SingletonService 派生,然后将其切换为从 TransientService 派生,并为您的注册提供遗传辅助方法(有限制),只接受特定类型的对象,这样您就可以得到一个编译器尝试对派生自 TransientService 的类型使用 RegisterSingleton 帮助程序时出错?

标签: c# asp.net-core dependency-injection .net-5


【解决方案1】:

如果您想对您的代码进行白痴验证,您可以向IServiceCollection 提供一个扩展方法,以完全按照应有的方式注册您的服务。

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

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

services.AddMyStatefulService();

【讨论】:

  • 这是大多数库使用的另一种方式,准备在 Microsoft DI 中使用。
  • 这并不妨碍直接注册服务,但我认为这是我们能拥有的最好的。
【解决方案2】:

另一种验证生命周期的方法是检查 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

【讨论】:

    【解决方案3】:

    要将 DI 组合移动到类的声明中,您可以使用一些自制的标记接口,例如 IRegisterTransientServiceAs&lt;T&gt;,并将其添加到您的类中,如下所示:

    public class MyStatefulService : IMyStatefulService, IRegisterSingletonServiceAs<IMyStatefulService>
    

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

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

    【讨论】:

      【解决方案4】:

      这是我在 cmets 中指出的一种方式

      让我们有一些空类型:

      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 {
      

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

      类型“YourApplication.RealService”不能用作泛型类型或方法“Startup.MyAddScoped(IServiceCollection)”中的类型参数“TImplementation”。没有从“WebApplication1.RealService”到“WebApplication1.ScopedService”的隐式引用转换。

      您的“负责注册的人”可以知道开发人员指出该服务不再可注册为 Scoped 并且可以对其进行更改(并且在更改之前构建将被破坏,这是防止意外释放错误代码)

      【讨论】:

      • 现在我明白你的意思了:使用标记类。这是一个有价值的解决方案,但是依赖于一个助手。如果服务实现者(定义服务的生命周期)使用助手/黑客就可以了。登记册应该(可能)不知道这一点。无论如何,谢谢。
      • 您编写的所有代码在概念上都是一个助手。编写您自己的注册器并使用它来代替/隐藏 AddScoped 方法。编写一些代码,如果通用 AddScoped 是,则该服务无法正常工作使用而不是 MyAddScoped。编写像@Slava 这样的代码建议在运行时检查生命周期与类型。 在某些时候,你将不得不依靠某人来正确地完成他们的工作 - 你不能跟随他们到他们生命的尽头,为他们做所有事情。这就是我们有规则和代码审查的原因。在您的问题范围内,您确实表明您对它们有合理的控制权。
      猜你喜欢
      • 2011-04-16
      • 1970-01-01
      • 2021-10-03
      • 2013-01-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-06
      • 2015-11-26
      相关资源
      最近更新 更多