444 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			C#
		
	
	
	
		
		
			
		
	
	
			444 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			C#
		
	
	
	
|  | using System; | ||
|  | using System.Collections.Generic; | ||
|  | using System.Collections.ObjectModel; | ||
|  | using System.ComponentModel; | ||
|  | using System.Diagnostics.CodeAnalysis; | ||
|  | using System.Globalization; | ||
|  | using System.IO; | ||
|  | using System.Linq; | ||
|  | using System.Net.Http; | ||
|  | using System.Net.Http.Formatting; | ||
|  | using System.Net.Http.Headers; | ||
|  | using System.Web.Http.Description; | ||
|  | using System.Xml.Linq; | ||
|  | using Newtonsoft.Json; | ||
|  | 
 | ||
|  | namespace WebAPI.Areas.HelpPage | ||
|  | { | ||
|  |     /// <summary> | ||
|  |     /// This class will generate the samples for the help page. | ||
|  |     /// </summary> | ||
|  |     public class HelpPageSampleGenerator | ||
|  |     { | ||
|  |         /// <summary> | ||
|  |         /// Initializes a new instance of the <see cref="HelpPageSampleGenerator"/> class. | ||
|  |         /// </summary> | ||
|  |         public HelpPageSampleGenerator() | ||
|  |         { | ||
|  |             ActualHttpMessageTypes = new Dictionary<HelpPageSampleKey, Type>(); | ||
|  |             ActionSamples = new Dictionary<HelpPageSampleKey, object>(); | ||
|  |             SampleObjects = new Dictionary<Type, object>(); | ||
|  |             SampleObjectFactories = new List<Func<HelpPageSampleGenerator, Type, object>> | ||
|  |             { | ||
|  |                 DefaultSampleObjectFactory, | ||
|  |             }; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets CLR types that are used as the content of <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/>. | ||
|  |         /// </summary> | ||
|  |         public IDictionary<HelpPageSampleKey, Type> ActualHttpMessageTypes { get; internal set; } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets the objects that are used directly as samples for certain actions. | ||
|  |         /// </summary> | ||
|  |         public IDictionary<HelpPageSampleKey, object> ActionSamples { get; internal set; } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets the objects that are serialized as samples by the supported formatters. | ||
|  |         /// </summary> | ||
|  |         public IDictionary<Type, object> SampleObjects { get; internal set; } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets factories for the objects that the supported formatters will serialize as samples. Processed in order, | ||
|  |         /// stopping when the factory successfully returns a non-<see langref="null"/> object. | ||
|  |         /// </summary> | ||
|  |         /// <remarks> | ||
|  |         /// Collection includes just <see cref="ObjectGenerator.GenerateObject(Type)"/> initially. Use | ||
|  |         /// <code>SampleObjectFactories.Insert(0, func)</code> to provide an override and | ||
|  |         /// <code>SampleObjectFactories.Add(func)</code> to provide a fallback.</remarks> | ||
|  |         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", | ||
|  |             Justification = "This is an appropriate nesting of generic types")] | ||
|  |         public IList<Func<HelpPageSampleGenerator, Type, object>> SampleObjectFactories { get; private set; } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets the request body samples for a given <see cref="ApiDescription"/>. | ||
|  |         /// </summary> | ||
|  |         /// <param name="api">The <see cref="ApiDescription"/>.</param> | ||
|  |         /// <returns>The samples keyed by media type.</returns> | ||
|  |         public IDictionary<MediaTypeHeaderValue, object> GetSampleRequests(ApiDescription api) | ||
|  |         { | ||
|  |             return GetSample(api, SampleDirection.Request); | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets the response body samples for a given <see cref="ApiDescription"/>. | ||
|  |         /// </summary> | ||
|  |         /// <param name="api">The <see cref="ApiDescription"/>.</param> | ||
|  |         /// <returns>The samples keyed by media type.</returns> | ||
|  |         public IDictionary<MediaTypeHeaderValue, object> GetSampleResponses(ApiDescription api) | ||
|  |         { | ||
|  |             return GetSample(api, SampleDirection.Response); | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets the request or response body samples. | ||
|  |         /// </summary> | ||
|  |         /// <param name="api">The <see cref="ApiDescription"/>.</param> | ||
|  |         /// <param name="sampleDirection">The value indicating whether the sample is for a request or for a response.</param> | ||
|  |         /// <returns>The samples keyed by media type.</returns> | ||
|  |         public virtual IDictionary<MediaTypeHeaderValue, object> GetSample(ApiDescription api, SampleDirection sampleDirection) | ||
|  |         { | ||
|  |             if (api == null) | ||
|  |             { | ||
|  |                 throw new ArgumentNullException("api"); | ||
|  |             } | ||
|  |             string controllerName = api.ActionDescriptor.ControllerDescriptor.ControllerName; | ||
|  |             string actionName = api.ActionDescriptor.ActionName; | ||
|  |             IEnumerable<string> parameterNames = api.ParameterDescriptions.Select(p => p.Name); | ||
|  |             Collection<MediaTypeFormatter> formatters; | ||
|  |             Type type = ResolveType(api, controllerName, actionName, parameterNames, sampleDirection, out formatters); | ||
|  |             var samples = new Dictionary<MediaTypeHeaderValue, object>(); | ||
|  | 
 | ||
|  |             // Use the samples provided directly for actions | ||
|  |             var actionSamples = GetAllActionSamples(controllerName, actionName, parameterNames, sampleDirection); | ||
|  |             foreach (var actionSample in actionSamples) | ||
|  |             { | ||
|  |                 samples.Add(actionSample.Key.MediaType, WrapSampleIfString(actionSample.Value)); | ||
|  |             } | ||
|  | 
 | ||
|  |             // Do the sample generation based on formatters only if an action doesn't return an HttpResponseMessage. | ||
|  |             // Here we cannot rely on formatters because we don't know what's in the HttpResponseMessage, it might not even use formatters. | ||
|  |             if (type != null && !typeof(HttpResponseMessage).IsAssignableFrom(type)) | ||
|  |             { | ||
|  |                 object sampleObject = GetSampleObject(type); | ||
|  |                 foreach (var formatter in formatters) | ||
|  |                 { | ||
|  |                     foreach (MediaTypeHeaderValue mediaType in formatter.SupportedMediaTypes) | ||
|  |                     { | ||
|  |                         if (!samples.ContainsKey(mediaType)) | ||
|  |                         { | ||
|  |                             object sample = GetActionSample(controllerName, actionName, parameterNames, type, formatter, mediaType, sampleDirection); | ||
|  | 
 | ||
|  |                             // If no sample found, try generate sample using formatter and sample object | ||
|  |                             if (sample == null && sampleObject != null) | ||
|  |                             { | ||
|  |                                 sample = WriteSampleObjectUsingFormatter(formatter, sampleObject, type, mediaType); | ||
|  |                             } | ||
|  | 
 | ||
|  |                             samples.Add(mediaType, WrapSampleIfString(sample)); | ||
|  |                         } | ||
|  |                     } | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return samples; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Search for samples that are provided directly through <see cref="ActionSamples"/>. | ||
|  |         /// </summary> | ||
|  |         /// <param name="controllerName">Name of the controller.</param> | ||
|  |         /// <param name="actionName">Name of the action.</param> | ||
|  |         /// <param name="parameterNames">The parameter names.</param> | ||
|  |         /// <param name="type">The CLR type.</param> | ||
|  |         /// <param name="formatter">The formatter.</param> | ||
|  |         /// <param name="mediaType">The media type.</param> | ||
|  |         /// <param name="sampleDirection">The value indicating whether the sample is for a request or for a response.</param> | ||
|  |         /// <returns>The sample that matches the parameters.</returns> | ||
|  |         public virtual object GetActionSample(string controllerName, string actionName, IEnumerable<string> parameterNames, Type type, MediaTypeFormatter formatter, MediaTypeHeaderValue mediaType, SampleDirection sampleDirection) | ||
|  |         { | ||
|  |             object sample; | ||
|  | 
 | ||
|  |             // First, try to get the sample provided for the specified mediaType, sampleDirection, controllerName, actionName and parameterNames. | ||
|  |             // If not found, try to get the sample provided for the specified mediaType, sampleDirection, controllerName and actionName regardless of the parameterNames. | ||
|  |             // If still not found, try to get the sample provided for the specified mediaType and type. | ||
|  |             // Finally, try to get the sample provided for the specified mediaType. | ||
|  |             if (ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType, sampleDirection, controllerName, actionName, parameterNames), out sample) || | ||
|  |                 ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType, sampleDirection, controllerName, actionName, new[] { "*" }), out sample) || | ||
|  |                 ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType, type), out sample) || | ||
|  |                 ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType), out sample)) | ||
|  |             { | ||
|  |                 return sample; | ||
|  |             } | ||
|  | 
 | ||
