纽威
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

450 lines
19 KiB

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.Specialized;
  5. using System.ComponentModel.DataAnnotations;
  6. using System.Globalization;
  7. using System.Reflection;
  8. using System.Runtime.Serialization;
  9. using System.Web.Http;
  10. using System.Web.Http.Description;
  11. using System.Xml.Serialization;
  12. using Newtonsoft.Json;
  13. namespace ICSSoft.WebAPI.Areas.HelpPage.ModelDescriptions
  14. {
  15. /// <summary>
  16. /// Generates model descriptions for given types.
  17. /// </summary>
  18. public class ModelDescriptionGenerator
  19. {
  20. // Modify this to support more data annotation attributes.
  21. private readonly IDictionary<Type, Func<object, string>> AnnotationTextGenerator = new Dictionary<Type, Func<object, string>>
  22. {
  23. { typeof(RequiredAttribute), a => "Required" },
  24. { typeof(RangeAttribute), a =>
  25. {
  26. RangeAttribute range = (RangeAttribute)a;
  27. return String.Format(CultureInfo.CurrentCulture, "Range: inclusive between {0} and {1}", range.Minimum, range.Maximum);
  28. }
  29. },
  30. { typeof(MaxLengthAttribute), a =>
  31. {
  32. MaxLengthAttribute maxLength = (MaxLengthAttribute)a;
  33. return String.Format(CultureInfo.CurrentCulture, "Max length: {0}", maxLength.Length);
  34. }
  35. },
  36. { typeof(MinLengthAttribute), a =>
  37. {
  38. MinLengthAttribute minLength = (MinLengthAttribute)a;
  39. return String.Format(CultureInfo.CurrentCulture, "Min length: {0}", minLength.Length);
  40. }
  41. },
  42. { typeof(StringLengthAttribute), a =>
  43. {
  44. StringLengthAttribute strLength = (StringLengthAttribute)a;
  45. return String.Format(CultureInfo.CurrentCulture, "String length: inclusive between {0} and {1}", strLength.MinimumLength, strLength.MaximumLength);
  46. }
  47. },
  48. { typeof(DataTypeAttribute), a =>
  49. {
  50. DataTypeAttribute dataType = (DataTypeAttribute)a;
  51. return String.Format(CultureInfo.CurrentCulture, "Data type: {0}", dataType.CustomDataType ?? dataType.DataType.ToString());
  52. }
  53. },
  54. { typeof(RegularExpressionAttribute), a =>
  55. {
  56. RegularExpressionAttribute regularExpression = (RegularExpressionAttribute)a;
  57. return String.Format(CultureInfo.CurrentCulture, "Matching regular expression pattern: {0}", regularExpression.Pattern);
  58. }
  59. },
  60. };
  61. // Modify this to add more default documentations.
  62. private readonly IDictionary<Type, string> DefaultTypeDocumentation = new Dictionary<Type, string>
  63. {
  64. { typeof(Int16), "integer" },
  65. { typeof(Int32), "integer" },
  66. { typeof(Int64), "integer" },
  67. { typeof(UInt16), "unsigned integer" },
  68. { typeof(UInt32), "unsigned integer" },
  69. { typeof(UInt64), "unsigned integer" },
  70. { typeof(Byte), "byte" },
  71. { typeof(Char), "character" },
  72. { typeof(SByte), "signed byte" },
  73. { typeof(Uri), "URI" },
  74. { typeof(Single), "decimal number" },
  75. { typeof(Double), "decimal number" },
  76. { typeof(Decimal), "decimal number" },
  77. { typeof(String), "string" },
  78. { typeof(Guid), "globally unique identifier" },
  79. { typeof(TimeSpan), "time interval" },
  80. { typeof(DateTime), "date" },
  81. { typeof(DateTimeOffset), "date" },
  82. { typeof(Boolean), "boolean" },
  83. };
  84. private Lazy<IModelDocumentationProvider> _documentationProvider;
  85. public ModelDescriptionGenerator(HttpConfiguration config)
  86. {
  87. if (config == null)
  88. {
  89. throw new ArgumentNullException("config");
  90. }
  91. _documentationProvider = new Lazy<IModelDocumentationProvider>(() => config.Services.GetDocumentationProvider() as IModelDocumentationProvider);
  92. GeneratedModels = new Dictionary<string, ModelDescription>(StringComparer.OrdinalIgnoreCase);
  93. }
  94. public Dictionary<string, ModelDescription> GeneratedModels { get; private set; }
  95. private IModelDocumentationProvider DocumentationProvider
  96. {
  97. get
  98. {
  99. return _documentationProvider.Value;
  100. }
  101. }
  102. public ModelDescription GetOrCreateModelDescription(Type modelType)
  103. {
  104. if (modelType == null)
  105. {
  106. throw new ArgumentNullException("modelType");
  107. }
  108. Type underlyingType = Nullable.GetUnderlyingType(modelType);
  109. if (underlyingType != null)
  110. {
  111. modelType = underlyingType;
  112. }
  113. ModelDescription modelDescription;
  114. string modelName = ModelNameHelper.GetModelName(modelType);
  115. if (GeneratedModels.TryGetValue(modelName, out modelDescription))
  116. {
  117. if (modelType != modelDescription.ModelType)
  118. {
  119. throw new InvalidOperationException(
  120. String.Format(
  121. CultureInfo.CurrentCulture,
  122. "A model description could not be created. Duplicate model name '{0}' was found for types '{1}' and '{2}'. " +
  123. "Use the [ModelName] attribute to change the model name for at least one of the types so that it has a unique name.",
  124. modelName,
  125. modelDescription.ModelType.FullName,
  126. modelType.FullName));
  127. }
  128. return modelDescription;
  129. }
  130. if (DefaultTypeDocumentation.ContainsKey(modelType))
  131. {
  132. return GenerateSimpleTypeModelDescription(modelType);
  133. }
  134. if (modelType.IsEnum)
  135. {
  136. return GenerateEnumTypeModelDescription(modelType);
  137. }
  138. if (modelType.IsGenericType)
  139. {
  140. Type[] genericArguments = modelType.GetGenericArguments();
  141. if (genericArguments.Length == 1)
  142. {
  143. Type enumerableType = typeof(IEnumerable<>).MakeGenericType(genericArguments);
  144. if (enumerableType.IsAssignableFrom(modelType))
  145. {
  146. return GenerateCollectionModelDescription(modelType, genericArguments[0]);
  147. }
  148. }
  149. if (genericArguments.Length == 2)
  150. {
  151. Type dictionaryType = typeof(IDictionary<,>).MakeGenericType(genericArguments);
  152. if (dictionaryType.IsAssignableFrom(modelType))
  153. {
  154. return GenerateDictionaryModelDescription(modelType, genericArguments[0], genericArguments[1]);
  155. }
  156. Type keyValuePairType = typeof(KeyValuePair<,>).MakeGenericType(genericArguments);
  157. if (keyValuePairType.IsAssignableFrom(modelType))
  158. {
  159. return GenerateKeyValuePairModelDescription(modelType, genericArguments[0], genericArguments[1]);
  160. }
  161. }
  162. }
  163. if (modelType.IsArray)
  164. {
  165. Type elementType = modelType.GetElementType();
  166. return GenerateCollectionModelDescription(modelType, elementType);
  167. }
  168. if (modelType == typeof(NameValueCollection))
  169. {
  170. return GenerateDictionaryModelDescription(modelType, typeof(string), typeof(string));
  171. }
  172. if (typeof(IDictionary).IsAssignableFrom(modelType))
  173. {
  174. return GenerateDictionaryModelDescription(modelType, typeof(object), typeof(object));
  175. }
  176. if (typeof(IEnumerable).IsAssignableFrom(modelType))
  177. {
  178. return GenerateCollectionModelDescription(modelType, typeof(object));
  179. }
  180. return GenerateComplexTypeModelDescription(modelType);
  181. }
  182. // Change this to provide different name for the member.
  183. private static string GetMemberName(MemberInfo member, bool hasDataContractAttribute)
  184. {
  185. JsonPropertyAttribute jsonProperty = member.GetCustomAttribute<JsonPropertyAttribute>();
  186. if (jsonProperty != null && !String.IsNullOrEmpty(jsonProperty.PropertyName))
  187. {
  188. return jsonProperty.PropertyName;
  189. }
  190. if (hasDataContractAttribute)
  191. {
  192. DataMemberAttribute dataMember = member.GetCustomAttribute<DataMemberAttribute>();
  193. if (dataMember != null && !String.IsNullOrEmpty(dataMember.Name))
  194. {
  195. return dataMember.Name;
  196. }
  197. }
  198. return member.Name;
  199. }
  200. private static bool ShouldDisplayMember(MemberInfo member, bool hasDataContractAttribute)
  201. {
  202. JsonIgnoreAttribute jsonIgnore = member.GetCustomAttribute<JsonIgnoreAttribute>();
  203. XmlIgnoreAttribute xmlIgnore = member.GetCustomAttribute<XmlIgnoreAttribute>();
  204. IgnoreDataMemberAttribute ignoreDataMember = member.GetCustomAttribute<IgnoreDataMemberAttribute>();
  205. NonSerializedAttribute nonSerialized = member.GetCustomAttribute<NonSerializedAttribute>();
  206. ApiExplorerSettingsAttribute apiExplorerSetting = member.GetCustomAttribute<ApiExplorerSettingsAttribute>();
  207. bool hasMemberAttribute = member.DeclaringType.IsEnum ?
  208. member.GetCustomAttribute<EnumMemberAttribute>() != null :
  209. member.GetCustomAttribute<DataMemberAttribute>() != null;
  210. // Display member only if all the followings are true:
  211. // no JsonIgnoreAttribute
  212. // no XmlIgnoreAttribute
  213. // no IgnoreDataMemberAttribute
  214. // no NonSerializedAttribute
  215. // no ApiExplorerSettingsAttribute with IgnoreApi set to true
  216. // no DataContractAttribute without DataMemberAttribute or EnumMemberAttribute
  217. return jsonIgnore == null &&
  218. xmlIgnore == null &&
  219. ignoreDataMember == null &&
  220. nonSerialized == null &&
  221. (apiExplorerSetting == null || !apiExplorerSetting.IgnoreApi) &&
  222. (!hasDataContractAttribute || hasMemberAttribute);
  223. }
  224. private string CreateDefaultDocumentation(Type type)
  225. {
  226. string documentation;
  227. if (DefaultTypeDocumentation.TryGetValue(type, out documentation))
  228. {
  229. return documentation;
  230. }
  231. if (DocumentationProvider != null)
  232. {
  233. documentation = DocumentationProvider.GetDocumentation(type);
  234. }
  235. return documentation;
  236. }
  237. private void GenerateAnnotations(MemberInfo property, ParameterDescription propertyModel)
  238. {
  239. List<ParameterAnnotation> annotations = new List<ParameterAnnotation>();
  240. IEnumerable<Attribute> attributes = property.GetCustomAttributes();
  241. foreach (Attribute attribute in attributes)
  242. {
  243. Func<object, string> textGenerator;
  244. if (AnnotationTextGenerator.TryGetValue(attribute.GetType(), out textGenerator))
  245. {
  246. annotations.Add(
  247. new ParameterAnnotation
  248. {
  249. AnnotationAttribute = attribute,
  250. Documentation = textGenerator(attribute)
  251. });
  252. }
  253. }
  254. // Rearrange the annotations
  255. annotations.Sort((x, y) =>
  256. {
  257. // Special-case RequiredAttribute so that it shows up on top
  258. if (x.AnnotationAttribute is RequiredAttribute)
  259. {
  260. return -1;
  261. }
  262. if (y.AnnotationAttribute is RequiredAttribute)
  263. {
  264. return 1;
  265. }
  266. // Sort the rest based on alphabetic order of the documentation
  267. return String.Compare(x.Documentation, y.Documentation, StringComparison.OrdinalIgnoreCase);
  268. });
  269. foreach (ParameterAnnotation annotation in annotations)
  270. {
  271. propertyModel.Annotations.Add(annotation);
  272. }
  273. }
  274. private CollectionModelDescription GenerateCollectionModelDescription(Type modelType, Type elementType)
  275. {
  276. ModelDescription collectionModelDescription = GetOrCreateModelDescription(elementType);
  277. if (collectionModelDescription != null)
  278. {
  279. return new CollectionModelDescription
  280. {
  281. Name = ModelNameHelper.GetModelName(modelType),
  282. ModelType = modelType,
  283. ElementDescription = collectionModelDescription
  284. };
  285. }
  286. return null;
  287. }
  288. private ModelDescription GenerateComplexTypeModelDescription(Type modelType)
  289. {
  290. ComplexTypeModelDescription complexModelDescription = new ComplexTypeModelDescription
  291. {
  292. Name = ModelNameHelper.GetModelName(modelType),
  293. ModelType = modelType,
  294. Documentation = CreateDefaultDocumentation(modelType)
  295. };
  296. GeneratedModels.Add(complexModelDescription.Name, complexModelDescription);
  297. bool hasDataContractAttribute = modelType.GetCustomAttribute<DataContractAttribute>() != null;
  298. PropertyInfo[] properties = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
  299. foreach (PropertyInfo property in properties)
  300. {
  301. if (ShouldDisplayMember(property, hasDataContractAttribute))
  302. {
  303. ParameterDescription propertyModel = new ParameterDescription
  304. {
  305. Name = GetMemberName(property, hasDataContractAttribute)
  306. };
  307. if (DocumentationProvider != null)
  308. {
  309. propertyModel.Documentation = DocumentationProvider.GetDocumentation(property);
  310. }
  311. GenerateAnnotations(property, propertyModel);
  312. complexModelDescription.Properties.Add(propertyModel);
  313. propertyModel.TypeDescription = GetOrCreateModelDescription(property.PropertyType);
  314. }
  315. }
  316. FieldInfo[] fields = modelType.GetFields(BindingFlags.Public | BindingFlags.Instance);
  317. foreach (FieldInfo field in fields)
  318. {
  319. if (ShouldDisplayMember(field, hasDataContractAttribute))
  320. {
  321. ParameterDescription propertyModel = new ParameterDescription
  322. {
  323. Name = GetMemberName(field, hasDataContractAttribute)
  324. };
  325. if (DocumentationProvider != null)
  326. {
  327. propertyModel.Documentation = DocumentationProvider.GetDocumentation(field);
  328. }
  329. complexModelDescription.Properties.Add(propertyModel);
  330. propertyModel.TypeDescription = GetOrCreateModelDescription(field.FieldType);
  331. }
  332. }
  333. return complexModelDescription;
  334. }
  335. private DictionaryModelDescription GenerateDictionaryModelDescription(Type modelType, Type keyType, Type valueType)
  336. {
  337. ModelDescription keyModelDescription = GetOrCreateModelDescription(keyType);
  338. ModelDescription valueModelDescription = GetOrCreateModelDescription(valueType);
  339. return new DictionaryModelDescription
  340. {
  341. Name = ModelNameHelper.GetModelName(modelType),
  342. ModelType = modelType,
  343. KeyModelDescription = keyModelDescription,
  344. ValueModelDescription = valueModelDescription
  345. };
  346. }
  347. private EnumTypeModelDescription GenerateEnumTypeModelDescription(Type modelType)
  348. {
  349. EnumTypeModelDescription enumDescription = new EnumTypeModelDescription
  350. {
  351. Name = ModelNameHelper.GetModelName(modelType),
  352. ModelType = modelType,
  353. Documentation = CreateDefaultDocumentation(modelType)
  354. };
  355. bool hasDataContractAttribute = modelType.GetCustomAttribute<DataContractAttribute>() != null;
  356. foreach (FieldInfo field in modelType.GetFields(BindingFlags.Public | BindingFlags.Static))
  357. {
  358. if (ShouldDisplayMember(field, hasDataContractAttribute))
  359. {
  360. EnumValueDescription enumValue = new EnumValueDescription
  361. {
  362. Name = field.Name,
  363. Value = field.GetRawConstantValue().ToString()
  364. };
  365. if (DocumentationProvider != null)
  366. {
  367. enumValue.Documentation = DocumentationProvider.GetDocumentation(field);
  368. }
  369. enumDescription.Values.Add(enumValue);
  370. }
  371. }
  372. GeneratedModels.Add(enumDescription.Name, enumDescription);
  373. return enumDescription;
  374. }
  375. private KeyValuePairModelDescription GenerateKeyValuePairModelDescription(Type modelType, Type keyType, Type valueType)
  376. {
  377. ModelDescription keyModelDescription = GetOrCreateModelDescription(keyType);
  378. ModelDescription valueModelDescription = GetOrCreateModelDescription(valueType);
  379. return new KeyValuePairModelDescription
  380. {
  381. Name = ModelNameHelper.GetModelName(modelType),
  382. ModelType = modelType,
  383. KeyModelDescription = keyModelDescription,
  384. ValueModelDescription = valueModelDescription
  385. };
  386. }
  387. private ModelDescription GenerateSimpleTypeModelDescription(Type modelType)
  388. {
  389. SimpleTypeModelDescription simpleModelDescription = new SimpleTypeModelDescription
  390. {
  391. Name = ModelNameHelper.GetModelName(modelType),
  392. ModelType = modelType,
  393. Documentation = CreateDefaultDocumentation(modelType)
  394. };
  395. GeneratedModels.Add(simpleModelDescription.Name, simpleModelDescription);
  396. return simpleModelDescription;
  397. }
  398. }
  399. }