ASP.NET Core 8.0 MVC 配置文件读写教程

ASP.NET Core 8.0 MVC 配置文件读写教程

先搞清楚原理

IConfiguration 默认是只读 的------启动时把 appsettings.json 加载到内存,运行时直接改文件,IConfiguration 不会自动感知。要做到"运行时改 + 永久保存 + 立即生效"三件齐全,核心就一句话:写完文件调 Reload()

方案一:直接读写 appsettings.json(最常用)

1. 装包

.NET 8 自带 System.Text.Json,不用额外装 NuGet。如果想用 Newtonsoft:

复制代码
dotnet add package Newtonsoft.Json
2. Controller 代码
csharp 复制代码
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text.Json.Nodes;

public class ConfigController : Controller
{
    private readonly IConfigurationRoot _configurationRoot;
    private readonly IOptionsMonitor<AppSettings> _options;
    private readonly string _configPath;

    public ConfigController(
        IConfiguration configuration,
        IOptionsMonitor<AppSettings> options,
        IWebHostEnvironment env)
    {
        // 注意:必须强转成 IConfigurationRoot 才能调 Reload
        _configurationRoot = (IConfigurationRoot)configuration;
        _options = options;
        _configPath = Path.Combine(env.ContentRootPath, "appsettings.json");
    }

    // 读取
    [HttpGet]
    public IActionResult Index() => View(_options.CurrentValue);

    // 写入 + 永久保存
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Save(AppSettings model)
    {
        if (!ModelState.IsValid) return View("Index", model);

        // 1. 读现有 JSON
        var json = await System.IO.File.ReadAllTextAsync(_configPath);
        var root = JsonNode.Parse(json)!;
        var section = root["AppSettings"]!.AsObject();

        // 2. 修改
        section["AppName"] = model.AppName;
        section["Version"] = model.Version;
        section["MaintenanceMode"] = model.MaintenanceMode;

        // 3. 写回文件(带缩进)
        await System.IO.File.WriteAllTextAsync(
            _configPath,
            root.ToJsonString(new JsonSerializerOptions { WriteIndented = true })
        );

        // 4. 关键一步:让 IConfiguration 重新加载
        _configurationRoot.Reload();

        TempData["Message"] = "保存成功";
        return RedirectToAction(nameof(Index));
    }
}

public class AppSettings
{
    public string AppName { get; set; } = "";
    public string Version { get; set; } = "";
    public bool MaintenanceMode { get; set; }
}
3. Program.cs 注册
csharp 复制代码
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
4. 视图
html 复制代码
@model AppSettings

@if (TempData["Message"] != null)
{
    <div class="alert alert-success">@TempData["Message"]</div>
}

<form asp-action="Save" method="post">
    <div class="mb-3">
        <label class="form-label">应用名</label>
        <input asp-for="AppName" class="form-control" />
    </div>
    <div class="mb-3">
        <label class="form-label">版本</label>
        <input asp-for="Version" class="form-control" />
    </div>
    <div class="mb-3 form-check">
        <input asp-for="MaintenanceMode" class="form-check-input" />
        <label class="form-check-label">维护模式</label>
    </div>
    <button type="submit" class="btn btn-primary">保存</button>
</form>

方案二:自定义 JSON 配置文件

不想动 appsettings.json,可以搞一个独立的:

Program.cs
csharp 复制代码
builder.Configuration.AddJsonFile("myconfig.json", optional: true, reloadOnChange: true);

reloadOnChange: true 这个参数很关键------文件一改就自动 reload,连手动调 Reload() 都省了 。改文件用方案一同样的代码,把路径换成 myconfig.json 就行。

二、区别与简单示例

1. 定义配置类 (POCO)

首先创建一个与配置文件结构匹配的类。该类必须是 public,且拥有公共无参构造函数和公共读写属性。

复制代码
csharppublic class MQTTSettings
{
    public string BrokerAddress { get; set; } = "localhost";
    public int Port { get; set; } = 1883;
    public string ClientId { get; set; } = "my-client";
    public bool UseTls { get; set; } = false;
}

对应的 appsettings.json 内容示例:

复制代码
json{
  "MQTTSettings": {
    "BrokerAddress": "mqtt.example.com",
    "Port": 8883,
    "ClientId": "prod-client-01",
    "UseTls": true
  }
}

2. 注册服务 (Program.cs)

Program.cs 中,将配置节绑定到选项模式。这是你问题中提到的关键步骤。

