注:
- 坑有点多,不推荐使用,如果你有好用的 .NET 配置管理工具请推荐给我
Provider 设置为 JSON 时,一定要保证相关项目已安装 Newtonsoft.Json
起因
寻找一款能在 .NET Framework 使用的配置管理工具,最好具备以下特性:
强类型:因为强类型可以支持 IntelliSense,方便使用
- 支持编辑 config 文件
- 支持跨项目使用:多个项目可以使用同一个 config 文件
在找第三方库的过程中,一篇文章(Building a better .NET Application Configuration Class - revisited)吸引了我的注意。文章中作者提到的配置管理的痛点和我的需求不谋而合,然后我就决定尝试作者自己写的配置管理工具 —— Westwind.ApplicationConfiguration。
试用的体验还不错,上面的 3 个需求都能满足。虽然有些相关的链接失效了,但源码和帮助文档都比较完备。
官方介绍
Strongly typed, code-first configuration classes for .NET applications.
资源:
- GitHub
- NuGet
- 上手参考 Building a better .NET Application Configuration Class - revisited
- Managing Configuration Settings with AppConfiguration
- 帮助文档 chm.zip
使用技巧
指定配置文件
通过指定配置文件,可以实现在多个项目中使用同一个 config 文件。
如下代码实现了在多个项目中都从 WestwindConfigurationTest.exe.config 读取配置:
Configuration = new MyConfiguration();var provider = new ConfigurationFileConfigurationProvider<MyConfiguration>{ConfigurationSection = "MyConfiguration",ConfigurationFile = "WestwindConfigurationTest.exe.config"};Configuration.Initialize(provider);
复杂类型配置
:::warning 谨记,每次在配置文件中加入一个自定义类型配置前,检查是否已为它手动编写了序列化代码。 :::
参考:Adding Complex Types in .config Files
复杂类型只要实现了 XML 或 JSON 序列化,Westwind.ApplicationConfiguration 就支持将该复杂类型的实例作为配置。
复杂类型指.NET .config 文件不支持的类型,主要指 自定义类型、包含子类的类型、列表 等。
通过 ToString()/FromString() 序列化
想要序列化复杂类型,只需在自定义类中重写 ToString() 方法并实现一个 static 的 FormString() 方法即可,详情参考 使用 StringSerializer 的官方示例/FromString())。
下面演示如何将 Json.NET 作为序列化工具,并使用 JsonFileConfigurationProvider。
复杂类型 SerialPortSettings:
public class SerialPortSettings{public string PortName;public int BaudRate;public int DataBits;public StopBits StopBits;public Parity Parity;public Handshake Handshake;public override string ToString(){return JsonConvert.SerializeObject(this);}public static SerialPortSettings FromString(string json){return JsonConvert.DeserializeObject<SerialPortSettings>(json);}}
Configuration class:
public class TesterConfiguration : Westwind.Utilities.Configuration.AppConfiguration{public SerialPortSettings DefaultPortSettings { get; set; }public SerialPortSettings UserPortSettings { get; set; }public TesterConfiguration(){DefaultPortSettings = new SerialPortSettings{BaudRate = 9600,DataBits = 8,StopBits = StopBits.One,Parity = Parity.None,Handshake = Handshake.XOnXOff};}}
初始化 Configs:
public class ConfigBll{public static TesterConfiguration Configs { get; }static ConfigBll(){Configs = new TesterConfiguration();var provider = new JsonFileConfigurationProvider<TesterConfiguration>{JsonConfigurationFile = "TesterConfig.json"};Configs.Initialize(provider);}}
配置初始化后的存储效果:
{"DefaultPortSettings": {"PortName": null,"BaudRate": 9600,"DataBits": 8,"StopBits": "One","Parity": "None","Handshake": "XOnXOff"},"UserPortSettings": null}
读取和更新配置:
var serialPortSettings = ConfigBll.Configs.DefaultPortSettings;defaultPortSettings.PortName = "COM2";ConfigBll.Configs.UserPortSettings = defaultPortSettings;ConfigBll.Configs.Write();
配置更新后的存储效果:
{"DefaultPortSettings": {"PortName": "COM2","BaudRate": 9600,"DataBits": 8,"StopBits": "One","Parity": "None","Handshake": "XOnXOff"},"UserPortSettings": {"PortName": "COM2","BaudRate": 9600,"DataBits": 8,"StopBits": "One","Parity": "None","Handshake": "XOnXOff"}}
通过 TypeConverter 序列化
参考代码:
[TypeConverter(typeof(CustomCustomerTypeConverter)), Serializable()]public class CustomCustomer{public string Name = "Rick Strahl";public string Company = "West Wind";public decimal OrderTotal = 100.90M;}public class CustomCustomerTypeConverter : System.ComponentModel.ExpandableObjectConverter{public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType){if (destinationType == typeof(string) )return true;return base.CanConvertTo (context, destinationType);}public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType){if (sourceType == typeof(string) )return true;return base.CanConvertFrom (context, sourceType);}public override object ConvertTo(ITypeDescriptorContext context,System.Globalization.CultureInfo culture,object value,Type destinationType){CustomCustomer Cust = (CustomCustomer) value;if ( destinationType == typeof(string) )return Cust.Name + "," + Cust.Company + "," + string.Format( culture.NumberFormat,"{0}",Cust.OrderTotal);return base.ConvertTo(context,culture,value,destinationType);}public override object ConvertFrom(ITypeDescriptorContext context,System.Globalization.CultureInfo culture, object value){if (! (value is string) )return base.ConvertFrom (context, culture, value );string Persisted = (string) value;CustomCustomer Cust = new CustomCustomer();if (Persisted != null || Persisted != ""){string[] Fields = Persisted.Split(new char[1] {','});Cust.Company = Fields[1];Cust.Name = Fields[0];Cust.OrderTotal = (decimal)wwUtils.StringToTypedValue(Fields[2],Cust.OrderTotal.GetType());}return Cust;}}
列表类型配置
想要将列表作为配置需要满足两点:
- 列表本身实现了 IList
- 列表中存储的元素是简单类型,或是支持 ToString/FromString(或 TypeConverter)的复杂类型
注:直接使用 List
计算加密字符串
如果能算出修改后的加密配置对应的加密字符串,就可以做到直接在 config 文件里面直接更新加密字符串进而使加密配置更新。
于是我去源码找到了作者默认对字符串加密的实现:
/// <summary>/// Replace this value with some unique key of your own/// Best set this in your App start up in a Static constructor/// </summary>public static string Key = "0a1f131c";/// <summary>/// Encodes a stream of bytes using DES encryption with a pass key. Lowest level method that/// handles all work./// </summary>/// <param name="InputString"></param>/// <param name="EncryptionKey"></param>/// <returns></returns>public static byte[] EncryptBytes(byte[] InputString, string EncryptionKey){if (EncryptionKey == null)EncryptionKey = Key;TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();MD5CryptoServiceProvider hashmd5 = new MD5CryptoServiceProvider();des.Key = hashmd5.ComputeHash(Encoding.ASCII.GetBytes(EncryptionKey));des.Mode = CipherMode.ECB;ICryptoTransform Transform = des.CreateEncryptor();byte[] Buffer = InputString;return Transform.TransformFinalBlock(Buffer, 0, Buffer.Length);}/// <summary>/// Encrypts a string into bytes using DES encryption with a Passkey./// </summary>/// <param name="InputString"></param>/// <param name="EncryptionKey"></param>/// <returns></returns>public static byte[] EncryptBytes(string DecryptString, string EncryptionKey){return EncryptBytes(Encoding.ASCII.GetBytes(DecryptString), EncryptionKey);}/// <summary>/// Encrypts a string using Triple DES encryption with a two way encryption key.String is returned as Base64 encoded value/// rather than binary./// </summary>/// <param name="InputString"></param>/// <param name="EncryptionKey"></param>/// <returns></returns>public static string EncryptString(string InputString, string EncryptionKey){return Convert.ToBase64String(EncryptBytes(Encoding.ASCII.GetBytes(InputString), EncryptionKey));}
解决配置文件没有写入权限
从 Win10 开始由于对权限管控的加强,当程序默认安装在 C 盘后,正常登录的用户只有 User 权限无法对安装目录下的文本文件进行写入,进而导致配置文件保存失败。
解决的方法是将配置文件保存到 My Documents 或 AppData 等 User 权限也能写入的文件夹中。
如下代码展示了通过 json 文件保存配置,并将该文件存储在 Appdata 路径下:
public class ConfigTool{public static RucConfiguration Configs { get; }static ConfigTool(){var folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),"RadarUpperComputer\\");Directory.CreateDirectory(folderPath);var filePath = Path.Combine(folderPath, "RucConfig.json");var provider = new JsonFileConfigurationProvider<RucConfiguration>{JsonConfigurationFile = filePath,EncryptionKey = "ruc",PropertiesToEncrypt = "LastLoginPassword"};Configs = new RucConfiguration();Configs.Initialize(provider);}}
已知问题
对List 支持不好
当我用 JSON 序列化 + List
例如下面代码中已屏蔽掉的 M3Series 在第一次程序生成配置文件时是正常的。第二次程序生成配置文件时,配置文件中将出现两个重复的 M3Series,第三次出现三个,依次类推,配置文件体积逐渐膨胀。
目前找到的临时解决方案就是用 string[] 替代 List
//public List<string> M3Series { get; set; } =new List<string>{// "M3100","M3200","M3300"//};public string[] M3Series { get; set; } ={"M3100","M3200","M3300"};
