451 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
		
		
			
		
	
	
			451 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
|  | using System; | ||
|  | using System.Collections; | ||
|  | using System.Collections.Generic; | ||
|  | using System.Collections.Specialized; | ||
|  | using System.ComponentModel.DataAnnotations; | ||
|  | using System.Globalization; | ||
|  | using System.Reflection; | ||
|  | using System.Runtime.Serialization; | ||
|  | using System.Web.Http; | ||
|  | using System.Web.Http.Description; | ||
|  | using System.Xml.Serialization; | ||
|  | using Newtonsoft.Json; | ||
|  | 
 | ||
|  | namespace WebAPI.Areas.HelpPage.ModelDescriptions | ||
|  | { | ||
|  |     /// <summary> | ||
|  |     /// Generates model descriptions for given types. | ||
|  |     /// </summary> | ||
|  |     public class ModelDescriptionGenerator | ||
|  |     { | ||
|  |         // Modify this to support more data annotation attributes. | ||
|  |         private readonly IDictionary<Type, Func<object, string>> AnnotationTextGenerator = new Dictionary<Type, Func<object, string>> | ||
|  |         { | ||
|  |             { typeof(RequiredAttribute), a => "Required" }, | ||
|  |             { typeof(RangeAttribute), a => | ||
|  |                 { | ||
|  |                     RangeAttribute range = (RangeAttribute)a; | ||
|  |                     return String.Format(CultureInfo.CurrentCulture, "Range: inclusive between {0} and {1}", range.Minimum, range.Maximum); | ||
|  |                 } | ||
|  |             }, | ||
|  |             { typeof(MaxLengthAttribute), a => | ||
|  |                 { | ||
|  |                     MaxLengthAttribute maxLength = (MaxLengthAttribute)a; | ||
|  |                     return String.Format(CultureInfo.CurrentCulture, "Max length: {0}", maxLength.Length); | ||
|  |                 } | ||
|  |             }, | ||
|  |             { typeof(MinLengthAttribute), a => | ||
|  |                 { | ||
|  |                     MinLengthAttribute minLength = (MinLengthAttribute)a; | ||
|  |                     return String.Format(CultureInfo.CurrentCulture, "Min length: {0}", minLength.Length); | ||
|  |                 } | ||
|  |             }, | ||
|  |             { typeof(StringLengthAttribute), a => | ||
|  |                 { | ||
|  |                     StringLengthAttribute strLength = (StringLengthAttribute)a; | ||
|  |                     return String.Format(CultureInfo.CurrentCulture, "String length: inclusive between {0} and {1}", strLength.MinimumLength, strLength.MaximumLength); | ||
|  |                 } | ||
|  |             }, | ||
|  |             { typeof(DataTypeAttribute), a => | ||
|  |                 { | ||
|  |                     DataTypeAttribute dataType = (DataTypeAttribute)a; | ||
|  |                     return String.Format(CultureInfo.CurrentCulture, "Data type: {0}", dataType.CustomDataType ?? dataType.DataType.ToString()); | ||
|  |                 } | ||
|  |             }, | ||
|  |             { typeof(RegularExpressionAttribute), a => | ||
|  |                 { | ||
|  |                     RegularExpressionAttribute regularExpression = (RegularExpressionAttribute)a; | ||
|  |                     return String.Format(CultureInfo.CurrentCulture, "Matching regular expression pattern: {0}", regularExpression.Pattern); | ||
|  |                 } | ||
|  |             }, | ||
|  |         }; | ||
|  | 
 | ||
|  |         // Modify this to add more default documentations. | ||
|  |         private readonly IDictionary<Type, string> DefaultTypeDocumentation = new Dictionary<Type, string> | ||
|  |         { | ||
|  |             { typeof(Int16), "integer" }, | ||
|  |             { typeof(Int32), "integer" }, | ||
|  |             { typeof(Int64), "integer" }, | ||
|  |             { typeof(UInt16), "unsigned integer" }, | ||
|  |             { typeof(UInt32), "unsigned integer" }, | ||
|  |             { typeof(UInt64), "unsigned integer" }, | ||
|  |             { typeof(Byte), "byte" }, | ||
|  |             { typeof(Char), "character" }, | ||
|  |             { typeof(SByte), "signed byte" }, | ||
|  |             { typeof(Uri), "URI" }, | ||
|  |             { typeof(Single), "decimal number" }, | ||
|  |             { typeof(Double), "decimal number" }, | ||
|  |             { typeof(Decimal), "decimal number" }, | ||
|  |             { typeof(String), "string" }, | ||
|  |             { typeof(Guid), "globally unique identifier" }, | ||
|  |             { typeof(TimeSpan), "time interval" }, | ||
|  |             { typeof(DateTime), "date" }, | ||
|  |             { typeof(DateTimeOffset), "date" }, | ||
|  |             { typeof(Boolean), "boolean" }, | ||
|  |         }; | ||
|  | 
 | ||
|  |         private Lazy<IModelDocumentationProvider> _documentationProvider; | ||
|  | 
 | ||
|  |         public ModelDescriptionGenerator(HttpConfiguration config) | ||
|  |         { | ||
|  |             if (config == null) | ||
|  |             { | ||
|  |                 throw new ArgumentNullException("config"); | ||
|  |             } | ||
|  | 
 | ||
|  |             _documentationProvider = new Lazy<IModelDocumentationProvider>(() => config.Services.GetDocumentationProvider() as IModelDocumentationProvider); | ||
|  |             GeneratedModels = new Dictionary<string, ModelDescription>(StringComparer.OrdinalIgnoreCase); | ||
|  |         } | ||
|  | 
 | ||
|  |         public Dictionary<string, ModelDescription> GeneratedModels { get; private set; } | ||
|  | 
 | ||
|  |         private IModelDocumentationProvider DocumentationProvider | ||
|  |         { | ||
|  |             get | ||
|  |             { | ||
|  |                 return _documentationProvider.Value; | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         public ModelDescription GetOrCreateModelDescription(Type modelType) | ||
|  |         { | ||
|  |             if (modelType == null) | ||
|  |             { | ||
|  |                 throw new ArgumentNullException("modelType"); | ||
|  |             } | ||
|  | 
 | ||
|  |             Type underlyingType = Nullable.GetUnderlyingType(modelType); | ||
|  |             if (underlyingType != null) | ||
|  |             { | ||
|  |                 modelType = underlyingType; | ||
|  |             } | ||
|  | 
 | ||
|  |             ModelDescription modelDescription; | ||
|  |             string modelName = ModelNameHelper.GetModelName(modelType); | ||
|  |             if (GeneratedModels.TryGetValue(modelName, out modelDescription)) | ||
|  |             { | ||
|  |                 if (modelType != modelDescription.ModelType) | ||
|  |                 { | ||
|  |                     throw new InvalidOperationException( | ||
|  |                         String.Format( | ||
|  |                             CultureInfo.CurrentCulture, | ||
|  |                             "A model description could not be created. Duplicate model name '{0}' was found for types '{1}' and '{2}'. " + | ||
|  |                             "Use the [ModelName] attribute to change the model name for at least one of the types so that it has a unique name.", | ||
|  |                             modelName, | ||
|  |                             modelDescription.ModelType.FullName, | ||
|  |                             modelType.FullName)); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 return modelDescription; | ||
|  |             } | ||
|  | 
 | ||
|  |             if (DefaultTypeDocumentation.ContainsKey(modelType)) | ||
|  |             { | ||
|  |                 return GenerateSimpleTypeModelDescription(modelType); | ||
|  |             } | ||
|  | 
 | ||
|  |             if (modelType.IsEnum) | ||
|  |             { | ||
|  |                 return GenerateEnumTypeModelDescription(modelType); | ||
|  |             } | ||
|  | 
 | ||
|  |             if (modelType.IsGenericType) | ||
|  |             { | ||
|  |                 Type[] genericArguments = modelType.GetGenericArguments(); | ||
|  | 
 | ||
|  |                 if (genericArguments.Length == 1) | ||
|  |                 { | ||
|  |                     Type enumerableType = typeof(IEnumerable<>).MakeGenericType(genericArguments); | ||
|  |                     if (enumerableType.IsAssignableFrom(modelType)) | ||
|  |                     { | ||
|  |                         return GenerateCollectionModelDescription(modelType, genericArguments[0]); | ||
|  |                     } | ||
|  |                 } | ||
|  |                 if (genericArguments.Length == 2) | ||
|  |                 { | ||
|  |                     Type dictionaryType = typeof(IDictionary<,>).MakeGenericType(genericArguments); | ||
|  |                     if (dictionaryType.IsAssignableFrom(modelType)) | ||
|  |                     { | ||
|  |                         return GenerateDictionaryModelDescription(modelType, genericArguments[0], genericArguments[1]); | ||
|  |                     } | ||
|  | 
 | ||
|  |                     Type keyValuePairType = typeof(KeyValuePair<,>).MakeGenericType(genericArguments); | ||
|  |                     if (keyValuePairType.IsAssignableFrom(modelType)) | ||
|  |                     { | ||
|  |                         return GenerateKeyValuePairModelDescription(modelType, genericArguments[0], genericArguments[1]); | ||
|  |                     } | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             if (modelType.IsArray) | ||
|  |             { | ||
|  |                 Type elementType = modelType.GetElementType(); | ||
|  |                 return GenerateCollectionModelDescription(modelType, elementType); | ||
|  |             } | ||
|  | 
 | ||
|  |             if (modelType == typeof(NameValueCollection)) | ||
|  |             { | ||
|  |                 return GenerateDictionaryModelDescription(modelType, typeof(string), typeof(string)); | ||
|  |             } | ||
|  | 
 | ||
|  |             if (typeof(IDictionary).IsAssignableFrom(modelType)) | ||
|  |             { | ||
|  |                 return GenerateDictionaryModelDescription(modelType, typeof(object), typeof(object)); | ||
|  |             } | ||
|  | 
 | ||
|  |             if (typeof(IEnumerable).IsAssignableFrom(modelType)) | ||
|  |             { | ||
|  |                 return GenerateCollectionModelDescription(modelType, typeof(object)); | ||
|  |             } | ||
|  | 
 | ||
|  |             return GenerateComplexTypeModelDescription(modelType); | ||
|  |         } | ||
|  | 
 | ||
|  |         // Change this to provide different name for the member. | ||
|  |         private static string GetMemberName(MemberInfo member, bool hasDataContractAttribute) | ||
|  |         { | ||
|  |             JsonPropertyAttribute jsonProperty = member.GetCustomAttribute<JsonPropertyAttribute>(); | ||
|  |             if (jsonProperty != null && !String.IsNullOrEmpty(jsonProperty.PropertyName)) | ||
|  |             { | ||
|  |                 return jsonProperty.PropertyName; | ||
|  |             } | ||
|  | 
 | ||
|  |             if (hasDataContractAttribute) | ||
|  |             { | ||
|  |                 DataMemberAttribute dataMember = member.GetCustomAttribute<DataMemberAttribute>(); | ||
|  |                 if (dataMember != null && !String.IsNullOrEmpty(dataMember.Name)) | ||
|  |                 { | ||
|  |                     return dataMember.Name; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return member.Name; | ||
|  |         } | ||
|  | 
 | ||
|  |         private static bool ShouldDisplayMember(MemberInfo member, bool hasDataContractAttribute) | ||
|  |         { | ||
|  |             JsonIgnoreAttribute jsonIgnore = member.GetCustomAttribute<JsonIgnoreAttribute>(); | ||
|  |             XmlIgnoreAttribute xmlIgnore = member.GetCustomAttribute<XmlIgnoreAttribute>(); | ||
|  |             IgnoreDataMemberAttribute ignoreDataMember = member.GetCustomAttribute<IgnoreDataMemberAttribute>(); | ||
|  |             NonSerializedAttribute nonSerialized = member.GetCustomAttribute<NonSerializedAttribute>(); | ||
|  |             ApiExplorerSettingsAttribute apiExplorerSetting = member.GetCustomAttribute<ApiExplorerSettingsAttribute>(); | ||
|  | 
 | ||
|  |             bool hasMemberAttribute = member.DeclaringType.IsEnum ? | ||
|  |                 member.GetCustomAttribute<EnumMemberAttribute>() != null : | ||
|  |                 member.GetCustomAttribute<DataMemberAttribute>() != null; | ||
|  | 
 | ||
|  |             // Display member only if all the followings are true: | ||
|  |             // no JsonIgnoreAttribute | ||
|  |             // no XmlIgnoreAttribute | ||
|  |             // no IgnoreDataMemberAttribute | ||
|  |             // no NonSerializedAttribute | ||
|  |             // no ApiExplorerSettingsAttribute with IgnoreApi set to true | ||
|  |             // no DataContractAttribute without DataMemberAttribute or EnumMemberAttribute | ||
|  |             return jsonIgnore == null && | ||
|  |                 xmlIgnore == null && | ||
|  |                 ignoreDataMember == null && | ||
|  |                 nonSerialized == null && | ||
|  |                 (apiExplorerSetting == null || !apiExplorerSetting.IgnoreApi) && | ||
|  |                 (!hasDataContractAttribute || hasMemberAttribute); | ||
|  |         } | ||
|  | 
 | ||
|  |         private string CreateDefaultDocumentation(Type type) | ||
|  |         { | ||
|  |             string documentation; | ||
|  |             if (DefaultTypeDocumentation.TryGetValue(type, out documentation)) | ||
|  |             { | ||
|  |                 return documentation; | ||
|  |             } | ||
|  |             if (DocumentationProvider != null) | ||
|  |             { | ||
|  |                 documentation = DocumentationProvider.GetDocumentation(type); | ||
|  |             } | ||
|  | 
 | ||
|  |             return documentation; | ||
|  |         } | ||
|  | 
 | ||
|  |         private void GenerateAnnotations(MemberInfo property, ParameterDescription propertyModel) | ||
|  |         { | ||
|  |             List<ParameterAnnotation> annotations = new List<ParameterAnnotation>(); | ||
|  | 
 | ||
|  |             IEnumerable<Attribute> attributes = property.GetCustomAttributes(); | ||
|  |             foreach (Attribute attribute in attributes) | ||
|  |             { | ||
|  |                 Func<object, string> textGenerator; | ||
|  |                 if (AnnotationTextGenerator.TryGetValue(attribute.GetType(), out textGenerator)) | ||
|  |                 { | ||
|  |                     annotations.Add( | ||
|  |                         new ParameterAnnotation | ||
|  |                         { | ||
|  |                             AnnotationAttribute = attribute, | ||
|  |                             Documentation = textGenerator(attribute) | ||
|  |                         }); | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             // Rearrange the annotations | ||
|  |             annotations.Sort((x, y) => | ||
|  |             { | ||
|  |                 // Special-case RequiredAttribute so that it shows up on top | ||
|  |                 if (x.AnnotationAttribute is RequiredAttribute) | ||
|  |                 { | ||
|  |                     return -1; | ||
|  |                 } | ||
|  |                 if (y.AnnotationAttribute is RequiredAttribute) | ||
|  |                 { | ||
|  |                     return 1; | ||
|  |                 } | ||
|  | 
 | ||
|  |                 // Sort the rest based on alphabetic order of the documentation | ||
|  |                 return String.Compare(x.Documentation, y.Documentation, StringComparison.OrdinalIgnoreCase); | ||
|  |             }); | ||
|  | 
 | ||
|  |             foreach (ParameterAnnotation annotation in annotations) | ||
|  |             { | ||
|  |                 propertyModel.Annotations.Add(annotation); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         private CollectionModelDescription GenerateCollectionModelDescription(Type modelType, Type elementType) | ||
|  |         { | ||
|  |             ModelDescription collectionModelDescription = GetOrCreateModelDescription(elementType); | ||
|  |             if (collectionModelDescription != null) | ||
|  |             { | ||
|  |                 return new CollectionModelDescription | ||
|  |                 { | ||
|  |                     Name = ModelNameHelper.GetModelName(modelType), | ||
|  |                     ModelType = modelType, | ||
|  |                     ElementDescription = collectionModelDescription | ||
|  |                 }; | ||
|  |             } | ||
|  | 
 | ||
|  |             return null; | ||
|  |         } | ||
|  | 
 | ||
|  |         private ModelDescription GenerateComplexTypeModelDescription(Type modelType) | ||
|  |         { | ||
|  |             ComplexTypeModelDescription complexModelDescription = new ComplexTypeModelDescription | ||
|  |             { | ||
|  |                 Name = ModelNameHelper.GetModelName(modelType), | ||
|  |                 ModelType = modelType, | ||
|  |                 Documentation = CreateDefaultDocumentation(modelType) | ||
|  |             }; | ||
|  | 
 | ||
|  |             GeneratedModels.Add(complexModelDescription.Name, complexModelDescription); | ||
|  |             bool hasDataContractAttribute = modelType.GetCustomAttribute<DataContractAttribute>() != null; | ||
|  |             PropertyInfo[] properties = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance); | ||
|  |             foreach (PropertyInfo property in properties) | ||
|  |             { | ||
|  |                 if (ShouldDisplayMember(property, hasDataContractAttribute)) | ||
|  |                 { | ||
|  |                     ParameterDescription propertyModel = new ParameterDescription | ||
|  |                     { | ||
|  |                         Name = GetMemberName(property, hasDataContractAttribute) | ||
|  |                     }; | ||
|  | 
 | ||
|  |                     if (DocumentationProvider != null) | ||
|  |                     { | ||
|  |                         propertyModel.Documentation = DocumentationProvider.GetDocumentation(property); | ||
|  |                     } | ||
|  | 
 | ||
|  |                     GenerateAnnotations(property, propertyModel); | ||
|  |                     complexModelDescription.Properties.Add(propertyModel); | ||
|  |                     propertyModel.TypeDescription = GetOrCreateModelDescription(property.PropertyType); | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             FieldInfo[] fields = modelType.GetFields(BindingFlags.Public | BindingFlags.Instance); | ||
|  |             foreach (FieldInfo field in fields) | ||
|  |             { | ||
|  |                 if (ShouldDisplayMember(field, hasDataContractAttribute)) | ||
|  |                 { | ||
|  |                     ParameterDescription propertyModel = new ParameterDescription | ||
|  |                     { | ||
|  |                         Name = GetMemberName(field, hasDataContractAttribute) | ||
|  |                     }; | ||
|  | 
 | ||
|  |                     if (DocumentationProvider != null) | ||
|  |                     { | ||
|  |                         propertyModel.Documentation = DocumentationProvider.GetDocumentation(field); | ||
|  |                     } | ||
|  | 
 | ||
|  |                     complexModelDescription.Properties.Add(propertyModel); | ||
|  |                     propertyModel.TypeDescription = GetOrCreateModelDescription(field.FieldType); | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return complexModelDescription; | ||
|  |         } | ||
|  | 
 | ||
|  |         private DictionaryModelDescription GenerateDictionaryModelDescription(Type modelType, Type keyType, Type valueType) | ||
|  |         { | ||
|  |             ModelDescription keyModelDescription = GetOrCreateModelDescription(keyType); | ||
|  |             ModelDescription valueModelDescription = GetOrCreateModelDescription(valueType); | ||
|  | 
 | ||
|  |             return new DictionaryModelDescription | ||
|  |             { | ||
|  |                 Name = ModelNameHelper.GetModelName(modelType), | ||
|  |                 ModelType = modelType, | ||
|  |                 KeyModelDescription = keyModelDescription, | ||
|  |                 ValueModelDescription = valueModelDescription | ||
|  |             }; | ||
|  |         } | ||
|  | 
 | ||
|  |         private EnumTypeModelDescription GenerateEnumTypeModelDescription(Type modelType) | ||
|  |         { | ||
|  |             EnumTypeModelDescription enumDescription = new EnumTypeModelDescription | ||
|  |             { | ||
|  |                 Name = ModelNameHelper.GetModelName(modelType), | ||
|  |                 ModelType = modelType, | ||
|  |                 Documentation = CreateDefaultDocumentation(modelType) | ||
|  |             }; | ||
|  |             bool hasDataContractAttribute = modelType.GetCustomAttribute<DataContractAttribute>() != null; | ||
|  |             foreach (FieldInfo field in modelType.GetFields(BindingFlags.Public | BindingFlags.Static)) | ||
|  |             { | ||
|  |                 if (ShouldDisplayMember(field, hasDataContractAttribute)) | ||
|  |                 { | ||
|  |                     EnumValueDescription enumValue = new EnumValueDescription | ||
|  |                     { | ||
|  |                         Name = field.Name, | ||
|  |                         Value = field.GetRawConstantValue().ToString() | ||
|  |                     }; | ||
|  |                     if (DocumentationProvider != null) | ||
|  |                     { | ||
|  |                         enumValue.Documentation = DocumentationProvider.GetDocumentation(field); | ||
|  |                     } | ||
|  |                     enumDescription.Values.Add(enumValue); | ||
|  |                 } | ||
|  |             } | ||
|  |             GeneratedModels.Add(enumDescription.Name, enumDescription); | ||
|  | 
 | ||
|  |             return enumDescription; | ||
|  |         } | ||
|  | 
 | ||
|  |         private KeyValuePairModelDescription GenerateKeyValuePairModelDescription(Type modelType, Type keyType, Type valueType) | ||
|  |         { | ||
|  |             ModelDescription keyModelDescription = GetOrCreateModelDescription(keyType); | ||
|  |             ModelDescription valueModelDescription = GetOrCreateModelDescription(valueType); | ||
|  | 
 | ||
|  |             return new KeyValuePairModelDescription | ||
|  |             { | ||
|  |                 Name = ModelNameHelper.GetModelName(modelType), | ||
|  |                 ModelType = modelType, | ||
|  |                 KeyModelDescription = keyModelDescription, | ||
|  |                 ValueModelDescription = valueModelDescription | ||
|  |             }; | ||
|  |         } | ||
|  | 
 | ||
|  |         private ModelDescription GenerateSimpleTypeModelDescription(Type modelType) | ||
|  |         { | ||
|  |             SimpleTypeModelDescription simpleModelDescription = new SimpleTypeModelDescription | ||
|  |             { | ||
|  |                 Name = ModelNameHelper.GetModelName(modelType), | ||
|  |                 ModelType = modelType, | ||
|  |                 Documentation = CreateDefaultDocumentation(modelType) | ||
|  |             }; | ||
|  |             GeneratedModels.Add(simpleModelDescription.Name, simpleModelDescription); | ||
|  | 
 | ||
|  |             return simpleModelDescription; | ||
|  |         } | ||
|  |     } | ||
|  | } |