|  |             return null; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Gets the sample object that will be serialized by the formatters.  | ||
|  |         /// First, it will look at the <see cref="SampleObjects"/>. If no sample object is found, it will try to create | ||
|  |         /// one using <see cref="DefaultSampleObjectFactory"/> (which wraps an <see cref="ObjectGenerator"/>) and other | ||
|  |         /// factories in <see cref="SampleObjectFactories"/>. | ||
|  |         /// </summary> | ||
|  |         /// <param name="type">The type.</param> | ||
|  |         /// <returns>The sample object.</returns> | ||
|  |         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", | ||
|  |             Justification = "Even if all items in SampleObjectFactories throw, problem will be visible as missing sample.")] | ||
|  |         public virtual object GetSampleObject(Type type) | ||
|  |         { | ||
|  |             object sampleObject; | ||
|  | 
 | ||
|  |             if (!SampleObjects.TryGetValue(type, out sampleObject)) | ||
|  |             { | ||
|  |                 // No specific object available, try our factories. | ||
|  |                 foreach (Func<HelpPageSampleGenerator, Type, object> factory in SampleObjectFactories) | ||
|  |                 { | ||
|  |                     if (factory == null) | ||
|  |                     { | ||
|  |                         continue; | ||
|  |                     } | ||
|  | 
 | ||
|  |                     try | ||
|  |                     { | ||
|  |                         sampleObject = factory(this, type); | ||
|  |                         if (sampleObject != null) | ||
|  |                         { | ||
|  |                             break; | ||
|  |                         } | ||
|  |                     } | ||
|  |                     catch | ||
|  |                     { | ||
|  |                         // Ignore any problems encountered in the factory; go on to the next one (if any). | ||
|  |                     } | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return sampleObject; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Resolves the actual type of <see cref="System.Net.Http.ObjectContent{T}"/> passed to the <see cref="System.Net.Http.HttpRequestMessage"/> in an action. | ||
|  |         /// </summary> | ||
|  |         /// <param name="api">The <see cref="ApiDescription"/>.</param> | ||
|  |         /// <returns>The type.</returns> | ||
|  |         public virtual Type ResolveHttpRequestMessageType(ApiDescription api) | ||
|  |         { | ||
|  |             string controllerName = api.ActionDescriptor.ControllerDescriptor.ControllerName; | ||
|  |             string actionName = api.ActionDescriptor.ActionName; | ||
|  |             IEnumerable<string> parameterNames = api.ParameterDescriptions.Select(p => p.Name); | ||
|  |             Collection<MediaTypeFormatter> formatters; | ||
|  |             return ResolveType(api, controllerName, actionName, parameterNames, SampleDirection.Request, out formatters); | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Resolves the type of the action parameter or return value when <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> is used. | ||
|  |         /// </summary> | ||
|  |         /// <param name="api">The <see cref="ApiDescription"/>.</param> | ||
|  |         /// <param name="controllerName">Name of the controller.</param> | ||
|  |         /// <param name="actionName">Name of the action.</param> | ||
|  |         /// <param name="parameterNames">The parameter names.</param> | ||
|  |         /// <param name="sampleDirection">The value indicating whether the sample is for a request or a response.</param> | ||
|  |         /// <param name="formatters">The formatters.</param> | ||
|  |         [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "This is only used in advanced scenarios.")] | ||
|  |         public virtual Type ResolveType(ApiDescription api, string controllerName, string actionName, IEnumerable<string> parameterNames, SampleDirection sampleDirection, out Collection<MediaTypeFormatter> formatters) | ||
|  |         { | ||
|  |             if (!Enum.IsDefined(typeof(SampleDirection), sampleDirection)) | ||
|  |             { | ||
|  |                 throw new InvalidEnumArgumentException("sampleDirection", (int)sampleDirection, typeof(SampleDirection)); | ||
|  |             } | ||
|  |             if (api == null) | ||
|  |             { | ||
|  |                 throw new ArgumentNullException("api"); | ||
|  |             } | ||
|  |             Type type; | ||
|  |             if (ActualHttpMessageTypes.TryGetValue(new HelpPageSampleKey(sampleDirection, controllerName, actionName, parameterNames), out type) || | ||
|  |                 ActualHttpMessageTypes.TryGetValue(new HelpPageSampleKey(sampleDirection, controllerName, actionName, new[] { "*" }), out type)) | ||
|  |             { | ||
|  |                 // Re-compute the supported formatters based on type | ||
|  |                 Collection<MediaTypeFormatter> newFormatters = new Collection<MediaTypeFormatter>(); | ||
|  |                 foreach (var formatter in api.ActionDescriptor.Configuration.Formatters) | ||
|  |                 { | ||
|  |                     if (IsFormatSupported(sampleDirection, formatter, type)) | ||
|  |                     { | ||
|  |                         newFormatters.Add(formatter); | ||
|  |                     } | ||
|  |                 } | ||
|  |                 formatters = newFormatters; | ||
|  |             } | ||
|  |             else | ||
|  |             { | ||
|  |                 switch (sampleDirection) | ||
|  |                 { | ||
|  |                     case SampleDirection.Request: | ||
|  |                         ApiParameterDescription requestBodyParameter = api.ParameterDescriptions.FirstOrDefault(p => p.Source == ApiParameterSource.FromBody); | ||
|  |                         type = requestBodyParameter == null ? null : requestBodyParameter.ParameterDescriptor.ParameterType; | ||
|  |                         formatters = api.SupportedRequestBodyFormatters; | ||
|  |                         break; | ||
|  |                     case SampleDirection.Response: | ||
|  |                     default: | ||
|  |                         type = api.ResponseDescription.ResponseType ?? api.ResponseDescription.DeclaredType; | ||
|  |                         formatters = api.SupportedResponseFormatters; | ||
|  |                         break; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return type; | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Writes the sample object using formatter. | ||
|  |         /// </summary> | ||
|  |         /// <param name="formatter">The formatter.</param> | ||
|  |         /// <param name="value">The value.</param> | ||
|  |         /// <param name="type">The type.</param> | ||
|  |         /// <param name="mediaType">Type of the media.</param> | ||
|  |         /// <returns></returns> | ||
|  |         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is recorded as InvalidSample.")] | ||
|  |         public virtual object WriteSampleObjectUsingFormatter(MediaTypeFormatter formatter, object value, Type type, MediaTypeHeaderValue mediaType) | ||
|  |         { | ||
|  |             if (formatter == null) | ||
|  |             { | ||
|  |                 throw new ArgumentNullException("formatter"); | ||
|  |             } | ||
|  |             if (mediaType == null) | ||
|  |             { | ||
|  |                 throw new ArgumentNullException("mediaType"); | ||
|  |             } | ||
|  | 
 | ||
|  |             object sample = String.Empty; | ||
|  |             MemoryStream ms = null; | ||
|  |             HttpContent content = null; | ||
|  |             try | ||
|  |             { | ||
|  |                 if (formatter.CanWriteType(type)) | ||
|  |                 { | ||
|  |                     ms = new MemoryStream(); | ||
|  |                     content = new ObjectContent(type, value, formatter, mediaType); | ||
|  |                     formatter.WriteToStreamAsync(type, value, ms, content, null).Wait(); | ||
|  |                     ms.Position = 0; | ||
|  |                     StreamReader reader = new StreamReader(ms); | ||
|  |                     string serializedSampleString = reader.ReadToEnd(); | ||
|  |                     if (mediaType.MediaType.ToUpperInvariant().Contains("XML")) | ||
|  |                     { | ||
|  |                         serializedSampleString = TryFormatXml(serializedSampleString); | ||
|  |                     } | ||
|  |                     else if (mediaType.MediaType.ToUpperInvariant().Contains("JSON")) | ||
|  |                     { | ||
|  |                         serializedSampleString = TryFormatJson(serializedSampleString); | ||
|  |                     } | ||
|  | 
 | ||
|  |                     sample = new TextSample(serializedSampleString); | ||
|  |                 } | ||
|  |                 else | ||
|  |                 { | ||
|  |                     sample = new InvalidSample(String.Format( | ||
|  |                         CultureInfo.CurrentCulture, | ||
|  |                         "Failed to generate the sample for media type '{0}'. Cannot use formatter '{1}' to write type '{2}'.", | ||
|  |                         mediaType, | ||
|  |                         formatter.GetType().Name, | ||
|  |                         type.Name)); | ||
|  |                 } | ||
|  |             } | ||
|  |             catch (Exception e) | ||
|  |             { | ||
|  |                 sample = new InvalidSample(String.Format( | ||
|  |                     CultureInfo.CurrentCulture, | ||
|  |                     "An exception has occurred while using the formatter '{0}' to generate sample for media type '{1}'. Exception message: {2}", | ||
|  |                     formatter.GetType().Name, | ||
|  |                     mediaType.MediaType, | ||
|  |                     UnwrapException(e).Message)); | ||
|  |             } | ||
|  |             finally | ||
|  |             { | ||
|  |                 if (ms != null) | ||
|  |                 { | ||
|  |                     ms.Dispose(); | ||
|  |                 } | ||
|  |                 if (content != null) | ||
|  |                 { | ||
|  |                     content.Dispose(); | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             return sample; | ||
|  |         } | ||
|  | 
 | ||
|  |         internal static Exception UnwrapException(Exception exception) | ||
|  |         { | ||
|  |             AggregateException aggregateException = exception as AggregateException; | ||
|  |             if (aggregateException != null) | ||
|  |             { | ||
|  |                 return aggregateException.Flatten().InnerException; | ||
|  |             } | ||
|  |             return exception; | ||
|  |         } | ||
|  | 
 | ||
|  |         // Default factory for sample objects | ||
|  |         private static object DefaultSampleObjectFactory(HelpPageSampleGenerator sampleGenerator, Type type) | ||
|  |         { | ||
|  |             // Try to create a default sample object | ||
|  |             ObjectGenerator objectGenerator = new ObjectGenerator(); | ||
|  |             return objectGenerator.GenerateObject(type); | ||
|  |         } | ||
|  | 
 | ||
|  |         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Handling the failure by returning the original string.")] | ||
|  |         private static string TryFormatJson(string str) | ||
|  |         { | ||
|  |             try | ||
|  |             { | ||
|  |                 object parsedJson = JsonConvert.DeserializeObject(str); | ||
|  |                 return JsonConvert.SerializeObject(parsedJson, Formatting.Indented); | ||
|  |             } | ||
|  |             catch | ||
|  |             { | ||
|  |                 // can't parse JSON, return the original string | ||
|  |                 return str; | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Handling the failure by returning the original string.")] | ||
|  |         private static string TryFormatXml(string str) | ||
|  |         { | ||
|  |             try | ||
|  |             { | ||
|  |                 XDocument xml = XDocument.Parse(str); | ||
|  |                 return xml.ToString(); | ||
|  |             } | ||
|  |             catch | ||
|  |             { | ||
|  |                 // can't parse XML, return the original string | ||
|  |                 return str; | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         private static bool IsFormatSupported(SampleDirection sampleDirection, MediaTypeFormatter formatter, Type type) | ||
|  |         { | ||
|  |             switch (sampleDirection) | ||
|  |             { | ||
|  |                 case SampleDirection.Request: | ||
|  |                     return formatter.CanReadType(type); | ||
|  |                 case SampleDirection.Response: | ||
|  |                     return formatter.CanWriteType(type); | ||
|  |             } | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         private IEnumerable<KeyValuePair<HelpPageSampleKey, object>> GetAllActionSamples(string controllerName, string actionName, IEnumerable<string> parameterNames, SampleDirection sampleDirection) | ||
|  |         { | ||
|  |             HashSet<string> parameterNamesSet = new HashSet<string>(parameterNames, StringComparer.OrdinalIgnoreCase); | ||
|  |             foreach (var sample in ActionSamples) | ||
|  |             { | ||
|  |                 HelpPageSampleKey sampleKey = sample.Key; | ||
|  |                 if (String.Equals(controllerName, sampleKey.ControllerName, StringComparison.OrdinalIgnoreCase) && | ||
|  |                     String.Equals(actionName, sampleKey.ActionName, StringComparison.OrdinalIgnoreCase) && | ||
|  |                     (sampleKey.ParameterNames.SetEquals(new[] { "*" }) || parameterNamesSet.SetEquals(sampleKey.ParameterNames)) && | ||
|  |                     sampleDirection == sampleKey.SampleDirection) | ||
|  |                 { | ||
|  |                     yield return sample; | ||
|  |                 } | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         private static object WrapSampleIfString(object sample) | ||
|  |         { | ||
|  |             string stringSample = sample as string; | ||
|  |             if (stringSample != null) | ||
|  |             { | ||
|  |                 return new TextSample(stringSample); | ||
|  |             } | ||
|  | 
 | ||
|  |             return sample; | ||
|  |         } | ||
|  |     } | ||
|  | } |