Browse Source

扩展:.env文件

黄中银 2 weeks ago
parent
commit
f33fc05576

+ 13 - 0
Apq.Cfg.Env/Apq.Cfg.Env.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <RootNamespace>Apq.Cfg.Env</RootNamespace>
+        <Description>Apq.Cfg 的 .env 文件配置源扩展</Description>
+        <PackageTags>configuration;config;env;dotenv;environment</PackageTags>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\Apq.Cfg\Apq.Cfg.csproj"/>
+    </ItemGroup>
+
+</Project>

+ 24 - 0
Apq.Cfg.Env/CfgBuilderExtensions.cs

@@ -0,0 +1,24 @@
+namespace Apq.Cfg.Env;
+
+/// <summary>
+/// CfgBuilder 的 .env 文件扩展方法
+/// </summary>
+public static class CfgBuilderExtensions
+{
+    /// <summary>
+    /// 添加 .env 文件配置源
+    /// </summary>
+    /// <param name="builder">配置构建器</param>
+    /// <param name="path">.env 文件路径</param>
+    /// <param name="level">配置层级,数值越大优先级越高</param>
+    /// <param name="writeable">是否可写</param>
+    /// <param name="optional">文件不存在时是否忽略</param>
+    /// <param name="reloadOnChange">文件变更时是否自动重载</param>
+    /// <param name="isPrimaryWriter">是否为默认写入目标</param>
+    /// <returns>配置构建器</returns>
+    public static CfgBuilder AddEnv(this CfgBuilder builder, string path, int level, bool writeable = false,
+        bool optional = true, bool reloadOnChange = true, bool isPrimaryWriter = false)
+    {
+        return builder.AddSource(new EnvFileCfgSource(path, level, writeable, optional, reloadOnChange, isPrimaryWriter));
+    }
+}

+ 300 - 0
Apq.Cfg.Env/EnvFileCfgSource.cs

