Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added WebAPI help page package

  • Loading branch information...
commit 4759fcdfcb3fa6d7d9875a780eda7932c03328dd 1 parent 92a0a81
@davidebbo davidebbo authored
Showing with 2,121 additions and 0 deletions.
  1. +39 −0 Kudu.Services.Web/Areas/HelpPage/ApiDescriptionExtensions.cs
  2. +51 −0 Kudu.Services.Web/Areas/HelpPage/App_Start/HelpPageConfig.cs
  3. +44 −0 Kudu.Services.Web/Areas/HelpPage/Controllers/HelpController.cs
  4. +19 −0 Kudu.Services.Web/Areas/HelpPage/HelpPage.css
  5. +248 −0 Kudu.Services.Web/Areas/HelpPage/HelpPageConfigurationExtensions.cs
  6. +43 −0 Kudu.Services.Web/Areas/HelpPage/Models/HelpPageApiModel.cs
  7. +373 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/HelpPageSampleGenerator.cs
  8. +178 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/HelpPageSampleKey.cs
  9. +41 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/ImageSample.cs
  10. +37 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/InvalidSample.cs
  11. +451 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/ObjectGenerator.cs
  12. +11 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/SampleDirection.cs
  13. +37 −0 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/TextSample.cs
  14. +33 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/Api.cshtml
  15. +30 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/ApiGroup.cshtml
  16. +56 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/HelpPageApiModel.cshtml
  17. +4 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/ImageSample.cshtml
  18. +11 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/InvalidSample.cshtml
  19. +42 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/Parameters.cshtml
  20. +37 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/Samples.cshtml
  21. +6 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/TextSample.cshtml
  22. +42 −0 Kudu.Services.Web/Areas/HelpPage/Views/Help/Index.cshtml
  23. +13 −0 Kudu.Services.Web/Areas/HelpPage/Views/Shared/_Layout.cshtml
  24. +62 −0 Kudu.Services.Web/Areas/HelpPage/Views/Web.config
  25. +4 −0 Kudu.Services.Web/Areas/HelpPage/Views/_ViewStart.cshtml
  26. +112 −0 Kudu.Services.Web/Areas/HelpPage/XmlDocumentationProvider.cs
  27. +26 −0 Kudu.Services.Web/Areas/HelpPageAreaRegistration.cs
  28. +1 −0  Kudu.Services.Web/Global.asax
  29. +14 −0 Kudu.Services.Web/Global.asax.cs
  30. +55 −0 Kudu.Services.Web/Kudu.Services.Web.csproj
  31. +1 −0  Kudu.Services.Web/packages.config
