在自定义 ASP.NET Core 配置提供程序中停止 SqlDependency

Posted

技术标签:

【中文标题】在自定义 ASP.NET Core 配置提供程序中停止 SqlDependency【英文标题】:Stop SqlDependency in custom ASP.NET Core Configuration Provider 【发布时间】:2019-06-01 23:37:46 【问题描述】:

我编写了一个自定义配置提供程序,按照此处的说明从数据库表中加载 ASP.NET Core 配置:

ASP.Net Custom Configuration Provider

如果数据库中的值发生变化,我的提供商使用SqlDependency 重新加载配置。

SqlDependency 的 documentation 声明:

必须为每个 Start 调用调用 Stop 方法。给定的侦听器只有在收到与启动请求相同数量的停止请求时才会完全关闭。

我不确定如何在 ASP.NET Core 的自定义配置提供程序中执行此操作。

下面是代码:

DbConfigurationSource

基本上是IDbProvider 的容器,用于处理从数据库中检索数据

public class DbConfigurationSource : IConfigurationSource

    /// <summary>
    /// Used to access the contents of the file.
    /// </summary>
    public virtual IDbProvider DbProvider  get; set; 


    /// <summary>
    /// Determines whether the source will be loaded if the underlying data changes.
    /// </summary>
    public virtual bool ReloadOnChange  get; set; 

    /// <summary>
    /// Will be called if an uncaught exception occurs in FileConfigurationProvider.Load.
    /// </summary>
    public Action<DbLoadExceptionContext> OnLoadException  get; set; 

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    
        return new DbConfigurationProvider(this);
    

DbConfigurationDataProvider

这是创建和监视SqlDependency 并从数据库加载数据的类。这也是Dispose() 呼叫是我想要Stop() SqlDependency 的地方。 Dispose() 当前未被调用。

public class DbConfigurationDataProvider : IDbProvider, IDisposable
        
    private readonly string _applicationName;
    private readonly string _connectionString;

    private ConfigurationReloadToken _reloadToken;

    public DbConfigurationDataProvider(string applicationName, string connectionString)
    
        if (string.IsNullOrWhiteSpace(applicationName))
        
            throw new ArgumentNullException(nameof(applicationName));
        

        if (string.IsNullOrWhiteSpace(connectionString))
        
            throw new ArgumentNullException(nameof(connectionString));
        

        _applicationName = applicationName;
        _connectionString = connectionString;

        _reloadToken = new ConfigurationReloadToken();

        SqlDependency.Start(_connectionString);
    

    void OnDependencyChange(object sender, SqlNotificationEventArgs e)
    
        var dependency = (SqlDependency)sender;
        dependency.OnChange -= OnDependencyChange;

        var previousToken = Interlocked.Exchange(
            ref _reloadToken,
            new ConfigurationReloadToken());

        previousToken.OnReload();
    

    public IChangeToken Watch()
    
        return _reloadToken;
    

    public List<ApplicationSettingDto> GetData()
    
        var settings = new List<ApplicationSettingDto>();

        var sql = "select parameter, value from dbo.settingsTable where application = @application";

        using (var connection = new SqlConnection(_connectionString))
        
            using (var command = new SqlCommand(sql, connection))
            
                command.Parameters.AddWithValue("application", _applicationName);

                var dependency = new SqlDependency(command);

                // Subscribe to the SqlDependency event.  
                dependency.OnChange += OnDependencyChange;

                connection.Open();

                using (var reader = command.ExecuteReader())
                
                    var keyIndex = reader.GetOrdinal("parameter");
                    var valueIndex = reader.GetOrdinal("value");

                    while (reader.Read())
                    
                        settings.Add(new ApplicationSettingDto
                            Key = reader.GetString(keyIndex), Value = reader.GetString(valueIndex));
                    
                
            
        

        Debug.WriteLine($"DateTime.Now: settings.Count settings loaded");

        return settings;
    

    public void Dispose()
    
        SqlDependency.Stop(_connectionString);
        Debug.WriteLine($"nameof(WhsConfigurationProvider) Disposed");
    

DbConfigurationProvider

该类监控DbConfigurationDataProvider中的changeToken并将新配置发布到应用程序。

