注:

  1. 坑有点多,不推荐使用,如果你有好用的 .NET 配置管理工具请推荐给我
  2. Provider 设置为 JSON 时,一定要保证相关项目已安装 Newtonsoft.Json

    起因

    寻找一款能在 .NET Framework 使用的配置管理工具,最好具备以下特性:

  3. 强类型:因为强类型可以支持 IntelliSense,方便使用

  4. 支持编辑 config 文件
  5. 支持跨项目使用:多个项目可以使用同一个 config 文件

在找第三方库的过程中,一篇文章(Building a better .NET Application Configuration Class - revisited)吸引了我的注意。文章中作者提到的配置管理的痛点和我的需求不谋而合,然后我就决定尝试作者自己写的配置管理工具 —— Westwind.ApplicationConfiguration

试用的体验还不错,上面的 3 个需求都能满足。虽然有些相关的链接失效了,但源码和帮助文档都比较完备。

官方介绍

Strongly typed, code-first configuration classes for .NET applications.

资源:

如下代码实现了在多个项目中都从 WestwindConfigurationTest.exe.config 读取配置:

  1. Configuration = new MyConfiguration();
  2. var provider = new ConfigurationFileConfigurationProvider<MyConfiguration>
  3. {
  4. ConfigurationSection = "MyConfiguration",
  5. ConfigurationFile = "WestwindConfigurationTest.exe.config"
  6. };
  7. 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:

  1. public class SerialPortSettings
  2. {
  3. public string PortName;
  4. public int BaudRate;
  5. public int DataBits;
  6. public StopBits StopBits;
  7. public Parity Parity;
  8. public Handshake Handshake;
  9. public override string ToString()
  10. {
  11. return JsonConvert.SerializeObject(this);
  12. }
  13. public static SerialPortSettings FromString(string json)
  14. {
  15. return JsonConvert.DeserializeObject<SerialPortSettings>(json);
  16. }
  17. }

Configuration class:

  1. public class TesterConfiguration : Westwind.Utilities.Configuration.AppConfiguration
  2. {
  3. public SerialPortSettings DefaultPortSettings { get; set; }
  4. public SerialPortSettings UserPortSettings { get; set; }
  5. public TesterConfiguration()
  6. {
  7. DefaultPortSettings = new SerialPortSettings
  8. {
  9. BaudRate = 9600,
  10. DataBits = 8,
  11. StopBits = StopBits.One,
  12. Parity = Parity.None,
  13. Handshake = Handshake.XOnXOff
  14. };
  15. }
  16. }

初始化 Configs:

  1. public class ConfigBll
  2. {
  3. public static TesterConfiguration Configs { get; }
  4. static ConfigBll()
  5. {
  6. Configs = new TesterConfiguration();
  7. var provider = new JsonFileConfigurationProvider<TesterConfiguration>
  8. {
  9. JsonConfigurationFile = "TesterConfig.json"
  10. };
  11. Configs.Initialize(provider);
  12. }
  13. }

配置初始化后的存储效果:

  1. {
  2. "DefaultPortSettings": {
  3. "PortName": null,
  4. "BaudRate": 9600,
  5. "DataBits": 8,
  6. "StopBits": "One",
  7. "Parity": "None",
  8. "Handshake": "XOnXOff"
  9. },
  10. "UserPortSettings": null
  11. }

读取和更新配置:

  1. var serialPortSettings = ConfigBll.Configs.DefaultPortSettings;
  2. defaultPortSettings.PortName = "COM2";
  3. ConfigBll.Configs.UserPortSettings = defaultPortSettings;
  4. ConfigBll.Configs.Write();

配置更新后的存储效果:

  1. {
  2. "DefaultPortSettings": {
  3. "PortName": "COM2",
  4. "BaudRate": 9600,
  5. "DataBits": 8,
  6. "StopBits": "One",
  7. "Parity": "None",
  8. "Handshake": "XOnXOff"
  9. },
  10. "UserPortSettings": {
  11. "PortName": "COM2",
  12. "BaudRate": 9600,
  13. "DataBits": 8,
  14. "StopBits": "One",
  15. "Parity": "None",
  16. "Handshake": "XOnXOff"
  17. }
  18. }

通过 TypeConverter 序列化

参考:TypeConverter

参考代码:

  1. [TypeConverter(typeof(CustomCustomerTypeConverter)), Serializable()]
  2. public class CustomCustomer
  3. {
  4. public string Name = "Rick Strahl";
  5. public string Company = "West Wind";
  6. public decimal OrderTotal = 100.90M;
  7. }
  8. public class CustomCustomerTypeConverter : System.ComponentModel.ExpandableObjectConverter
  9. {
  10. public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
  11. {
  12. if (destinationType == typeof(string) )
  13. return true;
  14. return base.CanConvertTo (context, destinationType);
  15. }
  16. public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  17. {
  18. if (sourceType == typeof(string) )
  19. return true;
  20. return base.CanConvertFrom (context, sourceType);
  21. }
  22. public override object ConvertTo(ITypeDescriptorContext context,
  23. System.Globalization.CultureInfo culture,
  24. object value,
  25. Type destinationType)
  26. {
  27. CustomCustomer Cust = (CustomCustomer) value;
  28. if ( destinationType == typeof(string) )
  29. return Cust.Name + "," + Cust.Company + "," + string.Format( culture.NumberFormat,"{0}",Cust.OrderTotal);
  30. return base.ConvertTo(context,culture,value,destinationType);
  31. }
  32. public override object ConvertFrom(ITypeDescriptorContext context,
  33. System.Globalization.CultureInfo culture, object value)
  34. {
  35. if (! (value is string) )
  36. return base.ConvertFrom (context, culture, value );
  37. string Persisted = (string) value;
  38. CustomCustomer Cust = new CustomCustomer();
  39. if (Persisted != null || Persisted != "")
  40. {
  41. string[] Fields = Persisted.Split(new char[1] {','});
  42. Cust.Company = Fields[1];
  43. Cust.Name = Fields[0];
  44. Cust.OrderTotal = (decimal)wwUtils.StringToTypedValue(Fields[2],Cust.OrderTotal.GetType());
  45. }
  46. return Cust;
  47. }
  48. }

