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.
451 lines
19 KiB
451 lines
19 KiB
7 months ago
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|