@@ -0,0 +1,300 @@
+using Apq.Cfg.Sources;
+using Apq.Cfg.Sources.File;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.FileProviders.Physical;
+
+namespace Apq.Cfg.Env;
+
+/// <summary>
+/// .env 文件配置源
+/// </summary>
+internal sealed class EnvFileCfgSource : FileCfgSourceBase, IWritableCfgSource
+{
+    public EnvFileCfgSource(string path, int level, bool writeable, bool optional, bool reloadOnChange,
+        bool isPrimaryWriter)
+        : base(path, level, writeable, optional, reloadOnChange, isPrimaryWriter)
+    {
+    }
+
+    public override IConfigurationSource BuildSource()
+    {
+        var (fp, file) = CreatePhysicalFileProviderForEnv(_path);
+        var src = new EnvConfigurationSource
+        {
+            FileProvider = fp,
+            Path = file,
+            Optional = _optional,
+            ReloadOnChange = _reloadOnChange
+        };
+        src.ResolveFileProvider();
+        return src;
+    }
+
+    /// <summary>
+    /// 创建 PhysicalFileProvider,允许访问以点开头的文件(如 .env)
+    /// </summary>
+    private (PhysicalFileProvider Provider, string FileName) CreatePhysicalFileProviderForEnv(string path)
+    {
+        var fullPath = Path.GetFullPath(path);
+        var dir = Path.GetDirectoryName(fullPath) ?? Directory.GetCurrentDirectory();
+        var fileName = Path.GetFileName(fullPath);
+        
+        // 使用 ExclusionFilters.None 来允许访问以点开头的文件
+        var provider = new PhysicalFileProvider(dir, ExclusionFilters.None);
+        return (provider, fileName);
+    }
+
+    public async Task ApplyChangesAsync(IReadOnlyDictionary<string, string?> changes, CancellationToken cancellationToken)
+    {
+        if (!IsWriteable)
+            throw new InvalidOperationException($"配置源 (层级 {Level}) 不可写");
+
+        EnsureDirectoryFor(_path);
+
+        var entries = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
+
+        if (File.Exists(_path))
+        {
+            var readEncoding = DetectEncodingEnhanced(_path);
+            var lines = await File.ReadAllLinesAsync(_path, readEncoding, cancellationToken).ConfigureAwait(false);
+
+            foreach (var line in lines)
+            {
+                var trimmed = line.Trim();
+                
+                // 跳过空行和注释
+                if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#"))
+                    continue;
+
+                var (key, value) = ParseEnvLine(trimmed);
+                if (key != null)
+                {
+                    entries[key] = value;
+                }
+            }
+        }
+
+        // 应用变更
+        foreach (var (key, value) in changes)
+        {
+            // 将配置键中的 : 转换为 __ (双下划线),这是 .env 文件的常见约定
+            var envKey = key.Replace(":", "__");
+            
+            if (value == null)
+                entries.Remove(envKey);
+            else
+                entries[envKey] = value;
+        }
+
+        // 写入文件
+        var sb = new System.Text.StringBuilder();
+        foreach (var (key, value) in entries)
+        {
+            if (value != null)
+            {
+                // 如果值包含特殊字符,使用双引号包裹
+                var formattedValue = NeedsQuoting(value) ? $"\"{EscapeValue(value)}\"" : value;
+                sb.Append(key).Append('=').Append(formattedValue).AppendLine();
+            }
+        }
+
+        await File.WriteAllTextAsync(_path, sb.ToString(), GetWriteEncoding(), cancellationToken).ConfigureAwait(false);
+    }
+
+    /// <summary>
+    /// 解析 .env 文件行
+    /// </summary>
+    private static (string? Key, string? Value) ParseEnvLine(string line)
+    {
+        // 支持 export KEY=VALUE 格式
+        if (line.StartsWith("export ", StringComparison.OrdinalIgnoreCase))
+        {
+            line = line.Substring(7).TrimStart();
+        }
+
+        var idx = line.IndexOf('=');
+        if (idx <= 0)
+            return (null, null);
+
+        var key = line.Substring(0, idx).Trim();
+        var value = line.Substring(idx + 1);
+
+        // 处理引号包裹的值
+        value = UnquoteValue(value);
+
+        return (key, value);
+    }
+
+    /// <summary>
+    /// 移除值的引号并处理转义
+    /// </summary>
+    private static string UnquoteValue(string value)
+    {
+        value = value.Trim();
+
+        if (value.Length >= 2)
+        {
+            // 双引号
+            if (value.StartsWith("\"") && value.EndsWith("\""))
+            {
+                value = value.Substring(1, value.Length - 2);
+                // 处理转义字符
+                value = value
+                    .Replace("\\n", "\n")
+                    .Replace("\\r", "\r")
+                    .Replace("\\t", "\t")
+                    .Replace("\\\"", "\"")
+                    .Replace("\\\\", "\\");
+            }
+            // 单引号(不处理转义)
+            else if (value.StartsWith("'") && value.EndsWith("'"))
+            {
+                value = value.Substring(1, value.Length - 2);
+            }
+        }
+
+        return value;
+    }
+
+    /// <summary>
+    /// 检查值是否需要引号包裹
+    /// </summary>
+    private static bool NeedsQuoting(string value)
+    {
+        if (string.IsNullOrEmpty(value))
+            return false;
+
+        // 包含空格、引号、换行符等特殊字符时需要引号
+        return value.Contains(' ') ||
+               value.Contains('"') ||
+               value.Contains('\'') ||
+               value.Contains('\n') ||
+               value.Contains('\r') ||
+               value.Contains('\t') ||
+               value.Contains('#') ||
+               value.StartsWith(" ") ||
+               value.EndsWith(" ");
+    }
+
+    /// <summary>
+    /// 转义值中的特殊字符
+    /// </summary>
+    private static string EscapeValue(string value)
+    {
+        return value
+            .Replace("\\", "\\\\")
+            .Replace("\"", "\\\"")
+            .Replace("\n", "\\n")
+            .Replace("\r", "\\r")
+            .Replace("\t", "\\t");
+    }
+}
+
+/// <summary>
+/// .env 文件配置源(用于 Microsoft.Extensions.Configuration)
+/// </summary>
+internal sealed class EnvConfigurationSource : FileConfigurationSource
+{
+    public override IConfigurationProvider Build(IConfigurationBuilder builder)
+    {
+        // 不调用 EnsureDefaults,因为它可能会覆盖我们设置的 FileProvider
+        // 只需要确保 OnLoadException 有默认值
+        if (OnLoadException == null && !Optional)
+        {
+            OnLoadException = context => { };
+        }
+        return new EnvConfigurationProvider(this);
+    }
+}
+
+/// <summary>
+/// .env 文件配置提供程序
+/// </summary>
+internal sealed class EnvConfigurationProvider : FileConfigurationProvider
+{
+    public EnvConfigurationProvider(EnvConfigurationSource source) : base(source)
+    {
+    }
+
+    public override void Load(Stream stream)
+    {
+        var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
+
+        using var reader = new StreamReader(stream);
+        string? line;
+        while ((line = reader.ReadLine()) != null)
+        {
+            var trimmed = line.Trim();
+
+            // 跳过空行和注释
+            if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith("#"))
+                continue;
+
+            var (key, value) = ParseEnvLine(trimmed);
+            if (key != null)
+            {
+                // 将 __ 转换为 : 以支持嵌套配置
+                var configKey = key.Replace("__", ConfigurationPath.KeyDelimiter);
+                data[configKey] = value;
+            }
+        }
+
+        Data = data;
+    }
+
+    /// <summary>
+    /// 解析 .env 文件行
+    /// </summary>
+    private static (string? Key, string? Value) ParseEnvLine(string line)
+    {
+        // 支持 export KEY=VALUE 格式
+        if (line.StartsWith("export ", StringComparison.OrdinalIgnoreCase))
+        {
+            line = line.Substring(7).TrimStart();
+        }
+
+        var idx = line.IndexOf('=');
+        if (idx <= 0)
+            return (null, null);
+
+        var key = line.Substring(0, idx).Trim();
+        var value = line.Substring(idx + 1);
+
+        // 处理引号包裹的值
+        value = UnquoteValue(value);
+
+        return (key, value);
+    }
+
+    /// <summary>
+    /// 移除值的引号并处理转义
+    /// </summary>
+    private static string UnquoteValue(string value)
+    {
+        value = value.Trim();
+
+        if (value.Length >= 2)
+        {
+            // 双引号
+            if (value.StartsWith("\"") && value.EndsWith("\""))
+            {
+                value = value.Substring(1, value.Length - 2);
+                // 处理转义字符
+                value = value
+                    .Replace("\\n", "\n")
+                    .Replace("\\r", "\r")
+                    .Replace("\\t", "\t")
+                    .Replace("\\\"", "\"")
+                    .Replace("\\\\", "\\");
+            }
+            // 单引号(不处理转义)
+            else if (value.StartsWith("'") && value.EndsWith("'"))
+            {
+                value = value.Substring(1, value.Length - 2);
+            }
+        }
+
+        return value;
+    }
+}

