初心 & 目标

很早就想通过阅读源码向大神学习了,但由于拖延一直未能开始,这次算是莽一波不再挑来挑去直接开读。

选择 Westwind.ApplicationConfiguration 的原因有三:最近在用,好奇某些问题它是如何解决的,代码量小。

预设目标:

  • 跨项目引用同一配置是如何实现的? 详见实现项目引用同一配置
  • XML Provider 为什么用起来总有问题? 之前的用法错了
  • Utilities 的工具类有什么亮点? 详见有亮点的代码片段

    项目结构

    Class Structure

    This library consists of the main AppConfiguration class plus provider logic. Providers are based on a IConfigurationProvider interface with a ConfigurationProviderBase class providing base functionality.

Westwind.ApplicationConfiguration - 图1

解决方案

image.png

有亮点的代码片段

加密 & 解密

源码内完整的加密流程是:

  1. 传入对象及该对象需加密的属性(或字段)名
  2. 通过反射获取属性(或字段)值
  3. 将值转换为字符串(仅支持加密能转换为字符串的属性值)
  4. Triple DES 加密字符串

下面展示 Encryption.cs 中用于 Triple DES 加密的代码片段:

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

解密:

  1. /// <summary>
  2. /// Decrypts a string using DES encryption and a pass key that was used for
  3. /// encryption.
  4. /// <seealso>Class wwEncrypt</seealso>
  5. /// </summary>
  6. /// <param name="decryptString"></param>
  7. /// <param name="encryptionKey"></param>
  8. /// <returns>String</returns>
  9. public static string DecryptString(string decryptString, string encryptionKey)
  10. {
  11. try
  12. {
  13. return Encoding.ASCII.GetString(DecryptBytes(Convert.FromBase64String(decryptString), encryptionKey));
  14. }
  15. catch
  16. {
  17. // Probably not encoded
  18. return string.Empty;
  19. }
  20. }
  21. /// <summary>
  22. /// Decrypts a Byte array from DES with an Encryption Key.
  23. /// </summary>
  24. /// <param name="decryptBuffer"></param>
  25. /// <param name="encryptionKey"></param>
  26. /// <returns></returns>
  27. public static byte[] DecryptBytes(byte[] decryptBuffer, string encryptionKey)
  28. {
  29. if (decryptBuffer == null || decryptBuffer.Length == 0) return null;
  30. if (encryptionKey == null) encryptionKey = Key;
  31. var des = new TripleDESCryptoServiceProvider();
  32. var hashmd5 = new MD5CryptoServiceProvider();
  33. des.Key = hashmd5.ComputeHash(Encoding.ASCII.GetBytes(encryptionKey));
  34. des.Mode = CipherMode.ECB;
  35. // 创建解密器对象
  36. ICryptoTransform transform = des.CreateDecryptor();
  37. return transform.TransformFinalBlock(decryptBuffer, 0, decryptBuffer.Length);
  38. }

通过反射实现的对象复制

