如何避免在多个地方使用 BuildServiceProvider 方法?

And*_*dyP 0 c# dependency-injection inversion-of-control asp.net-core

我有一个Asp.net Core 3.1使用Kestrel服务器的遗留应用程序,我们所有的GETPOST调用都可以正常工作。我们的遗留应用程序上已经有一堆中间件,我们根据端点的不同将每个中间件用于不同的目的。

这就是我们的旧应用程序的设置方式,如下所示。我试图通过只保留重要的事情来使事情变得简单。

下面是我们的BaseMiddleware类,它由我们拥有的一堆其他中间件扩展。大约我们有 10 多个中间件扩展BaseMiddleware类 -

基础中间件.cs

public abstract class BaseMiddleware {
  protected static ICatalogService catalogService;
  protected static ICustomerService customerService;
  private static IDictionary <string, Object> requiredServices;

  private readonly RequestDelegate _next;

  public abstract bool IsCorrectEndpoint(HttpContext context);
  public abstract string GetEndpoint(HttpContext context);
  public abstract Task HandleRequest(HttpContext context);

  public BaseMiddleware(RequestDelegate next) {
    var builder = new StringBuilder("");
    var isMissingService = false;
    foreach(var service in requiredServices) {
      if (service.Value == null) {
        isMissingService = true;
        builder.Append(service.Key).Append(", ");
      }
    }

    if (isMissingService) {
      var errorMessage = builder.Append("cannot start server.").ToString();
      throw new Exception(errorMessage);
    }

    _next = next;
  }

  public async Task Invoke(HttpContext context) {
    if (IsCorrectEndpoint(context)) {
      try {
        await HandleRequest(context);
      } catch (Exception ex) {
        // handle exception here
        return;
      }
      return;
    }

    await _next.Invoke(context);
  }

  public static void InitializeDependencies(IServiceProvider provider) {
    requiredServices = new Dictionary<string, Object>();

    var catalogServiceTask = Task.Run(() => provider.GetService<ICatalogService>());
    var customerServiceTask = Task.Run(() => provider.GetService<ICustomerService>());
    // .... few other services like above approx 10+ again

    Task.WhenAll(catalogServiceTask, landingServiceTask, customerServiceTask).Wait();

    requiredServices[nameof(catalogService)] = catalogService = catalogServiceTask.Result;
    requiredServices[nameof(customerService)] = customerService = customerServiceTask.Result;
    // ....
  }
}
Run Code Online (Sandbox Code Playgroud)

ICatalogService并且ICustomerService是普通接口,其中包含它们的实现类实现的一些方法。

下面是我们扩展的中间件示例之一BaseMiddleware。所有其他中间件都遵循与以下相同的逻辑 -

FirstServiceMiddleware.cs

public class FirstServiceMiddleware : BaseMiddleware
{
    public FirstServiceMiddleware(RequestDelegate next) : base(next) { }

    public override bool IsCorrectEndpoint(HttpContext context)
    {
        return context.Request.Path.StartsWithSegments("/first");
    }

    public override string GetEndpoint(HttpContext context) => "/first";

    public override async Task HandleRequest(HttpContext context)
    {
        context.Response.StatusCode = 200;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync("Hello World!");
    }
}

public static class FirstServiceMiddlewareExtension
{
    public static IApplicationBuilder UseFirstService(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<FirstServiceMiddleware>();
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是我的Startup课程的配置方式 -

启动文件

 private static ILoggingService _loggingService;

 public Startup(IHostingEnvironment env) {
   var builder = new ConfigurationBuilder()
     .SetBasePath(env.ContentRootPath)
     .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
     .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
     .AddEnvironmentVariables();
   Configuration = builder.Build();
 }

 public IConfigurationRoot Configuration { get; }

 public void ConfigureServices(IServiceCollection services) {
    services.AddResponseCompression(options =>
    {
        options.Providers.Add<GzipCompressionProvider>();
    });

    services.Configure<GzipCompressionProviderOptions>(options =>
    {
        options.Level = CompressionLevel.Fastest;
    });

    DependencyBootstrap.WireUpDependencies(services);
    var provider = services.BuildServiceProvider();
    if (_loggingService == null) _loggingService = provider.GetService<ILoggingService>();
    //.. some other code here

    BaseMiddleware.InitializeDependencies(provider);
 }

 public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime) {
   // old legacy middlewares
   app.UseFirstService();
   // .. few other middlewares here

 }
Run Code Online (Sandbox Code Playgroud)

下面是我的DependencyBootstrap课——

依赖引导程序

public static class DependencyBootstrap
{
    //.. some constants here

