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(); }}
常见错误与排查
- 注入得到 null :
- 确保在
Program.cs中调用了builder.Services.Configure<MQTTSettings>(...)。仅调用AddOptions()是不够的。 - 检查
GetSection("MQTTSettings")中的字符串是否与appsettings.json中的键名完全一致(包括大小写,虽然 JSON 通常不区分,但配置系统内部映射可能敏感,建议保持一致)。
- 确保在
- **属性值为默认值 (0, null, false)**:
- 检查 C# 类的属性名是否与 JSON 键名匹配。
- 确保属性有
public set或init访问器。 - 如果是嵌套对象,确保层级结构正确。
- **不要在构造函数中直接注入
MQTTSettings**:- DI 容器不知道如何直接创建
MQTTSettings实例,除非你手动注册了它(如services.AddSingleton(new MQTTSettings(...))),但这失去了配置绑定的优势。必须通过IOptions<>包装器注入。
- DI 容器不知道如何直接创建
总结选择指南
几个选项的区别(必看)
| 接口 | 生命周期 | 热更新支持 | 典型用途 |
|---|---|---|---|
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.BaseDirectory或Directory.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}");
}
}
}
常见陷阱与注意事项
- 中文乱码问题 :
- 原因:Windows 默认编码可能是 GBK/ANSI。
- 解决 :
File.ReadAllText和File.WriteAllText必须显式传入Encoding.UTF8。 - 转义问题 :默认
System.Text.Json会将非 ASCII 字符(如中文)转义为\uXXXX。若希望直接显示中文,需在JsonSerializerOptions中设置Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping。
- 属性名大小写不匹配 :
- JSON 常用
camelCase(brokerAddress),C# 常用PascalCase(BrokerAddress)。 - 解决 :在
JsonSerializerOptions中设置PropertyNameCaseInsensitive = true,或使用[JsonPropertyName("brokerAddress")]特性标注属性。
- JSON 常用
- 文件路径问题 :
- 不要使用硬编码的
"MQTT.json",这在某些部署环境(如 IIS、Docker、服务后台运行)中可能指向错误的目录。 - 解决 :始终使用
Path.Combine(AppContext.BaseDirectory, "MQTT.json")确保指向程序集所在的根目录。
- 不要使用硬编码的
- 并发写入冲突 :
- 如果多个线程或进程同时写入,直接
File.WriteAllText可能导致文件损坏。 - 解决 :使用上述代码中的"临时文件 +
File.Replace"策略,这是 Windows 下最安全的原子更新方式。
- 如果多个线程或进程同时写入,直接
- Newtonsoft.Json vs System.Text.Json :
- 如果是 **.NET Core 3.0+ / .NET 5+ / .NET 6+ 新项目, 强烈建议使用
System.Text.Json**(如上所示),因为它内置、速度快、内存占用低。 - 只有在需要兼容旧版 .NET Framework 或需要处理极特殊的 JSON 格式(如带注释的非标准 JSON、复杂的循环引用)时,才考虑使用
Newtonsoft.Json。
- 如果是 **.NET Core 3.0+ / .NET 5+ / .NET 6+ 新项目, 强烈建议使用