复制代码
csharpvar builder = WebApplication.CreateBuilder(args);

// 关键步骤:将配置节 "MQTTSettings" 绑定到 MQTTSettings 类
builder.Services.Configure<MQTTSettings>(builder.Configuration.GetSection("MQTTSettings"));

// 其他服务注册...
builder.Services.AddScoped<MqttService>();

var app = builder.Build();
app.Run();

3. 构造函数注入 (核心回答)

根据你的需求(是否需要热更新、生命周期要求),选择以下三种接口之一注入到服务的构造函数中。

方案 A:使用 IOptions<T> (推荐用于静态配置)
  • 生命周期‌:Singleton (单例)

  • 特点 ‌:只在应用启动时读取一次配置。如果配置文件更改,‌不会‌自动更新。性能最好。

  • 适用场景‌:数据库连接串、API 密钥等启动后不变的值。

    csharppublic class MqttService
    {
    private readonly MQTTSettings _settings;

    复制代码
      // 注入 IOptions<MQTTSettings>
      public MqttService(IOptions<MQTTSettings> options)
      {
          // 通过 .Value 获取实际配置对象
          _settings = options.Value;
      }
    
      public void Connect()
      {
          Console.WriteLine($"Connecting to {_settings.BrokerAddress}:{_settings.Port}");
      }

    }

方案 B:使用 IOptionsSnapshot<T> (推荐用于 Web 请求)
  • 生命周期‌:Scoped (作用域)

  • 特点 ‌:每个请求(或作用域)开始时重新读取配置。支持热更新(前提是配置源设置了 reloadOnChange: true)。

  • 适用场景‌:ASP.NET Core Controller 或 Service,希望在不重启服务的情况下生效新配置。

    csharppublic class MqttService
    {
    private readonly MQTTSettings _settings;

    复制代码
      // 注入 IOptionsSnapshot<MQTTSettings>
      public MqttService(IOptionsSnapshot<MQTTSettings> options)
      {
          _settings = options.Value;
      }
    
      public void Connect()
      {
          // 每次请求都会获取最新的配置值
          Console.WriteLine($"Current Config: {_settings.ClientId}");
      }

    }

方案 C:使用 IOptionsMonitor<T> (推荐用于后台服务/实时监听)
  • 生命周期‌:Singleton (单例)

  • 特点 ‌:实时监听配置变化。支持注册 OnChange 回调函数,当配置改变时立即执行逻辑。

  • 适用场景‌:后台 Worker Service、需要动态调整阈值或开关的场景。

    csharppublic class MqttService : IDisposable
    {
    private readonly MQTTSettings _currentSettings;
    private readonly IDisposable _registration;

    复制代码
      // 注入 IOptionsMonitor<MQTTSettings>
      public MqttService(IOptionsMonitor<MQTTSettings> monitor)
      {
          // 获取当前值
          _currentSettings = monitor.CurrentValue;
    
          // 注册变更回调 (可选)
          _registration = monitor.OnChange(settings =>
          {
              Console.WriteLine("MQTT Config Changed!");
              // 在这里处理重新连接等逻辑
          });
      }
    
      public void DoWork()
      {
          Console.WriteLine($"Using broker: {_currentSettings.BrokerAddress}");
      }
    
      public void Dispose()
      {
          _registration?.Dispose();
      }

    }

常见错误与排查

  1. 注入得到 null ‌:
    • 确保在 Program.cs 中调用了 builder.Services.Configure<MQTTSettings>(...)。仅调用 AddOptions() 是不够的。
    • 检查 GetSection("MQTTSettings") 中的字符串是否与 appsettings.json 中的键名‌完全一致‌(包括大小写,虽然 JSON 通常不区分,但配置系统内部映射可能敏感,建议保持一致)。
  2. ‌**属性值为默认值 (0, null, false)**‌:
    • 检查 C# 类的属性名是否与 JSON 键名匹配。
    • 确保属性有 public setinit 访问器。
    • 如果是嵌套对象,确保层级结构正确。
  3. ‌**不要在构造函数中直接注入 MQTTSettings**‌:
    • DI 容器不知道如何直接创建 MQTTSettings 实例,除非你手动注册了它(如 services.AddSingleton(new MQTTSettings(...))),但这失去了配置绑定的优势。必须通过 IOptions<> 包装器注入。

总结选择指南

几个选项的区别(必看)