源码:

  1. public const BindingFlags MemberAccess =
  2. BindingFlags.Public | BindingFlags.NonPublic |
  3. BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase;
  4. /// <summary>
  5. /// Copies the content of one object to another. The target object 'pulls' properties of the first.
  6. /// </summary>
  7. /// <param name="source"></param>
  8. /// <param name="target"></param>
  9. /// <param name="excludedProperties"></param>
  10. public static void CopyObjectData(object source, Object target, string excludedProperties)
  11. {
  12. CopyObjectData(source, target,excludedProperties, MemberAccess);
  13. }
  14. /// <summary>
  15. /// Copies the data of one object to another. The target object 'pulls' properties of the first.
  16. /// This any matching properties are written to the target.
  17. ///
  18. /// The object copy is a shallow copy only. Any nested types will be copied as
  19. /// whole values rather than individual property assignments (ie. via assignment)
  20. /// </summary>
  21. /// <param name="source">The source object to copy from</param>
  22. /// <param name="target">The object to copy to</param>
  23. /// <param name="excludedProperties">A comma delimited list of properties that should not be copied</param>
  24. /// <param name="memberAccess">Reflection binding access</param>
  25. public static void CopyObjectData(object source, object target, string excludedProperties, BindingFlags memberAccess)
  26. {
  27. string[] excluded = null;
  28. if (!string.IsNullOrEmpty(excludedProperties))
  29. {
  30. excluded = excludedProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
  31. }
  32. MemberInfo[] miTarget = target.GetType().GetMembers(memberAccess);
  33. foreach (MemberInfo field in miTarget)
  34. {
  35. string name = field.Name;
  36. // Skip over any property exceptions
  37. if (!string.IsNullOrEmpty(excludedProperties) && excluded.Contains(name))
  38. continue;
  39. if (field.MemberType == MemberTypes.Field)
  40. {
  41. FieldInfo sourceField = source.GetType().GetField(name);
  42. if (sourceField == null) continue;
  43. object sourceValue = sourceField.GetValue(source);
  44. ((FieldInfo)field).SetValue(target, sourceValue);
  45. }
  46. else if (field.MemberType == MemberTypes.Property)
  47. {
  48. PropertyInfo piSource = source.GetType().GetProperty(name, memberAccess);
  49. if (piSource == null) continue;
  50. PropertyInfo piTarget = field as PropertyInfo;
  51. if (piTarget != null && piTarget.CanWrite && piSource.CanRead)
  52. {
  53. object sourceValue = piSource.GetValue(source, null);
  54. piTarget.SetValue(target, sourceValue, null);
  55. }
  56. }
  57. }
  58. }

调用时传入无需复制的属性或字段名(通过 , 分隔):

  1. DataUtils.CopyObjectData(newConfig, config, "Provider,ErrorMessage");

ReflectionUtils.cs 里面还有很多关于反射的应用。

常见类型与字符串间的转换

