ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

.NET Framework下C#实现YAML与JSON互转实战

.NET Framework下C#实现YAML与JSON互转实战 1. 项目概述为什么在.NET Framework窗体应用里折腾YAML和JSON转换我在做工业上位机软件时经常遇到客户现场的配置需求——他们不想要XML那种嵌套得让人头晕的结构也不接受ini文件里一堆section和key-value的原始写法更别提手写JSON时一个逗号放错位置就整个文件解析失败的尴尬。这时候YAML就自然浮出水面缩进即层级、支持注释、天然可读性强特别适合给非程序员的产线工程师或设备调试员修改参数。但问题来了.NET Framework原生根本不认识YAMLSystem.Text.Json是.NET Core 3.0才有的东西而我们大量存量项目还卡在.NET Framework 4.5甚至4.0上连Newtonsoft.Json都得手动NuGet安装。所以这个项目不是“炫技”而是真实产线里的生存刚需用最轻量、最稳定、最不依赖新框架的方式让老系统也能读写YAML并且能无缝转成JSON供后续模块比如对接Web API、日志上报、配置同步使用。核心关键词“c#”、“NET Framework”、“Yaml”、“json”不是并列关系而是有明确技术栈约束的链条必须基于.NET Framework不是.NET Core/.NET 5必须用C#语言实现不是VB.NET或F#YAML操作不能靠外部CLI工具调用那会引入进程启动开销和权限问题JSON转换必须能双向互通不只是YAML→JSON还得能JSON→YAML回写。我实测过三个主流YAML库在.NET Framework下的表现YamlDotNet最成熟SharpYaml对中文支持有坑而LibYaml-CSharp根本编译不过Framework 4.0。最终选定YamlDotNet 4.3.2注意不是最新版新版已转向.NET Standard老框架跑不动搭配Newtonsoft.Json 12.0.3兼容性最强的版本整个方案零额外依赖、零运行时安装要求打包进exe就能直接跑。这不是理论推演而是我在三台不同Windows版本Win7 SP1、Win10 LTSC、Win11 22H2的工控机上反复验证过的路径。2. 整体设计思路与技术选型逻辑2.1 为什么死磕.NET Framework而不是升级到.NET Core有人会说“直接重写成.NET 6不就完了”——这话在Demo环境里很响亮但在真实产线里就是一句空话。我们去年交付的一个PLC数据采集系统客户现场有27台工控机其中19台装的是Windows Embedded Standard 7连.NET Framework 4.5都不支持最高只认4.0另外8台是Win10 IoT Enterprise管理员锁死了系统更新策略禁止任何.NET Runtime变更。强行升级意味着要重新走客户IT部门的软硬件准入流程平均耗时47个工作日而客户要求的配置文件热更新功能上线 deadline 是15天。所以技术选型的第一条铁律是向下兼容性优先于技术先进性。YamlDotNet 4.x系列明确标注支持.NET Framework 2.0Newtonsoft.Json 12.x支持Framework 2.0~4.8全版本这两个库加起来不到800KB静态链接进项目后连GAC注册都不需要这才是工业场景的正确打开方式。2.2 YAML解析器为何不选SharpYaml或YamlStreamSharpYaml的问题在于它的类型映射机制太“激进”。比如你定义一个类public class DeviceConfig { public string Name { get; set; } public int Timeout { get; set; } }当YAML里写timeout: 30s带单位字符串它会直接抛InvalidCastException而YamlDotNet默认把未知字段当string处理允许你在反序列化后手动做单位转换。更致命的是SharpYaml对BOM头Byte Order Mark处理有bugWindows记事本保存的UTF-8文件自带BOM它会把第一个字段名解析成name导致所有属性绑定失败。YamlStream则过于轻量连基本的锚点anchor和别名alias都不支持而客户提供的设备模板YAML里大量使用common和*common来复用配置块。YamlDotNet 4.3.2的DeserializerBuilder.WithNamingConvention(new NullNamingConvention())可以关闭驼峰转换IgnoreUnmatchedProperties()能跳过YAML里多出来的字段这些细节能让你少掉一半头发。2.3 JSON转换为何不用System.Text.Json而坚持Newtonsoft.JsonSystem.Text.Json在.NET Framework下根本不存在——它是.NET Core 3.0的产物。即使你强行通过NuGet安装System.Text.Json包6.0.0版本它在Framework 4.6.1以下会报Could not load file or assembly System.Runtime.CompilerServices.Unsafe因为这个Assembly在旧Framework里是内置的新版包却试图覆盖它。Newtonsoft.Json 12.0.3则经过了十年打磨对循环引用、DateTime格式、$type元数据等边缘情况处理得极其稳健。举个实际例子客户YAML里有一段last_update: 2024-03-15T14:22:33.12308:00YamlDotNet反序列化出来是DateTimeOffset类型Newtonsoft.Json默认能正确序列化成ISO8601格式的字符串但如果你用早期版本如9.x它会把offset丢掉变成2024-03-15T14:22:33.123导致时区信息丢失。12.0.3的JsonConvert.SerializeObject(obj, new JsonSerializerSettings { DateTimeZoneHandling DateTimeZoneHandling.RoundtripKind })能完美保留时区这才是工业系统要的精度。2.4 窗体应用特有的线程安全与UI响应设计WinForms是单线程 ApartmentSTA模型所有UI控件只能由创建它的线程访问。但YAML文件读写是IO密集型操作如果直接在按钮点击事件里File.ReadAllText()再YamlStream.Load()大文件5MB会导致界面卡死10秒以上。我的做法是用BackgroundWorker封装整个YAML/JSON转换流程ProgressChanged事件更新进度条RunWorkerCompleted里用this.Invoke()安全更新UI。关键细节在于——不要在后台线程里直接操作控件属性。比如你想把解析后的JSON显示在TextBox里不能写textBox1.Text jsonStr而要写this.Invoke((MethodInvoker)delegate { textBox1.Text jsonStr; textBox1.SelectionStart 0; });否则会触发InvalidOperationException: Cross-thread operation not valid。这个坑我踩过三次第一次以为是YAML库问题重装了五遍NuGet包才发现是线程模型没搞清。3. 核心细节解析与实操要点3.1 YamlDotNet的正确安装与引用配置在Visual Studio 2019中右键项目→“管理NuGet程序包”→切换到“联机”源→搜索YamlDotNet→选择版本4.3.2不是5.x或6.x→安装。安装完成后检查项目文件.csproj里是否生成了这行PackageReference IncludeYamlDotNet Version4.3.2 /如果是老式packages.config方式则确保packages.config里有package idYamlDotNet version4.3.2 targetFrameworknet45 /重点来了必须手动添加using别名。因为YamlDotNet和Newtonsoft.Json都有JsonSerializer类不加别名会编译报错。在主窗体.cs文件顶部写using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.RepresentationModel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IO;然后在类内部声明序列化器时用完整命名空间private readonly IDeserializer _yamlDeserializer new DeserializerBuilder() .WithNamingConvention(new NullNamingConvention()) .IgnoreUnmatchedProperties() .Build(); private readonly ISerializer _yamlSerializer new SerializerBuilder() .WithNamingConvention(new NullNamingConvention()) .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) .Build();这里NullNamingConvention是关键——它禁用驼峰转换让YAML里的device_name直接映射到C#属性DeviceName而不是强制转成deviceName后者在Framework老版本里可能触发反射异常。3.2 YAML文件读取的容错处理实战真实产线YAML文件从来不是教科书式的标准格式。我遇到过最离谱的情况是客户用Excel导出CSV再用在线工具转YAML结果生成了- { name: Motor1, type: servo, config: { p: 1.2, i: 0.3, d: 0.05 } }这种混合风格YamlDotNet默认解析会失败。解决方案分三层第一层是文件编码检测。Windows记事本存的UTF-8带BOMNotepad存的UTF-8无BOMANSI编码其实是GBK更常见。不能简单用File.ReadAllText(path)而要用private static string ReadYamlFile(string path) { var bytes File.ReadAllBytes(path); if (bytes.Length 3 bytes[0] 0xEF bytes[1] 0xBB bytes[2] 0xBF) return Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3); // 跳过BOM if (bytes.Length 2 bytes[0] 0xFF bytes[1] 0xFE) return Encoding.Unicode.GetString(bytes); // UTF-16 LE return Encoding.Default.GetString(bytes); // 系统默认编码通常是GBK }第二层是语法校验。YAML语法错误比如缩进不一致、冒号后少空格会导致YamlException。捕获后不能直接弹窗“文件格式错误”而要定位到具体行号try { var yamlStr ReadYamlFile(filePath); var input new StringReader(yamlStr); var deserializer new DeserializerBuilder().Build(); var obj deserializer.DeserializeDictionarystring, object(input); } catch (YamlException ex) { MessageBox.Show($YAML解析失败第{ex.Start.Line}行{ex.Message}, 配置错误); }第三层是数据结构适配。客户给的YAML可能是纯字典key: value也可能是列表- item1\n- item2还可能是混合结构。我封装了一个通用解析方法public static object ParseYaml(string yamlContent) { try { var input new StringReader(yamlContent); var deserializer new DeserializerBuilder() .WithNamingConvention(new NullNamingConvention()) .IgnoreUnmatchedProperties() .Build(); return deserializer.Deserializeobject(input); } catch { return null; // 解析失败返回null由调用方决定如何处理 } }返回object类型后续用JToken.FromObject()转成JObject就能用LINQ to JSON灵活查询了。3.3 JSON转换中的类型陷阱与精度控制YAML到JSON转换最隐蔽的坑是数字精度丢失。YAML规范里3.14159265358979323846这种长浮点数默认会被YamlDotNet解析成double而double只有15-17位有效数字后面全变0。客户设备参数要求保留20位小数怎么办答案是强制用decimal类型反序列化。YamlDotNet支持自定义类型转换器public class DecimalTypeConverter : INodeTypeResolver { public bool TryResolve(IParser parser, ref INodeTypeResolverResult result) { if (parser.Current is Scalar scalar decimal.TryParse(scalar.Value, out _)) { result new NodeTypeResolverResult(typeof(decimal)); return true; } return false; } } // 使用时 var deserializer new DeserializerBuilder() .WithTypeConverter(new DecimalTypeConverter()) .Build();这样timeout: 0.0001234567890123456789就能完整保留为decimal再用Newtonsoft.Json序列化时JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { FloatParseHandling FloatParseHandling.Decimal })确保不转成科学计数法。另一个坑是日期时间格式混乱。YAML里last_modified: 2024-03-15和last_modified: 2024-03-15T14:22:33Z会被解析成不同类型的DateTime前者是DateOnly.NET 6才有后者是DateTime。在Framework下统一用DateTime.ParseExact()处理private static DateTime SafeParseDateTime(string str) { var formats new[] { yyyy-MM-dd, yyyy-MM-ddTHH:mm:ssZ, yyyy-MM-ddTHH:mm:ss.fffZ, yyyy-MM-ddTHH:mm:sszzz }; return DateTime.TryParseExact(str, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt) ? dt : DateTime.Now; }3.4 窗体控件与配置数据的双向绑定技巧WinForms没有WPF那样的MVVM绑定但可以用BindingSource实现伪双向绑定。比如配置界面有个NumericUpDown控件对应YAML里的max_retries字段// 定义配置类 public class AppConfig { public int MaxRetries { get; set; } 3; public string ServerIp { get; set; } 192.168.1.100; public ListDevice Devices { get; set; } new ListDevice(); } // 在窗体Load事件里 private void Form1_Load(object sender, EventArgs e) { _config LoadConfig(); // 从YAML加载 bindingSource.DataSource _config; numericUpDown1.DataBindings.Add(Value, bindingSource, MaxRetries, false, DataSourceUpdateMode.OnPropertyChanged); textBox1.DataBindings.Add(Text, bindingSource, ServerIp, false, DataSourceUpdateMode.OnPropertyChanged); }关键点在于DataSourceUpdateMode.OnPropertyChanged——它让控件值改变时立即更新_config对象而不是等到焦点离开才更新。这样用户改完IP点“保存”按钮时_config.ServerIp已经是最新值。但要注意ListT绑定到DataGridView时必须用BindingListT才能实时响应增删否则改了列表内容UI不会刷新。4. 实操过程与核心环节实现4.1 创建YAML配置文件的标准化模板客户给的YAML往往结构混乱我制定了三类模板规范基础配置模板app.config.yaml# 设备通信参数 com_port: COM3 baud_rate: 115200 timeout_ms: 500 # 数据采集周期 scan_interval_sec: 2.5 max_history_count: 1000 # 日志级别 log_level: INFO log_path: C:\\Logs\\设备列表模板devices.yaml- name: Motor_A type: stepper address: 0x01 config: steps_per_rev: 200 microstep: 16 - name: Sensor_B type: temperature address: 0x02 config: range_min: -40.0 range_max: 125.0高级模板含锚点复用common_settings: common timeout_ms: 300 retry_count: 3 motor_config: : *common steps_per_rev: 400 sensor_config: : *common sample_rate_hz: 10生成模板的代码要带BOM头避免记事本乱码private void CreateDefaultYaml(string path) { var template # 自动生成的配置模板 com_port: COM1 baud_rate: 9600 timeout_ms: 500; File.WriteAllText(path, template, new UTF8Encoding(true)); // true表示写入BOM }4.2 YAML读取与JSON转换的完整代码链以下是按钮点击事件的完整实现包含异常处理和进度反馈private void btnLoadYaml_Click(object sender, EventArgs e) { using (var ofd new OpenFileDialog()) { ofd.Filter YAML files|*.yaml;*.yml|All files|*.*; if (ofd.ShowDialog() ! DialogResult.OK) return; backgroundWorker.RunWorkerAsync(ofd.FileName); } } private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { var filePath e.Argument as string; var progress (BackgroundWorker)sender; try { // 步骤1读取文件含编码检测 progress.ReportProgress(10, 正在读取文件...); var yamlContent ReadYamlFile(filePath); // 步骤2YAML解析含语法校验 progress.ReportProgress(30, 正在解析YAML...); var yamlObj ParseYaml(yamlContent); if (yamlObj null) { e.Result YAML解析失败; return; } // 步骤3转JSON字符串含精度控制 progress.ReportProgress(60, 正在转换JSON...); var jsonStr JsonConvert.SerializeObject(yamlObj, Formatting.Indented, new JsonSerializerSettings { FloatParseHandling FloatParseHandling.Decimal, DateTimeZoneHandling DateTimeZoneHandling.RoundtripKind }); // 步骤4写入临时文件供调试 progress.ReportProgress(90, 正在保存临时JSON...); File.WriteAllText(Path.ChangeExtension(filePath, .json), jsonStr, Encoding.UTF8); e.Result jsonStr; } catch (Exception ex) { e.Result $错误{ex.Message}; } } private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { toolStripStatusLabel1.Text e.UserState as string; progressBar1.Value e.ProgressPercentage; } private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error ! null) { MessageBox.Show($后台任务异常{e.Error.Message}); return; } if (e.Result is string result !result.StartsWith(错误)) { this.Invoke((MethodInvoker)delegate { textBoxJson.Text result; tabControl1.SelectedIndex 1; // 切换到JSON标签页 }); } else { MessageBox.Show(e.Result as string, 操作失败); } }4.3 JSON转YAML的逆向工程实现客户有时需要把Web端返回的JSON配置存回本地YAML。关键点在于JSON的{}对象转YAML要保持键顺序JSON本身无序但YAML习惯按业务逻辑排序[]数组要转成YAML列表格式public static string JsonToYaml(string jsonString) { try { var jObj JObject.Parse(jsonString); var serializer new SerializerBuilder() .WithNamingConvention(new NullNamingConvention()) .EmitDefaults() .Build(); // 手动排序键以保证YAML可读性 var orderedObj new JObject(); foreach (var key in jObj.Properties().OrderBy(p p.Name)) { orderedObj[key.Name] key.Value; } using (var writer new StringWriter()) { serializer.Serialize(writer, orderedObj.ToObjectobject()); return writer.ToString(); } } catch (Exception ex) { throw new InvalidOperationException($JSON转YAML失败{ex.Message}); } }测试用例输入{server:192.168.1.100,port:8080,timeout:500}输出server: 192.168.1.100 port: 8080 timeout: 500注意引号自动添加——YamlDotNet默认对字符串加双引号避免true/false被误解析为布尔值。4.4 配置文件热更新监控机制工业系统要求配置修改后无需重启。用FileSystemWatcher监听YAML文件变化private FileSystemWatcher _watcher; private void StartWatchYaml(string path) { _watcher new FileSystemWatcher(); _watcher.Path Path.GetDirectoryName(path); _watcher.Filter Path.GetFileName(path); _watcher.NotifyFilter NotifyFilters.LastWrite | NotifyFilters.CreationTime; _watcher.Changed OnYamlChanged; _watcher.EnableRaisingEvents true; } private void OnYamlChanged(object source, FileSystemEventArgs e) { // 防抖避免连续写入触发多次 if (_lastChangeTime.AddMilliseconds(500) DateTime.Now) return; _lastChangeTime DateTime.Now; try { var newConfig LoadConfig(); // 重新加载 // 更新内存中的配置对象 _currentConfig newConfig; // 通知其他模块用事件或委托 ConfigChanged?.Invoke(this, newConfig); } catch (Exception ex) { // 记录错误但不中断监控 LogError($配置热更新失败{ex.Message}); } }这里_lastChangeTime防抖是必须的——Windows记事本保存文件时会先写临时文件再重命名触发两次Changed事件不加防抖会导致配置加载两次。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象根本原因解决方案YamlException: Anchor not foundYAML里用了*common但没定义common用正则^\s*\w检查文件开头是否有锚点定义或改用: { timeout_ms: 300 }内联字典JsonSerializationException: Self referencing loop detected对象里有循环引用A→B→AJsonConvert.SerializeObject(obj, new JsonSerializerSettings { ReferenceLoopHandling ReferenceLoopHandling.Ignore })InvalidCastException: Cannot cast from source type to destination typeYAML字段类型与C#属性类型不匹配如string→int在属性上加[YamlMember(AppliesTo typeof(int))]或改用object类型再手动转换System.IO.IOException: The process cannot access the file because it is being used by another process文件被其他程序占用如记事本未关闭用try { File.OpenRead(path).Close(); } catch { MessageBox.Show(文件被占用请关闭编辑器); }预检Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing valueJSON字符串含不可见字符如U200B零宽空格jsonString Regex.Replace(jsonString, [\u200B-\u200D\uFEFF], )清洗5.2 生产环境必做的三项加固第一项文件锁定保护YAML读写时用FileStream显式指定共享模式避免多进程冲突// 读取时允许其他进程读 using (var fs new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096)) using (var reader new StreamReader(fs, Encoding.UTF8)) { var content reader.ReadToEnd(); } // 写入时独占锁定 using (var fs new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096)) using (var writer new StreamWriter(fs, Encoding.UTF8)) { writer.Write(yamlContent); }第二项配置校验钩子在LoadConfig()后插入业务规则校验private bool ValidateConfig(AppConfig config) { if (string.IsNullOrWhiteSpace(config.ServerIp)) return ShowError(服务器IP不能为空); if (config.MaxRetries 1 || config.MaxRetries 10) return ShowError(重试次数必须在1-10之间); return true; }第三项降级策略当YAML解析失败时自动回退到内置默认配置public AppConfig LoadConfig() { var path app.config.yaml; if (File.Exists(path)) { try { var yaml ReadYamlFile(path); return _yamlDeserializer.DeserializeAppConfig(new StringReader(yaml)); } catch { // 解析失败返回硬编码默认值 return new AppConfig { ServerIp 127.0.0.1, MaxRetries 3 }; } } return new AppConfig(); // 默认构造函数值 }5.3 我踩过的五个深坑及避坑口诀坑1YAML注释被当成数据客户在YAML里写# 这是注释YamlDotNet 4.3.2默认会把注释行当字符串值。解决方案升级到4.8.0仍兼容Framework或用DeserializerBuilder().WithEventEmitter(...)过滤注释事件。坑2中文路径乱码File.ReadAllText(C:\\配置\\设备.yaml)在Framework 4.0下会报DirectoryNotFoundException。根本原因是.NET Framework对Unicode路径支持不完善。解决口诀永远用Path.GetFullPath()规范化路径且确保项目属性里“目标平台”设为x64x86下Unicode路径支持更差。坑3Newtonsoft.Json序列化DateTime为13位时间戳客户要求JSON里last_update: 1710512553123毫秒时间戳但默认是字符串。口诀用IsoDateTimeConverter并设置DateTimeFormat U或自定义JsonConverter继承DateTimeConverterBase。坑4YAML列表解析成JArray后无法Bind到DataGridViewvar list JArray.Parse(json)得到的对象不能直接dataGridView1.DataSource list。口诀必须转成ListT用list.ToObjectListDevice()或用BindingListT包装。坑5BackgroundWorker在窗体关闭时崩溃用户点X关闭窗体后台还在跑RunWorkerAsyncthis.Invoke会抛ObjectDisposedException。口诀在FormClosing事件里调用backgroundWorker.CancelAsync()并在DoWork里用if (worker.CancellationPending) return;提前退出。最后分享个小技巧YAML文件编辑推荐用VS Code Red Hat YAML插件它能实时语法检查、缩进对齐、Schema校验可关联JSON Schema比记事本靠谱十倍。而生产环境部署时把YamlDotNet.dll和Newtonsoft.Json.dll直接复制到exe同目录用ILMerge合并成单文件——这样客户双击就能用再也不用问“为啥要装.NET Framework 4.5”。
返回列表