【问题标题】:How to avoid using using BuildServiceProvider method at multiple places?如何避免在多个地方使用 BuildServiceProvider 方法?
【发布时间】:2021-05-21 15:24:51
【问题描述】:

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

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

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

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 是普通接口,其中包含它们的实现类实现的一些方法。

下面是我们扩展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 类的配置方式-

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类-

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 核心框架。我已经读完了,它看起来像 -

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

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

【问题讨论】:

标签: c# asp.net-core dependency-injection inversion-of-control


【解决方案1】:

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

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

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

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

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

  • 一个步骤,您可以手动创建之前需要的几个单例
  • 将它们添加到您的容器构建器 (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 中想要实现的目标是什么。它是一个“方便”的辅助类,包含其派生类可能需要的所有依赖项。这可能是一个旧的设计,您可能已经意识到这一点,但是这个基类有很大的问题。具有依赖关系的基类几乎不是一个好主意,因为它们往往会变得很大,不断变化,并且混淆了它们的派生变得过于复杂的事实。在您的情况下,即使您使用的是Service Locator anti-pattern,这也不是一个好主意。

除此之外,BaseMiddleware 类中发生了很多对我来说毫无意义的事情,例如:

  • 它包含复杂的逻辑来验证是否所有依赖项都存在,同时还有更有效的方法可以做到这一点。最有效的方法是应用Constructor Injection,因为它将保证其必要的依赖项始终可用。最重要的是,您可以在构建时 validateIServiceCollection。与 BaseMiddleware 当前提供的相比,这为您的 DI 配置的正确性提供了更大的保证。
  • 它在后台线程中解决其所有服务,这意味着这些组件的构建在 CPU 或 I/O 上很重,这是一个问题。相反,合成应该很快,因为injection constructors should be simple,它可以让你compose object graph with confidence
  • 您在基类中进行异常处理,而它更适合在更高级别应用;例如,使用最外层的中间件。不过,为了简单起见,我的下一个示例将异常处理保留在基类中。那是因为我不知道你在那里做什么,这可能会影响我的回答。
  • 由于基类从根容器解析,中间件类只能使用单例依赖项。例如,通过 Entity Framework 连接到数据库将是一个问题,因为不应在 Singleton 消费者中捕获 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);      
    }
}

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

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

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

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

提示:如果你开始注意到一个中间件类有很大的构造函数,那可能是因为这样的类做了太多并且变得太复杂了。这意味着它应该被重构为多个更小的类。在这种情况下,您的班级将展示Constructor Over-Injection code smell。有许多可用的重构模式和设计模式可以帮助您摆脱这种情况。

【讨论】:

  • 有道理。感谢您的详细解释。我将探讨您建议的ILoggingService 示例的选项。但是如何修复我的 `BaseMiddleware.InitializeDependencies` 方法中的内容?我不知道为什么我们会这样,但知道我在整个班级里能做什么吗?具体BaseMiddleware构造函数和InitializeDependencies方法?
  • 真的很棒。感谢您的详细解释。让我通过所有这些要点并详细阅读。它基本上清除了我所有的疑虑。非常感谢您的帮助!
  • 抱歉,我离开了一段时间,无法回复。我尝试了这种方法,它对我有用,但我注意到在第一次调用任何中间件期间依赖项会被初始化。这意味着只有那些依赖于我们第一次调用的中间件的依赖项才会被初始化。如果第一次调用其对应的中间件,则其他依赖项将被初始化。有什么方法可以在 服务器启动 期间初始化 所有 依赖项,就像我在以前的方法中所做的那样? @史蒂文
  • 如果我的理解很清楚 BaseMiddleware.InitializeDependencies 在旧代码中是在服务器启动期间初始化所有依赖项,这样每当调用任何中间件时,所有依赖项都已经准备好。在我的一些依赖项中,我们在构造函数中完成了一些工作以将一些对象加载到内存中。
  • 再次仔细阅读我的更新要点。这些应该可以回答您的问题。
猜你喜欢
  • 2014-12-19
  • 1970-01-01
  • 1970-01-01
  • 2019-08-14
  • 2021-03-10
  • 2015-08-20
  • 2021-09-19
  • 2021-11-26
  • 1970-01-01
相关资源
最近更新 更多