ConfigurationFileConfigurationProvider.cs 里面的 TypedValueToStringStringToTypedValue 方法实现了各种常见类型与 string 间的互相转换(并且考虑了 CultureInfo):

  1. /// <summary>
  2. /// Converts a type to string if possible. This method supports an optional culture generically on any value.
  3. /// It calls the ToString() method on common types and uses a type converter on all other objects
  4. /// if available
  5. /// </summary>
  6. /// <param name="rawValue">The Value or Object to convert to a string</param>
  7. /// <param name="culture">Culture for numeric and DateTime values</param>
  8. /// <param name="unsupportedReturn">Return string for unsupported types</param>
  9. /// <returns>string</returns>
  10. private static string TypedValueToString(object rawValue, CultureInfo culture = null, string unsupportedReturn = null)
  11. {
  12. if (rawValue == null) return string.Empty;
  13. if (culture == null) culture = CultureInfo.CurrentCulture;
  14. Type valueType = rawValue.GetType();
  15. string returnValue;
  16. if (valueType == typeof(string))
  17. {
  18. returnValue = rawValue as string;
  19. }
  20. else if (valueType == typeof(int) || valueType == typeof(decimal) ||
  21. valueType == typeof(double) || valueType == typeof(float) || valueType == typeof(Single))
  22. {
  23. returnValue = string.Format(culture.NumberFormat, "{0}", rawValue);
  24. }
  25. else if (valueType == typeof(DateTime))
  26. {
  27. returnValue = string.Format(culture.DateTimeFormat, "{0}", rawValue);
  28. }
  29. else if (valueType == typeof(bool) || valueType == typeof(Byte) || valueType.IsEnum)
  30. {
  31. returnValue = rawValue.ToString();
  32. }
  33. else if (valueType == typeof(byte[]))
  34. {
  35. returnValue = Convert.ToBase64String(rawValue as byte[]);
  36. }
  37. else if (valueType == typeof(Guid?))
  38. {
  39. if (rawValue == null)
  40. {
  41. returnValue = string.Empty;
  42. }
  43. else
  44. {
  45. return rawValue.ToString();
  46. }
  47. }
  48. else if (rawValue is IList)
  49. {
  50. return "ILIST_TYPE";
  51. }
  52. else
  53. {
  54. // Any type that supports a type converter
  55. TypeConverter converter = TypeDescriptor.GetConverter(valueType);
  56. if (converter != null && converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string)))
  57. {
  58. returnValue = converter.ConvertToString(null, culture, rawValue);
  59. }
  60. else
  61. {
  62. // Last resort - just call ToString() on unknown type
  63. returnValue = !string.IsNullOrEmpty(unsupportedReturn) ? unsupportedReturn : rawValue.ToString();
  64. }
  65. }
  66. return returnValue;
  67. }
  68. /// <summary>
  69. /// Turns a string into a typed value generically.
  70. /// Explicitly assigns common types and falls back
  71. /// on using type converters for unhandled types.
  72. /// <seealso>Class ReflectionUtils</seealso>
  73. /// </summary>
  74. /// <param name="sourceString"> The string to convert from </param>
  75. /// <param name="targetType"> The type to convert to </param>
  76. /// <param name="culture"> Culture used for numeric and datetime values. </param>
  77. /// <returns>object. Throws exception if it cannot be converted.</returns>
  78. private static object StringToTypedValue(string sourceString, Type targetType, CultureInfo culture = null)
  79. {
  80. object result = null;
  81. bool isEmpty = string.IsNullOrEmpty(sourceString);
  82. if (culture == null) culture = CultureInfo.CurrentCulture;
  83. if (targetType == typeof(string))
  84. {
  85. result = sourceString;
  86. }
  87. else if (targetType == typeof(Int32) || targetType == typeof(int))
  88. {
  89. result = isEmpty ? 0 : Int32.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
  90. }
  91. else if (targetType == typeof(Int64))
  92. {
  93. result = isEmpty ? (Int64)0 : Int64.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
  94. }
  95. else if (targetType == typeof(Int16))
  96. {
  97. result = isEmpty ? (Int16)0 : Int16.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
  98. }
  99. else if (targetType == typeof(decimal))
  100. {
  101. result = isEmpty ? 0M : decimal.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
  102. }
  103. else if (targetType == typeof(DateTime))
  104. {
  105. result = isEmpty ? DateTime.MinValue : Convert.ToDateTime(sourceString, culture.DateTimeFormat);
  106. }
  107. else if (targetType == typeof(byte))
  108. {
  109. result = isEmpty ? 0 : Convert.ToByte(sourceString);
  110. }
  111. else if (targetType == typeof(double))
  112. {
  113. result = isEmpty ? 0F : Double.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
  114. }
  115. else if (targetType == typeof(Single))
  116. {
  117. result = isEmpty ? 0F : Single.Parse(sourceString, NumberStyles.Any, culture.NumberFormat);
  118. }
  119. else if (targetType == typeof(bool))
  120. {
  121. if (!isEmpty &&
  122. sourceString.ToLower() == "true" || sourceString.ToLower() == "on" || sourceString == "1")
  123. {
  124. result = true;
  125. }
  126. else
  127. {
  128. result = false;
  129. }
  130. }
  131. else if (targetType == typeof(Guid))
  132. {
  133. result = isEmpty ? Guid.Empty : new Guid(sourceString);
  134. }
  135. else if (targetType.IsEnum)
  136. {
  137. result = Enum.Parse(targetType, sourceString);
  138. }
  139. else if (targetType == typeof(byte[]))
  140. {
  141. result = Convert.FromBase64String(sourceString);
  142. }
  143. else if (targetType.Name.StartsWith("Nullable`"))
  144. {
  145. if (sourceString.ToLower() == "null" || sourceString == string.Empty)
  146. {
  147. result = null;
  148. }
  149. else
  150. {
  151. targetType = Nullable.GetUnderlyingType(targetType);
  152. result = StringToTypedValue(sourceString, targetType);
  153. }
  154. }
  155. else
  156. {
  157. // Check for TypeConverters or FromString static method
  158. TypeConverter converter = TypeDescriptor.GetConverter(targetType);
  159. if (converter != null && converter.CanConvertFrom(typeof(string)))
  160. {
  161. result = converter.ConvertFromString(null, culture, sourceString);
  162. }
  163. else
  164. {
  165. // Try to invoke a static FromString method if it exists
  166. try
  167. {
  168. var mi = targetType.GetMethod("FromString");
  169. if (mi != null)
  170. {
  171. return mi.Invoke(null, new object[] { sourceString });
  172. }
  173. }
  174. catch
  175. {
  176. // ignore error and assume not supported
  177. }
  178. Debug.Assert(false, $"Type Conversion not handled in StringToTypedValue for {targetType.Name} {sourceString}");
  179. //throw (new InvalidCastException(Resources.StringToTypedValueValueTypeConversionFailed + targetType.Name));
  180. }
  181. }
  182. return result;
  183. }

