Остановите SqlDependency в настраиваемом поставщике конфигурации ядра ASP.NET - PullRequest
0 голосов
/ 07 января 2019

Я написал собственный поставщик конфигурации для загрузки конфигурации ядра ASP.NET из таблицы базы данных в соответствии с инструкциями здесь:

Поставщик пользовательских настроек ASP.Net

Мой провайдер использует SqlDependency для перезагрузки конфигурации в случае изменения значений в базе данных.

Документация для SqlDependency гласит:

Метод Stop должен вызываться для каждого вызова Start. Данный слушатель полностью отключается только тогда, когда он получает столько же запросов на остановку, сколько и запросов на запуск.

В чем я не уверен, так это в том, как это сделать в провайдере нестандартной конфигурации для 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

Этот класс отслеживает changeToken в DbConfigurationDataProvider и публикует новую конфигурацию в приложении.

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

Подведем итог: как мне обеспечить, чтобы метод Dispose() вызывался в классе DbConfigurationDataProvider?

Единственная информация, которую я нашел до сих пор, отсюда: https://andrewlock.net/four-ways-to-dispose-idisposables-in-asp-net-core/

О том, как распоряжаться объектами:

  1. Внутри блоков кода с оператором using (Не применимо)
  2. В конце запроса (не применимо)
  3. Использование контейнера DI (неприменимо - не думаю?)
  4. Когда приложение заканчивается <- звучит многообещающе </strong>

Вариант 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

EDIT
Этот пример даже не называет SqlDependency.Stop(), может, это не так важно?

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

1 Ответ

0 голосов
/ 07 января 2019

«правильный» способ сделать это состоял бы в том, чтобы ваш поставщик конфигурации был одноразовым, а затем утилизировал все ваши SqlDependency объекты как часть утилизации поставщика конфигурации.

К сожалению, в 2.x инфраструктура конфигурации не поддерживает одноразовых провайдеров. Однако это может быть изменено как часть aspnet / Extensions # 786 и aspnet / Extensions # 861 .

Поскольку я принимал участие в разработке этого, я с гордостью могу объявить, что начиная с версии 3.0, будут поддерживаться поставщики одноразовых конфигураций .

С Microsoft.Extensions.Configuration 3.0 одноразовые провайдеры будут правильно расположены после удаления корневого каталога конфигурации. И корень конфигурации будет расположен в ASP.NET Core 3.0, когда (веб) хост будет удален. Таким образом, в конце концов, ваши поставщики одноразовых конфигураций будут утилизированы должным образом и больше не должны ничего пропускать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...