【问题标题】:Using a Scoped service in a Singleton in an Asp.Net Core app在 Asp.Net Core 应用程序的单例中使用 Scoped 服务
【发布时间】:2019-04-16 12:44:56
【问题描述】:

在我的 Asp.Net Core 应用程序中,我需要一个可以在应用程序的生命周期内重复使用的单例服务。要构建它,我需要一个 DbContext(来自 EF Core),但它是一个作用域服务而不是线程安全的。

因此,我使用以下模式来构建我的单例服务。它看起来有点 hacky,因此我想知道这是否是一种可接受的方法并且不会导致任何问题?

services.AddScoped<IPersistedConfigurationDbContext, PersistedConfigurationDbContext>();
services.AddSingleton<IPersistedConfigurationService>(s =>
{
    ConfigModel currentConfig;
    using (var scope = s.CreateScope())
    {
        var dbContext = scope.ServiceProvider.GetRequiredService<IPersistedConfigurationDbContext>();
        currentConfig = dbContext.retrieveConfig();            
    }
    return new PersistedConfigurationService(currentConfig);
});

...

public class ConfigModel
{
    string configParam { get; set; }
}

【问题讨论】:

  • 有许多重复项展示了如何执行此操作。后台服务的文档也是如此。 PersistedConfigurationService 的构造函数中应该有一个 IServiceProvider 参数。 应该在需要时创建范围。
  • ServiceProvider 只不过是一个“根”作用域;因此,您可以直接使用它(即无需创建范围)。
  • 检查Consuming a scoped service in a background task。它表明IServiceProvider 被注入到单例服务的构造函数中。实际需要作用域服务的方法DoWork 创建作用域并仅在实际需要时才请求服务。
  • @PanagiotisKanavos,感谢您分享带有示例的链接。我剩下的问题是我的方法是否很好,因为它正在做与你类似的事情,但在不同的地方(当服务的构建发生时,而不是在服务内部)
  • @TanvirArjel 是的,没错。很容易误解这一点,并认为我们有一个单例尝试使用范围服务,但我们没有。

标签: c# asp.net-core .net-core


【解决方案1】:

你做的不好,肯定会导致问题。由于这是在服务注册中完成的,所以当您的单例首次注入时,范围内的服务将被检索一次。换句话说,这里的代码只会在你注册的服务的生命周期内运行一次,因为它是一个单例,这意味着它只会发生一次,期间。此外,您在此处注入的上下文仅存在于您创建的范围内,一旦 using 语句关闭,该范围就会消失。因此,当你真正尝试在单例中使用上下文时,它已经被释放了,你会得到一个ObjectDisposedException

如果您需要在单例中使用作用域服务,则需要将IServiceProvider 注入单例中。然后,您需要创建一个作用域并在需要使用它时提取上下文,而这将需要每次在您需要使用它时完成。例如:

public class PersistedConfigurationService : IPersistedConfigurationService
{
    private readonly IServiceProvider _serviceProvider;

    public PersistedConfigurationService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task Foo()
    {
        using (var scope = _serviceProvider.CreateScope())
        {
             var context = scope.ServiceProvider.GetRequiredService<IPersistedConfigurationDbContext>();
             // do something with context
        }
    }
}

再次强调,您需要在需要使用范围服务(您的上下文)的每个方法中执行此操作。你不能把它坚持到 ivar 或其他东西上。如果您对代码感到厌烦,那么您应该是,因为这是一种反模式。如果您必须在单例中获得范围服务,则别无选择,但通常情况下,这是设计不佳的标志。如果一个服务需要使用作用域服务,它几乎总是应该是作用域本身,而不是单例的。只有少数情况您真正需要单例生命周期,而这些情况主要围绕处理信号量或其他需要在应用程序生命周期中保持的状态。除非有一个非常很好的理由让您的服务成为单例,否则您应该在所有情况下都选择作用域; scoped 应该是默认生命周期,除非您有理由不这样做。

【讨论】:

  • 感谢您提供如此详细的答案,但我认为您误解了我的问题。我很抱歉,因为不清楚。我已经用我认为隐含明确的部分更新了问题。
  • 而且我相信您的建议与我的问题基本相同:)
  • 没有。我不相信我完全误解了。如果您需要在单例中访问范围服务,我上面的答案中的方法是这样做的唯一方法。您拥有的代码从根本上被破坏了,根本无法工作。所以,这甚至不是“可接受”与否的问题,它根本行不通。
  • 我没有将作用域服务作为参数传递给单例的构造函数。我正在实例化一个作用域服务,使用它来检索一些数据,然后将其处理掉,再也看不到作用域对象。
  • 那么retrieveConfig返回一个永不改变的静态对象?
【解决方案2】:

虽然 ASP.NET Core 中的 Dependency injection: Service lifetimes 文档说:

从单例解析作用域服务是很危险的。可能导致服务在处理后续请求时状态不正确。

但在你的情况下,这不是问题。实际上,您并没有从单例解决范围服务。它只是在需要时从单例中获取范围服务的实例。 所以你的代码应该可以正常工作,没有任何处置的上下文错误!

但另一个潜在的解决方案是使用IHostedService。以下是有关它的详细信息:

Consuming a scoped service in a background task (IHostedService)

【讨论】:

  • 该文档还显示了如何正确执行此操作
  • @PanagiotisKanavos 好的,知道了!你是在说IHostedService吗?
  • IHostedService 只是单例服务的一个示例。
  • @PanagiotisKanavos 是的!我正在改进我的答案。再次感谢。
【解决方案3】:

查看此服务的名称 - 我认为您需要的是一个自定义配置提供程序,它在启动时从数据库加载配置(仅一次)。你为什么不做类似以下的事情呢?它是一种更好的设计,更符合框架的方法,而且您可以将其构建为共享库,其他人也可以从中受益(或者您可以从多个项目中受益)。


public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .ConfigureAppConfiguration((context, config) =>
                {
                    var builtConfig = config.Build();
                    var persistentConfigBuilder = new ConfigurationBuilder();
                    var connectionString = builtConfig["ConnectionString"];
                    persistentStorageBuilder.AddPersistentConfig(connectionString);
                    var persistentConfig = persistentConfigBuilder.Build();
                    config.AddConfiguration(persistentConfig);
                });
}

这里 - AddPersistentConfig 是一个扩展方法,构建为一个类似这样的库。


public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddPersistentConfig(this IConfigurationBuilder configurationBuilder, string connectionString)
    {
          return configurationBuilder.Add(new PersistentConfigurationSource(connectionString));
    }

}

class PersistentConfigurationSource : IConfigurationSource
{
    public string ConnectionString { get; set; }

    public PersistentConfigurationSource(string connectionString)    
    {
           ConnectionString = connectionString;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
         return new PersistentConfigurationProvider(new DbContext(ConnectionString));
    }

}

class PersistentConfigurationProvider : ConfigurationProvider
{
    private readonly DbContext _context;
    public PersistentConfigurationProvider(DbContext context)
    {
        _context = context;
    }


    public override void Load() 
    {
           // Using _dbContext
           // Load Configuration as valuesFromDb
           // Set Data
           // Data = valuesFromDb.ToDictionary<string, string>...
    }

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-12-05
    • 2021-07-07
    • 2020-09-03
    • 1970-01-01
    • 2023-03-03
    • 2021-01-22
    • 2017-07-18
    • 2021-03-15
    相关资源
    最近更新 更多