在Asp.Net Core应用程序的Singleton中使用范围服务

时间:2019-04-16 12:44:57

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

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

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

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

3 个答案:

答案 0 :(得分:0)

尽管ASP.NET Core中的Dependency injection: Service lifetimes文档说:

  

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

但是在您的情况下,这不是问题。实际上,您不是从单例解决作用域服务。它只是在需要时从单例获取作用域服务的实例。 因此您的代码应该可以正常运行,而不会出现任何上下文错误!

但是另一个可能的解决方案是使用IHostedService。这是有关它的详细信息:

Consuming a scoped service in a background task (IHostedService)

答案 1 :(得分:0)

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


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

}

答案 2 :(得分:-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应该是默认生存期。