    public static void WireUpDependencies(IServiceCollection services)
    {
        ThreadPool.SetMinThreads(100, 100);
        var provider = services.BuildServiceProvider();
        var loggingService = provider.GetService<ILoggingService>();
        // ... some other code here
        
        try
        {
            WireUp(services, loggingService);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }

    private static void WireUp(IServiceCollection services, ILoggingService loggingService)
    {
        // adding services here
        services.AddSingleton<....>();
        services.AddSingleton<....>();
        //....

        var localProvider = services.BuildServiceProvider();
        if (IS_DEVELOPMENT)
        {
            processClient = null;
        }
        else
        {
            processClient = localProvider.GetService<IProcessClient>();
        }

        services.AddSingleton<IData, DataImpl>();
        services.AddSingleton<ICatalogService, CatalogServiceImpl>();
        services.AddSingleton<ICustomerService, CustomerServiceImpl>();
        //.. some other services and singleton here

    }
}
Run Code Online (Sandbox Code Playgroud)

问题陈述

我最近开始使用 C# 和 asp.net 核心框架。我已经完成了阅读,看起来像 -

  • 我们的遗留应用程序无法Dependency Injection正常使用,因为我们有很多地方使用了BuildServiceProvider导致该警告的方法。我不知道为什么我们必须这样做。
  • 我们真的需要课堂上的InitializeDependencies方法BaseMiddleware吗?如果不是,那么我们如何正确初始化依赖项?看起来我们正在尝试在服务器启动期间初始化所有依赖项,以便在调用任何中间件时它们都准备就绪。如果可能的话,我想保持这种逻辑。

目前我很困惑DI在 asp.net core 中使用的最佳方法是什么,如果我的应用程序做错了,那么我该如何正确地做呢?很长一段时间以来,上面的代码在我们的应用程序中运行良好,但看起来我们可能完全错误地使用了DI.

Ste*_*ven 11

BuildServiceProvider多次调用会导致严重的问题,因为每次调用都会BuildServiceProvider产生一个带有自己缓存的新容器实例。这意味着预期具有单身生活方式的注册突然被创建了不止一次。这是一个名为Ambiguous Lifestyle的问题。

一些单身人士是无状态的,对他们来说,创建一个或一千个没有区别。但是其他注册为 Singleton 的组件可能有状态,并且应用程序的工作可能(间接)依赖于该状态不被复制。

更糟糕的是,虽然您的应用程序今天可能正常工作,但当您依赖的第三方或框架组件之一对其组件之一进行更改以使其成为问题时,这可能会在未来随时发生变化当该组件被多次创建时。

在你的榜样,你既解决ILoggingService,并IProcessClient从服务提供商。如果解析的组件是没有状态依赖的无状态对象,则不会造成真正的伤害。但是当它们变成有状态时,这可能会改变。同样,这可能通过更改其间接依赖项之一而发生,因此您可能没有意识到这一点。这可能会导致您或您的团队浪费很多时间;这样的问题很可能不容易被发现。

这意味着答案“简单地”是防止调用BuildServiceProvider()创建中间容器实例。但这可能说起来容易做起来难。但是,在您的情况下,您似乎需要ILoggerService 在连接所有依赖项之前依赖于。实现这一目标的典型方法是将注册阶段分为两个单独的步骤:

  • 一个步骤,您可以手动创建一些您需要的单例
  • 将它们添加到您的容器构建器 ( IServiceCollection)
  • 添加所有其他注册

例如:

private ILoggingService _loggingService;

public Startup(Confiration config)
{
    _loggingService = new MySpecialLoggingService(config.LogPath);
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(_loggingService);

    // More stuf here.
    ...
}
Run Code Online (Sandbox Code Playgroud)

这种结构的优点是,当一个依赖被添加到这个手动构建的构造函数中时MySpecialLoggingService,你的代码停止编译,你被迫查看这段代码。当该构造函数依赖于其他一些尚不可用的框架抽象或应用程序抽象时,您就知道自己遇到了麻烦,需要重新考虑您的设计。

最后一点,BuildServiceProvider多次调用本身并不是一件坏事。当您明确希望在您的应用程序中有多个独立的模块,每个模块都有自己的状态并且彼此独立运行时,这是可以的。例如,在同一进程中为多个有界上下文运行多个端点时。

更新


我想我开始明白你想要在你的BaseMiddleware. 它是一个“方便”的辅助类,包含其派生类可能需要的所有依赖项。这可能是一个旧的设计,您可能已经意识到这一点,但是这个基类有很大的问题。具有依赖关系的基类几乎不是一个好主意,因为它们往往变得很大,不断变化,并且混淆了它们的派生类变得过于复杂的事实。甚至在您的情况下,您正在使用Service Locator 反模式,这绝不是一个好主意。

除此之外,该BaseMiddleware课程中有很多事情——对我来说——毫无意义,例如:

