• 从Redis读取.NET Core配置


    在本文中,我们将创建一个自定义的.NET Core应用配置源和提供程序,用于从Redis中读取配置。在此之前,您需要稍微了解一些.NET Core配置提供程序的工作原理,相关的内容可以在Microsoft开发者官网搜索到。另外您可能还需要了解一些Redis的基础知识,比如Redis的基础数据类型,持久化等等。

    一、配置的数据格式

    .NET Core应用支持多种配置源(例如json、xml、ini文件,环境变量,内存字典,自定义源等),并且支持同时添加多个配置源,这也是本文的前提条件。应用程序会按照加入的先后顺序替换或补充配置。默认情况下,.NET Core应用的配置是存储在appsettings.json文件中的。在早期的.NET Core应用中,Program.cs的CreateHost方法里面还能看到AddJsonFile("appsettings.json").AddJsonFile($"appsetting.{env.Environment}.json")这样的代码,但是.NET 5以后,这段代码默认被隐藏了。

    看过源码的朋友应该知道,.NET Core应用读取配置后,会将数据转换为一个Key和Value都是string的字典。Key的格式为Node1:Node2:abc。例如:

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "ConnectionStrings": {
        "DefaultConnection": "Server=myserver;Database=mydb;User=myuser;Password=mypassword;"
      },
      "AppSettings": {
        "ApiBaseUrl": "https://api.example.com",
        "ApiKey": "your-api-key"
      },
      "AllowedHost":["foo1.com","foo2.com"]
    }
    

    转换后的数据为:

    Logging:LogLevel:Default=Information
    Logging:LogLevel:Microsoft=Warning
    Logging:LogLevel:Microsoft.Hosting.Lifetime=Information
    ConnectionStrings:DefaultConnection=Server=myserver;Database=mydb;User=myuser;Password=mypassword;
    AppSettings:ApiBaseUrl=https://api.example.com
    AppSettings:ApiKey=your-api-key
    AllowedHost:0=foo1.com
    AllowedHost:1=foo2.com
    

    二、Redis的Hash类型

    通过上面介绍,Redis的Hash数据结构刚好完美的切合了这一特点。先简单的介绍一下:

    在Redis中,Hash是一种数据结构,用于存储键值对的集合,其中每个键都映射到一个值。Redis的Hash是一个键值对的无序集合,其中的每个键都是唯一的。Hash是一个类似于字典或关联数组的概念,在其他编程语言中也称为Map或Dictionary。

    三、代码实现

    创建好项目之后,我们需要安装一个NuGet包,就是大家熟知的StackExchange.Redis,到目前为止应该是.NET应用程序使用最多的Redis客户端。

    PM> Install-Package StackExchange.Redis -v 2.7.10
    

    您也可以通过Visual Studio、Rider自带的NuGet客户端安装,或者是直接在csproj文件中加入

    RedisConfigurationProvider.cs

    public sealed class RedisConfigurationProvider : ConfigurationProvider, IAsyncDisposable
    {
    	private readonly ConnectionMultiplexer _connection;
        
        private readonly IDatabase _database;
        
        private readonly string _key;
    
    	public RedisConfigurationProvider(RedisConfigurationSource source)
    	{
            _key = source.Key;
    		_connection = ConnectionMultiplexer.Connect(source.ConnectionString);
            _database = _connection.GetDatabase(source.Database);
    	}
    
    	/// 
    	public override void Load()
    	{
    		Data = _connection.HashGetAll(_key).ToDictionary(x => x.Name.ToString(), x => ReadRedisValue(x.Value);
    	}
    
        private static string ReadRedisValue(RedisValue value)
    	{
    		if (value.IsNull)
    		{
    			return null;
    		}
    
    		return value.IsNullOrEmpty ? string.Empty : value.ToString();
    	}
        
    	/// 
    	public async ValueTask DisposeAsync()
    	{
    		await _connection.CloseAsync();
    		await _connection.DisposeAsync();
    	}
    }
    

    RedisConfigurationSource.cs

    public sealed class RedisConfigurationSource : IConfigurationSource
    {
    	/// 
    	/// The Redis connection string.
    	/// 
    	[DisallowNull]
    	public string ConnectionString { get; set; }
    
    	/// 
    	/// Gets or sets the Redis database ID.
    	/// 
    	public int Database { get; set; } = -1;
    
    	/// 
    	/// Gets or sets the Redis key this source will read from.
    	/// 
    	/// 
    	/// The key is expected to be a hash.
    	/// 
    	public string Key { get; set; } = "appsettings";
        
    	/// 
    	public IConfigurationProvider Build(IConfigurationBuilder builder)
    	{
    		return new RedisConfigurationProvider(this);
    	}
    }
    

    关键代码就这些,看上去似乎很简单……事实上确实很简单。

    添加配置源

    添加配置源的方法也很简单

    // Program.cs
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Configuration.Add(new RedisConfigurationSource
    {
    	ConnectionString = "localhost:6379",
    	Key = "appsettings.dev"
    });
    

    RedisConfigurationSource里面总共只有三个属性,ConnectionString用于配置Redis连接字符串,Database用于指定从哪个数据库读取数据,也可以在连接字符串里面指定。Key用于指定要读取的键名称。

    通过编写一些简单的代码,我们实现了一个能满足基本需求的分布式.NET Core配置提供程序。

    Starfish.Redis

    不想动手的朋友可以直接用我已经制作好的包

    https://www.nuget.org/packages/Starfish.Redis

    安装

    Visual Studio包管理器搜索Starfish.Redis,或者执行dotnet add package Starfish.Redis

    配置

    // Program.cs
    var builder = WebApplication.CreateBuilder(args);
    builder.Configuration.AddRedis("127.0.0.1:6379,defaultDatabase=0,connectTimeout=5000,connectRetry=3", "appsettings");
    

    启用Redis Keyspace Notifications

    Starfish.Redis有两种机制用于实现ReloadOnChanged(配置修改后重新加载数据),一种是定时查询指定的Key,时效性稍微差一些。另一种是利用Redis的Keyspace Event和Pub/Sub模式来实现,当订阅的Key发生变化(删除、修改、过期等)时会主动发送通知给订阅者,使用这种模式需要配置Redis服务的notify-keyspace-events。

    关于notify-keyspace-events配置,可参考下面的描述:

    • K:Keyspace事件,将会以__keyspace@__作为事件的前缀
    • E:Keyevent事件,将会以__keyevent@__作为事件的前缀
    • g:非特定类型的通用命令,例如DEL、EXPIRE、RENAME等
    • $:字符串命令,例如SET、INCR等
    • l:列表命令,例如LPUSH、LPOP等
    • s:集合命令,例如SADD、SREM等
    • h:哈希表命令,例如HSET、HINCRBY等
    • z:有序集合命令,例如ZSET、ZREM等
    • t:流命令,例如XADD、XDEL等
    • x:过期事件(在每个发生键过期的时侯产生)
    • e:淘汰事件(在每个发生键被淘汰的时候产生)
    • m:未命中事件(在访问某个不存在的键使产生)
    • A:配置g$lshztxe的别名,但不包括未命中事件m

    简单起见,我们直接配置为AKE(启用所有事件的通知)。

    方法一:redis-cli

    redis-cli config set notify-keyspace-events AKE
    

    方法二:docker参数

    docker run -d --name redisname -p 6379:6379 redis --notify-keyspace-events AKE
    

    方法三:配置文件

    找到并打开打开redis.conf,在末尾加上

    notify-keyspace-events AKE
    

    注意事项

    1. Redis本身自带持久化策略,但是有的企业/团队没有开启或者是特意关闭了持久化,因此需要谨慎使用此方案。
    2. 强烈建议将存储配置数据的key设置为永不过期(TTL设置为-1),避免key过期带来一些不必要的麻烦。

    导入appsettings.json到Redis

    微软.NET库提供了一个内部类JsonConfigurationFileParser用于将json格式的配置转换为Dictionary

    namespace Microsoft.Extensions.Configuration.Json
    {
        internal sealed class JsonConfigurationFileParser
        {
            private JsonConfigurationFileParser() { }
    
            private readonly Dictionary<string, string?> _data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
            private readonly Stack<string> _paths = new Stack<string>();
    
            public static IDictionary<string, string?> Parse(Stream input)
                => new JsonConfigurationFileParser().ParseStream(input);
    
            private Dictionary<string, string?> ParseStream(Stream input)
            {
                var jsonDocumentOptions = new JsonDocumentOptions
                {
                    CommentHandling = JsonCommentHandling.Skip,
                    AllowTrailingCommas = true,
                };
    
                using (var reader = new StreamReader(input))
                using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
                {
                    if (doc.RootElement.ValueKind != JsonValueKind.Object)
                    {
                        throw new FormatException(SR.Format(SR.Error_InvalidTopLevelJSONElement, doc.RootElement.ValueKind));
                    }
                    VisitObjectElement(doc.RootElement);
                }
    
                return _data;
            }
    
            private void VisitObjectElement(JsonElement element)
            {
                var isEmpty = true;
    
                foreach (JsonProperty property in element.EnumerateObject())
                {
                    isEmpty = false;
                    EnterContext(property.Name);
                    VisitValue(property.Value);
                    ExitContext();
                }
    
                SetNullIfElementIsEmpty(isEmpty);
            }
    
            private void VisitArrayElement(JsonElement element)
            {
                int index = 0;
    
                foreach (JsonElement arrayElement in element.EnumerateArray())
                {
                    EnterContext(index.ToString());
                    VisitValue(arrayElement);
                    ExitContext();
                    index++;
                }
    
                SetNullIfElementIsEmpty(isEmpty: index == 0);
            }
    
            private void SetNullIfElementIsEmpty(bool isEmpty)
            {
                if (isEmpty && _paths.Count > 0)
                {
                    _data[_paths.Peek()] = null;
                }
            }
    
            private void VisitValue(JsonElement value)
            {
                Debug.Assert(_paths.Count > 0);
    
                switch (value.ValueKind)
                {
                    case JsonValueKind.Object:
                        VisitObjectElement(value);
                        break;
    
                    case JsonValueKind.Array:
                        VisitArrayElement(value);
                        break;
    
                    case JsonValueKind.Number:
                    case JsonValueKind.String:
                    case JsonValueKind.True:
                    case JsonValueKind.False:
                    case JsonValueKind.Null:
                        string key = _paths.Peek();
                        if (_data.ContainsKey(key))
                        {
                            throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key));
                        }
                        _data[key] = value.ToString();
                        break;
    
                    default:
                        throw new FormatException(SR.Format(SR.Error_UnsupportedJSONToken, value.ValueKind));
                }
            }
    
            private void EnterContext(string context) =>
                _paths.Push(_paths.Count > 0 ?
                    _paths.Peek() + ConfigurationPath.KeyDelimiter + context :
                    context);
    
            private void ExitContext() => _paths.Pop();
        }
    }
    

    点关注,不迷路。

    如果您喜欢这篇文章,请不要忘记点赞、关注、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论……

    公众号

  • 相关阅读:
    三款免费的AI绘画网站对比分析,真正好用的居然是它
    PEX装机
    【网络编程】网络编程中的基本概念及Java实现UDP、TCP客户端服务器程序(万字博文)
    【云原生 | 从零开始学Kubernetes】十二、k8spod的生命周期与容器钩子
    【华为OD机试真题 JS】机器人走迷宫
    ARM可用的可信固件项目简介
    在列表中按照概率抽样choice()
    【Quark RISC-V】流水线CPU设计(2)理想流水线
    第五十八章 CSP的常见问题 - HTTP请求失败
    计算机网络 CTCP 对客户端的封装
  • 原文地址:https://www.cnblogs.com/zhaorong/p/aspnet-configuration-redis.html