123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- 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 WebAPiUtils_test.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>();
- }
-
- /// <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 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 get sample provided for a specific mediaType, controllerName, actionName and parameterNames.
- // If not found, try get the sample provided for a specific mediaType, controllerName and actionName regardless of the parameterNames
- // If still not found, try get the sample provided for a specific type and 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))
- {
- 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="ObjectGenerator"/>.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <returns>The sample object.</returns>
- public virtual object GetSampleObject(Type type)
- {
- object sampleObject;
-
- if (!SampleObjects.TryGetValue(type, out sampleObject))
- {
- // Try create a default sample object
- ObjectGenerator objectGenerator = new ObjectGenerator();
- sampleObject = objectGenerator.GenerateObject(type);
- }
-
- return sampleObject;
- }
-
- /// <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,
- e.Message));
- }
- finally
- {
- if (ms != null)
- {
- ms.Dispose();
- }
- if (content != null)
- {
- content.Dispose();
- }
- }
-
- return sample;
- }
-
- [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;
- }
- }
- }
|