初心 & 目标
很早就想通过阅读源码向大神学习了,但由于拖延一直未能开始,这次算是莽一波不再挑来挑去直接开读。
选择 Westwind.ApplicationConfiguration 的原因有三:最近在用,好奇某些问题它是如何解决的,代码量小。
预设目标:
- 跨项目引用同一配置是如何实现的? 详见实现跨项目引用同一配置
- XML Provider 为什么用起来总有问题? 之前的用法错了
- Utilities 的工具类有什么亮点? 详见有亮点的代码片段
项目结构
Class Structure
This library consists of the mainAppConfiguration
class plus provider logic. Providers are based on aIConfigurationProvider
interface with aConfigurationProviderBase
class providing base functionality.
解决方案
有亮点的代码片段
加密 & 解密
源码内完整的加密流程是:
- 传入对象及该对象需加密的属性(或字段)名
- 通过反射获取属性(或字段)值
- 将值转换为字符串(仅支持加密能转换为字符串的属性值)
- Triple DES 加密字符串
下面展示 Encryption.cs 中用于 Triple DES 加密的代码片段:
/// <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));
}
/// <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;
var des = new TripleDESCryptoServiceProvider();
var 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>
/// Decrypts a string using DES encryption and a pass key that was used for
/// encryption.
/// <seealso>Class wwEncrypt</seealso>
/// </summary>
/// <param name="decryptString"></param>
/// <param name="encryptionKey"></param>
/// <returns>String</returns>
public static string DecryptString(string decryptString, string encryptionKey)
{
try
{
return Encoding.ASCII.GetString(DecryptBytes(Convert.FromBase64String(decryptString), encryptionKey));
}
catch
{
// Probably not encoded
return string.Empty;
}
}
/// <summary>
/// Decrypts a Byte array from DES with an Encryption Key.
/// </summary>
/// <param name="decryptBuffer"></param>
/// <param name="encryptionKey"></param>
/// <returns></returns>
public static byte[] DecryptBytes(byte[] decryptBuffer, string encryptionKey)
{
if (decryptBuffer == null || decryptBuffer.Length == 0) return null;
if (encryptionKey == null) encryptionKey = Key;
var des = new TripleDESCryptoServiceProvider();
var hashmd5 = new MD5CryptoServiceProvider();
des.Key = hashmd5.ComputeHash(Encoding.ASCII.GetBytes(encryptionKey));
des.Mode = CipherMode.ECB;
// 创建解密器对象
ICryptoTransform transform = des.CreateDecryptor();
return transform.TransformFinalBlock(decryptBuffer, 0, decryptBuffer.Length);
}
通过反射实现的对象复制
源码:
public const BindingFlags MemberAccess =
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase;
/// <summary>
/// Copies the content of one object to another. The target object 'pulls' properties of the first.
/// </summary>
/// <param name="source"></param>
/// <param name="target"></param>
/// <param name="excludedProperties"></param>
public static void CopyObjectData(object source, Object target, string excludedProperties)
{
CopyObjectData(source, target,excludedProperties, MemberAccess);
}
/// <summary>
/// Copies the data of one object to another. The target object 'pulls' properties of the first.
/// This any matching properties are written to the target.
///
/// The object copy is a shallow copy only. Any nested types will be copied as
/// whole values rather than individual property assignments (ie. via assignment)
/// </summary>
/// <param name="source">The source object to copy from</param>
/// <param name="target">The object to copy to</param>
/// <param name="excludedProperties">A comma delimited list of properties that should not be copied</param>
/// <param name="memberAccess">Reflection binding access</param>
public static void CopyObjectData(object source, object target, string excludedProperties, BindingFlags memberAccess)
{
string[] excluded = null;
if (!string.IsNullOrEmpty(excludedProperties))
{
excluded = excludedProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
MemberInfo[] miTarget = target.GetType().GetMembers(memberAccess);
foreach (MemberInfo field in miTarget)
{
string name = field.Name;
// Skip over any property exceptions
if (!string.IsNullOrEmpty(excludedProperties) && excluded.Contains(name))
continue;
if (field.MemberType == MemberTypes.Field)
{
FieldInfo sourceField = source.GetType().GetField(name);
if (sourceField == null) continue;
object sourceValue = sourceField.GetValue(source);
((FieldInfo)field).SetValue(target, sourceValue);
}
else if (field.MemberType == MemberTypes.Property)
{
PropertyInfo piSource = source.GetType().GetProperty(name, memberAccess);
if (piSource == null) continue;
PropertyInfo piTarget = field as PropertyInfo;
if (piTarget != null && piTarget.CanWrite && piSource.CanRead)
{
object sourceValue = piSource.GetValue(source, null);
piTarget.SetValue(target, sourceValue, null);
}
}
}
}
调用时传入无需复制的属性或字段名(通过 ,
分隔):
DataUtils.CopyObjectData(newConfig, config, "Provider,ErrorMessage");
ReflectionUtils.cs 里面还有很多关于反射的应用。
常见类型与字符串间的转换
ConfigurationFileConfigurationProvider.cs 里面的 TypedValueToString
和 StringToTypedValue
方法实现了各种常见类型与 string 间的互相转换(并且考虑了 CultureInfo):
/// <summary>
/// Converts a type to string if possible. This method supports an optional culture generically on any value.
/// It calls the ToString() method on common types and uses a type converter on all other objects
/// if available
/// </summary>
/// <param name="rawValue">The Value or Object to convert to a string</param>
/// <param name="culture">Culture for numeric and DateTime values</param>
/// <param name="unsupportedReturn">Return string for unsupported types</param>
/// <returns>string</returns>
private static string TypedValueToString(object rawValue, CultureInfo culture = null, string unsupportedReturn = null)
{
if (rawValue == null) return string.Empty;
if (culture == null) culture = CultureInfo.CurrentCulture;
Type valueType = rawValue.GetType();
string returnValue;
if (valueType == typeof(string))
{
returnValue = rawValue as string;
}
else if (valueType == typeof(int) || valueType == typeof(decimal) ||
valueType == typeof(double) || valueType == typeof(float) || valueType == typeof(Single))
{
returnValue = string.Format(culture.NumberFormat, "{0}", rawValue);
}
else if (valueType == typeof(DateTime))
{
returnValue = string.Format(culture.DateTimeFormat, "{0}", rawValue);
}
else if (valueType == typeof(bool) || valueType == typeof(Byte) || valueType.IsEnum)
{
returnValue = rawValue.ToString();
}
else if (valueType == typeof(byte[]))
{
returnValue = Convert.ToBase64String(rawValue as byte[]);
}
else if (valueType == typeof(Guid?))
{
if (rawValue == null)
{
returnValue = string.Empty;
}
else
{
return rawValue.ToString();
}
}
else if (rawValue is IList)
{
return "ILIST_TYPE";
}
else
{
// Any type that supports a type converter
TypeConverter converter = TypeDescriptor.GetConverter(valueType);
if (converter != null && converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string)))
{
returnValue = converter.ConvertToString(null, culture, rawValue);
}
else
{
// Last resort - just call ToString() on unknown type
returnValue = !string.IsNullOrEmpty(unsupportedReturn) ? unsupportedReturn : rawValue.ToString();
}
}
return returnValue;
}
/// <summary>
/// Turns a string into a typed value generically.
/// Explicitly assigns common types and falls back
/// on using type converters for unhandled types.
/// <seealso>Class ReflectionUtils</seealso>
/// </summary>
/// <param name="sourceString"> The string to convert from </param>
/// <param name="targetType"> The type to convert to </param>
/// <param name="culture"> Culture used for numeric and datetime values. </param>
/// <returns>object. Throws exception if it cannot be converted.</returns>
private static object StringToTypedValue(string sourceString, Type targetType, CultureInfo culture = null)
{
object result = null;
bool isEmpty = string.IsNullOrEmpty(sourceString);
if (culture == null) culture = CultureInfo.CurrentCulture;
if (targetType == typeof(string))
{
result = sourceString;
}
else if (targetType == typeof(Int32) || targetType == typeof(int))
{
result = isEmpty ? 0 : Int32.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
}
else if (targetType == typeof(Int64))
{
result = isEmpty ? (Int64)0 : Int64.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
}
else if (targetType == typeof(Int16))
{
result = isEmpty ? (Int16)0 : Int16.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
}
else if (targetType == typeof(decimal))
{
result = isEmpty ? 0M : decimal.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
}
else if (targetType == typeof(DateTime))
{
result = isEmpty ? DateTime.MinValue : Convert.ToDateTime(sourceString, culture.DateTimeFormat);
}
else if (targetType == typeof(byte))
{
result = isEmpty ? 0 : Convert.ToByte(sourceString);
}
else if (targetType == typeof(double))
{
result = isEmpty ? 0F : Double.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
}
else if (targetType == typeof(Single))
{
result = isEmpty ? 0F : Single.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
}
else if (targetType == typeof(bool))
{
if (!isEmpty &&
sourceString.ToLower() == "true" || sourceString.ToLower() == "on" || sourceString == "1")
{
result = true;
}
else
{
result = false;
}
}
else if (targetType == typeof(Guid))
{
result = isEmpty ? Guid.Empty : new Guid(sourceString);
}
else if (targetType.IsEnum)
{
result = Enum.Parse(targetType, sourceString);
}
else if (targetType == typeof(byte[]))
{
result = Convert.FromBase64String(sourceString);
}
else if (targetType.Name.StartsWith("Nullable`"))
{
if (sourceString.ToLower() == "null" || sourceString == string.Empty)
{
result = null;
}
else
{
targetType = Nullable.GetUnderlyingType(targetType);
result = StringToTypedValue(sourceString, targetType);
}
}
else
{
// Check for TypeConverters or FromString static method
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
if (converter != null && converter.CanConvertFrom(typeof(string)))
{
result = converter.ConvertFromString(null, culture, sourceString);
}
else
{
// Try to invoke a static FromString method if it exists
try
{
var mi = targetType.GetMethod("FromString");
if (mi != null)
{
return mi.Invoke(null, new object[] { sourceString });
}
}
catch
{
// ignore error and assume not supported
}
Debug.Assert(false, $"Type Conversion not handled in StringToTypedValue for {targetType.Name} {sourceString}");
//throw (new InvalidCastException(Resources.StringToTypedValueValueTypeConversionFailed + targetType.Name));
}
}
return result;
}
实现跨项目引用同一配置
看 Westwind.ApplicationConfiguration 的介绍时就很好奇它是如何实现的多个项目可以引用同一个配置,而且在任意项目中都可以更新配置。
看了源码后才意识到是我想太多了,其实并不复杂,核心就在于 —— 保证不同项目能对同一个配置实体进行读写。
Westwind.ApplicationConfiguration 支持多个类型的配置实体,它们大致分为三类:
- 文件类,其本质就是一个 XML 或 Json 文件
- 数据库类,其本质就是数据库中的一张表
- .NET config 类,根据项目类型不同,其本质就是一个 Web.config 或 App.config 文件
文件类
这是最简单的:
- 使用属性存储配置文件路径,保证读写的都是同一文件
- 读取配置对应 Open FileStream
-
数据库类
使用属性存储数据库连接字符串和表名,保证读写的都是同一张表
- 配置表只有两列,一列存配置名,一列存配置序列化后的字符串
CREATE TABLE [{Tablename}] ( [id] [int] , [ConfigData] [ntext] )
- 读取配置对应
SELECT
-
.NET config 类
首先要有个认知 —— .NET config 文件本质上是一个 XML 文件。
当指定了配置文件路径时底层逻辑和 文件类 相似
- 未指定配置文件路径时:
- 文件路径使用
AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
,保证读写同一文件 - 读取配置对应
XmlDocument.Load
- 写入配置对应
XmlDocument.Save
- 文件路径使用
注:读写过程中还经常使用 ConfigurationManager.RefreshSection 来刷新配置的命名节,以保证配置的时效性。
编码技巧
方法返回值优先选择表征执行状态的值
很多方法既要返回执行结果又要返回执行状态(例如执行是否成功和状态代码等),对于这类方法推荐优先选择表征执行状态的值作为方法的返回值。
例如将对象序列化为字符串的 SerializeObject 方法执行后会有两个返回值,一是序列化是否成功(bool),二是序列化后得到的字符串(string)。在这种情况下有两种选择:
- 方法返回 string,序列化是否成功通过 out 传出
- 方法返回 bool,序列化字符串通过 out 传出
两种选择哪个更好,我们只需看看调用 SerializeObject 的代码便能理解:
// 方案 1
public static string SerializeObjectToString(object instance)
{
var xmlResultString = SerializeObject(instance, out bool serializeSuccess);
if (!serializeSuccess) return null;
return xmlResultString;
}
// 方案 2
public static string SerializeObjectToString(object instance)
{
if (!SerializeObject(instance, out string xmlResultString)) return null;
return xmlResultString;
}
不难看出,对于即返回执行结果又返回执行状态的方法,因为调用方在调用它们时大概率会优先关心执行状态,在确定执行状态后才会去使用具体的执行结果,所以从调用方的角度来看方案 2 的 SerializeObject 用起来更顺手,代码也更简洁。