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

How to avoid using using BuildServiceProvider method at multiple places?

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

这就是我们的遗留应用程序的设置方式,如下所示。我试图通过只保留重要的东西来保持简单。

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

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;
    // ....
  }
}

ICatalogServiceICustomerService 是普通接口,其中包含它们的实现 class 实现的一些方法。

下面是我们扩展 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>();
    }
}

下面是我的 Startup class 的配置方式 -

Startup.cs

 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

 }

下面是我的 DependencyBootstrap class -

DependencyBootstrap.cs

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

    }
}

问题陈述

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

目前我很困惑在 asp.net 核心中使用 DI 的最佳方式是什么,如果我的应用程序做错了那么我怎样才能以正确的方式做到这一点?上面的代码在很长一段时间内在我们的应用程序中运行良好,但看起来我们可能以完全错误的方式使用它 DI.

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

的问题

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

更糟糕的是,虽然您的应用程序今天可能正常运行,但当您依赖的第三方或框架组件之一以这种方式更改其组件之一时,这可能会在未来的任何时候发生变化当多次创建该组件时,它会成为一个问题。

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

这意味着答案“简单地”是为了防止调用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.
    ...
}

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

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

更新


我想我开始理解您在 BaseMiddleware 中想要实现的目标。它是一个 'convenient' 帮助程序 class,它包含其衍生产品可能需要的所有依赖项。这可能是一个旧的设计,您可能已经意识到这一点,但是这个基数 class 是很有问题的。具有依赖关系的基础 classes 几乎不是一个好主意,因为它们往往会变得很大,不断变化,并且混淆了它们的导数变得过于复杂的事实。在你的情况下,甚至,你正在使用 Service Locator anti-pattern 这绝不是一个好主意。

除此之外,还有很多 BaseMiddleware class 对我来说意义不大的事情,例如:

  • 它包含复杂的逻辑来验证是否所有依赖项都存在,同时还有更有效的方法来验证。最有效的方法是在构建时应用 Constructor Injection because it will guarantee that its necessary dependencies are always available. On top of that, you can validateIServiceCollection。与 BaseMiddleware 当前提供的相比,这为您的 DI 配置的正确性提供了更大的保证。
  • 它在后台线程中解析其所有服务,这意味着这些组件的构建要么在 CPU 上,要么在 I/O 上很重,这是一个问题。相反,组合应该很快,因为 injection constructors should be simple, which allows you to compose object graph with confidence.
  • 你在基层做异常处理class,而它更适合在更高层次上应用;例如,使用最外层的中间件。不过,为了简单起见,我的下一个示例将异常处理保留在基 class 中。那是因为我不知道你在那里做了什么样的事情,这可能会影响我的回答。
  • 由于基础 class 从根容器解析,中间件 classes 只能使用单例依赖项。例如,通过 Entity Framework 连接到数据库将是一个问题,因为 DbContext classes 不应在 Singleton 消费者中捕获。

因此,根据上述观察和建议,我建议将 BaseMiddleware class 减少为以下内容:

// 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);      
    }
}

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

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());
    }
}

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

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>();
}

提示:如果您开始注意到中间件 classes 有很大的构造函数,那很可能是因为 class 做的太多而且太复杂了。这意味着它应该被重构为多个更小的 classes。在这种情况下,您的 class 显示 Constructor Over-Injection code smell。有许多可用的重构模式和设计模式可以让您摆脱这种情况。