实现跨项目引用同一配置

看 Westwind.ApplicationConfiguration 的介绍时就很好奇它是如何实现的多个项目可以引用同一个配置,而且在任意项目中都可以更新配置。

看了源码后才意识到是我想太多了,其实并不复杂,核心就在于 —— 保证不同项目能对同一个配置实体进行读写。

Westwind.ApplicationConfiguration 支持多个类型的配置实体,它们大致分为三类:

  1. 文件类,其本质就是一个 XML 或 Json 文件
  2. 数据库类,其本质就是数据库中的一张表
  3. .NET config 类,根据项目类型不同,其本质就是一个 Web.config 或 App.config 文件

    文件类

    这是最简单的:
  • 使用属性存储配置文件路径,保证读写的都是同一文件
  • 读取配置对应 Open FileStream
  • 写入配置对应 Write FileStream

    数据库类

  • 使用属性存储数据库连接字符串和表名,保证读写的都是同一张表

    • 配置表只有两列,一列存配置名,一列存配置序列化后的字符串
    • CREATE TABLE [{Tablename}] ( [id] [int] , [ConfigData] [ntext] )
  • 读取配置对应 SELECT
  • 写入配置对应 UPDATE

    .NET config 类

    首先要有个认知 —— .NET config 文件本质上是一个 XML 文件。

  • 当指定了配置文件路径时底层逻辑和 文件类 相似

  • 未指定配置文件路径时:
    • 文件路径使用 AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,保证读写同一文件
    • 读取配置对应 XmlDocument.Load
    • 写入配置对应 XmlDocument.Save

注:读写过程中还经常使用 ConfigurationManager.RefreshSection 来刷新配置的命名节,以保证配置的时效性。

编码技巧

方法返回值优先选择表征执行状态的值

很多方法既要返回执行结果又要返回执行状态(例如执行是否成功和状态代码等),对于这类方法推荐优先选择表征执行状态的值作为方法的返回值。

例如将对象序列化为字符串的 SerializeObject 方法执行后会有两个返回值,一是序列化是否成功(bool),二是序列化后得到的字符串(string)。在这种情况下有两种选择:

  1. 方法返回 string,序列化是否成功通过 out 传出
  2. 方法返回 bool,序列化字符串通过 out 传出

两种选择哪个更好,我们只需看看调用 SerializeObject 的代码便能理解:

  1. // 方案 1
  2. public static string SerializeObjectToString(object instance)
  3. {
  4. var xmlResultString = SerializeObject(instance, out bool serializeSuccess);
  5. if (!serializeSuccess) return null;
  6. return xmlResultString;
  7. }
  8. // 方案 2
  9. public static string SerializeObjectToString(object instance)
  10. {
  11. if (!SerializeObject(instance, out string xmlResultString)) return null;
  12. return xmlResultString;
  13. }

不难看出,对于即返回执行结果又返回执行状态的方法,因为调用方在调用它们时大概率会优先关心执行状态,在确定执行状态后才会去使用具体的执行结果,所以从调用方的角度来看方案 2 的 SerializeObject 用起来更顺手,代码也更简洁。