Внедрите собственный поставщик конфигурации с помощью Redis и получайте мгновенные обновления с помощью IOptionsMonitor.

В предыдущем посте мы уже создали собственный поставщик конфигурации с помощью EFCore. Если вы хотите это проверить, просто следуйте.

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

Чтобы сделать работу проще и надежнее, мы будем использовать пакет StackExchange.Redis. Также мы будем использовать тип данных Redis HashSet. Можно использовать и другие типы данных, но с помощью HashSet мы можем изолировать все значения конфигурации в одном ключе Redis. Мы можем выполнить разделение разделов с помощью : (двоеточие).

Источник конфигурации

Чтобы реализовать собственный источник конфигурации, нам нужно создать как минимум два класса: один реализует IConfigurationSource, а другой наследуется от ConfigurationProvider. Начнем с RedisConfigurationSource:

public class RedisConfigurationSource : IConfigurationSource
{
    public readonly ConfigurationOptions ConfigurationOptions;
    public readonly string RedisConfigurationKey;
    public readonly bool ReloadOnChange;
    public readonly Action<ConfigurationRedisLoadExceptionContext>? OnLoadException;

    public RedisConfigurationSource(ConfigurationOptions configurationOptions,
        string redisConfigurationKey,
        bool reloadOnChange = true,
        Action<ConfigurationRedisLoadExceptionContext> onLoadException = null)
    {
        ConfigurationOptions = configurationOptions;
        RedisConfigurationKey = redisConfigurationKey;
        ReloadOnChange = reloadOnChange;
        OnLoadException = onLoadException;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder) => new RedisConfigurationProvider(this);
}

Как видно, это довольно простой класс.

  • ConfigurationOptions ConfigurationOptions: Чтобы подключить базу данных Redis, она содержит необходимые значения конфигурации, и мы используем ее непосредственно из пакета StackExchange.Redis.
  • string redisConfigurationKey: Суффикс ключа Redis для хранения значений конфигурации в HashSet и суффикс канала подписки Redis для мгновенного получения обновлений.
  • bool reloadOnChange: Включение автоматической перезагрузки при изменении значений настроек.
  • Action‹ConfigurationRedisLoadExceptionContext› onLoadException: Обратный вызов для исключения происходит при попытке загрузить или перезагрузить настройки из Redis.

В Build мы просто создаем новый экземпляр нашего класса-поставщика и возвращаем его.

Поставщик конфигурации

Вся логика загрузки и перезагрузки настроек из базы данных Redis находится в этом классе. Это основной компонент, предоставляющий значения параметров механизму конфигурации ASP.NET Core. Чтобы иметь работающий поставщик, все, что необходимо, — это переопределить метод Load, полученный из ConfigurationProvider. (Полную реализацию можно найти в репозитории GitHub.)

public class RedisConfigurationProvider : ConfigurationProvider, IDisposable
{
    private static readonly string RedisConfigurationKeyPrefix = "StackExchange_Redis_Configuration";
    private static readonly string RedisSubscriptionChannelPrefix = "StackExchange_Redis_Configuration_Subscription";

    private readonly RedisConfigurationSource _configurationSource;
    private bool _disposed;
    private readonly string _redisConfigurationKey;
    private readonly string _redisSubscriptionKey;
    private bool _isSubscribed = false;

    public RedisConfigurationProvider(RedisConfigurationSource configurationSource)
    {
        _configurationSource = configurationSource;

        _redisConfigurationKey = $"{RedisConfigurationKeyPrefix}:{_configurationSource.RedisConfigurationKey}";
        _redisSubscriptionKey = $"{RedisSubscriptionChannelPrefix}:{_configurationSource.RedisConfigurationKey}";
    }

    public override void Load()
    {
        try
        {
            Data = GetData();
        }
        catch (Exception ex)
        {
            var exceptionContext = new ConfigurationRedisLoadExceptionContext(_configurationSource, ex);
            _configurationSource.OnLoadException?.Invoke(exceptionContext);
            if (!exceptionContext.Ignore)
            {
                throw;
            }
        }

        if (_configurationSource.ReloadOnChange && !_isSubscribed)
        {
            SubscribeRedis();
        }
    }

    private void SubscribeRedis()
    {
        try
        {
            ISubscriber? subscriber = RedisConnection.GetInstance(_configurationSource.ConfigurationOptions).Connection?.GetSubscriber();
            
            if(subscriber is null)
            {
                return;
            }

            subscriber.Subscribe(_redisSubscriptionKey, (channel, message) =>
            {
                Dictionary<string, string?>? newData = JsonSerializer.Deserialize<Dictionary<string, string?>>(message.ToString());
                if(newData is null)
                {
                    return;
                }

                Data = newData;
                OnReload();
            });

            _isSubscribed = true;

        }
        catch (Exception ex)
        {
            var exceptionContext = new ConfigurationRedisLoadExceptionContext(_configurationSource, ex);
            _configurationSource.OnLoadException?.Invoke(exceptionContext);
            if (!exceptionContext.Ignore)
            {
                throw;
            }
        }
    }

public void Dispose();
private IDictionary<string, string>? GetData();

Логика довольно проста. Мы читаем значения при первой их загрузке. Также мы подписываемся на канал Redis, чтобы следить за изменениями.

Чтобы упростить задачу, мы ожидаем словарь JSON, когда прослушиваем канал подписки. Не проверяя, произошли ли какие-либо реальные изменения или нет, просто перезагрузите все доступные значения.

Полную реализацию можно увидеть на GitHub.

Использование с IOptionsMonitor

Чтобы использовать наш поставщик конфигурации Redis с IOptionsMonitor, дополнительных действий не требуется. Нам просто нужно зарегистрировать наш класс конфигурации в контейнере внедрения зависимостей (также известном как ServiceCollection) и при необходимости разрешить IOptionsMonitore.

IServiceCollection services = new ServiceCollection();

ConfigurationBuilder configuration = new ConfigurationBuilder();
configuration.Sources.Clear();
configuration.AddRedisConfiguration(ConfigurationOptions.Parse(redisConnectionConfiguration), "test");

IConfigurationRoot configurationRoot = configuration.Build();

services.AddOptions<MyConfigOptions>()
     .Bind(configurationRoot.GetSection(MyConfigOptions.MyConfig));

Заключение

Мы создаем полноценный поставщик конфигурации для Redis с функцией публикации/подписки для мгновенного получения обновлений. Наша реализация автоматически перезагружает изменения без опроса базы данных.

Подробную реализацию и документацию можно найти ниже репозитория GitHub.