View
39 Kudu.Services.Web/Areas/HelpPage/ApiDescriptionExtensions.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Text;
+using System.Web;
+using System.Web.Http.Description;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ public static class ApiDescriptionExtensions
+ {
+ /// <summary>
+ /// Generates an URI-friendly ID for the <see cref="ApiDescription"/>. E.g. "Get-Values-id_name" instead of "GetValues/{id}?name={name}"
+ /// </summary>
+ /// <param name="description">The <see cref="ApiDescription"/>.</param>
+ /// <returns>The ID as a string.</returns>
+ public static string GetFriendlyId(this ApiDescription description)
+ {
+ string path = description.RelativePath;
+ string[] urlParts = path.Split('?');
+ string localPath = urlParts[0];
+ string queryKeyString = null;
+ if (urlParts.Length > 1)
+ {
+ string query = urlParts[1];
+ string[] queryKeys = HttpUtility.ParseQueryString(query).AllKeys;
+ queryKeyString = String.Join("_", queryKeys);
+ }
+
+ StringBuilder friendlyPath = new StringBuilder();
+ friendlyPath.AppendFormat("{0}-{1}",
+ description.HttpMethod.Method,
+ localPath.Replace("/", "-").Replace("{", "").Replace("}", ""));
+ if (queryKeyString != null)
+ {
+ friendlyPath.AppendFormat("_{0}", queryKeyString);
+ }
+ return friendlyPath.ToString();
+ }
+ }
+}
View
51 Kudu.Services.Web/Areas/HelpPage/App_Start/HelpPageConfig.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http.Headers;
+using System.Web;
+using System.Web.Http;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// Use this class to customize the Help Page.
+ /// For example you can set a custom <see cref="System.Web.Http.Description.IDocumentationProvider"/> to supply the documentation
+ /// or you can provide the samples for the requests/responses.
+ /// </summary>
+ public static class HelpPageConfig
+ {
+ public static void Register(HttpConfiguration config)
+ {
+ //// Uncomment the following to use the documentation from XML documentation file.
+ //config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
+
+ //// Uncomment the following to use "sample string" as the sample for all actions that have string as the body parameter or return type.
+ //// Also, the string arrays will be used for IEnumerable<string>. The sample objects will be serialized into different media type
+ //// formats by the available formatters.
+ //config.SetSampleObjects(new Dictionary<Type, object>
+ //{
+ // {typeof(string), "sample string"},
+ // {typeof(IEnumerable<string>), new string[]{"sample 1", "sample 2"}}
+ //});
+
+ //// Uncomment the following to use "[0]=foo&[1]=bar" directly as the sample for all actions that support form URL encoded format
+ //// and have IEnumerable<string> as the body parameter or return type.
+ //config.SetSampleForType("[0]=foo&[1]=bar", new MediaTypeHeaderValue("application/x-www-form-urlencoded"), typeof(IEnumerable<string>));
+
+ //// Uncomment the following to use "1234" directly as the request sample for media type "text/plain" on the controller named "Values"
+ //// and action named "Put".
+ //config.SetSampleRequest("1234", new MediaTypeHeaderValue("text/plain"), "Values", "Put");
+
+ //// Uncomment the following to use the image on "../images/aspNetHome.png" directly as the response sample for media type "image/png"
+ //// on the controller named "Values" and action named "Get" with parameter "id".
+ //config.SetSampleResponse(new ImageSample("../images/aspNetHome.png"), new MediaTypeHeaderValue("image/png"), "Values", "Get", "id");
+
+ //// Uncomment the following to correct the sample request when the action expects an HttpRequestMessage with ObjectContent<string>.
+ //// The sample will be generated as if the controller named "Values" and action named "Get" were having string as the body parameter.
+ //config.SetActualRequestType(typeof(string), "Values", "Get");
+
+ //// Uncomment the following to correct the sample response when the action returns an HttpResponseMessage with ObjectContent<string>.
+ //// The sample will be generated as if the controller named "Values" and action named "Post" were returning a string.
+ //config.SetActualResponseType(typeof(string), "Values", "Post");
+ }
+ }
+}
View
44 Kudu.Services.Web/Areas/HelpPage/Controllers/HelpController.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Web.Http;
+using System.Web.Mvc;
+using Kudu.Services.Web.Areas.HelpPage.Models;
+
+namespace Kudu.Services.Web.Areas.HelpPage.Controllers
+{
+ /// <summary>
+ /// The controller that will handle requests for the help page.
+ /// </summary>
+ public class HelpController : Controller
+ {
+ public HelpController()
+ : this(GlobalConfiguration.Configuration)
+ {
+ }
+
+ public HelpController(HttpConfiguration config)
+ {
+ Configuration = config;
+ }
+
+ public HttpConfiguration Configuration { get; private set; }
+
+ public ActionResult Index()
+ {
+ return View(Configuration.Services.GetApiExplorer().ApiDescriptions);
+ }
+
+ public ActionResult Api(string apiId)
+ {
+ if (!String.IsNullOrEmpty(apiId))
+ {
+ HelpPageApiModel apiModel = Configuration.GetHelpPageApiModel(apiId);
+ if (apiModel != null)
+ {
+ return View(apiModel);
+ }
+ }
+
+ return View("Error");
+ }
+ }
+}
View
19 Kudu.Services.Web/Areas/HelpPage/HelpPage.css
@@ -0,0 +1,19 @@
+pre.wrapped
+{
+ white-space: -moz-pre-wrap;
+ white-space: -pre-wrap;
+ white-space: -o-pre-wrap;
+ white-space: pre-wrap
+}
+
+.warning-message-container
+{
+ margin-top: 20px;
+ padding: 0 .7em;
+}
+
+.warning-message-icon
+{
+ float: left;
+ margin-right: .3em;
+}
View
248 Kudu.Services.Web/Areas/HelpPage/HelpPageConfigurationExtensions.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Web.Http;
+using System.Web.Http.Description;
+using Kudu.Services.Web.Areas.HelpPage.Models;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ public static class HelpPageConfigurationExtensions
+ {
+ private const string ApiModelPrefix = "MS_HelpPageApiModel_";
+
+ /// <summary>
+ /// Sets the documentation provider for help page.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="documentationProvider">The documentation provider.</param>
+ public static void SetDocumentationProvider(this HttpConfiguration config, IDocumentationProvider documentationProvider)
+ {
+ config.Services.Replace(typeof(IDocumentationProvider), documentationProvider);
+ }
+
+ /// <summary>
+ /// Sets the objects that will be used by the formatters to produce sample requests/responses.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sampleObjects">The sample objects.</param>
+ public static void SetSampleObjects(this HttpConfiguration config, IDictionary<Type, object> sampleObjects)
+ {
+ config.GetHelpPageSampleGenerator().SampleObjects = sampleObjects;
+ }
+
+ /// <summary>
+ /// Sets the sample request directly for the specified media type and action.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sample">The sample request.</param>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ public static void SetSampleRequest(this HttpConfiguration config, object sample, MediaTypeHeaderValue mediaType, string controllerName, string actionName)
+ {
+ config.GetHelpPageSampleGenerator().ActionSamples.Add(new HelpPageSampleKey(mediaType, SampleDirection.Request, controllerName, actionName, new[] { "*" }), sample);
+ }
+
+ /// <summary>
+ /// Sets the sample request directly for the specified media type and action with parameters.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sample">The sample request.</param>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ /// <param name="parameterNames">The parameter names.</param>
+ public static void SetSampleRequest(this HttpConfiguration config, object sample, MediaTypeHeaderValue mediaType, string controllerName, string actionName, params string[] parameterNames)
+ {
+ config.GetHelpPageSampleGenerator().ActionSamples.Add(new HelpPageSampleKey(mediaType, SampleDirection.Request, controllerName, actionName, parameterNames), sample);
+ }
+
+ /// <summary>
+ /// Sets the sample request directly for the specified media type of the action.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sample">The sample response.</param>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ public static void SetSampleResponse(this HttpConfiguration config, object sample, MediaTypeHeaderValue mediaType, string controllerName, string actionName)
+ {
+ config.GetHelpPageSampleGenerator().ActionSamples.Add(new HelpPageSampleKey(mediaType, SampleDirection.Response, controllerName, actionName, new[] { "*" }), sample);
+ }
+
+ /// <summary>
+ /// Sets the sample response directly for the specified media type of the action with specific parameters.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sample">The sample response.</param>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ /// <param name="parameterNames">The parameter names.</param>
+ public static void SetSampleResponse(this HttpConfiguration config, object sample, MediaTypeHeaderValue mediaType, string controllerName, string actionName, params string[] parameterNames)
+ {
+ config.GetHelpPageSampleGenerator().ActionSamples.Add(new HelpPageSampleKey(mediaType, SampleDirection.Response, controllerName, actionName, parameterNames), sample);
+ }
+
+ /// <summary>
+ /// Sets the sample directly for all actions with the specified type and media type.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sample">The sample.</param>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="type">The parameter type or return type of an action.</param>
+ public static void SetSampleForType(this HttpConfiguration config, object sample, MediaTypeHeaderValue mediaType, Type type)
+ {
+ config.GetHelpPageSampleGenerator().ActionSamples.Add(new HelpPageSampleKey(mediaType, type), sample);
+ }
+
+ /// <summary>
+ /// Specifies the actual type of <see cref="System.Net.Http.ObjectContent{T}"/> passed to the <see cref="System.Net.Http.HttpRequestMessage"/> in an action.
+ /// The help page will use this information to produce more accurate request samples.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ public static void SetActualRequestType(this HttpConfiguration config, Type type, string controllerName, string actionName)
+ {
+ config.GetHelpPageSampleGenerator().ActualHttpMessageTypes.Add(new HelpPageSampleKey(SampleDirection.Request, controllerName, actionName, new[] { "*" }), type);
+ }
+
+ /// <summary>
+ /// Specifies the actual type of <see cref="System.Net.Http.ObjectContent{T}"/> passed to the <see cref="System.Net.Http.HttpRequestMessage"/> in an action.
+ /// The help page will use this information to produce more accurate request samples.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ /// <param name="parameterNames">The parameter names.</param>
+ public static void SetActualRequestType(this HttpConfiguration config, Type type, string controllerName, string actionName, params string[] parameterNames)
+ {
+ config.GetHelpPageSampleGenerator().ActualHttpMessageTypes.Add(new HelpPageSampleKey(SampleDirection.Request, controllerName, actionName, parameterNames), type);
+ }
+
+ /// <summary>
+ /// Specifies the actual type of <see cref="System.Net.Http.ObjectContent{T}"/> returned as part of the <see cref="System.Net.Http.HttpRequestMessage"/> in an action.
+ /// The help page will use this information to produce more accurate response samples.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ public static void SetActualResponseType(this HttpConfiguration config, Type type, string controllerName, string actionName)
+ {
+ config.GetHelpPageSampleGenerator().ActualHttpMessageTypes.Add(new HelpPageSampleKey(SampleDirection.Response, controllerName, actionName, new[] { "*" }), type);
+ }
+
+ /// <summary>
+ /// Specifies the actual type of <see cref="System.Net.Http.ObjectContent{T}"/> returned as part of the <see cref="System.Net.Http.HttpRequestMessage"/> in an action.
+ /// The help page will use this information to produce more accurate response samples.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ /// <param name="parameterNames">The parameter names.</param>
+ public static void SetActualResponseType(this HttpConfiguration config, Type type, string controllerName, string actionName, params string[] parameterNames)
+ {
+ config.GetHelpPageSampleGenerator().ActualHttpMessageTypes.Add(new HelpPageSampleKey(SampleDirection.Response, controllerName, actionName, parameterNames), type);
+ }
+
+ /// <summary>
+ /// Gets the help page sample generator.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <returns>The help page sample generator.</returns>
+ public static HelpPageSampleGenerator GetHelpPageSampleGenerator(this HttpConfiguration config)
+ {
+ return (HelpPageSampleGenerator)config.Properties.GetOrAdd(
+ typeof(HelpPageSampleGenerator),
+ k => new HelpPageSampleGenerator());
+ }
+
+ /// <summary>
+ /// Sets the help page sample generator.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="sampleGenerator">The help page sample generator.</param>
+ public static void SetHelpPageSampleGenerator(this HttpConfiguration config, HelpPageSampleGenerator sampleGenerator)
+ {
+ config.Properties.AddOrUpdate(
+ typeof(HelpPageSampleGenerator),
+ k => sampleGenerator,
+ (k, o) => sampleGenerator);
+ }
+
+ /// <summary>
+ /// Gets the model that represents an API displayed on the help page. The model is initialized on the first call and cached for subsequent calls.
+ /// </summary>
+ /// <param name="config">The <see cref="HttpConfiguration"/>.</param>
+ /// <param name="apiDescriptionId">The <see cref="ApiDescription"/> ID.</param>
+ /// <returns>
+ /// An <see cref="HelpPageApiModel"/>
+ /// </returns>
+ public static HelpPageApiModel GetHelpPageApiModel(this HttpConfiguration config, string apiDescriptionId)
+ {
+ object model;
+ string modelId = ApiModelPrefix + apiDescriptionId;
+ if (!config.Properties.TryGetValue(modelId, out model))
+ {
+ Collection<ApiDescription> apiDescriptions = config.Services.GetApiExplorer().ApiDescriptions;
+ ApiDescription apiDescription = apiDescriptions.FirstOrDefault(api => String.Equals(api.GetFriendlyId(), apiDescriptionId, StringComparison.OrdinalIgnoreCase));
+ if (apiDescription != null)
+ {
+ HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
+ model = GenerateApiModel(apiDescription, sampleGenerator);
+ config.Properties.TryAdd(modelId, model);
+ }
+ }
+
+ return (HelpPageApiModel)model;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is recorded as ErrorMessages.")]
+ private static HelpPageApiModel GenerateApiModel(ApiDescription apiDescription, HelpPageSampleGenerator sampleGenerator)
+ {
+ HelpPageApiModel apiModel = new HelpPageApiModel();
+ apiModel.ApiDescription = apiDescription;
+
+ try
+ {
+ foreach (var item in sampleGenerator.GetSampleRequests(apiDescription))
+ {
+ apiModel.SampleRequests.Add(item.Key, item.Value);
+ LogInvalidSampleAsError(apiModel, item.Value);
+
+ }
+
+ foreach (var item in sampleGenerator.GetSampleResponses(apiDescription))
+ {
+ apiModel.SampleResponses.Add(item.Key, item.Value);
+ LogInvalidSampleAsError(apiModel, item.Value);
+ }
+ }
+ catch (Exception e)
+ {
+ apiModel.ErrorMessages.Add(String.Format(CultureInfo.CurrentCulture, "An exception has occurred while generating the sample. Exception Message: {0}", e.Message));
+ }
+
+ return apiModel;
+ }
+
+ private static void LogInvalidSampleAsError(HelpPageApiModel apiModel, object sample)
+ {
+ InvalidSample invalidSample = sample as InvalidSample;
+ if (invalidSample != null)
+ {
+ apiModel.ErrorMessages.Add(invalidSample.ErrorMessage);
+ }
+ }
+ }
+}
View
43 Kudu.Services.Web/Areas/HelpPage/Models/HelpPageApiModel.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http.Headers;
+using System.Web.Http.Description;
+
+namespace Kudu.Services.Web.Areas.HelpPage.Models
+{
+ /// <summary>
+ /// The model that represents an API displayed on the help page.
+ /// </summary>
+ public class HelpPageApiModel
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HelpPageApiModel"/> class.
+ /// </summary>
+ public HelpPageApiModel()
+ {
+ SampleRequests = new Dictionary<MediaTypeHeaderValue, object>();
+ SampleResponses = new Dictionary<MediaTypeHeaderValue, object>();
+ ErrorMessages = new Collection<string>();
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="ApiDescription"/> that describes the API.
+ /// </summary>
+ public ApiDescription ApiDescription { get; set; }
+
+ /// <summary>
+ /// Gets the sample requests associated with the API.
+ /// </summary>
+ public IDictionary<MediaTypeHeaderValue, object> SampleRequests { get; private set; }
+
+ /// <summary>
+ /// Gets the sample responses associated with the API.
+ /// </summary>
+ public IDictionary<MediaTypeHeaderValue, object> SampleResponses { get; private set; }
+
+ /// <summary>
+ /// Gets the error messages associated with this model.
+ /// </summary>
+ public Collection<string> ErrorMessages { get; private set; }
+ }
+}
View
373 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/HelpPageSampleGenerator.cs
@@ -0,0 +1,373 @@
+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 Kudu.Services.Web.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.ActionDescriptor.ReturnType;
+ 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.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The sample generation is done synchronously.")]
+ 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;
+ }
+ }
+}
View
178 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/HelpPageSampleKey.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Net.Http.Headers;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// This is used to identify the place where the sample should be applied.
+ /// </summary>
+ public class HelpPageSampleKey
+ {
+ /// <summary>
+ /// Creates a new <see cref="HelpPageSampleKey"/> based on media type and CLR type.
+ /// </summary>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="type">The CLR type.</param>
+ public HelpPageSampleKey(MediaTypeHeaderValue mediaType, Type type)
+ {
+ if (mediaType == null)
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ ControllerName = string.Empty;
+ ActionName = string.Empty;
+ ParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ ParameterType = type;
+ MediaType = mediaType;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="HelpPageSampleKey"/> based on <see cref="SampleDirection"/>, controller name, action name and parameter names.
+ /// </summary>
+ /// <param name="sampleDirection">The <see cref="SampleDirection"/>.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ /// <param name="parameterNames">The parameter names.</param>
+ public HelpPageSampleKey(SampleDirection sampleDirection, string controllerName, string actionName, IEnumerable<string> parameterNames)
+ {
+ if (!Enum.IsDefined(typeof(SampleDirection), sampleDirection))
+ {
+ throw new InvalidEnumArgumentException("sampleDirection", (int)sampleDirection, typeof(SampleDirection));
+ }
+ if (controllerName == null)
+ {
+ throw new ArgumentNullException("controllerName");
+ }
+ if (actionName == null)
+ {
+ throw new ArgumentNullException("actionName");
+ }
+ if (parameterNames == null)
+ {
+ throw new ArgumentNullException("parameterNames");
+ }
+ ControllerName = controllerName;
+ ActionName = actionName;
+ ParameterNames = new HashSet<string>(parameterNames, StringComparer.OrdinalIgnoreCase);
+ SampleDirection = sampleDirection;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="HelpPageSampleKey"/> based on media type, <see cref="SampleDirection"/>, controller name, action name and parameter names.
+ /// </summary>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="sampleDirection">The <see cref="SampleDirection"/>.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <param name="actionName">Name of the action.</param>
+ /// <param name="parameterNames">The parameter names.</param>
+ public HelpPageSampleKey(MediaTypeHeaderValue mediaType, SampleDirection sampleDirection, string controllerName, string actionName, IEnumerable<string> parameterNames)
+ {
+ if (mediaType == null)
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+ if (!Enum.IsDefined(typeof(SampleDirection), sampleDirection))
+ {
+ throw new InvalidEnumArgumentException("sampleDirection", (int)sampleDirection, typeof(SampleDirection));
+ }
+ if (controllerName == null)
+ {
+ throw new ArgumentNullException("controllerName");
+ }
+ if (actionName == null)
+ {
+ throw new ArgumentNullException("actionName");
+ }
+ if (parameterNames == null)
+ {
+ throw new ArgumentNullException("parameterNames");
+ }
+ ControllerName = controllerName;
+ ActionName = actionName;
+ MediaType = mediaType;
+ ParameterNames = new HashSet<string>(parameterNames, StringComparer.OrdinalIgnoreCase);
+ SampleDirection = sampleDirection;
+ }
+
+ /// <summary>
+ /// Gets the name of the controller.
+ /// </summary>
+ /// <value>
+ /// The name of the controller.
+ /// </value>
+ public string ControllerName { get; private set; }
+
+ /// <summary>
+ /// Gets the name of the action.
+ /// </summary>
+ /// <value>
+ /// The name of the action.
+ /// </value>
+ public string ActionName { get; private set; }
+
+ /// <summary>
+ /// Gets the media type.
+ /// </summary>
+ /// <value>
+ /// The media type.
+ /// </value>
+ public MediaTypeHeaderValue MediaType { get; private set; }
+
+ /// <summary>
+ /// Gets the parameter names.
+ /// </summary>
+ public HashSet<string> ParameterNames { get; private set; }
+
+ public Type ParameterType { get; private set; }
+
+ /// <summary>
+ /// Gets the <see cref="SampleDirection"/>.
+ /// </summary>
+ public SampleDirection? SampleDirection { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ HelpPageSampleKey otherKey = obj as HelpPageSampleKey;
+ if (otherKey == null)
+ {
+ return false;
+ }
+
+ return String.Equals(ControllerName, otherKey.ControllerName, StringComparison.OrdinalIgnoreCase) &&
+ String.Equals(ActionName, otherKey.ActionName, StringComparison.OrdinalIgnoreCase) &&
+ (MediaType == otherKey.MediaType || (MediaType != null && MediaType.Equals(otherKey.MediaType))) &&
+ ParameterType == otherKey.ParameterType &&
+ SampleDirection == otherKey.SampleDirection &&
+ ParameterNames.SetEquals(otherKey.ParameterNames);
+ }
+
+ public override int GetHashCode()
+ {
+ int hashCode = ControllerName.ToUpperInvariant().GetHashCode() ^ ActionName.ToUpperInvariant().GetHashCode();
+ if (MediaType != null)
+ {
+ hashCode ^= MediaType.GetHashCode();
+ }
+ if (SampleDirection != null)
+ {
+ hashCode ^= SampleDirection.GetHashCode();
+ }
+ if (ParameterType != null)
+ {
+ hashCode ^= ParameterType.GetHashCode();
+ }
+ foreach (string parameterName in ParameterNames)
+ {
+ hashCode ^= parameterName.ToUpperInvariant().GetHashCode();
+ }
+
+ return hashCode;
+ }
+ }
+}
View
41 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/ImageSample.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// This represents an image sample on the help page. There's a display template named ImageSample associated with this class.
+ /// </summary>
+ public class ImageSample
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ImageSample"/> class.
+ /// </summary>
+ /// <param name="src">The URL of an image.</param>
+ public ImageSample(string src)
+ {
+ if (src == null)
+ {
+ throw new ArgumentNullException("src");
+ }
+ Src = src;
+ }
+
+ public string Src { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ ImageSample other = obj as ImageSample;
+ return other != null && Src == other.Src;
+ }
+
+ public override int GetHashCode()
+ {
+ return Src.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return Src;
+ }
+ }
+}
View
37 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/InvalidSample.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// This represents an invalid sample on the help page. There's a display template named InvalidSample associated with this class.
+ /// </summary>
+ public class InvalidSample
+ {
+ public InvalidSample(string errorMessage)
+ {
+ if (errorMessage == null)
+ {
+ throw new ArgumentNullException("errorMessage");
+ }
+ ErrorMessage = errorMessage;
+ }
+
+ public string ErrorMessage { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ InvalidSample other = obj as InvalidSample;
+ return other != null && ErrorMessage == other.ErrorMessage;
+ }
+
+ public override int GetHashCode()
+ {
+ return ErrorMessage.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return ErrorMessage;
+ }
+ }
+}
View
451 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/ObjectGenerator.cs
@@ -0,0 +1,451 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// This class will create an object of a given type and populate it with sample data.
+ /// </summary>
+ public class ObjectGenerator
+ {
+ private const int DefaultCollectionSize = 3;
+ private readonly SimpleTypeObjectGenerator SimpleObjectGenerator = new SimpleTypeObjectGenerator();
+
+ /// <summary>
+ /// Generates an object for a given type. The type needs to be public, have a public default constructor and settable public properties/fields. Currently it supports the following types:
+ /// Simple types: <see cref="int"/>, <see cref="string"/>, <see cref="Enum"/>, <see cref="DateTime"/>, <see cref="Uri"/>, etc.
+ /// Complex types: POCO types.
+ /// Nullables: <see cref="Nullable{T}"/>.
+ /// Arrays: arrays of simple types or complex types.
+ /// Key value pairs: <see cref="KeyValuePair{TKey,TValue}"/>
+ /// Tuples: <see cref="Tuple{T1}"/>, <see cref="Tuple{T1,T2}"/>, etc
+ /// Dictionaries: <see cref="IDictionary{TKey,TValue}"/> or anything deriving from <see cref="IDictionary{TKey,TValue}"/>.
+ /// Collections: <see cref="IList{T}"/>, <see cref="IEnumerable{T}"/>, <see cref="ICollection{T}"/>, <see cref="IList"/>, <see cref="IEnumerable"/>, <see cref="ICollection"/> or anything deriving from <see cref="ICollection{T}"/> or <see cref="IList"/>.
+ /// Queryables: <see cref="IQueryable"/>, <see cref="IQueryable{T}"/>.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>An object of the given type.</returns>
+ public object GenerateObject(Type type)
+ {
+ return GenerateObject(type, new Dictionary<Type, object>());
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Here we just want to return null if anything goes wrong.")]
+ private object GenerateObject(Type type, Dictionary<Type, object> createdObjectReferences)
+ {
+ try
+ {
+ if (SimpleTypeObjectGenerator.CanGenerateObject(type))
+ {
+ return SimpleObjectGenerator.GenerateObject(type);
+ }
+
+ if (type.IsArray)
+ {
+ return GenerateArray(type, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (type.IsGenericType)
+ {
+ return GenerateGenericType(type, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (type == typeof(IDictionary))
+ {
+ return GenerateDictionary(typeof(Hashtable), DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (typeof(IDictionary).IsAssignableFrom(type))
+ {
+ return GenerateDictionary(type, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (type == typeof(IList) ||
+ type == typeof(IEnumerable) ||
+ type == typeof(ICollection))
+ {
+ return GenerateCollection(typeof(ArrayList), DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (typeof(IList).IsAssignableFrom(type))
+ {
+ return GenerateCollection(type, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (type == typeof(IQueryable))
+ {
+ return GenerateQueryable(type, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (type.IsEnum)
+ {
+ return GenerateEnum(type);
+ }
+
+ if (type.IsPublic || type.IsNestedPublic)
+ {
+ return GenerateComplexObject(type, createdObjectReferences);
+ }
+ }
+ catch
+ {
+ // Returns null if anything fails
+ return null;
+ }
+
+ return null;
+ }
+
+ private static object GenerateGenericType(Type type, int DefaultCollectionSize, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type genericTypeDefinition = type.GetGenericTypeDefinition();
+ if (genericTypeDefinition == typeof(Nullable<>))
+ {
+ return GenerateNullable(type, createdObjectReferences);
+ }
+
+ if (genericTypeDefinition == typeof(KeyValuePair<,>))
+ {
+ return GenerateKeyValuePair(type, createdObjectReferences);
+ }
+
+ if (IsTuple(genericTypeDefinition))
+ {
+ return GenerateTuple(type, createdObjectReferences);
+ }
+
+ Type[] genericArguments = type.GetGenericArguments();
+ if (genericArguments.Length == 1)
+ {
+ if (genericTypeDefinition == typeof(IList<>) ||
+ genericTypeDefinition == typeof(IEnumerable<>) ||
+ genericTypeDefinition == typeof(ICollection<>))
+ {
+ Type collectionType = typeof(List<>).MakeGenericType(genericArguments);
+ return GenerateCollection(collectionType, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ if (genericTypeDefinition == typeof(IQueryable<>))
+ {
+ return GenerateQueryable(type, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ Type closedCollectionType = typeof(ICollection<>).MakeGenericType(genericArguments[0]);
+ if (closedCollectionType.IsAssignableFrom(type))
+ {
+ return GenerateCollection(type, DefaultCollectionSize, createdObjectReferences);
+ }
+ }
+
+ if (genericArguments.Length == 2)
+ {
+ if (genericTypeDefinition == typeof(IDictionary<,>))
+ {
+ Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(genericArguments);
+ return GenerateDictionary(dictionaryType, DefaultCollectionSize, createdObjectReferences);
+ }
+
+ Type closedDictionaryType = typeof(IDictionary<,>).MakeGenericType(genericArguments[0], genericArguments[1]);
+ if (closedDictionaryType.IsAssignableFrom(type))
+ {
+ return GenerateDictionary(type, DefaultCollectionSize, createdObjectReferences);
+ }
+ }
+
+ if (type.IsPublic || type.IsNestedPublic)
+ {
+ return GenerateComplexObject(type, createdObjectReferences);
+ }
+
+ return null;
+ }
+
+ private static object GenerateTuple(Type type, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type[] genericArgs = type.GetGenericArguments();
+ object[] parameterValues = new object[genericArgs.Length];
+ bool failedToCreateTuple = true;
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ for (int i = 0; i < genericArgs.Length; i++)
+ {
+ parameterValues[i] = objectGenerator.GenerateObject(genericArgs[i], createdObjectReferences);
+ failedToCreateTuple &= parameterValues[i] == null;
+ }
+ if (failedToCreateTuple)
+ {
+ return null;
+ }
+ object result = Activator.CreateInstance(type, parameterValues);
+ return result;
+ }
+
+ private static bool IsTuple(Type genericTypeDefinition)
+ {
+ return genericTypeDefinition == typeof(Tuple<>) ||
+ genericTypeDefinition == typeof(Tuple<,>) ||
+ genericTypeDefinition == typeof(Tuple<,,>) ||
+ genericTypeDefinition == typeof(Tuple<,,,>) ||
+ genericTypeDefinition == typeof(Tuple<,,,,>) ||
+ genericTypeDefinition == typeof(Tuple<,,,,,>) ||
+ genericTypeDefinition == typeof(Tuple<,,,,,,>) ||
+ genericTypeDefinition == typeof(Tuple<,,,,,,,>);
+ }
+
+ private static object GenerateKeyValuePair(Type keyValuePairType, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type[] genericArgs = keyValuePairType.GetGenericArguments();
+ Type typeK = genericArgs[0];
+ Type typeV = genericArgs[1];
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ object keyObject = objectGenerator.GenerateObject(typeK, createdObjectReferences);
+ object valueObject = objectGenerator.GenerateObject(typeV, createdObjectReferences);
+ if (keyObject == null && valueObject == null)
+ {
+ // Failed to create key and values
+ return null;
+ }
+ object result = Activator.CreateInstance(keyValuePairType, keyObject, valueObject);
+ return result;
+ }
+
+ private static object GenerateArray(Type arrayType, int size, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type type = arrayType.GetElementType();
+ Array result = Array.CreateInstance(type, size);
+ bool areAllElementsNull = true;
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ for (int i = 0; i < size; i++)
+ {
+ object element = objectGenerator.GenerateObject(type, createdObjectReferences);
+ result.SetValue(element, i);
+ areAllElementsNull &= element == null;
+ }
+
+ if (areAllElementsNull)
+ {
+ return null;
+ }
+
+ return result;
+ }
+
+ private static object GenerateDictionary(Type dictionaryType, int size, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type typeK = typeof(object);
+ Type typeV = typeof(object);
+ if (dictionaryType.IsGenericType)
+ {
+ Type[] genericArgs = dictionaryType.GetGenericArguments();
+ typeK = genericArgs[0];
+ typeV = genericArgs[1];
+ }
+
+ object result = Activator.CreateInstance(dictionaryType);
+ MethodInfo addMethod = dictionaryType.GetMethod("Add") ?? dictionaryType.GetMethod("TryAdd");
+ MethodInfo containsMethod = dictionaryType.GetMethod("Contains") ?? dictionaryType.GetMethod("ContainsKey");
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ for (int i = 0; i < size; i++)
+ {
+ object newKey = objectGenerator.GenerateObject(typeK, createdObjectReferences);
+ if (newKey == null)
+ {
+ // Cannot generate a valid key
+ return null;
+ }
+
+ bool containsKey = (bool)containsMethod.Invoke(result, new object[] { newKey });
+ if (!containsKey)
+ {
+ object newValue = objectGenerator.GenerateObject(typeV, createdObjectReferences);
+ addMethod.Invoke(result, new object[] { newKey, newValue });
+ }
+ }
+
+ return result;
+ }
+
+ private static object GenerateEnum(Type enumType)
+ {
+ Array possibleValues = Enum.GetValues(enumType);
+ if (possibleValues.Length > 0)
+ {
+ return possibleValues.GetValue(0);
+ }
+ return null;
+ }
+
+ private static object GenerateQueryable(Type queryableType, int size, Dictionary<Type, object> createdObjectReferences)
+ {
+ bool isGeneric = queryableType.IsGenericType;
+ object list;
+ if (isGeneric)
+ {
+ Type listType = typeof(List<>).MakeGenericType(queryableType.GetGenericArguments());
+ list = GenerateCollection(listType, size, createdObjectReferences);
+ }
+ else
+ {
+ list = GenerateArray(typeof(object[]), size, createdObjectReferences);
+ }
+ if (list == null)
+ {
+ return null;
+ }
+ if (isGeneric)
+ {
+ Type argumentType = typeof(IEnumerable<>).MakeGenericType(queryableType.GetGenericArguments());
+ MethodInfo asQueryableMethod = typeof(Queryable).GetMethod("AsQueryable", new[] { argumentType });
+ return asQueryableMethod.Invoke(null, new[] { list });
+ }
+
+ return Queryable.AsQueryable((IEnumerable)list);
+ }
+
+ private static object GenerateCollection(Type collectionType, int size, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type type = collectionType.IsGenericType ?
+ collectionType.GetGenericArguments()[0] :
+ typeof(object);
+ object result = Activator.CreateInstance(collectionType);
+ MethodInfo addMethod = collectionType.GetMethod("Add");
+ bool areAllElementsNull = true;
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ for (int i = 0; i < size; i++)
+ {
+ object element = objectGenerator.GenerateObject(type, createdObjectReferences);
+ addMethod.Invoke(result, new object[] { element });
+ areAllElementsNull &= element == null;
+ }
+
+ if (areAllElementsNull)
+ {
+ return null;
+ }
+
+ return result;
+ }
+
+ private static object GenerateNullable(Type nullableType, Dictionary<Type, object> createdObjectReferences)
+ {
+ Type type = nullableType.GetGenericArguments()[0];
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ return objectGenerator.GenerateObject(type, createdObjectReferences);
+ }
+
+ private static object GenerateComplexObject(Type type, Dictionary<Type, object> createdObjectReferences)
+ {
+ object result = null;
+
+ if (createdObjectReferences.TryGetValue(type, out result))
+ {
+ // The object has been created already, just return it. This will handle the circular reference case.
+ return result;
+ }
+
+ if (type.IsValueType)
+ {
+ result = Activator.CreateInstance(type);
+ }
+ else
+ {
+ ConstructorInfo defaultCtor = type.GetConstructor(Type.EmptyTypes);
+ if (defaultCtor == null)
+ {
+ // Cannot instantiate the type because it doesn't have a default constructor
+ return null;
+ }
+
+ result = defaultCtor.Invoke(new object[0]);
+ }
+ createdObjectReferences.Add(type, result);
+ SetPublicProperties(type, result, createdObjectReferences);
+ SetPublicFields(type, result, createdObjectReferences);
+ return result;
+ }
+
+ private static void SetPublicProperties(Type type, object obj, Dictionary<Type, object> createdObjectReferences)
+ {
+ PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ foreach (PropertyInfo property in properties)
+ {
+ if (property.CanWrite)
+ {
+ object propertyValue = objectGenerator.GenerateObject(property.PropertyType, createdObjectReferences);
+ property.SetValue(obj, propertyValue, null);
+ }
+ }
+ }
+
+ private static void SetPublicFields(Type type, object obj, Dictionary<Type, object> createdObjectReferences)
+ {
+ FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
+ ObjectGenerator objectGenerator = new ObjectGenerator();
+ foreach (FieldInfo field in fields)
+ {
+ object fieldValue = objectGenerator.GenerateObject(field.FieldType, createdObjectReferences);
+ field.SetValue(obj, fieldValue);
+ }
+ }
+
+ private class SimpleTypeObjectGenerator
+ {
+ private static readonly Dictionary<Type, Func<long, object>> DefaultGenerators = InitializeGenerators();
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple type factories and cannot be split up.")]
+ private static Dictionary<Type, Func<long, object>> InitializeGenerators()
+ {
+ return new Dictionary<Type, Func<long, object>>
+ {
+ {typeof(Boolean), index => true},
+ {typeof(Byte), index => (Byte)64},
+ {typeof(Char), index => (Char)65},
+ {typeof(DateTime), index => DateTime.Now},
+ {typeof(DateTimeOffset), index => new DateTimeOffset(DateTime.Now)},
+ {typeof(DBNull), index => DBNull.Value},
+ {typeof(Decimal), index => (Decimal)index},
+ {typeof(Double), index => (Double)(index + 0.1)},
+ {typeof(Guid), index => Guid.NewGuid()},
+ {typeof(Int16), index => (Int16)(index % Int16.MaxValue)},
+ {typeof(Int32), index => (Int32)(index % Int32.MaxValue)},
+ {typeof(Int64), index => (Int64)index},
+ {typeof(Object), index => new object()},
+ {typeof(SByte), index => (SByte)64},
+ {typeof(Single), index => (Single)(index + 0.1)},
+ {typeof(String), index =>
+ {
+ return String.Format(CultureInfo.CurrentCulture, "sample string {0}", index);
+ }},
+ {typeof(TimeSpan), index =>
+ {
+ return TimeSpan.FromTicks(1234567);
+ }},
+ {typeof(UInt16), index => (UInt16)(index % UInt16.MaxValue)},
+ {typeof(UInt32), index => (UInt32)(index % UInt32.MaxValue)},
+ {typeof(UInt64), index => (UInt64)index},
+ {typeof(Uri), index =>
+ {
+ return new Uri(String.Format(CultureInfo.CurrentCulture, "http://webapihelppage{0}.com", index));
+ }},
+ };
+ }
+
+ private long _index = 0;
+
+ public static bool CanGenerateObject(Type type)
+ {
+ return DefaultGenerators.ContainsKey(type);
+ }
+
+ public object GenerateObject(Type type)
+ {
+ return DefaultGenerators[type](++_index);
+ }
+ }
+ }
+}
View
11 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/SampleDirection.cs
@@ -0,0 +1,11 @@
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// Indicates whether the sample is used for request or response
+ /// </summary>
+ public enum SampleDirection
+ {
+ Request = 0,
+ Response
+ }
+}
View
37 Kudu.Services.Web/Areas/HelpPage/SampleGeneration/TextSample.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// This represents a preformatted text sample on the help page. There's a display template named TextSample associated with this class.
+ /// </summary>
+ public class TextSample
+ {
+ public TextSample(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+ Text = text;
+ }
+
+ public string Text { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ TextSample other = obj as TextSample;
+ return other != null && Text == other.Text;
+ }
+
+ public override int GetHashCode()
+ {
+ return Text.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return Text;
+ }
+ }
+}
View
33 Kudu.Services.Web/Areas/HelpPage/Views/Help/Api.cshtml
@@ -0,0 +1,33 @@
+@using System.Web.Http
+@using Kudu.Services.Web.Areas.HelpPage.Models
+@model HelpPageApiModel
+
+@{
+ var description = Model.ApiDescription;
+ ViewBag.Title = description.HttpMethod.Method + " " + description.RelativePath;
+}
+
+<div id="body">
+ <section class="featured">
+ <div class="content-wrapper">
+ <p>
+ @Html.ActionLink("Help Page Index", "Index")
+ </p>
+ </div>
+ </section>
+ <section class="content-wrapper main-content clear-fix">
+ @Html.DisplayFor(m => Model)
+ </section>
+</div>
+
+@section Scripts {
+ <link type="text/css" href="~/Areas/HelpPage/HelpPage.css" rel="stylesheet" />
+ <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/jquery-ui.min.js"></script>
+ <link type="text/css" href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/themes/smoothness/jquery-ui.css" rel="stylesheet" />
+ <script>
+ $(function () {
+ $('.mediaTypeTabs').tabs();
+ });
+ </script>
+}
+
View
30 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/ApiGroup.cshtml
@@ -0,0 +1,30 @@
+@using System.Web.Http
+@using System.Web.Http.Description
+@using Kudu.Services.Web.Areas.HelpPage
+@using Kudu.Services.Web.Areas.HelpPage.Models
+@model IGrouping<string, ApiDescription>
+
+<h2 id="@Model.Key">@Model.Key</h2>
+<table>
+ <thead class="ui-widget-header">
+ <tr><th>API</th><th>Description</th></tr>
+ </thead>
+ <tbody class="ui-widget-content">
+ @foreach (var api in Model)
+ {
+ <tr>
+ <td><a href="@Url.Action("Api", "Help", new { apiId = api.GetFriendlyId() })">@api.HttpMethod.Method @api.RelativePath</a></td>
+ <td>
+ @if (api.Documentation != null)
+ {
+ <p>@api.Documentation</p>
+ }
+ else
+ {
+ <p>No documentation available.</p>
+ }
+ </td>
+ </tr>
+ }
+ </tbody>
+</table>
View
56 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/HelpPageApiModel.cshtml
@@ -0,0 +1,56 @@
+@using System.Web.Http
+@using Kudu.Services.Web.Areas.HelpPage.Models
+@model HelpPageApiModel
+
+@{
+ var description = Model.ApiDescription;
+ bool hasParameters = description.ParameterDescriptions.Count > 0;
+ bool hasRequestSamples = Model.SampleRequests.Count > 0;
+ bool hasResponseSamples = Model.SampleResponses.Count > 0;
+}
+<h1>@description.HttpMethod.Method @description.RelativePath</h1>
+<div>
+ @if (description.Documentation != null)
+ {
+ <p>@description.Documentation</p>
+ }
+ else
+ {
+ <p>No documentation available.</p>
+ }
+
+ @if (hasParameters || hasRequestSamples)
+ {
+ <h2>Request Information</h2>
+ if (hasParameters)
+ {
+ <h3>Parameters</h3>
+ @Html.DisplayFor(apiModel => apiModel.ApiDescription.ParameterDescriptions, "Parameters")
+ }
+ if (hasRequestSamples)
+ {
+ <h3>Request body formats</h3>
+ @Html.DisplayFor(apiModel => apiModel.SampleRequests, "Samples", new { sampleClass = "request" })
+ }
+ }
+
+ @if (hasResponseSamples)
+ {
+ <h2>Response Information</h2>
+ <h3>Response body formats</h3>
+ @Html.DisplayFor(apiModel => apiModel.SampleResponses, "Samples", new { sampleClass = "response" })
+ }
+</div>
+<br />
+@if (HttpContext.Current.IsDebuggingEnabled && Model.ErrorMessages.Count > 0)
+{
+ foreach (string errorMessage in Model.ErrorMessages)
+ {
+ <div class="ui-state-highlight ui-corner-all warning-message-container">
+ <p>
+ <span class="ui-icon ui-icon-info warning-message-icon"></span>
+ @errorMessage
+ </p>
+ </div>
+ }
+}
View
4 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/ImageSample.cshtml
@@ -0,0 +1,4 @@
+@using Kudu.Services.Web.Areas.HelpPage
+@model ImageSample
+
+<img src="@Model.Src" />
View
11 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/InvalidSample.cshtml
@@ -0,0 +1,11 @@
+@using Kudu.Services.Web.Areas.HelpPage
+@model InvalidSample
+
+@if (HttpContext.Current.IsDebuggingEnabled)
+{
+ <pre class="wrapped ui-state-error">@Model.ErrorMessage</pre>
+}
+else
+{
+ <p>Sample not available.</p>
+}
View
42 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/Parameters.cshtml
@@ -0,0 +1,42 @@
+@using System.Collections.ObjectModel
+@using System.Web.Http.Description
+@using System.Threading
+@model Collection<ApiParameterDescription>
+
+<table>
+ <thead class="ui-widget-header">
+ <tr><th>Name</th><th>Description</th><th>Additional information</th></tr>
+ </thead>
+ <tbody class="ui-widget-content">
+ @foreach (ApiParameterDescription parameter in Model)
+ {
+ string parameterDocumentation = parameter.Documentation != null ?
+ parameter.Documentation :
+ "No documentation available.";
+
+ // Don't show CancellationToken because it's a special parameter
+ if (!typeof(CancellationToken).IsAssignableFrom(parameter.ParameterDescriptor.ParameterType))
+ {
+ <tr>
+ <td><b>@parameter.Name</b></td>
+ <td><pre>@parameterDocumentation</pre></td>
+ <td>
+ @switch (parameter.Source)
+ {
+ case ApiParameterSource.FromBody:
+ <p>Define this parameter in the request <b>body</b>.</p>
+ break;
+ case ApiParameterSource.FromUri:
+ <p>Define this parameter in the request <b>URI</b>.</p>
+ break;
+ case ApiParameterSource.Unknown:
+ default:
+ <p>None.</p>
+ break;
+ }
+ </td>
+ </tr>
+ }
+ }
+ </tbody>
+</table>
View
37 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/Samples.cshtml
@@ -0,0 +1,37 @@
+@using System.Net.Http.Headers
+@model Dictionary<MediaTypeHeaderValue, object>
+
+@{
+ // Group the samples into a single tab if they are the same.
+ Dictionary<string, object> samples = Model.GroupBy(pair => pair.Value).ToDictionary(
+ pair => String.Join(", ", pair.Select(m => m.Key.ToString()).ToArray()),
+ pair => pair.Key);
+ var mediaTypes = samples.Keys;
+}
+<div class="mediaTypeTabs">
+ <ul>
+ @{ int id = 0; }
+ @foreach (var mediaType in mediaTypes)
+ {
+ <li><a href="#@ViewData["sampleClass"]_@(id++)">@mediaType</a></li>
+ }
+ </ul>
+ @{ id = 0; }
+ @foreach (var mediaType in mediaTypes)
+ {
+ <div id="@ViewData["sampleClass"]_@(id++)">
+ <h3>Sample:</h3>
+ @{
+ var sample = samples[mediaType];
+ if (sample == null)
+ {
+ <p>Sample not available.</p>
+ }
+ else
+ {
+ @Html.DisplayFor(s => sample);
+ }
+ }
+ </div>
+ }
+</div>
View
6 Kudu.Services.Web/Areas/HelpPage/Views/Help/DisplayTemplates/TextSample.cshtml
@@ -0,0 +1,6 @@
+@using Kudu.Services.Web.Areas.HelpPage
+@model TextSample
+
+<pre class="wrapped">
+@Model.Text
+</pre>
View
42 Kudu.Services.Web/Areas/HelpPage/Views/Help/Index.cshtml
@@ -0,0 +1,42 @@
+@using System.Web.Http
+@using System.Web.Http.Description
+@using System.Collections.ObjectModel
+@using Kudu.Services.Web.Areas.HelpPage.Models
+@model Collection<ApiDescription>
+
+@{
+ ViewBag.Title = "ASP.NET Web API Help Page";
+
+ // Group APIs by controller
+ ILookup<string, ApiDescription> apiGroups = Model.ToLookup(api => api.ActionDescriptor.ControllerDescriptor.ControllerName);
+}
+
+<header>
+ <div class="content-wrapper">
+ <div class="float-left">
+ <h1>@ViewBag.Title</h1>
+ </div>
+ </div>
+</header>
+<div id="body">
+ <section class="featured">
+ <div class="content-wrapper">
+ <h2>Introduction</h2>
+ <p>
+ Provide a general description of your APIs here.
+ </p>
+ </div>
+ </section>
+ <section class="content-wrapper main-content clear-fix">
+ @foreach (var group in apiGroups)
+ {
+ @Html.DisplayFor(m => group, "ApiGroup")
+ }
+ </section>
+</div>
+
+@section Scripts {
+ <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/jquery-ui.min.js"></script>
+ <link type="text/css" href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/themes/smoothness/jquery-ui.css" rel="stylesheet" />
+}
+
View
13 Kudu.Services.Web/Areas/HelpPage/Views/Shared/_Layout.cshtml
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>@ViewBag.Title</title>
+ <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.2.min.js"></script>
+ @RenderSection("scripts", required: false)
+</head>
+<body>
+ @RenderBody()
+</body>
+</html>
View
62 Kudu.Services.Web/Areas/HelpPage/Views/Web.config
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+
+<configuration>
+ <configSections>
+ <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
+ <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
+ <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
+ </sectionGroup>
+ </configSections>
+
+ <system.web.webPages.razor>
+ <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
+ <pages pageBaseType="System.Web.Mvc.WebViewPage">
+ <namespaces>
+ <add namespace="System.Web.Mvc" />
+ <add namespace="System.Web.Mvc.Ajax" />
+ <add namespace="System.Web.Mvc.Html" />
+ <add namespace="System.Web.Routing" />
+ </namespaces>
+ </pages>
+ </system.web.webPages.razor>
+
+ <appSettings>
+ <add key="webpages:Enabled" value="false" />
+ </appSettings>
+
+ <system.web>
+ <httpHandlers>
+ <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
+ </httpHandlers>
+ <compilation debug="true">
+ <assemblies>
+ <add assembly="System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
+ </assemblies>
+ </compilation>
+ <!--
+ Enabling request validation in view pages would cause validation to occur
+ after the input has already been processed by the controller. By default
+ MVC performs request validation before a controller processes the input.
+ To change this behavior apply the ValidateInputAttribute to a
+ controller or action.
+ -->
+ <pages
+ validateRequest="false"
+ pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
+ pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
+ userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
+ <controls>
+ <add assembly="System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
+ </controls>
+ </pages>
+ </system.web>
+
+ <system.webServer>
+ <validation validateIntegratedModeConfiguration="false" />
+
+ <handlers>
+ <remove name="BlockViewHandler"/>
+ <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
+ </handlers>
+ </system.webServer>
+</configuration>
View
4 Kudu.Services.Web/Areas/HelpPage/Views/_ViewStart.cshtml
@@ -0,0 +1,4 @@
+@{
+ // Change the Layout path below to blend the look and feel of the help page with your existing web pages.
+ Layout = "~/Areas/HelpPage/Views/Shared/_Layout.cshtml";
+}
View
112 Kudu.Services.Web/Areas/HelpPage/XmlDocumentationProvider.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Controllers;
+using System.Web.Http.Description;
+using System.Xml.XPath;
+using System.Globalization;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ /// <summary>
+ /// A custom <see cref="IDocumentationProvider"/> that reads the API documentation from an XML documentation file.
+ /// </summary>
+ public class XmlDocumentationProvider : IDocumentationProvider
+ {
+ private XPathNavigator _documentNavigator;
+ private const string _methodExpression = "/doc/members/member[@name='M:{0}']";
+ private const string _parameterExpression = "param[@name='{0}']";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="XmlDocumentationProvider"/> class.
+ /// </summary>
+ /// <param name="documentPath">The physical path to XML document.</param>
+ public XmlDocumentationProvider(string documentPath)
+ {
+ if (documentPath == null)
+ {
+ throw new ArgumentNullException("documentPath");
+ }
+ XPathDocument xpath = new XPathDocument(documentPath);
+ _documentNavigator = xpath.CreateNavigator();
+ }
+
+ public virtual string GetDocumentation(HttpActionDescriptor actionDescriptor)
+ {
+ XPathNavigator methodNode = GetMethodNode(actionDescriptor);
+ if (methodNode != null)
+ {
+ XPathNavigator summaryNode = methodNode.SelectSingleNode("summary");
+ if (summaryNode != null)
+ {
+ return summaryNode.Value.Trim();
+ }
+ }
+
+ return null;
+ }
+
+ public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
+ {
+ ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
+ if (reflectedParameterDescriptor != null)
+ {
+ XPathNavigator methodNode = GetMethodNode(reflectedParameterDescriptor.ActionDescriptor);
+ if (methodNode != null)
+ {
+ string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
+ XPathNavigator parameterNode = methodNode.SelectSingleNode(String.Format(CultureInfo.InvariantCulture, _parameterExpression, parameterName));
+ if (parameterNode != null)
+ {
+ return parameterNode.Value.Trim();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private XPathNavigator GetMethodNode(HttpActionDescriptor actionDescriptor)
+ {
+ ReflectedHttpActionDescriptor reflectedActionDescriptor = actionDescriptor as ReflectedHttpActionDescriptor;
+ if (reflectedActionDescriptor != null)
+ {
+ string selectExpression = String.Format(CultureInfo.InvariantCulture, _methodExpression, GetMemberName(reflectedActionDescriptor.MethodInfo));
+ return _documentNavigator.SelectSingleNode(selectExpression);
+ }
+
+ return null;
+ }
+
+ private static string GetMemberName(MethodInfo method)
+ {
+ string name = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", method.DeclaringType.FullName, method.Name);
+ ParameterInfo[] parameters = method.GetParameters();
+ if (parameters.Length != 0)
+ {
+ string[] parameterTypeNames = parameters.Select(param => GetTypeName(param.ParameterType)).ToArray();
+ name += String.Format(CultureInfo.InvariantCulture, "({0})", String.Join(",", parameterTypeNames));
+ }
+
+ return name;
+ }
+
+ private static string GetTypeName(Type type)
+ {
+ if (type.IsGenericType)
+ {
+ // Format the generic type name to something like: Generic{System.Int32,System.String}
+ Type genericType = type.GetGenericTypeDefinition();
+ Type[] genericArguments = type.GetGenericArguments();
+ string typeName = genericType.FullName;
+
+ // Trim the generic parameter counts from the name
+ typeName = typeName.Substring(0, typeName.IndexOf('`'));
+ string[] argumentTypeNames = genericArguments.Select(t => GetTypeName(t)).ToArray();
+ return String.Format(CultureInfo.InvariantCulture, "{0}{{{1}}}", typeName, String.Join(",", argumentTypeNames));
+ }
+
+ return type.FullName;
+ }
+ }
+}
View
26 Kudu.Services.Web/Areas/HelpPageAreaRegistration.cs
@@ -0,0 +1,26 @@
+using System.Web.Http;
+using System.Web.Mvc;
+
+namespace Kudu.Services.Web.Areas.HelpPage
+{
+ public class HelpPageAreaRegistration : AreaRegistration
+ {
+ public override string AreaName
+ {
+ get
+ {
+ return "HelpPage";
+ }
+ }
+
+ public override void RegisterArea(AreaRegistrationContext context)
+ {
+ context.MapRoute(
+ "HelpPage_Default",
+ "Help/{action}/{apiId}",
+ new { controller = "Help", action = "Index", apiId = UrlParameter.Optional }
+ );
+ HelpPageConfig.Register(GlobalConfiguration.Configuration);
+ }
+ }
+}
View
1  Kudu.Services.Web/Global.asax
@@ -0,0 +1 @@
+<%@ Application Codebehind="Global.asax.cs" Inherits="Kudu.Services.Web.MvcApplication" Language="C#" %>
View
14 Kudu.Services.Web/Global.asax.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Kudu.Services.Web
+{
+ public class MvcApplication : HttpApplication
+ {
+ protected void Application_Start(object sender, EventArgs e)
+ {
+ AreaRegistration.RegisterAllAreas();
+ }
+ }
+}
View
55 Kudu.Services.Web/Kudu.Services.Web.csproj
@@ -151,8 +151,10 @@
</Reference>
</ItemGroup>
<ItemGroup>
+ <Content Include="Areas\HelpPage\HelpPage.css" />
<Content Include="Env.aspx" />
<Content Include="Default.aspx" />
+ <Content Include="Global.asax" />
<Content Include="Web.config">
<SubType>Designer</SubType>
</Content>
@@ -172,6 +174,23 @@
</Compile>
<Compile Include="AppSettings.cs" />
<Compile Include="App_Start\ModuleInit.cs" />
+ <Compile Include="Areas\HelpPageAreaRegistration.cs" />
+ <Compile Include="Areas\HelpPage\ApiDescriptionExtensions.cs" />
+ <Compile Include="Areas\HelpPage\App_Start\HelpPageConfig.cs" />
+ <Compile Include="Areas\HelpPage\Controllers\HelpController.cs" />
+ <Compile Include="Areas\HelpPage\HelpPageConfigurationExtensions.cs" />
+ <Compile Include="Areas\HelpPage\Models\HelpPageApiModel.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\HelpPageSampleGenerator.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\HelpPageSampleKey.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\ImageSample.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\InvalidSample.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\ObjectGenerator.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\SampleDirection.cs" />
+ <Compile Include="Areas\HelpPage\SampleGeneration\TextSample.cs" />
+ <Compile Include="Areas\HelpPage\XmlDocumentationProvider.cs" />
+ <Compile Include="Global.asax.cs">
+ <DependentUpon>Global.asax</DependentUpon>
+ </Compile>
<Compile Include="Infrastruture\HttpHandlerRouteHandler.cs" />
<Compile Include="Security\BlockLocalhostModule.cs" />
<Compile Include="Services\DeploymentEnvironment.cs" />
@@ -203,6 +222,42 @@
<ItemGroup>
<Content Include="packages.config" />
</ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Web.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Shared\_Layout.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\Index.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\TextSample.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\Samples.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\Parameters.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\InvalidSample.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\ImageSample.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\HelpPageApiModel.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\DisplayTemplates\ApiGroup.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\Help\Api.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="Areas\HelpPage\Views\_ViewStart.cshtml" />
+ </ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
View
1  Kudu.Services.Web/packages.config
@@ -4,6 +4,7 @@
<package id="Microsoft.AspNet.Razor" version="2.0.20710.0" targetFramework="net40" />
<package id="Microsoft.AspNet.WebApi.Client" version="4.0.20710.0" targetFramework="net40" />
<package id="Microsoft.AspNet.WebApi.Core" version="4.0.20710.0" targetFramework="net40" />
+ <package id="Microsoft.AspNet.WebApi.HelpPage" version="0.1.0-alpha-120814" targetFramework="net40" />
<package id="Microsoft.AspNet.WebApi.OData" version="0.1.0-alpha-121012" targetFramework="net40" />
<package id="Microsoft.AspNet.WebApi.WebHost" version="4.0.20710.0" targetFramework="net40" />
<package id="Microsoft.AspNet.WebPages" version="2.0.20710.0" targetFramework="net40" />
Please sign in to comment.
Something went wrong with that request. Please try again.