接口 生命周期 热更新支持 典型用途
IOptions<T> Singleton ❌ 否 启动后不变的全局配置
IOptionsSnapshot<T> Scoped ✅ 是 (每请求) Web API 控制器、常规业务服务
IOptionsMonitor<T> Singleton ✅ 是 (实时) 后台任务、需要OnChange回调的场景
接口 生命周期 运行时变更 适用场景
IOptions<T> Singleton 启动时确定的配置
IOptionsSnapshot<T> Scoped 每次请求重新创建
IOptionsMonitor<T> Singleton ✅ + 支持监听变更 运行时改配置首选

要拿最新值,用 IOptionsMonitor<T>.CurrentValue

对于大多数 Web 应用场景,‌**IOptionsSnapshot<T>‌ 是最平衡的选择;如果配置绝对不变,使用 ‌ IOptions<T>**‌ 性能略优。

三、读写根目录,自定义的MQTT.json 示例

在 C# (.NET 6+/Core) 中读写根目录下的自定义 MQTT.json 文件,推荐使用内置的 ‌**System.Text.Json**‌ 库。它性能更好、无需额外依赖,且是微软官方首选方案。

以下是完整的实现代码,包含‌定义模型 ‌、‌读取配置 ‌、‌写入配置 ‌以及‌路径处理的最佳实践‌。

1. 定义配置模型 (Model)

首先创建一个与 JSON 结构匹配的 C# 类。

复制代码
csharpusing System.Text.Json.Serialization;

public class MqttConfig
{
    // 如果 JSON 中的键名是小写 camelCase,而 C# 属性是 PascalCase,
    // System.Text.Json 默认可以自动匹配(不区分大小写需配置,见下文),
    // 或者使用 [JsonPropertyName] 显式指定。
    
    public string BrokerAddress { get; set; } = "localhost";
    public int Port { get; set; } = 1883;
    public string ClientId { get; set; } = "my-client";
    public bool UseTls { get; set; } = false;
    public string? Username { get; set; }
    public string? Password { get; set; }
}

对应的 MQTT.json 文件内容示例:

复制代码
json{
  "BrokerAddress": "mqtt.example.com",
  "Port": 8883,
  "ClientId": "prod-client-01",
  "UseTls": true,
  "Username": "admin",
  "Password": "secret"
}

2. 核心工具类:读写操作

为了代码复用和健壮性,建议封装一个静态 helper 类。

‌**关键点:**‌

  • 路径获取 ‌:使用 AppContext.BaseDirectoryDirectory.GetCurrentDirectory() 获取程序运行根目录。

  • 编码 ‌:写入时必须指定 Encoding.UTF8,防止中文乱码。

  • 格式化 ‌:读取时建议开启 PropertyNameCaseInsensitive;写入时建议开启 WriteIndented 以便人工查看。

  • 原子写入‌:先写入临时文件,再替换原文件,防止写入过程中程序崩溃导致文件损坏。

    csharpusing System;
    using System.IO;
    using System.Text.Json;
    using System.Text;

    public static class JsonFileHelper
    {
    // 推荐复用 JsonSerializerOptions 以提升性能
    private static readonly JsonSerializerOptions ReadOptions = new JsonSerializerOptions
    {
    PropertyNameCaseInsensitive = true, // 忽略大小写匹配
    ReadCommentHandling = JsonCommentHandling.Skip, // 允许跳过注释
    AllowTrailingCommas = true // 允许尾随逗号
    };

    复制代码
      private static readonly JsonSerializerOptions WriteOptions = new JsonSerializerOptions
      {
          WriteIndented = true, // 美化输出,方便阅读
          Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping // 避免中文被转义为 \uXXXX
      };
    
      /// <summary>
      /// 从根目录读取 MQTT.json
      /// </summary>
      public static MqttConfig ReadMqttConfig()
      {
          // 1. 确定文件路径 (程序运行根目录)
          string filePath = Path.Combine(AppContext.BaseDirectory, "MQTT.json");
    
          // 2. 检查文件是否存在
          if (!File.Exists(filePath))
          {
              // 如果文件不存在,返回默认配置或抛出异常,视业务需求而定
              Console.WriteLine($"配置文件未找到: {filePath},使用默认配置。");
              return new MqttConfig(); 
          }
    
          try
          {
              // 3. 读取文件内容
              string jsonString = File.ReadAllText(filePath, Encoding.UTF8);
              
              // 4. 反序列化为对象
              var config = JsonSerializer.Deserialize<MqttConfig>(jsonString, ReadOptions);
              
              return config ?? new MqttConfig(); // 防止反序列化结果为 null
          }
          catch (JsonException ex)
          {
              Console.WriteLine($"JSON 格式错误: {ex.Message}");
              throw;
          }
          catch (IOException ex)
          {
              Console.WriteLine($"文件读取错误: {ex.Message}");
              throw;
          }
      }
    
      /// <summary>
      /// 将配置写入根目录 MQTT.json
      /// </summary>
      public static void WriteMqttConfig(MqttConfig config)
      {
          if (config == null) throw new ArgumentNullException(nameof(config));
    
          // 1. 确定文件路径
          string filePath = Path.Combine(AppContext.BaseDirectory, "MQTT.json");
          
          // 2. 确保目录存在 (虽然根目录通常存在,但以防万一)
          string? directory = Path.GetDirectoryName(filePath);
          if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
          {
              Directory.CreateDirectory(directory);
          }
    
          // 3. 序列化对象
          string jsonString = JsonSerializer.Serialize(config, WriteOptions);
    
          // 4. 原子写入策略:先写临时文件,再替换
          string tempFilePath = filePath + ".tmp";
          try
          {
              File.WriteAllText(tempFilePath, jsonString, Encoding.UTF8);
              File.Replace(tempFilePath, filePath, null); // .Replace 会自动处理备份和原子替换
          }
          catch (Exception ex)
          {
              // 清理临时文件
              if (File.Exists(tempFilePath))
              {
                  File.Delete(tempFilePath);
              }
              Console.WriteLine($"文件写入失败: {ex.Message}");
              throw;
          }
      }

    }