+ 109 - 0
Apq.Cfg.Env/README.md

@@ -0,0 +1,109 @@
+# Apq.Cfg.Env
+
+[![Gitee](https://img.shields.io/badge/Gitee-Apq.Cfg-red)](https://gitee.com/apq/Apq.Cfg)
+
+.env 文件配置源扩展包。
+
+**仓库地址**:https://gitee.com/apq/Apq.Cfg
+
+## 依赖
+
+- Apq.Cfg
+
+## 用法
+
+```csharp
+using Apq.Cfg;
+using Apq.Cfg.Env;
+
+var cfg = new CfgBuilder()
+    .AddEnv(".env", level: 0, writeable: true)
+    .Build();
+
+// 读取配置
+var dbHost = cfg.Get("DATABASE__HOST");
+var dbPort = cfg.Get("DATABASE__PORT");
+
+// 使用配置节访问(__ 会自动转换为 :)
+var dbSection = cfg.GetSection("DATABASE");
+var host = dbSection.Get("HOST");
+```
+
+## 方法签名
+
+```csharp
+public static CfgBuilder AddEnv(
+    this CfgBuilder builder,
+    string path,
+    int level,
+    bool writeable = false,
+    bool optional = true,
+    bool reloadOnChange = true,
+    bool isPrimaryWriter = false)
+```
+
+## 参数说明
+
+| 参数 | 说明 |
+|------|------|
+| `path` | .env 文件路径 |
+| `level` | 配置层级,数值越大优先级越高 |
+| `writeable` | 是否可写 |
+| `optional` | 文件不存在时是否忽略 |
+| `reloadOnChange` | 文件变更时是否自动重载 |
+| `isPrimaryWriter` | 是否为默认写入目标 |
+
+## .env 格式示例
+
+```env
+# 这是注释
+APP_NAME=MyApp
+APP_DEBUG=true
+
+# 数据库配置(使用 __ 表示嵌套)
+DATABASE__HOST=localhost
+DATABASE__PORT=5432
+DATABASE__NAME=mydb
+
+# 支持引号包裹的值
+MESSAGE="Hello, World!"
+MULTILINE="Line1\nLine2"
+
+# 支持单引号(不处理转义)
+RAW_VALUE='Hello\nWorld'
+
+# 支持 export 前缀
+export API_KEY=secret123
+```
+
+## 配置键映射
+
+.env 文件使用双下划线 `__` 来表示配置层级,读取时会自动转换为 `:`:
+
+| .env 键 | 配置键 |
+|---------|--------|
+| `APP_NAME` | `APP_NAME` |
+| `DATABASE__HOST` | `DATABASE:HOST` |
+| `DATABASE__CONNECTION__STRING` | `DATABASE:CONNECTION:STRING` |
+
+## 支持的特性
+
+- ✅ 注释(以 `#` 开头)
+- ✅ 双引号包裹的值(支持转义字符)
+- ✅ 单引号包裹的值(原样保留)
+- ✅ `export` 前缀
+- ✅ 嵌套配置(使用 `__`)
+- ✅ 文件变更自动重载
+- ✅ 可写配置源
+
+## 许可证
+
+MIT License
+
+## 作者
+
+- 邮箱:[email protected]
+
+## 仓库
+
+- Gitee:https://gitee.com/apq/Apq.Cfg

+ 14 - 0
Apq.Cfg.sln

@@ -55,6 +55,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决
 		Directory.Build.props = Directory.Build.props
 	EndProjectSection
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Apq.Cfg.Env", "Apq.Cfg.Env\Apq.Cfg.Env.csproj", "{795EC4F0-E355-44D9-9693-CD83EF7370F7}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -293,6 +295,18 @@ Global
 		{2D6CC3DA-8F9B-4F5D-9A3C-7D8E9F0A1B2C}.Release|x64.Build.0 = Release|Any CPU
 		{2D6CC3DA-8F9B-4F5D-9A3C-7D8E9F0A1B2C}.Release|x86.ActiveCfg = Release|Any CPU
 		{2D6CC3DA-8F9B-4F5D-9A3C-7D8E9F0A1B2C}.Release|x86.Build.0 = Release|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Debug|x64.Build.0 = Debug|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Debug|x86.Build.0 = Debug|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Release|Any CPU.Build.0 = Release|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Release|x64.ActiveCfg = Release|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Release|x64.Build.0 = Release|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Release|x86.ActiveCfg = Release|Any CPU
+		{795EC4F0-E355-44D9-9693-CD83EF7370F7}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 41 - 3
README.md

@@ -8,7 +8,7 @@
 
 ## 特性
 
-- **多格式支持**:JSON、INI、XML、YAML、TOML、Redis、数据库
+- **多格式支持**:JSON、INI、XML、YAML、TOML、Env、Redis、数据库
 - **远程配置中心**:支持 Consul、Etcd、Nacos、Apollo 等配置中心,支持热重载
 - **智能编码处理**:
   - 读取时自动检测(BOM 优先,UTF.Unknown 库辅助,支持缓存)
@@ -39,6 +39,7 @@
 | [Apq.Cfg.Xml](https://www.nuget.org/packages/Apq.Cfg.Xml) | XML 格式支持 |
 | [Apq.Cfg.Yaml](https://www.nuget.org/packages/Apq.Cfg.Yaml) | YAML 格式支持 |
 | [Apq.Cfg.Toml](https://www.nuget.org/packages/Apq.Cfg.Toml) | TOML 格式支持 |
+| [Apq.Cfg.Env](https://www.nuget.org/packages/Apq.Cfg.Env) | .env 文件格式支持 |
 | [Apq.Cfg.Redis](https://www.nuget.org/packages/Apq.Cfg.Redis) | Redis 配置源 |
 | [Apq.Cfg.Database](https://www.nuget.org/packages/Apq.Cfg.Database) | 数据库配置源 |
 | [Apq.Cfg.Consul](https://www.nuget.org/packages/Apq.Cfg.Consul) | Consul 配置中心 |
@@ -157,9 +158,45 @@ cfg.ConfigChanges.Subscribe(e =>
 - **层级覆盖感知**:只有当最终合并值真正发生变化时才触发通知
 - **多源支持**:支持多个配置源同时存在的场景
 
+### .env 文件支持
+
+支持 .env 文件格式,常用于开发环境配置:
+
+```csharp
+using Apq.Cfg;
+using Apq.Cfg.Env;
+
+var cfg = new CfgBuilder()
+    .AddEnv(".env", level: 0, writeable: true)
+    .AddEnv(".env.local", level: 1, writeable: true, isPrimaryWriter: true)
+    .Build();
+
+// 读取配置(DATABASE__HOST 自动转换为 DATABASE:HOST)
+var dbHost = cfg.Get("DATABASE:HOST");
+var dbPort = cfg.Get<int>("DATABASE:PORT");
+```
+
+.env 文件示例:
+```env
+# 应用配置
+APP_NAME=MyApp
+APP_DEBUG=true
+
+# 数据库配置(使用 __ 表示嵌套)
+DATABASE__HOST=localhost
+DATABASE__PORT=5432
+
+# 支持引号包裹的值
+MESSAGE="Hello, World!"
+MULTILINE="Line1\nLine2"
+
+# 支持 export 前缀
+export API_KEY=secret123
+```
+
 ### 编码处理
 
-所有文件配置源(JSON、INI、XML、YAML、TOML)均支持智能编码处理:
+所有文件配置源(JSON、INI、XML、YAML、TOML、Env)均支持智能编码处理:
 
 - **读取时自动检测**:
   - BOM 优先检测(UTF-8、UTF-16 LE/BE、UTF-32 LE/BE)
@@ -470,6 +507,7 @@ Apq.Cfg.Ini/                 # INI 格式支持
 Apq.Cfg.Xml/                 # XML 格式支持
 Apq.Cfg.Yaml/                # YAML 格式支持
 Apq.Cfg.Toml/                # TOML 格式支持
+Apq.Cfg.Env/                 # .env 文件格式支持
 Apq.Cfg.Redis/               # Redis 配置源
 Apq.Cfg.Database/            # 数据库配置源
 Apq.Cfg.Consul/              # Consul 配置中心
@@ -479,7 +517,7 @@ Apq.Cfg.Apollo/              # Apollo 配置中心
 Apq.Cfg.Zookeeper/           # Zookeeper 配置中心
 Apq.Cfg.Vault/               # HashiCorp Vault 密钥管理
 Apq.Cfg.SourceGenerator/     # 源生成器(Native AOT 支持)
-tests/                       # 单元测试(290 个测试用例)
+tests/                       # 单元测试(346 个测试用例)
 benchmarks/                  # 性能基准测试(18 个测试类)
 docs/                        # 技术文档
 Samples/                     # 示例项目

+ 8 - 6
docs/解决方案分析报告_2025-12-26.md

@@ -11,7 +11,7 @@
 | 方面 | 评分 | 说明 |
 |------|------|------|
 | **架构设计** | 9/10 | 清晰的分层架构、插件化配置源、遵循 SOLID 原则 |
-| **功能完整性** | 9/10 | 支持 12 种配置源(5 种文件格式 + 4 种远程配置中心 + Redis/DB/环境变量)|
+| **功能完整性** | 9/10 | 支持 13 种配置源(6 种文件格式 + 4 种远程配置中心 + Redis/DB/环境变量)|
 | **代码质量** | 8/10 | 命名规范统一、注释完善、异步支持良好 |
 | **测试覆盖** | 9/10 | 290 个单元测试,API 覆盖率 100%,完整性能基准 |
 | **文档质量** | 8/10 | README 详细,有完整的使用示例 |
@@ -27,14 +27,17 @@ Apq.Cfg.Ini/               # INI 格式支持
 Apq.Cfg.Xml/               # XML 格式支持
 Apq.Cfg.Yaml/              # YAML 格式支持
 Apq.Cfg.Toml/              # TOML 格式支持
+Apq.Cfg.Env/               # .env 文件格式支持
 Apq.Cfg.Redis/             # Redis 配置源
 Apq.Cfg.Database/          # 数据库配置源
 Apq.Cfg.Consul/            # Consul 配置中心
 Apq.Cfg.Etcd/              # Etcd 配置中心
 Apq.Cfg.Nacos/             # Nacos 配置中心
 Apq.Cfg.Apollo/            # Apollo 配置中心
+Apq.Cfg.Zookeeper/         # Zookeeper 配置中心
+Apq.Cfg.Vault/             # HashiCorp Vault 密钥管理
 Apq.Cfg.SourceGenerator/   # 源生成器(Native AOT 支持)
-tests/                     # 单元测试(290 个测试用例)
+tests/                     # 单元测试(346 个测试用例)
 benchmarks/                # 性能基准测试(18 个测试类)
 Samples/                   # 示例项目
 ```
@@ -105,12 +108,11 @@ Samples/                   # 示例项目
 
 | 扩展项 | 优先级 | 说明 |
 |--------|--------|------|
-| **Apq.Cfg.Zookeeper** | 高 | 支持 Zookeeper 配置中心 |
-| **Apq.Cfg.Vault** | 高 | HashiCorp Vault 密钥管理集成 |
 | **Apq.Cfg.AWS** | 中 | AWS Parameter Store / Secrets Manager |
 | **Apq.Cfg.Azure** | 中 | Azure App Configuration |
 | **Apq.Cfg.K8s** | 中 | Kubernetes ConfigMap/Secret |
-| **Apq.Cfg.Env** | 低 | .env 文件格式支持 |
+
+> ✅ 已完成:Apq.Cfg.Zookeeper、Apq.Cfg.Vault、Apq.Cfg.Env
 
 ### 4.4 开发体验改进
 
@@ -171,7 +173,7 @@ Samples/                   # 示例项目
 
 Apq.Cfg 是一个**成熟度较高**的统一配置管理库,具备以下核心优势:
 
-1. **功能全面**:支持 12 种配置源,覆盖本地文件和远程配置中心
+1. **功能全面**:支持 13 种配置源,覆盖本地文件和远程配置中心
 2. **性能优秀**:批量操作零分配、层级缓存、防抖重载
 3. **开发友好**:完善的 DI 集成、Rx 支持、源生成器 AOT 支持
 4. **测试完备**:290 个单元测试,API 覆盖率 100%

+ 357 - 0
tests/Apq.Cfg.Tests.Shared/EnvCfgTests.cs

@@ -0,0 +1,357 @@
+using Apq.Cfg.Env;
+
+namespace Apq.Cfg.Tests;
+
+/// <summary>
+/// .env 配置源测试
+/// </summary>
+public class EnvCfgTests : IDisposable
+{
+    private readonly string _testDir;
+
+    public EnvCfgTests()
+    {
+        _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgTests_{Guid.NewGuid():N}");
+        Directory.CreateDirectory(_testDir);
+    }
+
+    public void Dispose()
+    {
+        if (Directory.Exists(_testDir))
+        {
+            Directory.Delete(_testDir, true);
+        }
+    }
+
+    [Fact]
+    public void Get_EnvValue_ReturnsCorrectValue()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            APP_NAME=TestApp
+            APP_DEBUG=true
+            DATABASE__HOST=localhost
+            DATABASE__PORT=5432
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal("TestApp", cfg.Get("APP_NAME"));
+        Assert.Equal("true", cfg.Get("APP_DEBUG"));
+        Assert.Equal("localhost", cfg.Get("DATABASE:HOST"));
+        Assert.Equal("5432", cfg.Get("DATABASE:PORT"));
+    }
+
+    [Fact]
+    public void Get_TypedValue_ReturnsCorrectType()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            MAX_RETRIES=5
+            ENABLED=true
+            TIMEOUT=30.5
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal(5, cfg.Get<int>("MAX_RETRIES"));
+        Assert.True(cfg.Get<bool>("ENABLED"));
+        Assert.Equal(30.5, cfg.Get<double>("TIMEOUT"));
+    }
+
+    [Fact]
+    public void Get_QuotedValue_ReturnsUnquotedValue()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            DOUBLE_QUOTED="Hello, World!"
+            SINGLE_QUOTED='Hello, World!'
+            WITH_SPACES="  spaces  "
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal("Hello, World!", cfg.Get("DOUBLE_QUOTED"));
+        Assert.Equal("Hello, World!", cfg.Get("SINGLE_QUOTED"));
+        Assert.Equal("  spaces  ", cfg.Get("WITH_SPACES"));
+    }
+
+    [Fact]
+    public void Get_EscapedValue_ReturnsUnescapedValue()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            MULTILINE="Line1\nLine2"
+            WITH_TAB="Col1\tCol2"
+            WITH_QUOTE="Say \"Hello\""
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal("Line1\nLine2", cfg.Get("MULTILINE"));
+        Assert.Equal("Col1\tCol2", cfg.Get("WITH_TAB"));
+        Assert.Equal("Say \"Hello\"", cfg.Get("WITH_QUOTE"));
+    }
+
+    [Fact]
+    public void Get_SingleQuotedValue_PreservesEscapeSequences()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            RAW_VALUE='Hello\nWorld'
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert - 单引号不处理转义
+        Assert.Equal("Hello\\nWorld", cfg.Get("RAW_VALUE"));
+    }
+
+    [Fact]
+    public void Get_ExportPrefix_IgnoresExport()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            export API_KEY=secret123
+            export DATABASE__URL=postgres://localhost
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal("secret123", cfg.Get("API_KEY"));
+        Assert.Equal("postgres://localhost", cfg.Get("DATABASE:URL"));
+    }
+
+    [Fact]
+    public void Get_Comments_AreIgnored()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            # This is a comment
+            APP_NAME=TestApp
+            # Another comment
+            APP_VERSION=1.0.0
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal("TestApp", cfg.Get("APP_NAME"));
+        Assert.Equal("1.0.0", cfg.Get("APP_VERSION"));
+    }
+
+    [Fact]
+    public async Task Set_AndSave_PersistsValue()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            ORIGINAL=Value
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: true, isPrimaryWriter: true)
+            .Build();
+
+        // Act
+        cfg.Set("NEW_KEY", "NewValue");
+        await cfg.SaveAsync();
+
+        // Assert
+        using var cfg2 = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        Assert.Equal("NewValue", cfg2.Get("NEW_KEY"));
+        Assert.Equal("Value", cfg2.Get("ORIGINAL"));
+    }
+
+    [Fact]
+    public async Task Set_NestedKey_UsesDoubleUnderscore()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, "");
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: true, isPrimaryWriter: true)
+            .Build();
+
+        // Act
+        cfg.Set("DATABASE:HOST", "localhost");
+        cfg.Set("DATABASE:PORT", "5432");
+        await cfg.SaveAsync();
+
+        // Assert - 读取文件内容验证格式
+        var content = await File.ReadAllTextAsync(envPath);
+        Assert.Contains("DATABASE__HOST=localhost", content);
+        Assert.Contains("DATABASE__PORT=5432", content);
+
+        // 验证可以正确读取
+        using var cfg2 = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        Assert.Equal("localhost", cfg2.Get("DATABASE:HOST"));
+        Assert.Equal("5432", cfg2.Get("DATABASE:PORT"));
+    }
+
+    [Fact]
+    public void Exists_ExistingKey_ReturnsTrue()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            KEY=Value
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.True(cfg.Exists("KEY"));
+        Assert.False(cfg.Exists("NonExistent"));
+    }
+
+    [Fact]
+    public async Task Remove_AndSave_RemovesKey()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            TO_REMOVE=Value
+            TO_KEEP=Value2
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: true, isPrimaryWriter: true)
+            .Build();
+
+        // Act
+        cfg.Remove("TO_REMOVE");
+        await cfg.SaveAsync();
+
+        // Assert
+        using var cfg2 = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        var removedValue = cfg2.Get("TO_REMOVE");
+        Assert.True(string.IsNullOrEmpty(removedValue));
+        Assert.Equal("Value2", cfg2.Get("TO_KEEP"));
+    }
+
+    [Fact]
+    public void GetSection_ReturnsNestedValues()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            DATABASE__HOST=localhost
+            DATABASE__PORT=5432
+            DATABASE__NAME=mydb
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act
+        var dbSection = cfg.GetSection("DATABASE");
+
+        // Assert
+        Assert.Equal("localhost", dbSection.Get("HOST"));
+        Assert.Equal("5432", dbSection.Get("PORT"));
+        Assert.Equal("mydb", dbSection.Get("NAME"));
+    }
+
+    [Fact]
+    public async Task Set_ValueWithSpecialChars_QuotesValue()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, "");
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: true, isPrimaryWriter: true)
+            .Build();
+
+        // Act
+        cfg.Set("MESSAGE", "Hello World");  // 包含空格
+        cfg.Set("COMMENT", "Value # with hash");  // 包含 #
+        await cfg.SaveAsync();
+
+        // Assert
+        var content = await File.ReadAllTextAsync(envPath);
+        Assert.Contains("MESSAGE=\"Hello World\"", content);
+        Assert.Contains("COMMENT=\"Value # with hash\"", content);
+
+        // 验证可以正确读取
+        using var cfg2 = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        Assert.Equal("Hello World", cfg2.Get("MESSAGE"));
+        Assert.Equal("Value # with hash", cfg2.Get("COMMENT"));
+    }
+
+    [Fact]
+    public void Optional_MissingFile_DoesNotThrow()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, "nonexistent.env");
+
+        // Act & Assert - 不应抛出异常
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false, optional: true)
+            .Build();
+
+        Assert.Null(cfg.Get("ANY_KEY"));
+    }
+
+    [Fact]
+    public void EmptyValue_ReturnsEmptyString()
+    {
+        // Arrange
+        var envPath = Path.Combine(_testDir, ".env");
+        File.WriteAllText(envPath, """
+            EMPTY_VALUE=
+            QUOTED_EMPTY=""
+            """);
+
+        using var cfg = new CfgBuilder()
+            .AddEnv(envPath, level: 0, writeable: false)
+            .Build();
+
+        // Act & Assert
+        Assert.Equal("", cfg.Get("EMPTY_VALUE"));
+        Assert.Equal("", cfg.Get("QUOTED_EMPTY"));
+    }
+}

+ 1 - 0
tests/Directory.Build.props

@@ -48,6 +48,7 @@
   <!-- 项目引用 -->
   <ItemGroup>
     <ProjectReference Include="..\..\Apq.Cfg\Apq.Cfg.csproj" />
+    <ProjectReference Include="..\..\Apq.Cfg.Env\Apq.Cfg.Env.csproj" />
     <ProjectReference Include="..\..\Apq.Cfg.Ini\Apq.Cfg.Ini.csproj" />
     <ProjectReference Include="..\..\Apq.Cfg.Xml\Apq.Cfg.Xml.csproj" />
     <ProjectReference Include="..\..\Apq.Cfg.Yaml\Apq.Cfg.Yaml.csproj" />

+ 72 - 70
tests/README.md

@@ -25,12 +25,13 @@ dotnet test tests/Apq.Cfg.Tests.Net9/
 dotnet test --filter "FullyQualifiedName~JsonCfgTests"
 ```
 
-## 测试统计(共 294 个测试)
+## 测试统计(共 346 个测试)
 
 | 测试类 | 测试数量 | 说明 |
 |--------|----------|------|
 | JsonCfgTests | 15 | JSON 配置源测试 |
 | EnvVarsCfgTests | 4 | 环境变量配置源测试 |
+| EnvCfgTests | 15 | .env 文件配置源测试 |
 | IniCfgTests | 5 | INI 文件配置源测试 |
 | XmlCfgTests | 5 | XML 文件配置源测试 |
 | YamlCfgTests | 6 | YAML 文件配置源测试 |
@@ -59,92 +60,93 @@ dotnet test --filter "FullyQualifiedName~JsonCfgTests"
 
 ## 公开 API 覆盖矩阵
 
-| API | Json | Env | Ini | Xml | Yaml | Toml | Redis | DB | Zk | Apollo | Consul | Etcd | Nacos | Vault |
-|-----|:----:|:---:|:---:|:---:|:----:|:----:|:-----:|:--:|:--:|:------:|:------:|:----:|:-----:|:-----:|
+| API | Json | EnvVar | .env | Ini | Xml | Yaml | Toml | Redis | DB | Zk | Apollo | Consul | Etcd | Nacos | Vault |
+|-----|:----:|:------:|:----:|:---:|:---:|:----:|:----:|:-----:|:--:|:--:|:------:|:------:|:----:|:-----:|:-----:|
 | **ICfgRoot** |
-| `Get(key)` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
-| `Get<T>(key)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | - | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
-| `Exists(key)` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
-| `GetMany(keys)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `GetMany<T>(keys)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `GetMany(keys, callback)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `GetMany<T>(keys, callback)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `Set(key, value)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ | ✅ | - | ✅ |
-| `SetMany(values)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `Set(key, value, targetLevel)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `Remove(key)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ | ✅ | - | ✅ |
-| `Remove(key, targetLevel)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `SaveAsync()` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ | ✅ | - | ✅ |
-| `SaveAsync(targetLevel)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `ToMicrosoftConfiguration()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `ToMicrosoftConfiguration(options)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `ConfigChanges` | ✅ | - | - | - | - | - | - | - | ✅ | - | ✅ | ✅ | - | - |
-| `GetSection(path)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | - | - | - | - | - | - | - | - |
-| `GetChildKeys()` | ✅ | - | ✅ | ✅ | ✅ | ✅ | - | - | - | - | - | - | - | - |
-| `Dispose/DisposeAsync` | ✅ | - | - | - | - | - | - | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| `Get(key)` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| `Get<T>(key)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | - | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| `Exists(key)` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| `GetMany(keys)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `GetMany<T>(keys)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `GetMany(keys, callback)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `GetMany<T>(keys, callback)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `Set(key, value)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ | ✅ | - | ✅ |
+| `SetMany(values)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `Set(key, value, targetLevel)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `Remove(key)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ | ✅ | - | ✅ |
+| `Remove(key, targetLevel)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `SaveAsync()` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - | ✅ | ✅ | - | ✅ |
+| `SaveAsync(targetLevel)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `ToMicrosoftConfiguration()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `ToMicrosoftConfiguration(options)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `ConfigChanges` | ✅ | - | - | - | - | - | - | - | - | ✅ | - | ✅ | ✅ | - | - |
+| `GetSection(path)` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | - | - | - | - | - | - | - | - |
+| `GetChildKeys()` | ✅ | - | ✅ | ✅ | ✅ | ✅ | ✅ | - | - | - | - | - | - | - | - |
+| `Dispose/DisposeAsync` | ✅ | - | - | - | - | - | - | - | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
 | **CfgBuilder** |
-| `AddJson()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `AddEnvironmentVariables()` | - | ✅ | - | - | - | - | - | - | - | - | - | - | - | - |
-| `AddSource()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `WithEncodingConfidenceThreshold()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `AddReadEncodingMapping()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `AddWriteEncodingMapping()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `ConfigureEncodingMapping()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `WithEncodingDetectionLogging()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `Build()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| `AddJson()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddEnvironmentVariables()` | - | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddEnv()` | - | - | ✅ | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddSource()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `WithEncodingConfidenceThreshold()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddReadEncodingMapping()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddWriteEncodingMapping()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `ConfigureEncodingMapping()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `WithEncodingDetectionLogging()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `Build()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
 | **CfgRootExtensions** |
-| `TryGet<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `GetRequired<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `GetOrDefault<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `TryGet<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `GetRequired<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `GetOrDefault<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
 | **FileCfgSourceBase** |
-| `EncodingDetector` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `EncodingConfidenceThreshold` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `EncodingDetector` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `EncodingConfidenceThreshold` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
 | **扩展包** |
-| `AddIni()` | - | - | ✅ | - | - | - | - | - | - | - | - | - | - | - |
-| `AddXml()` | - | - | - | ✅ | - | - | - | - | - | - | - | - | - | - |
-| `AddYaml()` | - | - | - | - | ✅ | - | - | - | - | - | - | - | - | - |
-| `AddToml()` | - | - | - | - | - | ✅ | - | - | - | - | - | - | - | - |
-| `AddRedis()` | - | - | - | - | - | - | ✅ | - | - | - | - | - | - | - |
-| `AddDatabase()` | - | - | - | - | - | - | - | ✅ | - | - | - | - | - | - |
-| `AddZookeeper()` | - | - | - | - | - | - | - | - | ✅ | - | - | - | - | - |
-| `AddApollo()` | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - |
-| `AddConsul()` | - | - | - | - | - | - | - | - | - | - | ✅ | - | - | - |
-| `AddEtcd()` | - | - | - | - | - | - | - | - | - | - | - | ✅ | - | - |
-| `AddNacos()` | - | - | - | - | - | - | - | - | - | - | - | - | ✅ | - |
-| `AddVault()` | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ |
-| `AddVaultV1()` | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ |
-| `AddVaultV2()` | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ |
+| `AddIni()` | - | - | - | ✅ | - | - | - | - | - | - | - | - | - | - | - |
+| `AddXml()` | - | - | - | - | ✅ | - | - | - | - | - | - | - | - | - | - |
+| `AddYaml()` | - | - | - | - | - | ✅ | - | - | - | - | - | - | - | - | - |
+| `AddToml()` | - | - | - | - | - | - | ✅ | - | - | - | - | - | - | - | - |
+| `AddRedis()` | - | - | - | - | - | - | - | ✅ | - | - | - | - | - | - | - |
+| `AddDatabase()` | - | - | - | - | - | - | - | - | ✅ | - | - | - | - | - | - |
+| `AddZookeeper()` | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - | - |
+| `AddApollo()` | - | - | - | - | - | - | - | - | - | - | ✅ | - | - | - | - |
+| `AddConsul()` | - | - | - | - | - | - | - | - | - | - | - | ✅ | - | - | - |
+| `AddEtcd()` | - | - | - | - | - | - | - | - | - | - | - | - | ✅ | - | - |
+| `AddNacos()` | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ | - |
+| `AddVault()` | - | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ |
+| `AddVaultV1()` | - | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ |
+| `AddVaultV2()` | - | - | - | - | - | - | - | - | - | - | - | - | - | - | ✅ |
 | **依赖注入扩展** |
-| `AddApqCfg()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `AddApqCfg<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `ConfigureApqCfg<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `ConfigureApqCfg<T>(onChange)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `IOptions<T>` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `IOptionsMonitor<T>` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `IOptionsSnapshot<T>` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddApqCfg()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `AddApqCfg<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `ConfigureApqCfg<T>()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `ConfigureApqCfg<T>(onChange)` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `IOptions<T>` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `IOptionsMonitor<T>` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `IOptionsSnapshot<T>` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
 | **源生成器** |
-| `[CfgSection]` 特性 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `BindFrom()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| `BindTo()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| 简单类型绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| 嵌套对象绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| 数组绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| 列表绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| 字典绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
-| 枚举绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `[CfgSection]` 特性 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `BindFrom()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| `BindTo()` | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| 简单类型绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| 嵌套对象绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| 数组绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| 列表绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| 字典绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
+| 枚举绑定 | ✅ | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
 | **多层级覆盖** |
-| 高层级覆盖低层级 | ✅ | ✅ | - | - | - | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| 高层级覆盖低层级 | ✅ | ✅ | ✅ | - | - | - | - | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
 
 > 说明:
 > - `✅` 表示已有测试覆盖
 > - `-` 表示该配置源不支持此功能(如环境变量只读、Apollo/Nacos 通常只读)或该功能只需测试一次
-> - `Zk` = Zookeeper, `DB` = Database
+> - `EnvVar` = 环境变量, `.env` = .env 文件, `Zk` = Zookeeper, `DB` = Database
 
 ## 测试场景覆盖
 
 | 场景类别 | 测试文件 | 测试数量 |
 |----------|----------|----------|
-| 基本读写 | JsonCfgTests, 各格式测试 | 47 |
+| 基本读写 | JsonCfgTests, EnvCfgTests, 各格式测试 | 62 |
 | 类型转换 | JsonCfgTests | 15 |
 | 编码检测 | EncodingDetectionTests | 14 |
 | 编码映射 | EncodingTests | 33 |