Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Added support for ApiExplorer #4

Merged
merged 1 commit into from

2 participants

@davidsavagejr

This is a very crude implementation but it bridges the gap for the missing support.

  • Expects the {version} parameter to be an integer, matching the format "VersionX"
  • I had to re-use a bunch of logic that is currently private in the latest version of the WebApi

I included a basic view. I had issues with the nuget packages so this may require a little merging with SLN file.

Let me know if you have any issues with it.

@Sebazzz Sebazzz merged commit 1b0ddc4 into Sebazzz:master
@Sebazzz
Owner

Looks good. Thanks for contributing.

@Sebazzz
Owner

I will upload a nuget package tomorrow.

@davidsavagejr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 10, 2012
  1. @davidsavagejr
This page is out of date. Refresh to see the latest.
View
357 src/SDammann.WebApi.Versioning/VersionedApiExplorer.cs
@@ -0,0 +1,357 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Description;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Routing;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+
+namespace SDammann.WebApi.Versioning
+{
+ public class VersionedApiExplorer : IApiExplorer
+ {
+ private readonly HttpConfiguration configuration;
+ private Lazy<Collection<ApiDescription>> apiDescription;
+ private const string ActionVariableName = "action";
+ private const string ControllerVariableName = "controller";
+ private static readonly Regex _actionVariableRegex = new Regex(String.Format(CultureInfo.CurrentCulture, "{{{0}}}", ActionVariableName), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ private static readonly Regex _controllerVariableRegex = new Regex(String.Format(CultureInfo.CurrentCulture, "{{{0}}}", ControllerVariableName), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+
+ private ApiExplorer DefaultExplorer { get; set; }
+
+ public VersionedApiExplorer(HttpConfiguration configuration)
+ {
+ this.configuration = configuration;
+ this.apiDescription = new Lazy<Collection<ApiDescription>>(InitializeApiDescriptions);
+ this.DefaultExplorer = new ApiExplorer(configuration);
+ }
+
+ public Collection<ApiDescription> ApiDescriptions
+ {
+ get { return this.apiDescription.Value; }
+ }
+
+ private Collection<ApiDescription> InitializeApiDescriptions()
+ {
+ Collection<ApiDescription> apiDescriptions = new Collection<ApiDescription>();
+ var controllerSelector = configuration.Services.GetHttpControllerSelector();
+ IDictionary<string, HttpControllerDescriptor> controllerMappings = controllerSelector.GetControllerMapping();
+ if (controllerMappings != null)
+ {
+ foreach (var route in configuration.Routes)
+ ExploreRouteControllers(controllerMappings, route, apiDescriptions);
+ }
+ return apiDescriptions;
+ }
+
+ private void ExploreRouteControllers(IDictionary<string, HttpControllerDescriptor> controllerMappings, IHttpRoute route, Collection<ApiDescription> apiDescriptions)
+ {
+ string routeTemplate = route.RouteTemplate;
+ object controllerVariableValue;
+ if (_controllerVariableRegex.IsMatch(routeTemplate))
+ {
+ // unbound controller variable, {controller}
+ foreach (KeyValuePair<string, HttpControllerDescriptor> controllerMapping in controllerMappings)
+ {
+ controllerVariableValue = controllerMapping.Key;
+ HttpControllerDescriptor controllerDescriptor = controllerMapping.Value;
+
+ if (DefaultExplorer.ShouldExploreController(controllerVariableValue.ToString(), controllerDescriptor, route))
+ {
+ // expand {controller} variable
+ string expandedRouteTemplate = _controllerVariableRegex.Replace(routeTemplate, controllerVariableValue.ToString());
+ ExploreRouteActions(route, expandedRouteTemplate, controllerDescriptor, apiDescriptions);
+ }
+ }
+ }
+ else
+ {
+ // bound controller variable, {controller = "controllerName"}
+ if (route.Defaults.TryGetValue(ControllerVariableName, out controllerVariableValue))
+ {
+ HttpControllerDescriptor controllerDescriptor;
+ if (controllerMappings.TryGetValue(controllerVariableValue.ToString(), out controllerDescriptor) && DefaultExplorer.ShouldExploreController(controllerVariableValue.ToString(), controllerDescriptor, route))
+ {
+ ExploreRouteActions(route, routeTemplate, controllerDescriptor, apiDescriptions);
+ }
+ }
+ }
+ }
+
+ private void ExploreRouteActions(IHttpRoute route, string localPath, HttpControllerDescriptor controllerDescriptor, Collection<ApiDescription> apiDescriptions)
+ {
+ ServicesContainer controllerServices = controllerDescriptor.Configuration.Services;
+ ILookup<string, HttpActionDescriptor> actionMappings = controllerServices.GetActionSelector().GetActionMapping(controllerDescriptor);
+ object actionVariableValue;
+ if (actionMappings != null)
+ {
+ if (_actionVariableRegex.IsMatch(localPath))
+ {
+ // unbound action variable, {action}
+ foreach (IGrouping<string, HttpActionDescriptor> actionMapping in actionMappings)
+ {
+ // expand {action} variable
+ actionVariableValue = actionMapping.Key;
+ string expandedLocalPath = _actionVariableRegex.Replace(localPath, actionVariableValue.ToString());
+ PopulateActionDescriptions(actionMapping, actionVariableValue.ToString(), route, expandedLocalPath, apiDescriptions);
+ }
+ }
+ else if (route.Defaults.TryGetValue(ActionVariableName, out actionVariableValue))
+ {
+ // bound action variable, { action = "actionName" }
+ PopulateActionDescriptions(actionMappings[actionVariableValue.ToString()], actionVariableValue.ToString(), route, localPath, apiDescriptions);
+ }
+ else
+ {
+ // no {action} specified, e.g. {controller}/{id}
+ foreach (IGrouping<string, HttpActionDescriptor> actionMapping in actionMappings)
+ {
+ PopulateActionDescriptions(actionMapping, null, route, localPath, apiDescriptions);
+ }
+ }
+ }
+ }
+
+ private void PopulateActionDescriptions(IEnumerable<HttpActionDescriptor> actionDescriptors, string actionVariableValue, IHttpRoute route, string localPath, Collection<ApiDescription> apiDescriptions)
+ {
+ foreach (HttpActionDescriptor actionDescriptor in actionDescriptors)
+ {
+ if (DefaultExplorer.ShouldExploreAction(actionVariableValue, actionDescriptor, route))
+ {
+ PopulateActionDescriptions(actionDescriptor, route, localPath, apiDescriptions);
+ }
+ }
+ }
+
+ private void PopulateActionDescriptions(HttpActionDescriptor actionDescriptor, IHttpRoute route, string localPath, Collection<ApiDescription> apiDescriptions)
+ {
+ string apiDocumentation = GetApiDocumentation(actionDescriptor);
+
+ // parameters
+ IList<ApiParameterDescription> parameterDescriptions = CreateParameterDescriptions(actionDescriptor);
+
+ // expand all parameter variables
+ string finalPath;
+
+ if (!TryExpandUriParameters(route, localPath, actionDescriptor, parameterDescriptions, out finalPath))
+ {
+ // the action cannot be reached due to parameter mismatch, e.g. routeTemplate = "/users/{name}" and GetUsers(id)
+ return;
+ }
+
+ // request formatters
+ ApiParameterDescription bodyParameter = parameterDescriptions.FirstOrDefault(description => description.Source == ApiParameterSource.FromBody);
+ IEnumerable<MediaTypeFormatter> supportedRequestBodyFormatters = bodyParameter != null ?
+ actionDescriptor.Configuration.Formatters.Where(f => f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) :
+ Enumerable.Empty<MediaTypeFormatter>();
+
+ // response formatters
+ Type returnType = actionDescriptor.ReturnType;
+ IEnumerable<MediaTypeFormatter> supportedResponseFormatters = returnType != null ?
+ actionDescriptor.Configuration.Formatters.Where(f => f.CanWriteType(returnType)) :
+ Enumerable.Empty<MediaTypeFormatter>();
+
+ // get HttpMethods supported by an action. Usually there is one HttpMethod per action but we allow multiple of them per action as well.
+ IList<HttpMethod> supportedMethods = DefaultExplorer.GetHttpMethodsSupportedByAction(route, actionDescriptor);
+
+ foreach (HttpMethod method in supportedMethods)
+ {
+ apiDescriptions.Add(new VersionedApiDescription()
+ {
+ Documentation = apiDocumentation,
+ HttpMethod = method,
+ RelativePath = finalPath,
+ ActionDescriptor = actionDescriptor,
+ Route = route,
+ SupportedResponseFormatters = new Collection<MediaTypeFormatter>(supportedResponseFormatters.ToList()),
+ SupportedRequestBodyFormatters = new Collection<MediaTypeFormatter>(supportedRequestBodyFormatters.ToList()),
+ ParameterDescriptions = new Collection<ApiParameterDescription>(parameterDescriptions)
+ });
+ }
+ }
+
+ private IList<ApiParameterDescription> CreateParameterDescriptions(HttpActionDescriptor actionDescriptor)
+ {
+ IList<ApiParameterDescription> parameterDescriptions = new List<ApiParameterDescription>();
+ HttpActionBinding actionBinding = GetActionBinding(actionDescriptor);
+
+ // try get parameter binding information if available
+ if (actionBinding != null)
+ {
+ HttpParameterBinding[] parameterBindings = actionBinding.ParameterBindings;
+ if (parameterBindings != null)
+ {
+ foreach (HttpParameterBinding parameter in parameterBindings)
+ {
+ parameterDescriptions.Add(CreateParameterDescriptionFromBinding(parameter));
+ }
+ }
+ }
+ else
+ {
+ Collection<HttpParameterDescriptor> parameters = actionDescriptor.GetParameters();
+ if (parameters != null)
+ {
+ foreach (HttpParameterDescriptor parameter in parameters)
+ {
+ parameterDescriptions.Add(CreateParameterDescriptionFromDescriptor(parameter));
+ }
+ }
+ }
+
+ return parameterDescriptions;
+ }
+
+ private ApiParameterDescription CreateParameterDescriptionFromDescriptor(HttpParameterDescriptor parameter)
+ {
+ ApiParameterDescription parameterDescription = new ApiParameterDescription();
+ parameterDescription.ParameterDescriptor = parameter;
+ parameterDescription.Name = parameter.Prefix ?? parameter.ParameterName;
+ parameterDescription.Documentation = GetApiParameterDocumentation(parameter);
+ parameterDescription.Source = ApiParameterSource.Unknown;
+ return parameterDescription;
+ }
+
+ private ApiParameterDescription CreateParameterDescriptionFromBinding(HttpParameterBinding parameterBinding)
+ {
+ ApiParameterDescription parameterDescription = CreateParameterDescriptionFromDescriptor(parameterBinding.Descriptor);
+ if (parameterBinding.WillReadBody)
+ {
+ parameterDescription.Source = ApiParameterSource.FromBody;
+ }
+ else if (parameterBinding.WillReadUri())
+ {
+ parameterDescription.Source = ApiParameterSource.FromUri;
+ }
+
+ return parameterDescription;
+ }
+
+ private static bool TryExpandUriParameters(IHttpRoute route, string routeTemplate, HttpActionDescriptor actionDescriptor, ICollection<ApiParameterDescription> parameterDescriptions, out string expandedRouteTemplate)
+ {
+ Dictionary<string, object> parameterValuesForRoute = new Dictionary<string, object>();
+ StringBuilder paramString = new StringBuilder();
+ foreach (var paramDescriptor in parameterDescriptions)
+ {
+ Type parameterType = paramDescriptor.ParameterDescriptor.ParameterType;
+ if (paramDescriptor.Source == ApiParameterSource.FromUri)
+ {
+ parameterValuesForRoute.Add(paramDescriptor.Name, "{" + paramDescriptor.Name + "}");
+ }
+ }
+ if (parameterDescriptions.Any())
+ {
+ if (!actionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get))
+ paramString.Append("/");
+ else
+ paramString.Append("?");
+
+ foreach (var param in parameterValuesForRoute)
+ paramString.AppendFormat("{0}={1}", param.Key, param.Value);
+ }
+
+ expandedRouteTemplate = route.RouteTemplate.Replace("{id}", actionDescriptor.ActionName)
+ .Replace("{version}", actionDescriptor.ControllerDescriptor.Version())
+ .Replace("{controller}", actionDescriptor.ControllerDescriptor.ControllerName)
+ + paramString.ToString();
+ return true;
+ }
+
+ private string GetApiDocumentation(HttpActionDescriptor actionDescriptor)
+ {
+ IDocumentationProvider documentationProvider = DefaultExplorer.DocumentationProvider ?? actionDescriptor.Configuration.Services.GetDocumentationProvider();
+ if (documentationProvider == null)
+ {
+ return "No documentation available.";
+ }
+
+ return documentationProvider.GetDocumentation(actionDescriptor);
+ }
+
+ private string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor)
+ {
+ IDocumentationProvider documentationProvider = DefaultExplorer.DocumentationProvider ?? parameterDescriptor.Configuration.Services.GetDocumentationProvider();
+ if (documentationProvider == null)
+ {
+ return "No documentation available.";
+ }
+
+ return documentationProvider.GetDocumentation(parameterDescriptor);
+ }
+
+ private static HttpActionBinding GetActionBinding(HttpActionDescriptor actionDescriptor)
+ {
+ HttpControllerDescriptor controllerDescriptor = actionDescriptor.ControllerDescriptor;
+ if (controllerDescriptor == null)
+ {
+ return null;
+ }
+
+ ServicesContainer controllerServices = controllerDescriptor.Configuration.Services;
+ IActionValueBinder actionValueBinder = controllerServices.GetActionValueBinder();
+ HttpActionBinding actionBinding = actionValueBinder != null ? actionValueBinder.GetBinding(actionDescriptor) : null;
+ return actionBinding;
+ }
+ }
+
+ internal static class HttpParameterBindingExtensions
+ {
+ public static bool WillReadUri(this HttpParameterBinding parameterBinding)
+ {
+ if (parameterBinding == null)
+ return false;
+
+ IValueProviderParameterBinding valueProviderParameterBinding = parameterBinding as IValueProviderParameterBinding;
+ if (valueProviderParameterBinding != null)
+ {
+ IEnumerable<ValueProviderFactory> valueProviderFactories = valueProviderParameterBinding.ValueProviderFactories;
+ if (valueProviderFactories.Any() && valueProviderFactories.All(factory => factory is QueryStringValueProviderFactory || factory is RouteDataValueProviderFactory))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static string Version(this HttpControllerDescriptor controllerDescriptor)
+ {
+ string version = "???";
+ if (controllerDescriptor != null)
+ {
+ var parts = controllerDescriptor.ControllerType.Namespace.Split('.');
+ foreach (var part in parts.Where(p => p.ToLower().StartsWith("version")))
+ {
+ int v;
+ if (int.TryParse(part.ToLower().Replace("version", ""), out v))
+ version = v.ToString();
+ }
+ }
+ return version;
+ }
+ }
+
+ internal class VersionedApiDescription : ApiDescription
+ {
+ public VersionedApiDescription()
+ {
+ SupportedRequestBodyFormatters = new Collection<MediaTypeFormatter>();
+ SupportedResponseFormatters = new Collection<MediaTypeFormatter>();
+ ParameterDescriptions = new Collection<ApiParameterDescription>();
+ }
+
+ public new Collection<MediaTypeFormatter> SupportedResponseFormatters { get; internal set; }
+ public new Collection<MediaTypeFormatter> SupportedRequestBodyFormatters { get; internal set; }
+ public new Collection<ApiParameterDescription> ParameterDescriptions { get; internal set; }
+ }
+}
View
8 src/VersioningTestApp/Api/Version1/HelloController.cs
@@ -2,8 +2,16 @@
using System.Web.Http;
public sealed class HelloController : ApiController {
+ /// <summary>
+ /// Gets this instance.
+ /// </summary>
+ /// <returns></returns>
public Message Get() {
return new Message("Hello World from API version 1!", "Hello World");
}
+ public void Delete(string messageId)
+ {
+ // this should show up as a delete.
+ }
}
}
View
20 src/VersioningTestApp/Controllers/HomeController.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using System.Web.Mvc;
+using VersioningTestApp.Models;
+using System.Web.Http;
+using SDammann.WebApi.Versioning;
+
+namespace VersioningTestApp.Controllers
+{
+ public class HomeController : Controller
+ {
+ public ActionResult Index()
+ {
+ return View(new DocumentationModel(new VersionedApiExplorer(GlobalConfiguration.Configuration)));
+ }
+
+ }
+}
View
4 src/VersioningTestApp/Global.asax.cs
@@ -1,6 +1,7 @@
namespace VersioningTestApp {
using System.Web;
using System.Web.Http;
+ using System.Web.Http.Description;
using System.Web.Http.Dispatcher;
using System.Web.Mvc;
using System.Web.Routing;
@@ -17,8 +18,7 @@ public class WebApiApplication : HttpApplication {
RouteConfig.RegisterRoutes(RouteTable.Routes);
// enable API versioning
- GlobalConfiguration.Configuration.Services.Replace(typeof (IHttpControllerSelector),
- new RouteVersionedControllerSelector(GlobalConfiguration.Configuration));
+ GlobalConfiguration.Configuration.Services.Replace(typeof (IHttpControllerSelector), new RouteVersionedControllerSelector(GlobalConfiguration.Configuration));
}
}
}
View
20 src/VersioningTestApp/Models/DocumentationModel.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using System.Web.Http.Description;
+
+namespace VersioningTestApp.Models
+{
+ public class DocumentationModel
+ {
+ public DocumentationModel(IApiExplorer explorer)
+ {
+ if (explorer == null)
+ throw new ArgumentNullException("explorer");
+ this.Explorer = explorer;
+ }
+
+ public IApiExplorer Explorer { get; set; }
+ }
+}
View
27 src/VersioningTestApp/Views/Home/Index.cshtml
@@ -0,0 +1,27 @@
+@model VersioningTestApp.Models.DocumentationModel
+
+<h1>Documentation</h1>
+<hr />
+<ul>
+@foreach (var version in Model.Explorer.ApiDescriptions.GroupBy(a => a.ActionDescriptor.ControllerDescriptor.ControllerType.Namespace))
+{
+ <li>
+ <h3>@version.Key.Split('.').Last()</h3>
+ <ul>
+ @foreach (var api in version)
+ {
+ <li>
+ (@api.HttpMethod) @api.RelativePath <br />
+ <ul>
+ <li><em>@api.Documentation</em></li>
+ @if(api.ActionDescriptor.ReturnType != null)
+ {
+ <li><em>returns</em> @api.ActionDescriptor.ReturnType.ToString()</li>
+ }
+ </ul>
+ </li>
+ }
+ </ul>
+ </li>
+}
+</ul>
View
12 src/VersioningTestApp/Views/Shared/_Layout.cshtml.cshtml
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>Versioning Test App</title>
+</head>
+<body>
+ @RenderBody()
+ @RenderSection("scripts", required: false)
+</body>
+</html>
View
3  src/VersioningTestApp/Views/Shared/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "~/Views/Shared/_Layout.cshtml";
+}
View
58 src/VersioningTestApp/Views/Web.config
@@ -0,0 +1,58 @@
+<?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>
+
+ <!--
+ 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>
Something went wrong with that request. Please try again.