注:
- 坑有点多,不推荐使用,如果你有好用的 .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"
};