  • 它包含复杂的逻辑来验证是否所有依赖项都存在,同时还有更有效的方法来做到这一点。最有效的方法是应用构造函数注入,因为它将保证其必要的依赖项始终可用。最重要的是,你可以验证IServiceCollection上构建。与 BaseMiddleware 当前提供的相比,这为您提供了对 DI 配置正确性的更大保证。
  • 它在后台线程中解决其所有服务,这意味着这些组件的构建要么占用 CPU 要么占用大量 I/O,这是一个问题。相反,组合应该很快,因为注入构造函数应该很简单,这让您可以自信组合对象图
  • 您在基类中进行异常处理,而它更适合在更高级别应用;例如,使用最外层的中间件。不过,为了简单起见,我的下一个示例将异常处理保留在基类中。那是因为我不知道你在那里做什么,这可能会影响我的回答。
  • 由于基类是从根容器解析的,中间件类只能使用单例依赖项。例如,通过实体框架连接到数据库将是一个问题,因为DbContext不应在单例消费者中捕获类。

因此,根据上面的观察和建议,我建议将BaseMiddleware课程减少到以下内容:

// Your middleware classes should implement IMiddleware; this allows middleware
// classes to be transient and have scoped dependencies.
public abstract class ImprovedBaseMiddleware : IMiddleware
{
    public abstract bool IsCorrectEndpoint(HttpContext context);
    public abstract string GetEndpoint(HttpContext context);
    public abstract Task HandleRequest(HttpContext context);

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (IsCorrectEndpoint(context)) {
            try {
                await HandleRequest(context);
            }
            catch (Exception ex) {
                // handle exception here
                return;
            }
            return;
        }

        await next(context);      
    }
}
Run Code Online (Sandbox Code Playgroud)

现在基于这个新的基类,创建类似于下一个示例的中间件实现:

public class ImprovedFirstServiceMiddleware : ImprovedBaseMiddleware
{
    private readonly ICatalogService _catalogService;
    
    // Add all dependencies required by this middleware in the constructor.
    public FirstServiceMiddleware(ICatalogService catalogService)
    {
        _catalogService = catalogService;
    }

    public override bool IsCorrectEndpoint(HttpContext context) =>
         context.Request.Path.StartsWithSegments("/first");

    public override string GetEndpoint(HttpContext context) => "/first";

    public override async Task HandleRequest(HttpContext context)
    {
        context.Response.StatusCode = 200;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync("Hello from "
            + _catalogService.SomeValue());
    }
}
Run Code Online (Sandbox Code Playgroud)

在您的应用程序中,您可以按如下方式注册您的中间件类:

public void ConfigureServices(IServiceCollection services) {
    // When middleware implements IMiddleware, it must be registered. But
    // that's okay, because it allows the middleware with its
    // dependencies to be 'verified on build'.
    services.AddTransient<ImprovedFirstServiceMiddleware>();
    
    // If you have many middleware classes, you can use
    // Auto-Registration instead. e.g.:
    var middlewareTypes =
        from type in typeof(HomeController).Assembly.GetTypes()
        where !type.IsAbstract && !type.IsGenericType
        where typeof(IMiddleware).IsAssignableFrom(type)
        select type;

    foreach (var middlewareType in middlewareTypes)
        services.AddTransient(middlewareType);
    
    ...
}

public void Configure(
    IApplicationBuilder app, IHostApplicationLifetime lifetime)
{
    // Add your middleware in the correct order as you did previously.
    builder.UseMiddleware<ImprovedFirstServiceMiddleware>();
}
Run Code Online (Sandbox Code Playgroud)

提示:如果您开始注意到中间件类有很大的构造函数,那很可能是因为这样的类做的太多并且变得太复杂了。这意味着它应该被重构为多个更小的类。在这种情况下,您的类表现出构造函数过度注入代码的味道。有许多可用的重构模式和设计模式可以让您摆脱这种情况。