3. 调用示例

复制代码
csharpclass Program
{
    static void Main(string[] args)
    {
        try
        {
            // --- 读取配置 ---
            Console.WriteLine("正在读取 MQTT 配置...");
            MqttConfig config = JsonFileHelper.ReadMqttConfig();
            
            Console.WriteLine($"Broker: {config.BrokerAddress}:{config.Port}");
            Console.WriteLine($"ClientID: {config.ClientId}");

            // --- 修改配置 ---
            config.BrokerAddress = "new-broker.example.com";
            config.Port = 1883;
            config.ClientId = "updated-client";

            // --- 写入配置 ---
            Console.WriteLine("正在保存新配置...");
            JsonFileHelper.WriteMqttConfig(config);
            
            Console.WriteLine("配置保存成功!");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"发生错误: {ex.Message}");
        }
    }
}

常见陷阱与注意事项

  1. 中文乱码问题 ‌:
    • 原因‌:Windows 默认编码可能是 GBK/ANSI。
    • 解决 ‌:File.ReadAllTextFile.WriteAllText 必须显式传入 Encoding.UTF8
    • 转义问题 ‌:默认 System.Text.Json 会将非 ASCII 字符(如中文)转义为 \uXXXX。若希望直接显示中文,需在 JsonSerializerOptions 中设置 Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  2. 属性名大小写不匹配 ‌:
    • JSON 常用 camelCase (brokerAddress),C# 常用 PascalCase (BrokerAddress)。
    • 解决 ‌:在 JsonSerializerOptions 中设置 PropertyNameCaseInsensitive = true,或使用 [JsonPropertyName("brokerAddress")] 特性标注属性。
  3. 文件路径问题 ‌:
    • 不要使用硬编码的 "MQTT.json",这在某些部署环境(如 IIS、Docker、服务后台运行)中可能指向错误的目录。
    • 解决 ‌:始终使用 Path.Combine(AppContext.BaseDirectory, "MQTT.json") 确保指向程序集所在的根目录。
  4. 并发写入冲突 ‌:
    • 如果多个线程或进程同时写入,直接 File.WriteAllText 可能导致文件损坏。
    • 解决 ‌:使用上述代码中的"临时文件 + File.Replace"策略,这是 Windows 下最安全的原子更新方式。
  5. Newtonsoft.Json vs System.Text.Json ‌:
    • 如果是 ‌**.NET Core 3.0+ / .NET 5+ / .NET 6+‌ 新项目,‌ 强烈建议使用 System.Text.Json**‌(如上所示),因为它内置、速度快、内存占用低。
    • 只有在需要兼容旧版 .NET Framework 或需要处理极特殊的 JSON 格式(如带注释的非标准 JSON、复杂的循环引用)时,才考虑使用 Newtonsoft.Json