public class DbConfigurationProvider : ConfigurationProvider

    private DbConfigurationSource Source  get; 

    public DbConfigurationProvider(DbConfigurationSource source)
    
        Source = source ?? throw new ArgumentNullException(nameof(source));

        if (Source.ReloadOnChange && Source.DbProvider != null)
        
            ChangeToken.OnChange(
                () => Source.DbProvider.Watch(),
                () =>
                                        
                    Load(reload: true);
                );                
                   
    

    private void Load(bool reload)
    
        // Always create new Data on reload to drop old keys
        if (reload)
        
            Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        

        var settings = Source.DbProvider.GetData();

        try
        
            Load(settings);
        
        catch (Exception e)
        
            HandleException(e);
        

        OnReload();
    

    public override void Load()
    
        Load(reload: false);
    

    public void Load(List<ApplicationSettingDto> settings)
    
        Data = settings.ToDictionary(s => s.Key, s => s.Value, StringComparer.OrdinalIgnoreCase);                       
    

    private void HandleException(Exception e)
    
            // Removed for brevity
         

DbConfigurationExtensions

为设置所有内容而调用的扩展方法。

public static class DbConfigurationExtensions

    public static IConfigurationBuilder AddDbConfiguration(this IConfigurationBuilder builder, IConfiguration config, string applicationName = "")
    
        if (string.IsNullOrWhiteSpace(applicationName))
        
            applicationName = config.GetValue<string>("ApplicationName");
        

        // DB Server and Catalog loaded from Environment Variables for now
        var server = config.GetValue<string>("DbConfigurationServer");
        var database = config.GetValue<string>("DbConfigurationDatabase");

        if (string.IsNullOrWhiteSpace(server))
        
            // Removed for brevity
        

        if (string.IsNullOrWhiteSpace(database))
        
            // Removed for brevity
        

        var sqlBuilder = new SqlConnectionStringBuilder
        
            DataSource = server,
            InitialCatalog = database,
            IntegratedSecurity = true
        ;

        return builder.Add(new DbConfigurationSource
        
             DbProvider = new DbConfigurationDataProvider(applicationName, sqlBuilder.ToString()),                
             ReloadOnChange = true
         );
    

最后,设置整个事情的调用:

public class Program

    public static void Main(string[] args)
                            
        CreateWebHostBuilder(args).Build().Run();            
    

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
    
        config.AddDbConfiguration(hostingContext.Configuration, "TestApp");            
    ).UseStartup<Startup>();

总结一下:如何确保在DbConfigurationDataProvider 类中调用Dispose() 方法?

到目前为止,我发现的唯一信息来自这里: https://andrewlock.net/four-ways-to-dispose-idisposables-in-asp-net-core/

其中包括如何处理对象:

    在带有 using 语句的代码块内(不适用) 请求结束时(不适用) 使用 DI 容器(不适用 - 我不认为?) 当应用程序结束时

选项 4 如下所示:

public void Configure(IApplicationBuilder app, IApplicationLifetime applicationLifetime,
                        SingletonAddedManually toDispose)

        applicationLifetime.ApplicationStopping.Register(OnShutdown, toDispose);

         // configure middleware etc


private void OnShutdown(object toDispose)

    ((IDisposable)toDispose).Dispose();

SingletonAddedManually 在我的例子中是 DbConfigurationDataProvider 类,但这远远超出了 Startup 类的范围。

更多关于IApplicationLifetime接口的信息:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-2.2

编辑 这个例子甚至都懒得打SqlDependency.Stop(),也许没那么重要?

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/sqldependency-in-an-aspnet-app

【问题讨论】:

嘿,我只是想通知您,对一次性提供程序的支持正在 3.0 中提供,并且应该已经可以在当前的 .NET Core 3.0 预览版 5 中进行测试。 【参考方案1】:

执行此操作的“正确”方法是让您的配置提供程序是一次性的,然后将您的所有SqlDependency 对象作为配置提供程序处置的一部分进行处置。

不幸的是,在 2.x 中,配置框架不支持一次性提供程序。但是,作为aspnet/Extensions#786 和aspnet/Extensions#861 的一部分,这可能会发生变化。

由于我参与了此项目的开发,我可以自豪地宣布,从 3.0 开始,将支持一次性配置提供程序

Microsoft.Extensions.Configuration 3.0 中,一次性提供程序将在配置根被处置时被正确处置。当(Web)主机被释放时,配置根将在 ASP.NET Core 3.0 中释放。所以最终,您的一次性配置提供程序将被妥善处理,并且不应再泄漏任何东西。

【讨论】:

以上是关于在自定义 ASP.NET Core 配置提供程序中停止 SqlDependency的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core 配置

ASP.NET Core 配置 - 创建自定义配置提供程序

理解ASP.NET Core

如何为 ASP.NET Core 应用配置 Azure 应用服务日志记录提供程序?

ASP.NET Core学习——2

[Asp.Net Core]Asp.Net Core与配置系统的集成