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;
 | |
|         }
 | |
|     }
 | |
| } |