如何正确解析要在 ASP.NET Core 3.1 中的 ConfigureServices() 中使用的服务?

How to correctly resolve services to use in the ConfigureServices() in ASP.NET Core 3.1?

我有一个 ASP.NET 基于 Core 3.1 的应用程序。在应用程序启动期间,在 ConfigureServices(IServiceCollection services) 我想注册我的服务。但是在配置服务时,我想根据数据库中的设置启动应用程序。

这是我的代码

public void ConfigureServices(IServiceCollection services)
{
    // Register context
    services.AddDbContext<AppDbContext>(options =>
    {
        options.UseMySql(Configuration.GetConnectionString("MySqlServerConnection"));
    });

    // Reister the setting provider
    services.AddSingleton<IAppSetting, AppSettings>();

    // Create a resolver which is WRONG!!
    var resolver = services.BuildServiceProvider();

    var setting = resolver.GetService<IAppSetting>();

    List<string> allowedCors = new List<string>()
    {
         setting.MainUrl.ToString()
    };

    if (setting.HasCustomAssetsUri)
    {
        allowedCors.Add(settings.AssetsUrl.ToString());
    }

    if (settings.HasCustomPhotosUri)
    {
        allowedCors.Add(settings.PhotosUrl.ToString());
    }

    services.AddCors(options =>
    {
        options.AddPolicy("AllowSubDomainTraffic",
        builder =>
        {
            builder.WithOrigins(allowedCors.ToArray())
                   .AllowAnyHeader()
                   .AllowAnyMethod();
        });
    });

    // More services
}

正如您在上面的代码中看到的,我注册了 IAppSetting 但我立即想使用它来访问数据库,因此获取当前配置。我目前正在调用 services.BuildServiceProvider() 来创建一个新的解析器,然后我可以在其中使用它来创建 IAppSetting.

的实例

我的 AppSetting 实现依赖于同样注册的 AppDbContext。这是我的 AppSetting

public class AppSetting : IAppSetting
{
    private AppDbContext context;

    public AppSetting(AppDbContext context)
    {
        this.context = context;
    }

    // methods...
}

调用BuildServiceProvider() 将导致创建单独服务的额外副本。所以我尽量避免这样做。

如何在 ConfigureServices() 方法中正确注册然后解析 IAppSetting 以便我可以使用它来访问在数据库中找到的设置?

已更新

我尝试使用 Nkosi 提供的解决方案,但出现以下错误

System.InvalidOperationException: 'Cannot resolve scoped service 'IAppSetting' from root provider.'

这是完整的堆栈跟踪。

This exception was originally thrown at this call stack:
    Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(System.Type, Microsoft.Extensions.DependencyInjection.IServiceScope, Microsoft.Extensions.DependencyInjection.IServiceScope)
    Microsoft.Extensions.DependencyInjection.ServiceProvider.Microsoft.Extensions.DependencyInjection.ServiceLookup.IServiceProviderEngineCallback.OnResolve(System.Type, Microsoft.Extensions.DependencyInjection.IServiceScope)
    Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(System.Type, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope)
    Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(System.Type)
    Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(System.IServiceProvider, System.Type)
    Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(Microsoft.Extensions.DependencyInjection.ServiceLookup.FactoryCallSite, Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext)
    Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCallSite, Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext)
    ...
    [Call Stack Truncated]
services.AddSingleton<AppSetting>();
services.AddSingleton<IAppSetting>(ctx =>
    {
      var setting = ctx.GetRequiredService<AppSetting>();
      return setting;
    });

引用Use DI services to configure options

使用 DI 配置 CORS 选项,将所有逻辑移至配置委托中

//...

// Register context
services.AddDbContext<AppDbContext>(options => {
    options.UseMySql(Configuration.GetConnectionString("MySqlServerConnection"));
});

// Reister the setting provider
services.AddScoped<IAppSetting, AppSettings>(); 

//configure CORS options using DI
services.AddOptions<CorsOptions>()
    .Configure<IServiceScopeFactory>((options, sp) => {
        using(var scope = sp.CreateScope()) {
            IAppSetting settings = scope.ServiceProvider.GetRequiredService<IAppSetting>();
            List<string> allowedCors = new List<string>() {
                 setting.MainUrl.ToString()
            };

            if (setting.HasCustomAssetsUri) {
                allowedCors.Add(settings.AssetsUrl.ToString());
            }

            if (settings.HasCustomPhotosUri) {
                allowedCors.Add(settings.PhotosUrl.ToString());
            }
            options.AddPolicy("AllowSubDomainTraffic", builder => {
                builder.WithOrigins(allowedCors.ToArray())
                       .AllowAnyHeader()
                       .AllowAnyMethod();
            });
        }
    });

services.AddCors();

//...

这样一来,现在一切都推迟到实际需要的时候,您可以避免过早地构建服务集合。

考虑在以下示例中使用 IConfigureOptions(避免需要自己创建范围):

public class ConfigureCorsOptions : IConfigureOptions<CorsOptions>
{
    private readonly IAppSetting _settings;

    public ConfigureCorsOptions(IAppSetting settings) => _settings = settings;


    public void Configure(CorsOptions options)
    {
        List<string> allowedCors = new List<string>()
        {
             _setting.MainUrl.ToString()
        };

        if (setting.HasCustomAssetsUri)
        {
            allowedCors.Add(_settings.AssetsUrl.ToString());
        }

        if (_settings.HasCustomPhotosUri)
        {
            allowedCors.Add(_settings.PhotosUrl.ToString());
        }

        options.AddPolicy("AllowSubDomainTraffic", builder =>
        {
           builder.WithOrigins(allowedCors.ToArray())
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
    }
}

要将此 class 注册到 DI 容器,您可以使用类似的东西:

services.AddTransient<IConfigureOptions<CorsOptions>, ConfigureCorsOptions>();