纽威
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.

443 lines
20 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.ComponentModel;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.Globalization;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Net.Http;
  10. using System.Net.Http.Formatting;
  11. using System.Net.Http.Headers;
  12. using System.Web.Http.Description;
  13. using System.Xml.Linq;
  14. using Newtonsoft.Json;
  15. namespace ICSSoft.WebAPI.Areas.HelpPage
  16. {
  17. /// <summary>
  18. /// This class will generate the samples for the help page.
  19. /// </summary>
  20. public class HelpPageSampleGenerator
  21. {
  22. /// <summary>
  23. /// Initializes a new instance of the <see cref="HelpPageSampleGenerator"/> class.
  24. /// </summary>
  25. public HelpPageSampleGenerator()
  26. {
  27. ActualHttpMessageTypes = new Dictionary<HelpPageSampleKey, Type>();
  28. ActionSamples = new Dictionary<HelpPageSampleKey, object>();
  29. SampleObjects = new Dictionary<Type, object>();
  30. SampleObjectFactories = new List<Func<HelpPageSampleGenerator, Type, object>>
  31. {
  32. DefaultSampleObjectFactory,
  33. };
  34. }
  35. /// <summary>
  36. /// Gets CLR types that are used as the content of <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/>.
  37. /// </summary>
  38. public IDictionary<HelpPageSampleKey, Type> ActualHttpMessageTypes { get; internal set; }
  39. /// <summary>
  40. /// Gets the objects that are used directly as samples for certain actions.
  41. /// </summary>
  42. public IDictionary<HelpPageSampleKey, object> ActionSamples { get; internal set; }
  43. /// <summary>
  44. /// Gets the objects that are serialized as samples by the supported formatters.
  45. /// </summary>
  46. public IDictionary<Type, object> SampleObjects { get; internal set; }
  47. /// <summary>
  48. /// Gets factories for the objects that the supported formatters will serialize as samples. Processed in order,
  49. /// stopping when the factory successfully returns a non-<see langref="null"/> object.
  50. /// </summary>
  51. /// <remarks>
  52. /// Collection includes just <see cref="ObjectGenerator.GenerateObject(Type)"/> initially. Use
  53. /// <code>SampleObjectFactories.Insert(0, func)</code> to provide an override and
  54. /// <code>SampleObjectFactories.Add(func)</code> to provide a fallback.</remarks>
  55. [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
  56. Justification = "This is an appropriate nesting of generic types")]
  57. public IList<Func<HelpPageSampleGenerator, Type, object>> SampleObjectFactories { get; private set; }
  58. /// <summary>
  59. /// Gets the request body samples for a given <see cref="ApiDescription"/>.
  60. /// </summary>
  61. /// <param name="api">The <see cref="ApiDescription"/>.</param>
  62. /// <returns>The samples keyed by media type.</returns>
  63. public IDictionary<MediaTypeHeaderValue, object> GetSampleRequests(ApiDescription api)
  64. {
  65. return GetSample(api, SampleDirection.Request);
  66. }
  67. /// <summary>
  68. /// Gets the response body samples for a given <see cref="ApiDescription"/>.
  69. /// </summary>
  70. /// <param name="api">The <see cref="ApiDescription"/>.</param>
  71. /// <returns>The samples keyed by media type.</returns>
  72. public IDictionary<MediaTypeHeaderValue, object> GetSampleResponses(ApiDescription api)
  73. {
  74. return GetSample(api, SampleDirection.Response);
  75. }
  76. /// <summary>
  77. /// Gets the request or response body samples.
  78. /// </summary>
  79. /// <param name="api">The <see cref="ApiDescription"/>.</param>
  80. /// <param name="sampleDirection">The value indicating whether the sample is for a request or for a response.</param>
  81. /// <returns>The samples keyed by media type.</returns>
  82. public virtual IDictionary<MediaTypeHeaderValue, object> GetSample(ApiDescription api, SampleDirection sampleDirection)
  83. {
  84. if (api == null)
  85. {
  86. throw new ArgumentNullException("api");
  87. }
  88. string controllerName = api.ActionDescriptor.ControllerDescriptor.ControllerName;
  89. string actionName = api.ActionDescriptor.ActionName;
  90. IEnumerable<string> parameterNames = api.ParameterDescriptions.Select(p => p.Name);
  91. Collection<MediaTypeFormatter> formatters;
  92. Type type = ResolveType(api, controllerName, actionName, parameterNames, sampleDirection, out formatters);
  93. var samples = new Dictionary<MediaTypeHeaderValue, object>();
  94. // Use the samples provided directly for actions
  95. var actionSamples = GetAllActionSamples(controllerName, actionName, parameterNames, sampleDirection);
  96. foreach (var actionSample in actionSamples)
  97. {
  98. samples.Add(actionSample.Key.MediaType, WrapSampleIfString(actionSample.Value));
  99. }
  100. // Do the sample generation based on formatters only if an action doesn't return an HttpResponseMessage.
  101. // Here we cannot rely on formatters because we don't know what's in the HttpResponseMessage, it might not even use formatters.
  102. if (type != null && !typeof(HttpResponseMessage).IsAssignableFrom(type))
  103. {
  104. object sampleObject = GetSampleObject(type);
  105. foreach (var formatter in formatters)
  106. {
  107. foreach (MediaTypeHeaderValue mediaType in formatter.SupportedMediaTypes)
  108. {
  109. if (!samples.ContainsKey(mediaType))
  110. {
  111. object sample = GetActionSample(controllerName, actionName, parameterNames, type, formatter, mediaType, sampleDirection);
  112. // If no sample found, try generate sample using formatter and sample object
  113. if (sample == null && sampleObject != null)
  114. {
  115. sample = WriteSampleObjectUsingFormatter(formatter, sampleObject, type, mediaType);
  116. }
  117. samples.Add(mediaType, WrapSampleIfString(sample));
  118. }
  119. }
  120. }
  121. }
  122. return samples;
  123. }
  124. /// <summary>
  125. /// Search for samples that are provided directly through <see cref="ActionSamples"/>.
  126. /// </summary>
  127. /// <param name="controllerName">Name of the controller.</param>
  128. /// <param name="actionName">Name of the action.</param>
  129. /// <param name="parameterNames">The parameter names.</param>
  130. /// <param name="type">The CLR type.</param>
  131. /// <param name="formatter">The formatter.</param>
  132. /// <param name="mediaType">The media type.</param>
  133. /// <param name="sampleDirection">The value indicating whether the sample is for a request or for a response.</param>
  134. /// <returns>The sample that matches the parameters.</returns>
  135. public virtual object GetActionSample(string controllerName, string actionName, IEnumerable<string> parameterNames, Type type, MediaTypeFormatter formatter, MediaTypeHeaderValue mediaType, SampleDirection sampleDirection)
  136. {
  137. object sample;
  138. // First, try to get the sample provided for the specified mediaType, sampleDirection, controllerName, actionName and parameterNames.
  139. // If not found, try to get the sample provided for the specified mediaType, sampleDirection, controllerName and actionName regardless of the parameterNames.
  140. // If still not found, try to get the sample provided for the specified mediaType and type.
  141. // Finally, try to get the sample provided for the specified mediaType.
  142. if (ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType, sampleDirection, controllerName, actionName, parameterNames), out sample) ||
  143. ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType, sampleDirection, controllerName, actionName, new[] { "*" }), out sample) ||
  144. ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType, type), out sample) ||
  145. ActionSamples.TryGetValue(new HelpPageSampleKey(mediaType), out sample))
  146. {
  147. return sample;
  148. }
  149. return null;
  150. }
  151. /// <summary>
  152. /// Gets the sample object that will be serialized by the formatters.
  153. /// First, it will look at the <see cref="SampleObjects"/>. If no sample object is found, it will try to create
  154. /// one using <see cref="DefaultSampleObjectFactory"/> (which wraps an <see cref="ObjectGenerator"/>) and other
  155. /// factories in <see cref="SampleObjectFactories"/>.
  156. /// </summary>
  157. /// <param name="type">The type.</param>
  158. /// <returns>The sample object.</returns>
  159. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
  160. Justification = "Even if all items in SampleObjectFactories throw, problem will be visible as missing sample.")]
  161. public virtual object GetSampleObject(Type type)
  162. {
  163. object sampleObject;
  164. if (!SampleObjects.TryGetValue(type, out sampleObject))
  165. {
  166. // No specific object available, try our factories.
  167. foreach (Func<HelpPageSampleGenerator, Type, object> factory in SampleObjectFactories)
  168. {
  169. if (factory == null)
  170. {
  171. continue;
  172. }
  173. try
  174. {
  175. sampleObject = factory(this, type);
  176. if (sampleObject != null)
  177. {
  178. break;
  179. }
  180. }
  181. catch
  182. {
  183. // Ignore any problems encountered in the factory; go on to the next one (if any).
  184. }
  185. }
  186. }
  187. return sampleObject;
  188. }
  189. /// <summary>
  190. /// Resolves the actual type of <see cref="System.Net.Http.ObjectContent{T}"/> passed to the <see cref="System.Net.Http.HttpRequestMessage"/> in an action.
  191. /// </summary>
  192. /// <param name="api">The <see cref="ApiDescription"/>.</param>
  193. /// <returns>The type.</returns>
  194. public virtual Type ResolveHttpRequestMessageType(ApiDescription api)
  195. {
  196. string controllerName = api.ActionDescriptor.ControllerDescriptor.ControllerName;
  197. string actionName = api.ActionDescriptor.ActionName;
  198. IEnumerable<string> parameterNames = api.ParameterDescriptions.Select(p => p.Name);
  199. Collection<MediaTypeFormatter> formatters;
  200. return ResolveType(api, controllerName, actionName, parameterNames, SampleDirection.Request, out formatters);
  201. }
  202. /// <summary>
  203. /// Resolves the type of the action parameter or return value when <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> is used.
  204. /// </summary>
  205. /// <param name="api">The <see cref="ApiDescription"/>.</param>
  206. /// <param name="controllerName">Name of the controller.</param>
  207. /// <param name="actionName">Name of the action.</param>
  208. /// <param name="parameterNames">The parameter names.</param>
  209. /// <param name="sampleDirection">The value indicating whether the sample is for a request or a response.</param>
  210. /// <param name="formatters">The formatters.</param>
  211. [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "This is only used in advanced scenarios.")]
  212. public virtual Type ResolveType(ApiDescription api, string controllerName, string actionName, IEnumerable<string> parameterNames, SampleDirection sampleDirection, out Collection<MediaTypeFormatter> formatters)
  213. {
  214. if (!Enum.IsDefined(typeof(SampleDirection), sampleDirection))
  215. {
  216. throw new InvalidEnumArgumentException("sampleDirection", (int)sampleDirection, typeof(SampleDirection));
  217. }
  218. if (api == null)
  219. {
  220. throw new ArgumentNullException("api");
  221. }
  222. Type type;
  223. if (ActualHttpMessageTypes.TryGetValue(new HelpPageSampleKey(sampleDirection, controllerName, actionName, parameterNames), out type) ||
  224. ActualHttpMessageTypes.TryGetValue(new HelpPageSampleKey(sampleDirection, controllerName, actionName, new[] { "*" }), out type))
  225. {
  226. // Re-compute the supported formatters based on type
  227. Collection<MediaTypeFormatter> newFormatters = new Collection<MediaTypeFormatter>();
  228. foreach (var formatter in api.ActionDescriptor.Configuration.Formatters)
  229. {
  230. if (IsFormatSupported(sampleDirection, formatter, type))
  231. {
  232. newFormatters.Add(formatter);
  233. }
  234. }
  235. formatters = newFormatters;
  236. }
  237. else
  238. {
  239. switch (sampleDirection)
  240. {
  241. case SampleDirection.Request:
  242. ApiParameterDescription requestBodyParameter = api.ParameterDescriptions.FirstOrDefault(p => p.Source == ApiParameterSource.FromBody);
  243. type = requestBodyParameter == null ? null : requestBodyParameter.ParameterDescriptor.ParameterType;
  244. formatters = api.SupportedRequestBodyFormatters;
  245. break;
  246. case SampleDirection.Response:
  247. default:
  248. type = api.ResponseDescription.ResponseType ?? api.ResponseDescription.DeclaredType;
  249. formatters = api.SupportedResponseFormatters;
  250. break;
  251. }
  252. }
  253. return type;
  254. }
  255. /// <summary>
  256. /// Writes the sample object using formatter.
  257. /// </summary>
  258. /// <param name="formatter">The formatter.</param>
  259. /// <param name="value">The value.</param>
  260. /// <param name="type">The type.</param>
  261. /// <param name="mediaType">Type of the media.</param>
  262. /// <returns></returns>
  263. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is recorded as InvalidSample.")]
  264. public virtual object WriteSampleObjectUsingFormatter(MediaTypeFormatter formatter, object value, Type type, MediaTypeHeaderValue mediaType)
  265. {
  266. if (formatter == null)
  267. {
  268. throw new ArgumentNullException("formatter");
  269. }
  270. if (mediaType == null)
  271. {
  272. throw new ArgumentNullException("mediaType");
  273. }
  274. object sample = String.Empty;
  275. MemoryStream ms = null;
  276. HttpContent content = null;
  277. try
  278. {
  279. if (formatter.CanWriteType(type))
  280. {
  281. ms = new MemoryStream();
  282. content = new ObjectContent(type, value, formatter, mediaType);
  283. formatter.WriteToStreamAsync(type, value, ms, content, null).Wait();
  284. ms.Position = 0;
  285. StreamReader reader = new StreamReader(ms);
  286. string serializedSampleString = reader.ReadToEnd();
  287. if (mediaType.MediaType.ToUpperInvariant().Contains("XML"))
  288. {
  289. serializedSampleString = TryFormatXml(serializedSampleString);
  290. }
  291. else if (mediaType.MediaType.ToUpperInvariant().Contains("JSON"))
  292. {
  293. serializedSampleString = TryFormatJson(serializedSampleString);
  294. }
  295. sample = new TextSample(serializedSampleString);
  296. }
  297. else
  298. {
  299. sample = new InvalidSample(String.Format(
  300. CultureInfo.CurrentCulture,
  301. "Failed to generate the sample for media type '{0}'. Cannot use formatter '{1}' to write type '{2}'.",
  302. mediaType,
  303. formatter.GetType().Name,
  304. type.Name));
  305. }
  306. }
  307. catch (Exception e)
  308. {
  309. sample = new InvalidSample(String.Format(
  310. CultureInfo.CurrentCulture,
  311. "An exception has occurred while using the formatter '{0}' to generate sample for media type '{1}'. Exception message: {2}",
  312. formatter.GetType().Name,
  313. mediaType.MediaType,
  314. UnwrapException(e).Message));
  315. }
  316. finally
  317. {
  318. if (ms != null)
  319. {
  320. ms.Dispose();
  321. }
  322. if (content != null)
  323. {
  324. content.Dispose();
  325. }
  326. }
  327. return sample;
  328. }
  329. internal static Exception UnwrapException(Exception exception)
  330. {
  331. AggregateException aggregateException = exception as AggregateException;
  332. if (aggregateException != null)
  333. {
  334. return aggregateException.Flatten().InnerException;
  335. }
  336. return exception;
  337. }
  338. // Default factory for sample objects
  339. private static object DefaultSampleObjectFactory(HelpPageSampleGenerator sampleGenerator, Type type)
  340. {
  341. // Try to create a default sample object
  342. ObjectGenerator objectGenerator = new ObjectGenerator();
  343. return objectGenerator.GenerateObject(type);
  344. }
  345. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Handling the failure by returning the original string.")]
  346. private static string TryFormatJson(string str)
  347. {
  348. try
  349. {
  350. object parsedJson = JsonConvert.DeserializeObject(str);
  351. return JsonConvert.SerializeObject(parsedJson, Formatting.Indented);
  352. }
  353. catch
  354. {
  355. // can't parse JSON, return the original string
  356. return str;
  357. }
  358. }
  359. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Handling the failure by returning the original string.")]
  360. private static string TryFormatXml(string str)
  361. {
  362. try
  363. {
  364. XDocument xml = XDocument.Parse(str);
  365. return xml.ToString();
  366. }
  367. catch
  368. {
  369. // can't parse XML, return the original string
  370. return str;
  371. }
  372. }
  373. private static bool IsFormatSupported(SampleDirection sampleDirection, MediaTypeFormatter formatter, Type type)
  374. {
  375. switch (sampleDirection)
  376. {
  377. case SampleDirection.Request:
  378. return formatter.CanReadType(type);
  379. case SampleDirection.Response:
  380. return formatter.CanWriteType(type);
  381. }
  382. return false;
  383. }
  384. private IEnumerable<KeyValuePair<HelpPageSampleKey, object>> GetAllActionSamples(string controllerName, string actionName, IEnumerable<string> parameterNames, SampleDirection sampleDirection)
  385. {
  386. HashSet<string> parameterNamesSet = new HashSet<string>(parameterNames, StringComparer.OrdinalIgnoreCase);
  387. foreach (var sample in ActionSamples)
  388. {
  389. HelpPageSampleKey sampleKey = sample.Key;
  390. if (String.Equals(controllerName, sampleKey.ControllerName, StringComparison.OrdinalIgnoreCase) &&
  391. String.Equals(actionName, sampleKey.ActionName, StringComparison.OrdinalIgnoreCase) &&
  392. (sampleKey.ParameterNames.SetEquals(new[] { "*" }) || parameterNamesSet.SetEquals(sampleKey.ParameterNames)) &&
  393. sampleDirection == sampleKey.SampleDirection)
  394. {
  395. yield return sample;
  396. }
  397. }
  398. }
  399. private static object WrapSampleIfString(object sample)
  400. {
  401. string stringSample = sample as string;
  402. if (stringSample != null)
  403. {
  404. return new TextSample(stringSample);
  405. }
  406. return sample;
  407. }
  408. }
  409. }