列表类型配置

想要将列表作为配置需要满足两点:

  1. 列表本身实现了 IList
  2. 列表中存储的元素是简单类型,或是支持 ToString/FromString(或 TypeConverter)的复杂类型

注:直接使用 List 作为配置可能会遇到配置文件内容重复的问题

计算加密字符串

如果能算出修改后的加密配置对应的加密字符串,就可以做到直接在 config 文件里面直接更新加密字符串进而使加密配置更新。

于是我去源码找到了作者默认对字符串加密的实现:

  1. /// <summary>
  2. /// Replace this value with some unique key of your own
  3. /// Best set this in your App start up in a Static constructor
  4. /// </summary>
  5. public static string Key = "0a1f131c";
  6. /// <summary>
  7. /// Encodes a stream of bytes using DES encryption with a pass key. Lowest level method that
  8. /// handles all work.
  9. /// </summary>
  10. /// <param name="InputString"></param>
  11. /// <param name="EncryptionKey"></param>
  12. /// <returns></returns>
  13. public static byte[] EncryptBytes(byte[] InputString, string EncryptionKey)
  14. {
  15. if (EncryptionKey == null)
  16. EncryptionKey = Key;
  17. TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider();
  18. MD5CryptoServiceProvider hashmd5 = new MD5CryptoServiceProvider();
  19. des.Key = hashmd5.ComputeHash(Encoding.ASCII.GetBytes(EncryptionKey));
  20. des.Mode = CipherMode.ECB;
  21. ICryptoTransform Transform = des.CreateEncryptor();
  22. byte[] Buffer = InputString;
  23. return Transform.TransformFinalBlock(Buffer, 0, Buffer.Length);
  24. }
  25. /// <summary>
  26. /// Encrypts a string into bytes using DES encryption with a Passkey.
  27. /// </summary>
  28. /// <param name="InputString"></param>
  29. /// <param name="EncryptionKey"></param>
  30. /// <returns></returns>
  31. public static byte[] EncryptBytes(string DecryptString, string EncryptionKey)
  32. {
  33. return EncryptBytes(Encoding.ASCII.GetBytes(DecryptString), EncryptionKey);
  34. }
  35. /// <summary>
  36. /// Encrypts a string using Triple DES encryption with a two way encryption key.String is returned as Base64 encoded value
  37. /// rather than binary.
  38. /// </summary>
  39. /// <param name="InputString"></param>
  40. /// <param name="EncryptionKey"></param>
  41. /// <returns></returns>
  42. public static string EncryptString(string InputString, string EncryptionKey)
  43. {
  44. return Convert.ToBase64String(EncryptBytes(Encoding.ASCII.GetBytes(InputString), EncryptionKey));
  45. }

解决配置文件没有写入权限

从 Win10 开始由于对权限管控的加强,当程序默认安装在 C 盘后,正常登录的用户只有 User 权限无法对安装目录下的文本文件进行写入,进而导致配置文件保存失败。

解决的方法是将配置文件保存到 My Documents 或 AppData 等 User 权限也能写入的文件夹中。

如下代码展示了通过 json 文件保存配置,并将该文件存储在 Appdata 路径下:

  1. public class ConfigTool
  2. {
  3. public static RucConfiguration Configs { get; }
  4. static ConfigTool()
  5. {
  6. var folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
  7. "RadarUpperComputer\\");
  8. Directory.CreateDirectory(folderPath);
  9. var filePath = Path.Combine(folderPath, "RucConfig.json");
  10. var provider = new JsonFileConfigurationProvider<RucConfiguration>
  11. {
  12. JsonConfigurationFile = filePath,
  13. EncryptionKey = "ruc",
  14. PropertiesToEncrypt = "LastLoginPassword"
  15. };
  16. Configs = new RucConfiguration();
  17. Configs.Initialize(provider);
  18. }
  19. }

已知问题

对List 支持不好

当我用 JSON 序列化 + List 类型的配置时,遇到了“每次程序内重写生成新配置文件时都产生重复配置”的问题。

例如下面代码中已屏蔽掉的 M3Series 在第一次程序生成配置文件时是正常的。第二次程序生成配置文件时,配置文件中将出现两个重复的 M3Series,第三次出现三个,依次类推,配置文件体积逐渐膨胀。

目前找到的临时解决方案就是用 string[] 替代 List

  1. //public List<string> M3Series { get; set; } =new List<string>{
  2. // "M3100","M3200","M3300"
  3. //};
  4. public string[] M3Series { get; set; } ={
  5. "M3100","M3200","M3300"
  6. };