From dcfb0cf17566453431042a7eb8d16967b0260909 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Tue, 1 Dec 2015 09:38:13 -0800 Subject: [PATCH 01/12] Create the ODataApiExplorer --- .../App_Start/SwaggerConfig.cs | 3 + Swashbuckle.OData/ODataApiExplorer.cs | 60 +++++++++++++++++++ Swashbuckle.OData/ODataSwaggerProvider.cs | 2 + Swashbuckle.OData/RouteValueKeys.cs | 9 +++ Swashbuckle.OData/Swashbuckle.OData.csproj | 2 + 5 files changed, 76 insertions(+) create mode 100644 Swashbuckle.OData/ODataApiExplorer.cs create mode 100644 Swashbuckle.OData/RouteValueKeys.cs diff --git a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs index 2a33550..c885b9f 100644 --- a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs @@ -1,4 +1,5 @@ using System.Web.Http; +using System.Web.Http.Description; using Swashbuckle.Application; using Swashbuckle.OData; using SwashbuckleODataSample; @@ -12,6 +13,8 @@ public class SwaggerConfig { public static void Register() { + //GlobalConfiguration.Configuration.Services.Replace(typeof(IApiExplorer), new ODataApiExplorer(GlobalConfiguration.Configuration)); + GlobalConfiguration.Configuration.EnableSwagger(c => { // By default, the service root url is inferred from the request used to access the docs. diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/ODataApiExplorer.cs new file mode 100644 index 0000000..19dea26 --- /dev/null +++ b/Swashbuckle.OData/ODataApiExplorer.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.Contracts; +using System.Text.RegularExpressions; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Description; +using System.Web.Http.Routing; + +namespace Swashbuckle.OData +{ + public class ODataApiExplorer : ApiExplorer + { + public ODataApiExplorer(HttpConfiguration configuration) : base(configuration) + { + } + + /// + /// Determines whether the controller should be considered for generation. + /// Called when initializing the . + /// + /// The controller variable value from the route. + /// The controller descriptor. + /// The route. + /// + /// true if the controller should be considered for generation, + /// false otherwise. + /// + public override bool ShouldExploreController(string controllerVariableValue, HttpControllerDescriptor controllerDescriptor, IHttpRoute route) + { + Contract.Requires(controllerDescriptor != null); + Contract.Requires(route != null); + + //var setting = controllerDescriptor.GetCustomAttributes().FirstOrDefault(); + + //return (setting == null || !setting.IgnoreApi) && MatchRegexConstraint(route, RouteValueKeys.Controller, controllerVariableValue); + return MatchRegexConstraint(route, RouteValueKeys.Controller, controllerVariableValue); + } + + private static bool MatchRegexConstraint(IHttpRoute route, string parameterName, string parameterValue) + { + var constraints = route.Constraints; + if (constraints != null) + { + object constraint; + if (constraints.TryGetValue(parameterName, out constraint)) + { + // treat the constraint as a string which represents a Regex. + // note that we don't support custom constraint (IHttpRouteConstraint) because it might rely on the request and some runtime states + var constraintsRule = constraint as string; + if (constraintsRule != null) + { + var constraintsRegEx = "^(" + constraintsRule + ")$"; + return parameterValue != null && Regex.IsMatch(parameterValue, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index 0f468ac..1b842ac 100644 --- a/Swashbuckle.OData/ODataSwaggerProvider.cs +++ b/Swashbuckle.OData/ODataSwaggerProvider.cs @@ -48,6 +48,8 @@ public ODataSwaggerProvider(ISwaggerProvider defaultProvider, SwaggerDocsConfig public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) { + var apiDescriptions = _httpConfigurationProvider().Services.GetApiExplorer().ApiDescriptions; + var oDataRoute = _httpConfigurationProvider().Routes.SingleOrDefault(route => route is ODataRoute) as ODataRoute; if (oDataRoute != null) diff --git a/Swashbuckle.OData/RouteValueKeys.cs b/Swashbuckle.OData/RouteValueKeys.cs new file mode 100644 index 0000000..6317a71 --- /dev/null +++ b/Swashbuckle.OData/RouteValueKeys.cs @@ -0,0 +1,9 @@ +namespace Swashbuckle.OData +{ + internal static class RouteValueKeys + { + // Used to provide the action and controller name + public const string Action = "action"; + public const string Controller = "controller"; + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 536a0ad..0d58ef1 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -169,10 +169,12 @@ + + From cebb915e6c6cf8f3cd4298ab333e4d5270e25d64 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Wed, 2 Dec 2015 17:27:12 -0800 Subject: [PATCH 02/12] Incorporate Microsoft's ApiExplorer --- .../ApiControllers/Client.cs | 13 + .../ApiControllers/ClientsController.cs | 115 ++ .../ApiControllers/Project.cs | 21 + .../ApiControllers/ProjectsController.cs | 115 ++ .../App_Start/SwaggerConfig.cs | 3 +- .../App_Start/WebApiConfig.cs | 6 + .../{Models => ODataControllers}/Customer.cs | 0 .../CustomersController.cs | 0 .../{Models => ODataControllers}/Order.cs | 0 .../OrdersController.cs | 0 .../SwashbuckleODataContext.cs | 4 + .../Swashbuckle.OData.Sample.csproj | 14 +- .../ApiParameterDescriptionExtensions.cs | 30 + .../ApiExplorer/BoundRouteTemplate.cs | 16 + .../ApiExplorer/CandidateAction.cs | 35 + .../ApiExplorer/CollectionModelBinderUtil.cs | 130 ++ .../CommonWebApiResources.Designer.cs | 126 ++ .../ApiExplorer/CommonWebApiResources.resx | 141 ++ .../ApiExplorer/DictionaryExtensions.cs | 128 ++ .../ApiExplorer/EmptyReadOnlyDictionary.cs | 17 + Swashbuckle.OData/ApiExplorer/Error.cs | 271 ++++ .../HttpActionDescriptorExtensions.cs | 34 + .../HttpControllerDescriptorExtensions.cs | 34 + .../HttpParameterBindingExtensions.cs | 32 + .../ApiExplorer/HttpParsedRoute.cs | 825 +++++++++++ .../ApiExplorer/HttpRouteDataExtensions.cs | 105 ++ .../ApiExplorer/HttpRouteExtensions.cs | 109 ++ .../ApiExplorer/ODataApiExplorer.cs | 778 ++++++++++ .../ApiExplorer/PathContentSegment.cs | 70 + .../ApiExplorer/PathLiteralSubsegment.cs | 36 + .../ApiExplorer/PathParameterSubsegment.cs | 48 + Swashbuckle.OData/ApiExplorer/PathSegment.cs | 21 + .../ApiExplorer/PathSeparatorSegment.cs | 29 + .../ApiExplorer/PathSubsegment.cs | 21 + .../ApiExplorer/RouteCollectionRoute.cs | 164 +++ .../ApiExplorer/RouteDataTokenKeys.cs | 22 + Swashbuckle.OData/ApiExplorer/RouteParser.cs | 368 +++++ .../{ => ApiExplorer}/RouteValueKeys.cs | 4 +- .../ApiExplorer/RoutingContext.cs | 39 + .../ApiExplorer/SRResources.Designer.cs | 1282 +++++++++++++++++ .../ApiExplorer/SRResources.resx | 529 +++++++ Swashbuckle.OData/ApiExplorer/TypeHelper.cs | 115 ++ Swashbuckle.OData/ODataApiExplorer.cs | 60 - Swashbuckle.OData/ODataSwaggerConverter.cs | 15 +- Swashbuckle.OData/ODataSwaggerProvider.cs | 21 +- Swashbuckle.OData/ODataSwaggerUtilities.cs | 151 +- Swashbuckle.OData/Properties/AssemblyInfo.cs | 6 +- Swashbuckle.OData/Swashbuckle.OData.csproj | 53 +- Swashbuckle.OData/packages.config | 25 +- 49 files changed, 5977 insertions(+), 204 deletions(-) create mode 100644 Swashbuckle.OData.Sample/ApiControllers/Client.cs create mode 100644 Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs create mode 100644 Swashbuckle.OData.Sample/ApiControllers/Project.cs create mode 100644 Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs rename Swashbuckle.OData.Sample/{Models => ODataControllers}/Customer.cs (100%) rename Swashbuckle.OData.Sample/{Controllers => ODataControllers}/CustomersController.cs (100%) rename Swashbuckle.OData.Sample/{Models => ODataControllers}/Order.cs (100%) rename Swashbuckle.OData.Sample/{Controllers => ODataControllers}/OrdersController.cs (100%) rename Swashbuckle.OData.Sample/{Models => ODataControllers}/SwashbuckleODataContext.cs (87%) create mode 100644 Swashbuckle.OData/ApiExplorer/ApiParameterDescriptionExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/BoundRouteTemplate.cs create mode 100644 Swashbuckle.OData/ApiExplorer/CandidateAction.cs create mode 100644 Swashbuckle.OData/ApiExplorer/CollectionModelBinderUtil.cs create mode 100644 Swashbuckle.OData/ApiExplorer/CommonWebApiResources.Designer.cs create mode 100644 Swashbuckle.OData/ApiExplorer/CommonWebApiResources.resx create mode 100644 Swashbuckle.OData/ApiExplorer/DictionaryExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/EmptyReadOnlyDictionary.cs create mode 100644 Swashbuckle.OData/ApiExplorer/Error.cs create mode 100644 Swashbuckle.OData/ApiExplorer/HttpActionDescriptorExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/HttpControllerDescriptorExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/HttpParameterBindingExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/HttpParsedRoute.cs create mode 100644 Swashbuckle.OData/ApiExplorer/HttpRouteDataExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/HttpRouteExtensions.cs create mode 100644 Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs create mode 100644 Swashbuckle.OData/ApiExplorer/PathContentSegment.cs create mode 100644 Swashbuckle.OData/ApiExplorer/PathLiteralSubsegment.cs create mode 100644 Swashbuckle.OData/ApiExplorer/PathParameterSubsegment.cs create mode 100644 Swashbuckle.OData/ApiExplorer/PathSegment.cs create mode 100644 Swashbuckle.OData/ApiExplorer/PathSeparatorSegment.cs create mode 100644 Swashbuckle.OData/ApiExplorer/PathSubsegment.cs create mode 100644 Swashbuckle.OData/ApiExplorer/RouteCollectionRoute.cs create mode 100644 Swashbuckle.OData/ApiExplorer/RouteDataTokenKeys.cs create mode 100644 Swashbuckle.OData/ApiExplorer/RouteParser.cs rename Swashbuckle.OData/{ => ApiExplorer}/RouteValueKeys.cs (54%) create mode 100644 Swashbuckle.OData/ApiExplorer/RoutingContext.cs create mode 100644 Swashbuckle.OData/ApiExplorer/SRResources.Designer.cs create mode 100644 Swashbuckle.OData/ApiExplorer/SRResources.resx create mode 100644 Swashbuckle.OData/ApiExplorer/TypeHelper.cs delete mode 100644 Swashbuckle.OData/ODataApiExplorer.cs diff --git a/Swashbuckle.OData.Sample/ApiControllers/Client.cs b/Swashbuckle.OData.Sample/ApiControllers/Client.cs new file mode 100644 index 0000000..ca9780c --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/Client.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace SwashbuckleODataSample.Models +{ + public class Client + { + public int Id { get; set; } + + public string Name { get; set; } + + public IList Projects { get; set; } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs b/Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs new file mode 100644 index 0000000..8f8183a --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs @@ -0,0 +1,115 @@ +using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using SwashbuckleODataSample.Models; + +namespace SwashbuckleODataSample.ApiControllers +{ + public class ClientsController : ApiController + { + private readonly SwashbuckleODataContext db = new SwashbuckleODataContext(); + + // GET: api/Clients + public IQueryable GetClients() + { + return db.Clients; + } + + // GET: api/Clients/5 + [ResponseType(typeof (Client))] + public async Task GetClient(int id) + { + var client = await db.Clients.FindAsync(id); + if (client == null) + { + return NotFound(); + } + + return Ok(client); + } + + // PUT: api/Clients/5 + [ResponseType(typeof (void))] + public async Task PutClient(int id, Client client) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (id != client.Id) + { + return BadRequest(); + } + + db.Entry(client).State = EntityState.Modified; + + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ClientExists(id)) + { + return NotFound(); + } + throw; + } + + return StatusCode(HttpStatusCode.NoContent); + } + + // POST: api/Clients + [ResponseType(typeof (Client))] + public async Task PostClient(Client client) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + db.Clients.Add(client); + await db.SaveChangesAsync(); + + return CreatedAtRoute("DefaultApi", new + { + id = client.Id + }, client); + } + + // DELETE: api/Clients/5 + [ResponseType(typeof (Client))] + public async Task DeleteClient(int id) + { + var client = await db.Clients.FindAsync(id); + if (client == null) + { + return NotFound(); + } + + db.Clients.Remove(client); + await db.SaveChangesAsync(); + + return Ok(client); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + db.Dispose(); + } + base.Dispose(disposing); + } + + private bool ClientExists(int id) + { + return db.Clients.Count(e => e.Id == id) > 0; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ApiControllers/Project.cs b/Swashbuckle.OData.Sample/ApiControllers/Project.cs new file mode 100644 index 0000000..f41e19e --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/Project.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Web.OData.Builder; +using Microsoft.OData.Edm; + +namespace SwashbuckleODataSample.Models +{ + public class Project + { + [Key] + public int ProjectId { get; set; } + + public string ProjectName { get; set; } + + public int ClientId { get; set; } + + [ForeignKey("ClientId")] + [ActionOnDelete(EdmOnDeleteAction.Cascade)] + public Client Client { get; set; } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs b/Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs new file mode 100644 index 0000000..7cc16c7 --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs @@ -0,0 +1,115 @@ +using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using SwashbuckleODataSample.Models; + +namespace SwashbuckleODataSample.ApiControllers +{ + public class ProjectsController : ApiController + { + private readonly SwashbuckleODataContext db = new SwashbuckleODataContext(); + + [Route("Projects/v1")] + public IQueryable GetProjects() + { + return db.Projects; + } + + [Route("Projects/v1/{id}")] + [ResponseType(typeof (Project))] + public async Task GetProject(int id) + { + var project = await db.Projects.FindAsync(id); + if (project == null) + { + return NotFound(); + } + + return Ok(project); + } + + [Route("Projects/v1/{id}")] + [ResponseType(typeof (void))] + public async Task PutProject(int id, Project project) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (id != project.ProjectId) + { + return BadRequest(); + } + + db.Entry(project).State = EntityState.Modified; + + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ProjectExists(id)) + { + return NotFound(); + } + throw; + } + + return StatusCode(HttpStatusCode.NoContent); + } + + [Route("Projects/v1")] + [ResponseType(typeof (Project))] + public async Task PostProject(Project project) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + db.Projects.Add(project); + await db.SaveChangesAsync(); + + return CreatedAtRoute("DefaultApi", new + { + id = project.ProjectId + }, project); + } + + [Route("Projects/v1/{id}")] + [ResponseType(typeof (Project))] + public async Task DeleteProject(int id) + { + var project = await db.Projects.FindAsync(id); + if (project == null) + { + return NotFound(); + } + + db.Projects.Remove(project); + await db.SaveChangesAsync(); + + return Ok(project); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + db.Dispose(); + } + base.Dispose(disposing); + } + + private bool ProjectExists(int id) + { + return db.Projects.Count(e => e.ProjectId == id) > 0; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs index c885b9f..ed84b71 100644 --- a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs @@ -2,6 +2,7 @@ using System.Web.Http.Description; using Swashbuckle.Application; using Swashbuckle.OData; +using Swashbuckle.OData.ApiExplorer; using SwashbuckleODataSample; using WebActivatorEx; @@ -13,7 +14,7 @@ public class SwaggerConfig { public static void Register() { - //GlobalConfiguration.Configuration.Services.Replace(typeof(IApiExplorer), new ODataApiExplorer(GlobalConfiguration.Configuration)); + GlobalConfiguration.Configuration.Services.Replace(typeof(IApiExplorer), new ODataApiExplorer(GlobalConfiguration.Configuration)); GlobalConfiguration.Configuration.EnableSwagger(c => { diff --git a/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs b/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs index de850dc..d903407 100644 --- a/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs @@ -16,6 +16,12 @@ public static void Register(HttpConfiguration config) // Web API routes config.MapHttpAttributeRoutes(); + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional } + ); + var builder = new ODataConventionModelBuilder(); builder.EntitySet("Customers"); builder.EntitySet("Orders"); diff --git a/Swashbuckle.OData.Sample/Models/Customer.cs b/Swashbuckle.OData.Sample/ODataControllers/Customer.cs similarity index 100% rename from Swashbuckle.OData.Sample/Models/Customer.cs rename to Swashbuckle.OData.Sample/ODataControllers/Customer.cs diff --git a/Swashbuckle.OData.Sample/Controllers/CustomersController.cs b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs similarity index 100% rename from Swashbuckle.OData.Sample/Controllers/CustomersController.cs rename to Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs diff --git a/Swashbuckle.OData.Sample/Models/Order.cs b/Swashbuckle.OData.Sample/ODataControllers/Order.cs similarity index 100% rename from Swashbuckle.OData.Sample/Models/Order.cs rename to Swashbuckle.OData.Sample/ODataControllers/Order.cs diff --git a/Swashbuckle.OData.Sample/Controllers/OrdersController.cs b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs similarity index 100% rename from Swashbuckle.OData.Sample/Controllers/OrdersController.cs rename to Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs diff --git a/Swashbuckle.OData.Sample/Models/SwashbuckleODataContext.cs b/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs similarity index 87% rename from Swashbuckle.OData.Sample/Models/SwashbuckleODataContext.cs rename to Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs index 357138e..95bd4b8 100644 --- a/Swashbuckle.OData.Sample/Models/SwashbuckleODataContext.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs @@ -18,5 +18,9 @@ public SwashbuckleODataContext() : base("name=SwashbuckleODataContext") public DbSet Customers { get; set; } public DbSet Orders { get; set; } + + public DbSet Clients { get; set; } + + public DbSet Projects { get; set; } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj b/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj index 87c1148..4d02b0a 100644 --- a/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj +++ b/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj @@ -119,16 +119,20 @@ + + + + - - + + + + Global.asax - - - + diff --git a/Swashbuckle.OData/ApiExplorer/ApiParameterDescriptionExtensions.cs b/Swashbuckle.OData/ApiExplorer/ApiParameterDescriptionExtensions.cs new file mode 100644 index 0000000..def127f --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/ApiParameterDescriptionExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Web.Http.Controllers; +using System.Web.Http.Description; + +namespace Swashbuckle.OData.ApiExplorer +{ + public static class ApiParameterDescriptionExtensions + { + public static IEnumerable GetBindableProperties(this HttpParameterDescriptor httpParameterDescriptor) + { + return GetBindableProperties(httpParameterDescriptor.ParameterType); + } + + public static bool CanConvertPropertiesFromString(this ApiParameterDescription apiParameterDescription) + { + return apiParameterDescription.ParameterDescriptor.GetBindableProperties().All(p => TypeHelper.CanConvertFromString(p.PropertyType)); + } + + public static IEnumerable GetBindableProperties(Type type) + { + return type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetGetMethod() != null && p.GetSetMethod() != null); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/BoundRouteTemplate.cs b/Swashbuckle.OData/ApiExplorer/BoundRouteTemplate.cs new file mode 100644 index 0000000..88abd99 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/BoundRouteTemplate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Web.Http.Routing; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// Represents a URI generated from a . + /// + internal class BoundRouteTemplate + { + public string BoundTemplate { get; set; } + + public HttpRouteValueDictionary Values { get; set; } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/CandidateAction.cs b/Swashbuckle.OData/ApiExplorer/CandidateAction.cs new file mode 100644 index 0000000..0bf8c78 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/CandidateAction.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Net.Http; +using System.Web.Http.Controllers; + +namespace Swashbuckle.OData.ApiExplorer +{ + // This is static description of an action and can be shared across requests. + // Direct routes may cache a list of these. + [DebuggerDisplay("{DebuggerToString()}")] + internal class CandidateAction + { + public HttpActionDescriptor ActionDescriptor { get; set; } + public int Order { get; set; } + public decimal Precedence { get; set; } + + public bool MatchName(string actionName) + { + return string.Equals(ActionDescriptor.ActionName, actionName, StringComparison.OrdinalIgnoreCase); + } + + public bool MatchVerb(HttpMethod method) + { + return ActionDescriptor.SupportedHttpMethods.Contains(method); + } + + internal string DebuggerToString() + { + return string.Format(CultureInfo.CurrentCulture, "{0}, Order={1}, Prec={2}", ActionDescriptor.ActionName, Order, Precedence); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/CollectionModelBinderUtil.cs b/Swashbuckle.OData/ApiExplorer/CollectionModelBinderUtil.cs new file mode 100644 index 0000000..84ae540 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/CollectionModelBinderUtil.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Web.Http.ModelBinding; +using System.Web.Http.ValueProviders; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal static class CollectionModelBinderUtil + { + internal static void CreateOrReplaceCollection(ModelBindingContext bindingContext, IEnumerable incomingElements, Func> creator) + { + var collection = bindingContext.Model as ICollection; + if (collection == null || collection.IsReadOnly) + { + collection = creator(); + bindingContext.Model = collection; + } + + collection.Clear(); + foreach (var element in incomingElements) + { + collection.Add(element); + } + } + + internal static void CreateOrReplaceDictionary(ModelBindingContext bindingContext, IEnumerable> incomingElements, Func> creator) + { + var dictionary = bindingContext.Model as IDictionary; + if (dictionary == null || dictionary.IsReadOnly) + { + dictionary = creator(); + bindingContext.Model = dictionary; + } + + dictionary.Clear(); + foreach (var element in incomingElements) + { + if (element.Key != null) + { + dictionary[element.Key] = element.Value; + } + } + } + + /// + /// Instantiate a generic binder. + /// + /// Type that is updatable by this binder. + /// Type that will be created by the binder if necessary. + /// Model binder type. + /// Model type. + /// + // Example: GetGenericBinder(typeof(IList<>), typeof(List<>), typeof(ListBinder<>), ...) means that the ListBinder + // type can update models that implement IList, and if for some reason the existing model instance is not + // updatable the binder will create a List object and bind to that instead. This method will return a ListBinder + // or null, depending on whether the type and updatability checks succeed. + internal static IModelBinder GetGenericBinder(Type supportedInterfaceType, Type newInstanceType, Type openBinderType, Type modelType) + { + Contract.Assert(supportedInterfaceType != null); + Contract.Assert(openBinderType != null); + Contract.Assert(modelType != null); + + var modelTypeArguments = GetGenericBinderTypeArgs(supportedInterfaceType, modelType); + + if (modelTypeArguments == null) + { + return null; + } + + var closedNewInstanceType = newInstanceType.MakeGenericType(modelTypeArguments); + if (!modelType.IsAssignableFrom(closedNewInstanceType)) + { + return null; + } + + var closedBinderType = openBinderType.MakeGenericType(modelTypeArguments); + var binder = (IModelBinder) Activator.CreateInstance(closedBinderType); + return binder; + } + + // Get the generic arguments for the binder, based on the model type. Or null if not compatible. + internal static Type[] GetGenericBinderTypeArgs(Type supportedInterfaceType, Type modelType) + { + if (!modelType.IsGenericType || modelType.IsGenericTypeDefinition) + { + // not a closed generic type + return null; + } + + var modelTypeArguments = modelType.GetGenericArguments(); + if (modelTypeArguments.Length != supportedInterfaceType.GetGenericArguments().Length) + { + // wrong number of generic type arguments + return null; + } + + return modelTypeArguments; + } + + [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Http.ValueProviders.ValueProviderResult.ConvertTo(System.Type)", Justification = "The ValueProviderResult already has the necessary context to perform a culture-aware conversion.")] + internal static IEnumerable GetIndexNamesFromValueProviderResult(ValueProviderResult valueProviderResultIndex) + { + IEnumerable indexNames = null; + if (valueProviderResultIndex != null) + { + var indexes = (string[]) valueProviderResultIndex.ConvertTo(typeof (string[])); + if (indexes != null && indexes.Length > 0) + { + indexNames = indexes; + } + } + return indexNames; + } + + internal static IEnumerable GetZeroBasedIndexes() + { + var i = 0; + while (true) + { + yield return i.ToString(CultureInfo.InvariantCulture); + i++; + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/CommonWebApiResources.Designer.cs b/Swashbuckle.OData/ApiExplorer/CommonWebApiResources.Designer.cs new file mode 100644 index 0000000..7b8222a --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/CommonWebApiResources.Designer.cs @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Swashbuckle.OData.ApiExplorer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CommonWebApiResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CommonWebApiResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Swashbuckle.OData.ApiExplorer.CommonWebApiResources", typeof(CommonWebApiResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Relative URI values are not supported: '{0}'. The URI must be absolute.. + /// + internal static string ArgumentInvalidAbsoluteUri { + get { + return ResourceManager.GetString("ArgumentInvalidAbsoluteUri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported URI scheme: '{0}'. The URI scheme must be either '{1}' or '{2}'.. + /// + internal static string ArgumentInvalidHttpUriScheme { + get { + return ResourceManager.GetString("ArgumentInvalidHttpUriScheme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be greater than or equal to {0}.. + /// + internal static string ArgumentMustBeGreaterThanOrEqualTo { + get { + return ResourceManager.GetString("ArgumentMustBeGreaterThanOrEqualTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be less than or equal to {0}.. + /// + internal static string ArgumentMustBeLessThanOrEqualTo { + get { + return ResourceManager.GetString("ArgumentMustBeLessThanOrEqualTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The argument '{0}' is null or empty.. + /// + internal static string ArgumentNullOrEmpty { + get { + return ResourceManager.GetString("ArgumentNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to URI must not contain a query component or a fragment identifier.. + /// + internal static string ArgumentUriHasQueryOrFragment { + get { + return ResourceManager.GetString("ArgumentUriHasQueryOrFragment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value of argument '{0}' ({1}) is invalid for Enum type '{2}'.. + /// + internal static string InvalidEnumArgument { + get { + return ResourceManager.GetString("InvalidEnumArgument", resourceCulture); + } + } + } +} diff --git a/Swashbuckle.OData/ApiExplorer/CommonWebApiResources.resx b/Swashbuckle.OData/ApiExplorer/CommonWebApiResources.resx new file mode 100644 index 0000000..3fa56b1 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/CommonWebApiResources.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Relative URI values are not supported: '{0}'. The URI must be absolute. + + + Unsupported URI scheme: '{0}'. The URI scheme must be either '{1}' or '{2}'. + + + Value must be greater than or equal to {0}. + + + Value must be less than or equal to {0}. + + + The argument '{0}' is null or empty. + + + URI must not contain a query component or a fragment identifier. + + + The value of argument '{0}' ({1}) is invalid for Enum type '{2}'. + + \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/DictionaryExtensions.cs b/Swashbuckle.OData/ApiExplorer/DictionaryExtensions.cs new file mode 100644 index 0000000..5d96f9a --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/DictionaryExtensions.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.Contracts; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// Extension methods for . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class DictionaryExtensions + { + /// + /// Remove entries from dictionary that match the removeCondition. + /// + public static void RemoveFromDictionary(this IDictionary dictionary, Func, bool> removeCondition) + { + // Pass the delegate as the state to avoid a delegate and closure + dictionary.RemoveFromDictionary((entry, innerCondition) => { return innerCondition(entry); }, removeCondition); + } + + /// + /// Remove entries from dictionary that match the removeCondition. + /// + public static void RemoveFromDictionary(this IDictionary dictionary, Func, TState, bool> removeCondition, TState state) + { + Contract.Assert(dictionary != null); + Contract.Assert(removeCondition != null); + + // Because it is not possible to delete while enumerating, a copy of the keys must be taken. Use the size of the dictionary as an upper bound + // to avoid creating more than one copy of the keys. + var removeCount = 0; + var keys = new TKey[dictionary.Count]; + foreach (var entry in dictionary) + { + if (removeCondition(entry, state)) + { + keys[removeCount] = entry.Key; + removeCount++; + } + } + for (var i = 0; i < removeCount; i++) + { + dictionary.Remove(keys[i]); + } + } + + /// + /// Gets the value of associated with the specified key or default value if + /// either the key is not present or the value is not of type . + /// + /// The type of the value associated with the specified key. + /// The instance where TValue is object. + /// The key whose value to get. + /// + /// When this method returns, the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the value parameter. + /// + /// + /// true if key was found, value is non-null, and value is of type ; otherwise + /// false. + /// + public static bool TryGetValue(this IDictionary collection, string key, out T value) + { + Contract.Assert(collection != null); + + object valueObj; + if (collection.TryGetValue(key, out valueObj)) + { + if (valueObj is T) + { + value = (T) valueObj; + return true; + } + } + + value = default(T); + return false; + } + + internal static IEnumerable> FindKeysWithPrefix(this IDictionary dictionary, string prefix) + { + Contract.Assert(dictionary != null); + Contract.Assert(prefix != null); + + TValue exactMatchValue; + if (dictionary.TryGetValue(prefix, out exactMatchValue)) + { + yield return new KeyValuePair(prefix, exactMatchValue); + } + + foreach (var entry in dictionary) + { + var key = entry.Key; + + if (key.Length <= prefix.Length) + { + continue; + } + + if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Everything is prefixed by the empty string + if (prefix.Length == 0) + { + yield return entry; + } + else + { + var charAfterPrefix = key[prefix.Length]; + switch (charAfterPrefix) + { + case '[': + case '.': + yield return entry; + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/EmptyReadOnlyDictionary.cs b/Swashbuckle.OData/ApiExplorer/EmptyReadOnlyDictionary.cs new file mode 100644 index 0000000..b8b0cd4 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/EmptyReadOnlyDictionary.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal class EmptyReadOnlyDictionary + { + private static readonly ReadOnlyDictionary _value = new ReadOnlyDictionary(new Dictionary()); + + public static IDictionary Value + { + get { return _value; } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/Error.cs b/Swashbuckle.OData/ApiExplorer/Error.cs new file mode 100644 index 0000000..cc6c061 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/Error.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// Utility class for creating and unwrapping instances. + /// + internal static class Error + { + private const string HttpScheme = "http"; + private const string HttpsScheme = "https"; + + /// + /// Formats the specified resource string using . + /// + /// A composite format string. + /// An object array that contains zero or more objects to format. + /// The formatted string. + internal static string Format(string format, params object[] args) + { + return string.Format(CultureInfo.CurrentCulture, format, args); + } + + /// + /// Creates an with the provided properties. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentException Argument(string messageFormat, params object[] messageArgs) + { + return new ArgumentException(Format(messageFormat, messageArgs)); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentException Argument(string parameterName, string messageFormat, params object[] messageArgs) + { + return new ArgumentException(Format(messageFormat, messageArgs), parameterName); + } + + /// + /// Creates an with a message saying that the argument must be an "http" or "https" + /// URI. + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The logged . + internal static ArgumentException ArgumentUriNotHttpOrHttpsScheme(string parameterName, Uri actualValue) + { + return new ArgumentException(Format(CommonWebApiResources.ArgumentInvalidHttpUriScheme, actualValue, HttpScheme, HttpsScheme), parameterName); + } + + /// + /// Creates an with a message saying that the argument must be an absolute URI. + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The logged . + internal static ArgumentException ArgumentUriNotAbsolute(string parameterName, Uri actualValue) + { + return new ArgumentException(Format(CommonWebApiResources.ArgumentInvalidAbsoluteUri, actualValue), parameterName); + } + + /// + /// Creates an with a message saying that the argument must be an absolute URI + /// without a query or fragment identifier and then logs it with . + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The logged . + internal static ArgumentException ArgumentUriHasQueryOrFragment(string parameterName, Uri actualValue) + { + return new ArgumentException(Format(CommonWebApiResources.ArgumentUriHasQueryOrFragment, actualValue), parameterName); + } + + /// + /// Creates an with the provided properties. + /// + /// The logged . + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this API is to return an error for properties")] + internal static ArgumentNullException PropertyNull() + { + return new ArgumentNullException("value"); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// The logged . + internal static ArgumentNullException ArgumentNull(string parameterName) + { + return new ArgumentNullException(parameterName); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentNullException ArgumentNull(string parameterName, string messageFormat, params object[] messageArgs) + { + return new ArgumentNullException(parameterName, Format(messageFormat, messageArgs)); + } + + /// + /// Creates an with a default message. + /// + /// The name of the parameter that caused the current exception. + /// The logged . + internal static ArgumentException ArgumentNullOrEmpty(string parameterName) + { + return Argument(parameterName, CommonWebApiResources.ArgumentNullOrEmpty, parameterName); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentOutOfRangeException ArgumentOutOfRange(string parameterName, object actualValue, string messageFormat, params object[] messageArgs) + { + return new ArgumentOutOfRangeException(parameterName, actualValue, Format(messageFormat, messageArgs)); + } + + /// + /// Creates an with a message saying that the argument must be greater than + /// or equal to . + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The minimum size of the argument. + /// The logged . + internal static ArgumentOutOfRangeException ArgumentMustBeGreaterThanOrEqualTo(string parameterName, object actualValue, object minValue) + { + return new ArgumentOutOfRangeException(parameterName, actualValue, Format(CommonWebApiResources.ArgumentMustBeGreaterThanOrEqualTo, minValue)); + } + + /// + /// Creates an with a message saying that the argument must be less than or + /// equal to . + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The maximum size of the argument. + /// The logged . + internal static ArgumentOutOfRangeException ArgumentMustBeLessThanOrEqualTo(string parameterName, object actualValue, object maxValue) + { + return new ArgumentOutOfRangeException(parameterName, actualValue, Format(CommonWebApiResources.ArgumentMustBeLessThanOrEqualTo, maxValue)); + } + + /// + /// Creates an with a message saying that the key was not found. + /// + /// The logged . + internal static KeyNotFoundException KeyNotFound() + { + return new KeyNotFoundException(); + } + + /// + /// Creates an with a message saying that the key was not found. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static KeyNotFoundException KeyNotFound(string messageFormat, params object[] messageArgs) + { + return new KeyNotFoundException(Format(messageFormat, messageArgs)); + } + + /// + /// Creates an initialized according to guidelines. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ObjectDisposedException ObjectDisposed(string messageFormat, params object[] messageArgs) + { + // Pass in null, not disposedObject.GetType().FullName as per the above guideline + return new ObjectDisposedException(null, Format(messageFormat, messageArgs)); + } + + /// + /// Creates an initialized with the provided parameters. + /// + /// The logged . + internal static OperationCanceledException OperationCanceled() + { + return new OperationCanceledException(); + } + + /// + /// Creates an initialized with the provided parameters. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static OperationCanceledException OperationCanceled(string messageFormat, params object[] messageArgs) + { + return new OperationCanceledException(Format(messageFormat, messageArgs)); + } + + /// + /// Creates an for an invalid enum argument. + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that failed. + /// A that represents the enumeration class with the valid values. + /// The logged . + internal static ArgumentException InvalidEnumArgument(string parameterName, int invalidValue, Type enumClass) + { +#if NETFX_CORE + return new ArgumentException(Error.Format(CommonWebApiResources.InvalidEnumArgument, parameterName, invalidValue, enumClass.Name), parameterName); +#else + return new InvalidEnumArgumentException(parameterName, invalidValue, enumClass); +#endif + } + + /// + /// Creates an . + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static InvalidOperationException InvalidOperation(string messageFormat, params object[] messageArgs) + { + return new InvalidOperationException(Format(messageFormat, messageArgs)); + } + + /// + /// Creates an . + /// + /// Inner exception + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static InvalidOperationException InvalidOperation(Exception innerException, string messageFormat, params object[] messageArgs) + { + return new InvalidOperationException(Format(messageFormat, messageArgs), innerException); + } + + /// + /// Creates an . + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static NotSupportedException NotSupported(string messageFormat, params object[] messageArgs) + { + return new NotSupportedException(Format(messageFormat, messageArgs)); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/HttpActionDescriptorExtensions.cs b/Swashbuckle.OData/ApiExplorer/HttpActionDescriptorExtensions.cs new file mode 100644 index 0000000..c439bce --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/HttpActionDescriptorExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Web.Http.Controllers; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal static class HttpActionDescriptorExtensions + { + private const string AttributeRoutedPropertyKey = "MS_IsAttributeRouted"; + + public static bool IsAttributeRouted(this HttpActionDescriptor actionDescriptor) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + + object value; + actionDescriptor.Properties.TryGetValue(AttributeRoutedPropertyKey, out value); + return value as bool? ?? false; + } + + public static void SetIsAttributeRouted(this HttpActionDescriptor actionDescriptor, bool value) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + + actionDescriptor.Properties[AttributeRoutedPropertyKey] = value; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/HttpControllerDescriptorExtensions.cs b/Swashbuckle.OData/ApiExplorer/HttpControllerDescriptorExtensions.cs new file mode 100644 index 0000000..2cbd07b --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/HttpControllerDescriptorExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Web.Http.Controllers; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal static class HttpControllerDescriptorExtensions + { + private const string AttributeRoutedPropertyKey = "MS_IsAttributeRouted"; + + public static bool IsAttributeRouted(this HttpControllerDescriptor controllerDescriptor) + { + if (controllerDescriptor == null) + { + throw new ArgumentNullException("controllerDescriptor"); + } + + object value; + controllerDescriptor.Properties.TryGetValue(AttributeRoutedPropertyKey, out value); + return value as bool? ?? false; + } + + public static void SetIsAttributeRouted(this HttpControllerDescriptor controllerDescriptor, bool value) + { + if (controllerDescriptor == null) + { + throw new ArgumentNullException("controllerDescriptor"); + } + + controllerDescriptor.Properties[AttributeRoutedPropertyKey] = value; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/HttpParameterBindingExtensions.cs b/Swashbuckle.OData/ApiExplorer/HttpParameterBindingExtensions.cs new file mode 100644 index 0000000..3937113 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/HttpParameterBindingExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Linq; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using System.Web.Http.ValueProviders; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal static class HttpParameterBindingExtensions + { + public static bool WillReadUri(this HttpParameterBinding parameterBinding) + { + if (parameterBinding == null) + { + throw Error.ArgumentNull("parameterBinding"); + } + + var valueProviderParameterBinding = parameterBinding as IValueProviderParameterBinding; + if (valueProviderParameterBinding != null) + { + var valueProviderFactories = valueProviderParameterBinding.ValueProviderFactories; + if (valueProviderFactories.Any() && valueProviderFactories.All(factory => factory is IUriValueProviderFactory)) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/HttpParsedRoute.cs b/Swashbuckle.OData/ApiExplorer/HttpParsedRoute.cs new file mode 100644 index 0000000..608a8d1 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/HttpParsedRoute.cs @@ -0,0 +1,825 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.Http.Routing; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal sealed class HttpParsedRoute + { + public HttpParsedRoute(List pathSegments) + { + Contract.Assert(pathSegments != null); + PathSegments = pathSegments; + } + + public List PathSegments { get; } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")] + public BoundRouteTemplate Bind(IDictionary currentValues, IDictionary values, HttpRouteValueDictionary defaultValues, HttpRouteValueDictionary constraints) + { + if (currentValues == null) + { + currentValues = new HttpRouteValueDictionary(); + } + + if (values == null) + { + values = new HttpRouteValueDictionary(); + } + + if (defaultValues == null) + { + defaultValues = new HttpRouteValueDictionary(); + } + + // The set of values we should be using when generating the URI in this route + var acceptedValues = new HttpRouteValueDictionary(); + + // Keep track of which new values have been used + var unusedNewValues = new HashSet(values.Keys, StringComparer.OrdinalIgnoreCase); + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + // If it's a parameter subsegment, examine the current value to see if it matches the new value + var parameterName = parameterSubsegment.ParameterName; + + object newParameterValue; + var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + if (hasNewParameterValue) + { + unusedNewValues.Remove(parameterName); + } + + object currentParameterValue; + var hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + return false; + } + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + acceptedValues.Add(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + acceptedValues.Add(parameterName, currentParameterValue); + } + } + return true; + }); + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var newValue in values) + { + if (IsRoutePartNonEmpty(newValue.Value)) + { + if (!acceptedValues.ContainsKey(newValue.Key)) + { + acceptedValues.Add(newValue.Key, newValue.Value); + } + } + } + + // Add all current values that aren't in the URI at all + foreach (var currentValue in currentValues) + { + var parameterName = currentValue.Key; + if (!acceptedValues.ContainsKey(parameterName)) + { + var parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName); + if (parameterSubsegment == null) + { + acceptedValues.Add(parameterName, currentValue.Value); + } + } + } + + // Add all remaining default values from the route to the list of values we will use for URI generation + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) + { + object defaultValue; + if (!IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + acceptedValues.Add(parameterSubsegment.ParameterName, defaultValue); + } + } + return true; + }); + + // All required parameters in this URI must have values from somewhere (i.e. the accepted values) + var hasAllRequiredValues = ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + object defaultValue; + if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) + { + if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) + { + // If the route parameter value is required that means there's + // no default value, so if there wasn't a new value for it + // either, this route won't match. + return false; + } + } + return true; + }); + if (!hasAllRequiredValues) + { + return null; + } + + // All other default values must match if they are explicitly defined in the new values + var otherDefaultValues = new HttpRouteValueDictionary(defaultValues); + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + otherDefaultValues.Remove(parameterSubsegment.ParameterName); + return true; + }); + + foreach (var defaultValue in otherDefaultValues) + { + object value; + if (values.TryGetValue(defaultValue.Key, out value)) + { + unusedNewValues.Remove(defaultValue.Key); + if (!RoutePartsEqual(value, defaultValue.Value)) + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + + // Step 2: If the route is a match generate the appropriate URI + + var uri = new StringBuilder(); + var pendingParts = new StringBuilder(); + + var pendingPartsAreAllSafe = false; + var blockAllUriAppends = false; + + for (var i = 0; i < PathSegments.Count; i++) + { + var pathSegment = PathSegments[i]; // parsedRouteUriPart + + if (pathSegment is PathSeparatorSegment) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts); + pendingParts.Length = 0; + } + } + pendingPartsAreAllSafe = false; + + // Guard against appending multiple separators for empty segments + if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') + { + // Dev10 676725: Route should not be matched if that causes mismatched tokens + // Dev11 86819: We will allow empty matches if all subsequent segments are null + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI (without the trailing slash) and prevent any future appends + uri.Append(pendingParts.ToString(0, pendingParts.Length - 1)); + pendingParts.Length = 0; + blockAllUriAppends = true; + } + else + { + pendingParts.Append("/"); + } + } + else + { + var contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + var addedAnySubsegments = false; + + for (var j = 0; j < contentPathSegment.Subsegments.Count; j++) + { + var subsegment = contentPathSegment.Subsegments[j]; + var literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // If it's a literal we hold on to it until we are sure we need to add it + pendingPartsAreAllSafe = true; + pendingParts.Append(literalSubsegment.Literal); + } + else + { + var parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts); + pendingParts.Length = 0; + + addedAnySubsegments = true; + } + } + pendingPartsAreAllSafe = false; + + // If it's a parameter, get its value + object acceptedParameterValue; + var hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue); + if (hasAcceptedParameterValue) + { + unusedNewValues.Remove(parameterSubsegment.ParameterName); + } + + object defaultParameterValue; + defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue); + + if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue)) + { + // If the accepted value is the same as the default value, mark it as pending since + // we won't necessarily add it to the URI we generate. + pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + } + else + { + if (blockAllUriAppends) + { + return null; + } + + // Add the new part to the URI as well as any pending parts + if (pendingParts.Length > 0) + { + // Append any pending literals to the URI + uri.Append(pendingParts); + pendingParts.Length = 0; + } + uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + + addedAnySubsegments = true; + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (addedAnySubsegments) + { + // See comment above about why we add the pending parts + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts); + pendingParts.Length = 0; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts); + } + } + + // Process constraints keys + if (constraints != null) + { + // If there are any constraints, mark all the keys as being used so that we don't + // generate query string items for custom constraints that don't appear as parameters + // in the URI format. + foreach (var constraintsItem in constraints) + { + unusedNewValues.Remove(constraintsItem.Key); + } + } + + // Encode the URI before we append the query string, otherwise we would double encode the query string + var encodedUri = new StringBuilder(); + encodedUri.Append(UriEncode(uri.ToString())); + uri = encodedUri; + + // Add remaining new values as query string parameters to the URI + if (unusedNewValues.Count > 0) + { + // Generate the query string + var firstParam = true; + foreach (var unusedNewValue in unusedNewValues) + { + object value; + if (acceptedValues.TryGetValue(unusedNewValue, out value)) + { + uri.Append(firstParam ? '?' : '&'); + firstParam = false; + uri.Append(Uri.EscapeDataString(unusedNewValue)); + uri.Append('='); + uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture))); + } + } + } + + return new BoundRouteTemplate + { + BoundTemplate = uri.ToString(), + Values = acceptedValues + }; + } + + private static string EscapeReservedCharacters(Match m) + { + return Uri.HexEscape(m.Value[0]); + } + + private static bool ForEachParameter(List pathSegments, Func action) + { + for (var i = 0; i < pathSegments.Count; i++) + { + var pathSegment = pathSegments[i]; + + if (pathSegment is PathSeparatorSegment) + { + // We only care about parameter subsegments, so skip this + } + else + { + var contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + for (var j = 0; j < contentPathSegment.Subsegments.Count; j++) + { + var subsegment = contentPathSegment.Subsegments[j]; + var literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // We only care about parameter subsegments, so skip this + } + else + { + var parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + if (!action(parameterSubsegment)) + { + return false; + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + return true; + } + + private static PathParameterSubsegment GetParameterSubsegment(List pathSegments, string parameterName) + { + PathParameterSubsegment foundParameterSubsegment = null; + + ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + if (string.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase)) + { + foundParameterSubsegment = parameterSubsegment; + return false; + } + return true; + }); + + return foundParameterSubsegment; + } + + private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, HttpRouteValueDictionary defaultValues, out object defaultValue) + { + if (parameterSubsegment.IsCatchAll) + { + defaultValue = null; + return false; + } + + return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue); + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + var routePartString = routePart as string; + if (routePartString != null) + { + return routePartString.Length > 0; + } + return routePart != null; + } + + public HttpRouteValueDictionary Match(RoutingContext context, HttpRouteValueDictionary defaultValues) + { + var requestPathSegments = context.PathSegments; + + if (defaultValues == null) + { + defaultValues = new HttpRouteValueDictionary(); + } + + var matchedValues = new HttpRouteValueDictionary(); + + // This flag gets set once all the data in the URI has been parsed through, but + // the route we're trying to match against still has more parts. At this point + // we'll only continue matching separator characters and parameters that have + // default values. + var ranOutOfStuffToParse = false; + + // This value gets set once we start processing a catchall parameter (if there is one + // at all). Once we set this value we consume all remaining parts of the URI into its + // parameter value. + var usedCatchAllParameter = false; + + for (var i = 0; i < PathSegments.Count; i++) + { + var pathSegment = PathSegments[i]; + + if (requestPathSegments.Count <= i) + { + ranOutOfStuffToParse = true; + } + + var requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i]; + + if (pathSegment is PathSeparatorSegment) + { + if (ranOutOfStuffToParse) + { + // If we're trying to match a separator in the route but there's no more content, that's OK + } + else + { + if (!string.Equals(requestPathSegment, "/", StringComparison.Ordinal)) + { + return null; + } + } + } + else + { + var contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + if (contentPathSegment.IsCatchAll) + { + Contract.Assert(i == PathSegments.Count - 1, "If we're processing a catch-all, we should be on the last route segment."); + MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues); + usedCatchAllParameter = true; + } + else + { + if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) + { + return null; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (!usedCatchAllParameter) + { + if (PathSegments.Count < requestPathSegments.Count) + { + // If we've already gone through all the parts defined in the route but the URI + // still contains more content, check that the remaining content is all separators. + for (var i = PathSegments.Count; i < requestPathSegments.Count; i++) + { + if (!RouteParser.IsSeparator(requestPathSegments[i])) + { + return null; + } + } + } + } + + // Copy all remaining default values to the route data + if (defaultValues != null) + { + foreach (var defaultValue in defaultValues) + { + if (!matchedValues.ContainsKey(defaultValue.Key)) + { + matchedValues.Add(defaultValue.Key, defaultValue.Value); + } + } + } + + return matchedValues; + } + + private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable remainingRequestSegments, HttpRouteValueDictionary defaultValues, HttpRouteValueDictionary matchedValues) + { + var remainingRequest = string.Join(string.Empty, remainingRequestSegments.ToArray()); + + var catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment; + + object catchAllValue; + + if (remainingRequest.Length > 0) + { + catchAllValue = remainingRequest; + } + else + { + defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue); + } + + matchedValues.Add(catchAllSegment.ParameterName, catchAllValue); + } + + private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, HttpRouteValueDictionary defaultValues, HttpRouteValueDictionary matchedValues) + { + if (string.IsNullOrEmpty(requestPathSegment)) + { + // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match + + if (routeSegment.Subsegments.Count > 1) + { + return false; + } + + var parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment; + if (parameterSubsegment == null) + { + return false; + } + + // We must have a default value since there's no value in the request URI + object parameterValue; + if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) + { + // If there's a default value for this parameter, use that default value + matchedValues.Add(parameterSubsegment.ParameterName, parameterValue); + return true; + } + // If there's no default value, this segment doesn't match + return false; + } + + // Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal + if (routeSegment.Subsegments.Count == 1) + { + return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues); + } + + // Find last literal segment and get its last index in the string + + var lastIndex = requestPathSegment.Length; + var indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1; + + PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered + + while (indexOfLastSegmentUsed >= 0) + { + var newLastIndex = lastIndex; + + var parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment; + if (parameterSubsegment != null) + { + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = parameterSubsegment; + } + else + { + var literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment; + if (literalSubsegment != null) + { + lastLiteral = literalSubsegment; + + var startIndex = lastIndex - 1; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) + { + startIndex--; + } + + if (startIndex < 0) + { + return false; + } + + var indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase); + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } + + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == routeSegment.Subsegments.Count - 1) + { + if (indexOfLiteral + literalSubsegment.Literal.Length != requestPathSegment.Length) + { + return false; + } + } + + newLastIndex = indexOfLiteral; + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + + if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; + + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) + { + parameterStartIndex = 0; + } + else + { + parameterStartIndex = newLastIndex; + Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); + } + parameterTextLength = lastIndex; + } + else + { + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) + { + parameterStartIndex = 0; + parameterTextLength = lastIndex; + } + else + { + parameterStartIndex = newLastIndex + lastLiteral.Literal.Length; + parameterTextLength = lastIndex - parameterStartIndex; + } + } + + var parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength); + + if (string.IsNullOrEmpty(parameterValueString)) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; + } + // If there's a value in the segment for this parameter, use the subsegment value + matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); + + parameterNeedsValue = null; + lastLiteral = null; + } + + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } + + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + return (lastIndex == 0) || routeSegment.Subsegments[0] is PathParameterSubsegment; + } + + private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, HttpRouteValueDictionary matchedValues) + { + var parameterSubsegment = pathSubsegment as PathParameterSubsegment; + if (parameterSubsegment == null) + { + // Handle a single literal segment + var literalSubsegment = pathSubsegment as PathLiteralSubsegment; + Contract.Assert(literalSubsegment != null, "Invalid path segment type"); + return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase); + } + // Handle a single parameter segment + matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment); + return true; + } + + private static bool RoutePartsEqual(object a, object b) + { + var sa = a as string; + var sb = b as string; + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + // At least one of them is null. Return true if they both are + return a == b; + } + + private static string UriEncode(string str) + { + var escape = Uri.EscapeUriString(str); + return Regex.Replace(escape, "([#?])", EscapeReservedCharacters); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/HttpRouteDataExtensions.cs b/Swashbuckle.OData/ApiExplorer/HttpRouteDataExtensions.cs new file mode 100644 index 0000000..4b3da93 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/HttpRouteDataExtensions.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Web.Http; +using System.Web.Http.Routing; + +namespace Swashbuckle.OData.ApiExplorer +{ + public static class HttpRouteDataExtensions + { + /// + /// Remove all optional parameters that do not have a value from the route data. + /// + /// route data, to be mutated in-place. + public static void RemoveOptionalRoutingParameters(this IHttpRouteData routeData) + { + RemoveOptionalRoutingParameters(routeData.Values); + + var subRouteData = routeData.GetSubRoutes(); + if (subRouteData != null) + { + foreach (var sub in subRouteData) + { + RemoveOptionalRoutingParameters(sub); + } + } + } + + private static void RemoveOptionalRoutingParameters(IDictionary routeValueDictionary) + { + Contract.Assert(routeValueDictionary != null); + + // Get all keys for which the corresponding value is 'Optional'. + // Having a separate array is necessary so that we don't manipulate the dictionary while enumerating. + // This is on a hot-path and linq expressions are showing up on the profile, so do array manipulation. + var max = routeValueDictionary.Count; + var i = 0; + var matching = new string[max]; + foreach (var kv in routeValueDictionary) + { + if (kv.Value == RouteParameter.Optional) + { + matching[i] = kv.Key; + i++; + } + } + for (var j = 0; j < i; j++) + { + var key = matching[j]; + routeValueDictionary.Remove(key); + } + } + + /// + /// If a route is really a union of other routes, return the set of sub routes. + /// + /// a union route data + /// set of sub soutes contained within this route + public static IEnumerable GetSubRoutes(this IHttpRouteData routeData) + { + IHttpRouteData[] subRoutes = null; + if (routeData.Values.TryGetValue(RouteCollectionRoute.SubRouteDataKey, out subRoutes)) + { + return subRoutes; + } + return null; + } + + // If routeData is from an attribute route, get the action descriptors, order and precedence that it may match + // to. Caller still needs to run action selection to pick the specific action. + // Else return null. + internal static CandidateAction[] GetDirectRouteCandidates(this IHttpRouteData routeData) + { + Contract.Assert(routeData != null); + var subRoutes = routeData.GetSubRoutes(); + + // Possible this is being called on a subroute. This can happen after ElevateRouteData. Just chain. + if (subRoutes == null) + { + if (routeData.Route == null) + { + // If the matched route is a System.Web.Routing.Route (in web host) then routeData.Route + // will be null. Normally a System.Web.Routing.Route match would go through an MVC handler + // but we can get here through HttpRoutingDispatcher in WebAPI batching. If that happens, + // then obviously it's not a WebAPI attribute routing match. + return null; + } + return routeData.Route.GetDirectRouteCandidates(); + } + + var list = new List(); + + foreach (var subData in subRoutes) + { + var candidates = subData.Route.GetDirectRouteCandidates(); + if (candidates != null) + { + list.AddRange(candidates); + } + } + return list.ToArray(); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/HttpRouteExtensions.cs b/Swashbuckle.OData/ApiExplorer/HttpRouteExtensions.cs new file mode 100644 index 0000000..6cd6108 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/HttpRouteExtensions.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + + +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Web.Http.Controllers; +using System.Web.Http.Routing; + +namespace Swashbuckle.OData.ApiExplorer +{ + internal static class HttpRouteExtensions + { + // If route is a direct route, get the action descriptors, order and precedence it may map to. + public static CandidateAction[] GetDirectRouteCandidates(this IHttpRoute route) + { + Contract.Assert(route != null); + + var dataTokens = route.DataTokens; + if (dataTokens == null) + { + return null; + } + + var candidates = new List(); + + HttpActionDescriptor[] directRouteActions = null; + HttpActionDescriptor[] possibleDirectRouteActions; + if (dataTokens.TryGetValue(RouteDataTokenKeys.Actions, out possibleDirectRouteActions)) + { + if (possibleDirectRouteActions != null && possibleDirectRouteActions.Length > 0) + { + directRouteActions = possibleDirectRouteActions; + } + } + + if (directRouteActions == null) + { + return null; + } + + var order = 0; + int possibleOrder; + if (dataTokens.TryGetValue(RouteDataTokenKeys.Order, out possibleOrder)) + { + order = possibleOrder; + } + + var precedence = 0M; + decimal possiblePrecedence; + + if (dataTokens.TryGetValue(RouteDataTokenKeys.Precedence, out possiblePrecedence)) + { + precedence = possiblePrecedence; + } + + foreach (var actionDescriptor in directRouteActions) + { + candidates.Add(new CandidateAction + { + ActionDescriptor = actionDescriptor, + Order = order, + Precedence = precedence + }); + } + + return candidates.ToArray(); + } + + public static HttpActionDescriptor[] GetTargetActionDescriptors(this IHttpRoute route) + { + Contract.Assert(route != null); + var dataTokens = route.DataTokens; + + if (dataTokens == null) + { + return null; + } + + HttpActionDescriptor[] actions; + + if (!dataTokens.TryGetValue(RouteDataTokenKeys.Actions, out actions)) + { + return null; + } + + return actions; + } + + public static HttpControllerDescriptor GetTargetControllerDescriptor(this IHttpRoute route) + { + Contract.Assert(route != null); + var dataTokens = route.DataTokens; + + if (dataTokens == null) + { + return null; + } + + HttpControllerDescriptor controller; + + if (!dataTokens.TryGetValue(RouteDataTokenKeys.Controller, out controller)) + { + return null; + } + + return controller; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs b/Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs new file mode 100644 index 0000000..9196f16 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs @@ -0,0 +1,778 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Description; +using System.Web.Http.ModelBinding.Binders; +using System.Web.Http.Routing; +using System.Web.Http.Services; +using System.Web.OData.Routing; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// Explores the URI space of the service based on routes, controllers and actions available in the system. + /// + public class ODataApiExplorer : IApiExplorer + { + private static readonly Regex _actionVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", RouteValueKeys.Action), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + //private static readonly Regex _controllerVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", RouteValueKeys.Controller), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private static readonly Regex _controllerVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", ODataRouteConstants.ODataPath), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private readonly Lazy> _apiDescriptions; + private readonly HttpConfiguration _config; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public ODataApiExplorer(HttpConfiguration configuration) + { + _config = configuration; + _apiDescriptions = new Lazy>(InitializeApiDescriptions); + } + + /// + /// Gets or sets the documentation provider. The provider will be responsible for documenting the API. + /// + /// + /// The documentation provider. + /// + public IDocumentationProvider DocumentationProvider { get; set; } + + /// + /// Gets the API descriptions. The descriptions are initialized on the first access. + /// + public Collection ApiDescriptions + { + get { return _apiDescriptions.Value; } + } + + /// + /// Determines whether the controller should be considered for generation. + /// Called when initializing the . + /// + /// The controller variable value from the route. + /// The controller descriptor. + /// The route. + /// + /// true if the controller should be considered for generation, + /// false otherwise. + /// + public virtual bool ShouldExploreController(string controllerVariableValue, HttpControllerDescriptor controllerDescriptor, IHttpRoute route) + { + if (controllerDescriptor == null) + { + throw Error.ArgumentNull("controllerDescriptor"); + } + + if (route == null) + { + throw Error.ArgumentNull("route"); + } + + var setting = controllerDescriptor.GetCustomAttributes().FirstOrDefault(); + return (setting == null || !setting.IgnoreApi) && MatchRegexConstraint(route, RouteValueKeys.Controller, controllerVariableValue); + } + + /// + /// Determines whether the action should be considered for generation. + /// Called when initializing the . + /// + /// The action variable value from the route. + /// The action descriptor. + /// The route. + /// + /// true if the action should be considered for generation, + /// false otherwise. + /// + public virtual bool ShouldExploreAction(string actionVariableValue, HttpActionDescriptor actionDescriptor, IHttpRoute route) + { + if (actionDescriptor == null) + { + throw Error.ArgumentNull("actionDescriptor"); + } + + if (route == null) + { + throw Error.ArgumentNull("route"); + } + + var setting = actionDescriptor.GetCustomAttributes().FirstOrDefault(); + return (setting == null || !setting.IgnoreApi) && MatchRegexConstraint(route, RouteValueKeys.Action, actionVariableValue); + } + + /// + /// Gets a collection of HttpMethods supported by the action. Called when initializing the + /// . + /// + /// The route. + /// The action descriptor. + /// A collection of HttpMethods supported by the action. + public virtual Collection GetHttpMethodsSupportedByAction(IHttpRoute route, HttpActionDescriptor actionDescriptor) + { + if (route == null) + { + throw Error.ArgumentNull("route"); + } + + if (actionDescriptor == null) + { + throw Error.ArgumentNull("actionDescriptor"); + } + + IList supportedMethods = new List(); + IList actionHttpMethods = actionDescriptor.SupportedHttpMethods; + var httpMethodConstraint = route.Constraints.Values.FirstOrDefault(c => typeof (HttpMethodConstraint).IsAssignableFrom(c.GetType())) as HttpMethodConstraint; + + if (httpMethodConstraint == null) + { + supportedMethods = actionHttpMethods; + } + else + { + supportedMethods = httpMethodConstraint.AllowedMethods.Intersect(actionHttpMethods).ToList(); + } + + return new Collection(supportedMethods); + } + + private IEnumerable FlattenRoutes(IEnumerable routes) + { + foreach (var route in routes) + { + var nested = route as IEnumerable; + if (nested != null) + { + foreach (var subRoute in FlattenRoutes(nested)) + { + yield return subRoute; + } + } + else + { + yield return route; + } + } + } + + private static HttpControllerDescriptor GetDirectRouteController(CandidateAction[] directRouteCandidates) + { + if (directRouteCandidates != null) + { + // Set the controller descriptor for the first action descriptor + var controllerDescriptor = directRouteCandidates[0].ActionDescriptor.ControllerDescriptor; + + // Check that all other action descriptors share the same controller descriptor + for (var i = 1; i < directRouteCandidates.Length; i++) + { + if (directRouteCandidates[i].ActionDescriptor.ControllerDescriptor != controllerDescriptor) + { + // This can happen if a developer puts the same route template on different actions + // in different controllers. + return null; + } + } + + return controllerDescriptor; + } + + return null; + } + + private Collection InitializeApiDescriptions() + { + + var apiDescriptions = new Collection(); + var controllerSelector = _config.Services.GetHttpControllerSelector(); + var controllerMappings = controllerSelector.GetControllerMapping(); + if (controllerMappings != null) + { + var descriptionComparer = new ApiDescriptionComparer(); + foreach (var route in FlattenRoutes(_config.Routes)) + { + var directRouteCandidates = route.GetDirectRouteCandidates(); + + var directRouteController = GetDirectRouteController(directRouteCandidates); + var descriptionsFromRoute = directRouteController != null && directRouteCandidates != null ? ExploreDirectRoute(directRouteController, directRouteCandidates, route) : ExploreRouteControllers(controllerMappings, route); + + // Remove ApiDescription that will lead to ambiguous action matching. + // E.g. a controller with Post() and PostComment(). When the route template is {controller}, it produces POST /controller and POST /controller. + descriptionsFromRoute = RemoveInvalidApiDescriptions(descriptionsFromRoute); + + foreach (var description in descriptionsFromRoute) + { + // Do not add the description if the previous route has a matching description with the same HTTP method and relative path. + // E.g. having two routes with the templates "api/Values/{id}" and "api/{controller}/{id}" can potentially produce the same + // relative path "api/Values/{id}" but only the first one matters. + if (!apiDescriptions.Contains(description, descriptionComparer)) + { + apiDescriptions.Add(description); + } + } + } + } + + return apiDescriptions; + } + + private Collection ExploreDirectRoute(HttpControllerDescriptor controllerDescriptor, CandidateAction[] candidates, IHttpRoute route) + { + var descriptions = new Collection(); + + if (ShouldExploreController(controllerDescriptor.ControllerName, controllerDescriptor, route)) + { + foreach (var action in candidates) + { + var actionDescriptor = action.ActionDescriptor; + var actionName = actionDescriptor.ActionName; + + if (ShouldExploreAction(actionName, actionDescriptor, route)) + { + var routeTemplate = route.RouteTemplate; + if (_actionVariableRegex.IsMatch(routeTemplate)) + { + // expand {action} variable + routeTemplate = _actionVariableRegex.Replace(routeTemplate, actionName); + } + + PopulateActionDescriptions(actionDescriptor, route, routeTemplate, descriptions); + } + } + } + + return descriptions; + } + + private Collection ExploreRouteControllers(IDictionary controllerMappings, IHttpRoute route) + { + var apiDescriptions = new Collection(); + var routeTemplate = route.RouteTemplate; + string controllerVariableValue; + if (_controllerVariableRegex.IsMatch(routeTemplate)) + { + // unbound controller variable, {controller} + foreach (var controllerMapping in controllerMappings) + { + controllerVariableValue = controllerMapping.Key; + var controllerDescriptor = controllerMapping.Value; + if (ShouldExploreController(controllerVariableValue, controllerDescriptor, route)) + { + // expand {controller} variable + var expandedRouteTemplate = _controllerVariableRegex.Replace(routeTemplate, controllerVariableValue); + ExploreRouteActions(route, expandedRouteTemplate, controllerDescriptor, apiDescriptions); + } + } + } + else if (route.Defaults.TryGetValue(RouteValueKeys.Controller, out controllerVariableValue)) + { + // bound controller variable, {controller = "controllerName"} + HttpControllerDescriptor controllerDescriptor; + if (controllerMappings.TryGetValue(controllerVariableValue, out controllerDescriptor) && ShouldExploreController(controllerVariableValue, controllerDescriptor, route)) + { + ExploreRouteActions(route, routeTemplate, controllerDescriptor, apiDescriptions); + } + } + + return apiDescriptions; + } + + private void ExploreRouteActions(IHttpRoute route, string localPath, HttpControllerDescriptor controllerDescriptor, Collection apiDescriptions) + { + // exclude controllers that are marked with route attributes. + if (!controllerDescriptor.IsAttributeRouted()) + { + var controllerServices = controllerDescriptor.Configuration.Services; + var actionMappings = controllerServices.GetActionSelector().GetActionMapping(controllerDescriptor); + string actionVariableValue; + if (actionMappings != null) + { + if (_actionVariableRegex.IsMatch(localPath)) + { + // unbound action variable, {action} + foreach (var actionMapping in actionMappings) + { + // expand {action} variable + actionVariableValue = actionMapping.Key; + var expandedLocalPath = _actionVariableRegex.Replace(localPath, actionVariableValue); + PopulateActionDescriptions(actionMapping, actionVariableValue, route, expandedLocalPath, apiDescriptions); + } + } + else if (route.Defaults.TryGetValue(RouteValueKeys.Action, out actionVariableValue)) + { + // bound action variable, { action = "actionName" } + PopulateActionDescriptions(actionMappings[actionVariableValue], actionVariableValue, route, localPath, apiDescriptions); + } + else + { + // no {action} specified, e.g. {controller}/{id} + foreach (var actionMapping in actionMappings) + { + PopulateActionDescriptions(actionMapping, null, route, localPath, apiDescriptions); + } + } + } + } + } + + private void PopulateActionDescriptions(IEnumerable actionDescriptors, string actionVariableValue, IHttpRoute route, string localPath, Collection apiDescriptions) + { + foreach (var actionDescriptor in actionDescriptors) + { + if (ShouldExploreAction(actionVariableValue, actionDescriptor, route)) + { + // exclude actions that are marked with route attributes except for the inherited actions. + if (!actionDescriptor.IsAttributeRouted()) + { + PopulateActionDescriptions(actionDescriptor, route, localPath, apiDescriptions); + } + } + } + } + + private void PopulateActionDescriptions(HttpActionDescriptor actionDescriptor, IHttpRoute route, string localPath, Collection apiDescriptions) + { + var apiDocumentation = GetApiDocumentation(actionDescriptor); + + var parsedRoute = RouteParser.Parse(localPath); + + // parameters + var parameterDescriptions = CreateParameterDescriptions(actionDescriptor, parsedRoute, route.Defaults); + + // expand all parameter variables + string finalPath; + + if (!TryExpandUriParameters(route, parsedRoute, parameterDescriptions, out finalPath)) + { + // the action cannot be reached due to parameter mismatch, e.g. routeTemplate = "/users/{name}" and GetUsers(id) + return; + } + + // request formatters + var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.Source == ApiParameterSource.FromBody); + var supportedRequestBodyFormatters = bodyParameter != null ? actionDescriptor.Configuration.Formatters.Where(f => f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) : Enumerable.Empty(); + + // response formatters + var responseDescription = CreateResponseDescription(actionDescriptor); + var returnType = responseDescription.ResponseType ?? responseDescription.DeclaredType; + var supportedResponseFormatters = returnType != null && returnType != typeof (void) ? actionDescriptor.Configuration.Formatters.Where(f => f.CanWriteType(returnType)) : Enumerable.Empty(); + + // Replacing the formatter tracers with formatters if tracers are present. + supportedRequestBodyFormatters = GetInnerFormatters(supportedRequestBodyFormatters); + supportedResponseFormatters = GetInnerFormatters(supportedResponseFormatters); + + // get HttpMethods supported by an action. Usually there is one HttpMethod per action but we allow multiple of them per action as well. + IList supportedMethods = GetHttpMethodsSupportedByAction(route, actionDescriptor); + + foreach (var method in supportedMethods) + { + var apiDescription = new ApiDescription + { + Documentation = apiDocumentation, + HttpMethod = method, + RelativePath = finalPath, + ActionDescriptor = actionDescriptor, + Route = route + }; + + foreach (var supportedResponseFormatter in supportedResponseFormatters) + { + apiDescription.SupportedResponseFormatters.Add(supportedResponseFormatter); + } + + foreach (var supportedRequestBodyFormatter in supportedRequestBodyFormatters) + { + apiDescription.SupportedRequestBodyFormatters.Add(supportedRequestBodyFormatter); + } + + foreach (var apiParameterDescription in parameterDescriptions) + { + apiDescription.ParameterDescriptions.Add(apiParameterDescription); + } + + //apiDescription.ResponseDescription = responseDescription + + apiDescriptions.Add(apiDescription); + } + } + + private ResponseDescription CreateResponseDescription(HttpActionDescriptor actionDescriptor) + { + var responseTypeAttribute = actionDescriptor.GetCustomAttributes(); + var responseType = responseTypeAttribute.Select(attribute => attribute.ResponseType).FirstOrDefault(); + + return new ResponseDescription + { + DeclaredType = actionDescriptor.ReturnType, + ResponseType = responseType, + Documentation = GetApiResponseDocumentation(actionDescriptor) + }; + } + + private static IEnumerable GetInnerFormatters(IEnumerable mediaTypeFormatters) + { + foreach (var formatter in mediaTypeFormatters) + { + yield return Decorator.GetInner(formatter); + } + } + + private static bool ShouldEmitPrefixes(ICollection parameterDescriptions) + { + // Determine if there are two or more complex objects from the Uri so TryExpandUriParameters needs to emit prefixes. + return parameterDescriptions.Count(parameter => parameter.Source == ApiParameterSource.FromUri && parameter.ParameterDescriptor != null && !TypeHelper.CanConvertFromString(parameter.ParameterDescriptor.ParameterType) && parameter.CanConvertPropertiesFromString()) > 1; + } + + // Set as internal for the unit test. + internal static bool TryExpandUriParameters(IHttpRoute route, HttpParsedRoute parsedRoute, ICollection parameterDescriptions, out string expandedRouteTemplate) + { + var parameterValuesForRoute = new Dictionary(StringComparer.OrdinalIgnoreCase); + var emitPrefixes = ShouldEmitPrefixes(parameterDescriptions); + var prefix = string.Empty; + foreach (var parameterDescription in parameterDescriptions) + { + if (parameterDescription.Source == ApiParameterSource.FromUri) + { + if (parameterDescription.ParameterDescriptor == null) + { + // Undeclared route parameter handling generates query string like + // "?name={name}" + AddPlaceholder(parameterValuesForRoute, parameterDescription.Name); + } + else if (TypeHelper.CanConvertFromString(parameterDescription.ParameterDescriptor.ParameterType)) + { + // Simple type generates query string like + // "?name={name}" + AddPlaceholder(parameterValuesForRoute, parameterDescription.Name); + } + else if (IsBindableCollection(parameterDescription.ParameterDescriptor.ParameterType)) + { + var parameterName = parameterDescription.ParameterDescriptor.ParameterName; + var innerType = GetCollectionElementType(parameterDescription.ParameterDescriptor.ParameterType); + var innerTypeProperties = ApiParameterDescriptionExtensions.GetBindableProperties(innerType).ToArray(); + if (innerTypeProperties.Any()) + { + // Complex array and collection generate query string like + // "?name[0].foo={name[0].foo}&name[0].bar={name[0].bar} + // &name[1].foo={name[1].foo}&name[1].bar={name[1].bar}" + AddPlaceholderForProperties(parameterValuesForRoute, innerTypeProperties, parameterName + "[0]."); + AddPlaceholderForProperties(parameterValuesForRoute, innerTypeProperties, parameterName + "[1]."); + } + else + { + // Simple array and collection generate query string like + // "?name[0]={name[0]}&name[1]={name[1]}". + AddPlaceholder(parameterValuesForRoute, parameterName + "[0]"); + AddPlaceholder(parameterValuesForRoute, parameterName + "[1]"); + } + } + else if (IsBindableKeyValuePair(parameterDescription.ParameterDescriptor.ParameterType)) + { + // KeyValuePair generates query string like + // "?key={key}&value={value}" + AddPlaceholder(parameterValuesForRoute, "key"); + AddPlaceholder(parameterValuesForRoute, "value"); + } + else if (IsBindableDictionry(parameterDescription.ParameterDescriptor.ParameterType)) + { + // Dictionary generates query string like + // "?dict[0].key={dict[0].key}&dict[0].value={dict[0].value} + // &dict[1].key={dict[1].key}&dict[1].value={dict[1].value}" + var parameterName = parameterDescription.ParameterDescriptor.ParameterName; + AddPlaceholder(parameterValuesForRoute, parameterName + "[0].key"); + AddPlaceholder(parameterValuesForRoute, parameterName + "[0].value"); + AddPlaceholder(parameterValuesForRoute, parameterName + "[1].key"); + AddPlaceholder(parameterValuesForRoute, parameterName + "[1].value"); + } + else if (parameterDescription.CanConvertPropertiesFromString()) + { + if (emitPrefixes) + { + prefix = parameterDescription.Name + "."; + } + + // Inserting the individual properties of the object in the query string + // as all the complex object can not be converted from string, but all its + // individual properties can. + AddPlaceholderForProperties(parameterValuesForRoute, parameterDescription.ParameterDescriptor.GetBindableProperties(), prefix); + } + } + } + + var boundRouteTemplate = parsedRoute.Bind(null, parameterValuesForRoute, new HttpRouteValueDictionary(route.Defaults), new HttpRouteValueDictionary(route.Constraints)); + if (boundRouteTemplate == null) + { + expandedRouteTemplate = null; + return false; + } + + expandedRouteTemplate = Uri.UnescapeDataString(boundRouteTemplate.BoundTemplate); + return true; + } + + private static Type GetCollectionElementType(Type collectionType) + { + Contract.Assert(!typeof (IDictionary).IsAssignableFrom(collectionType)); + + var elementType = collectionType.GetElementType(); + if (elementType == null) + { + elementType = CollectionModelBinderUtil.GetGenericBinderTypeArgs(typeof (ICollection<>), collectionType).First(); + } + return elementType; + } + + private static void AddPlaceholderForProperties(Dictionary parameterValuesForRoute, IEnumerable properties, string prefix) + { + foreach (var property in properties) + { + var queryParameterName = prefix + property.Name; + AddPlaceholder(parameterValuesForRoute, queryParameterName); + } + } + + private static bool IsBindableCollection(Type type) + { + Contract.Assert(type != null); + + return type.IsArray || new CollectionModelBinderProvider().GetBinder(null, type) != null; + } + + private static bool IsBindableDictionry(Type type) + { + Contract.Assert(type != null); + + return new DictionaryModelBinderProvider().GetBinder(null, type) != null; + } + + private static bool IsBindableKeyValuePair(Type type) + { + Contract.Assert(type != null); + + return TypeHelper.GetTypeArgumentsIfMatch(type, typeof (KeyValuePair<,>)) != null; + } + + private static void AddPlaceholder(Dictionary parameterValuesForRoute, string queryParameterName) + { + if (!parameterValuesForRoute.ContainsKey(queryParameterName)) + { + parameterValuesForRoute.Add(queryParameterName, "{" + queryParameterName + "}"); + } + } + + private IList CreateParameterDescriptions(HttpActionDescriptor actionDescriptor, HttpParsedRoute parsedRoute, IDictionary routeDefaults) + { + IList parameterDescriptions = new List(); + var actionBinding = GetActionBinding(actionDescriptor); + + // try get parameter binding information if available + if (actionBinding != null) + { + var parameterBindings = actionBinding.ParameterBindings; + if (parameterBindings != null) + { + foreach (var parameter in parameterBindings) + { + parameterDescriptions.Add(CreateParameterDescriptionFromBinding(parameter)); + } + } + } + else + { + var parameters = actionDescriptor.GetParameters(); + if (parameters != null) + { + foreach (var parameter in parameters) + { + parameterDescriptions.Add(CreateParameterDescriptionFromDescriptor(parameter)); + } + } + } + + // Adding route parameters not declared on the action. We're doing this because route parameters may or + // may not be part of the action parameters and we want to have them in the description. + AddUndeclaredRouteParameters(parsedRoute, routeDefaults, parameterDescriptions); + + return parameterDescriptions; + } + + private static void AddUndeclaredRouteParameters(HttpParsedRoute parsedRoute, IDictionary routeDefaults, IList parameterDescriptions) + { + foreach (var path in parsedRoute.PathSegments) + { + var content = path as PathContentSegment; + if (content != null) + { + foreach (var subSegment in content.Subsegments) + { + var parameter = subSegment as PathParameterSubsegment; + if (parameter != null) + { + object parameterValue; + var parameterName = parameter.ParameterName; + if (!parameterDescriptions.Any(p => string.Equals(p.Name, parameterName, StringComparison.OrdinalIgnoreCase)) && (!routeDefaults.TryGetValue(parameterName, out parameterValue) || parameterValue != RouteParameter.Optional)) + { + parameterDescriptions.Add(new ApiParameterDescription + { + Name = parameterName, + Source = ApiParameterSource.FromUri + }); + } + } + } + } + } + } + + private ApiParameterDescription CreateParameterDescriptionFromDescriptor(HttpParameterDescriptor parameter) + { + Contract.Assert(parameter != null); + return new ApiParameterDescription + { + ParameterDescriptor = parameter, + Name = parameter.Prefix ?? parameter.ParameterName, + Documentation = GetApiParameterDocumentation(parameter), + Source = ApiParameterSource.Unknown + }; + } + + private ApiParameterDescription CreateParameterDescriptionFromBinding(HttpParameterBinding parameterBinding) + { + var parameterDescription = CreateParameterDescriptionFromDescriptor(parameterBinding.Descriptor); + if (parameterBinding.WillReadBody) + { + parameterDescription.Source = ApiParameterSource.FromBody; + } + else if (parameterBinding.WillReadUri()) + { + parameterDescription.Source = ApiParameterSource.FromUri; + } + + return parameterDescription; + } + + private string GetApiDocumentation(HttpActionDescriptor actionDescriptor) + { + var documentationProvider = DocumentationProvider ?? actionDescriptor.Configuration.Services.GetDocumentationProvider(); + if (documentationProvider != null) + { + return documentationProvider.GetDocumentation(actionDescriptor); + } + + return null; + } + + private string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor) + { + var documentationProvider = DocumentationProvider ?? parameterDescriptor.Configuration.Services.GetDocumentationProvider(); + if (documentationProvider != null) + { + return documentationProvider.GetDocumentation(parameterDescriptor); + } + + return null; + } + + private string GetApiResponseDocumentation(HttpActionDescriptor actionDescriptor) + { + var documentationProvider = DocumentationProvider ?? actionDescriptor.Configuration.Services.GetDocumentationProvider(); + if (documentationProvider != null) + { + return documentationProvider.GetResponseDocumentation(actionDescriptor); + } + + return null; + } + + // remove ApiDescription that will lead to ambiguous action matching. + private static Collection RemoveInvalidApiDescriptions(Collection apiDescriptions) + { + var duplicateApiDescriptionIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var visitedApiDescriptionIds = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var description in apiDescriptions) + { + var apiDescriptionId = description.ID; + if (visitedApiDescriptionIds.Contains(apiDescriptionId)) + { + duplicateApiDescriptionIds.Add(apiDescriptionId); + } + else + { + visitedApiDescriptionIds.Add(apiDescriptionId); + } + } + + var filteredApiDescriptions = new Collection(); + foreach (var apiDescription in apiDescriptions) + { + var apiDescriptionId = apiDescription.ID; + if (!duplicateApiDescriptionIds.Contains(apiDescriptionId)) + { + filteredApiDescriptions.Add(apiDescription); + } + } + + return filteredApiDescriptions; + } + + private static bool MatchRegexConstraint(IHttpRoute route, string parameterName, string parameterValue) + { + var constraints = route.Constraints; + if (constraints != null) + { + object constraint; + if (constraints.TryGetValue(parameterName, out constraint)) + { + // treat the constraint as a string which represents a Regex. + // note that we don't support custom constraint (IHttpRouteConstraint) because it might rely on the request and some runtime states + var constraintsRule = constraint as string; + if (constraintsRule != null) + { + var constraintsRegEx = "^(" + constraintsRule + ")$"; + return parameterValue != null && Regex.IsMatch(parameterValue, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } + } + } + + return true; + } + + private static HttpActionBinding GetActionBinding(HttpActionDescriptor actionDescriptor) + { + var controllerDescriptor = actionDescriptor.ControllerDescriptor; + if (controllerDescriptor == null) + { + return null; + } + + var controllerServices = controllerDescriptor.Configuration.Services; + var actionValueBinder = controllerServices.GetActionValueBinder(); + var actionBinding = actionValueBinder != null ? actionValueBinder.GetBinding(actionDescriptor) : null; + return actionBinding; + } + + private sealed class ApiDescriptionComparer : IEqualityComparer + { + public bool Equals(ApiDescription x, ApiDescription y) + { + return string.Equals(x.ID, y.ID, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(ApiDescription obj) + { + return obj.ID.ToUpperInvariant().GetHashCode(); + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/PathContentSegment.cs b/Swashbuckle.OData/ApiExplorer/PathContentSegment.cs new file mode 100644 index 0000000..6e42234 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/PathContentSegment.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters. + internal sealed class PathContentSegment : PathSegment + { + public PathContentSegment(List subsegments) + { + Subsegments = subsegments; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")] + public bool IsCatchAll + { + get + { + // TODO: Verify this is correct. Maybe add an assert. + // Performance sensitive + // Caching count is faster for IList + var subsegmentCount = Subsegments.Count; + for (var i = 0; i < subsegmentCount; i++) + { + var seg = Subsegments[i]; + var paramterSubSegment = seg as PathParameterSubsegment; + if (paramterSubSegment != null && paramterSubSegment.IsCatchAll) + { + return true; + } + } + return false; + } + } + + public List Subsegments { get; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + List s = new List(); + foreach (PathSubsegment subsegment in Subsegments) + { + s.Add(subsegment.LiteralText); + } + return String.Join(String.Empty, s.ToArray()); + } + } + + public override string ToString() + { + List s = new List(); + foreach (PathSubsegment subsegment in Subsegments) + { + s.Add(subsegment.ToString()); + } + return "[ " + String.Join(", ", s.ToArray()) + " ]"; + } +#endif + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/PathLiteralSubsegment.cs b/Swashbuckle.OData/ApiExplorer/PathLiteralSubsegment.cs new file mode 100644 index 0000000..04b461d --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/PathLiteralSubsegment.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // Represents a literal subsegment of a ContentPathSegment + internal sealed class PathLiteralSubsegment : PathSubsegment + { + public PathLiteralSubsegment(string literal) + { + Literal = literal; + } + + public string Literal { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return Literal; + } + } + + public override string ToString() + { + return "\"" + Literal + "\""; + } +#endif + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/PathParameterSubsegment.cs b/Swashbuckle.OData/ApiExplorer/PathParameterSubsegment.cs new file mode 100644 index 0000000..357d599 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/PathParameterSubsegment.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + + +using System; + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // Represents a parameter subsegment of a ContentPathSegment + internal sealed class PathParameterSubsegment : PathSubsegment + { + public PathParameterSubsegment(string parameterName) + { + if (parameterName.StartsWith("*", StringComparison.Ordinal)) + { + ParameterName = parameterName.Substring(1); + IsCatchAll = true; + } + else + { + ParameterName = parameterName; + } + } + + public bool IsCatchAll { get; private set; } + + public string ParameterName { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; + } + } + + public override string ToString() + { + return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; + } +#endif + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/PathSegment.cs b/Swashbuckle.OData/ApiExplorer/PathSegment.cs new file mode 100644 index 0000000..e34c28b --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/PathSegment.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // Represents a segment of a URI such as a separator or content + internal abstract class PathSegment + { +#if ROUTE_DEBUGGING + public abstract string LiteralText + { + get; + } +#endif + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/PathSeparatorSegment.cs b/Swashbuckle.OData/ApiExplorer/PathSeparatorSegment.cs new file mode 100644 index 0000000..13a4340 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/PathSeparatorSegment.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // Represents a "/" separator in a URI + internal sealed class PathSeparatorSegment : PathSegment + { +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return "/"; + } + } + + public override string ToString() + { + return "\"/\""; + } +#endif + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/PathSubsegment.cs b/Swashbuckle.OData/ApiExplorer/PathSubsegment.cs new file mode 100644 index 0000000..4ef7014 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/PathSubsegment.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // Represents a subsegment of a ContentPathSegment such as a parameter or a literal. + internal abstract class PathSubsegment + { +#if ROUTE_DEBUGGING + public abstract string LiteralText + { + get; + } +#endif + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/RouteCollectionRoute.cs b/Swashbuckle.OData/ApiExplorer/RouteCollectionRoute.cs new file mode 100644 index 0000000..b9a1629 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/RouteCollectionRoute.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Net.Http; +using System.Web.Http.Routing; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// A single route that is the composite of multiple "sub routes". + /// + /// + /// Corresponds to the MVC implementation of attribute routing in System.Web.Mvc.Routing.RouteCollectionRoute. + /// + internal class RouteCollectionRoute : IHttpRoute, IReadOnlyCollection + { + // Key for accessing SubRoutes on a RouteData. + // We expose this through the RouteData.Values instead of a derived class because + // RouteData can get wrapped in another type, but Values still gets persisted through the wrappers. + // Prefix with a \0 to protect against conflicts with user keys. + public const string SubRouteDataKey = "MS_SubRoutes"; + + private static readonly IDictionary _empty = EmptyReadOnlyDictionary.Value; + + // This will enumerate all controllers and action descriptors, which will run those + // Initialization hooks, which may try to initialize controller-specific config, which + // may call back to the initialize hook. So guard against that reentrancy. + private bool _beingInitialized; + + private IReadOnlyCollection _subRoutes; + + private IReadOnlyCollection SubRoutes + { + get + { + // Caller should have already explicitly called EnsureInitialize. + // Avoid lazy initilization from within the route table because the route table + // is shared resource and init can happen + if (_subRoutes == null) + { + var msg = Error.Format("The object has not yet been initialized. Ensure that HttpConfiguration.EnsureInitialized() is called in the application's startup code after all other initialization code."); + throw new InvalidOperationException(msg); + } + + return _subRoutes; + } + } + + public string RouteTemplate + { + get { return string.Empty; } + } + + public IDictionary Defaults + { + get { return _empty; } + } + + public IDictionary Constraints + { + get { return _empty; } + } + + public IDictionary DataTokens + { + get { return null; } + } + + public HttpMessageHandler Handler + { + get { return null; } + } + + // Returns null if no match. + // Else, returns a composite route data that encapsulates the possible routes this may match against. + public IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request) + { + var matches = new List(); + foreach (var route in SubRoutes) + { + var match = route.GetRouteData(virtualPathRoot, request); + if (match != null) + { + matches.Add(match); + } + } + if (matches.Count == 0) + { + return null; // no matches + } + + return new RouteCollectionRouteData(this, matches.ToArray()); + } + + public IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary values) + { + // Use LinkGenerationRoute stubs to get placeholders for all the sub routes. + return null; + } + + public int Count + { + get { return SubRoutes.Count; } + } + + public IEnumerator GetEnumerator() + { + return SubRoutes.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return SubRoutes.GetEnumerator(); + } + + // deferred hook for initializing the sub routes. The composite route can be added during the middle of + // intializing, but then the actual sub routes can get populated after initialization has finished. + public void EnsureInitialized(Func> initializer) + { + if (_beingInitialized && _subRoutes == null) + { + // Avoid reentrant initialization + return; + } + + try + { + _beingInitialized = true; + + _subRoutes = initializer(); + Contract.Assert(_subRoutes != null); + } + finally + { + _beingInitialized = false; + } + } + + // Represents a union of multiple IHttpRouteDatas. + private class RouteCollectionRouteData : IHttpRouteData + { + public RouteCollectionRouteData(IHttpRoute parent, IHttpRouteData[] subRouteDatas) + { + Route = parent; + + // Each sub route may have different values. Callers need to enumerate the subroutes + // and individually query each. + // Find sub-routes via the SubRouteDataKey; don't expose as a property since the RouteData + // can be wrapped in an outer type that doesn't propagate properties. + Values = new HttpRouteValueDictionary + { + {SubRouteDataKey, subRouteDatas} + }; + } + + public IHttpRoute Route { get; } + + public IDictionary Values { get; } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/RouteDataTokenKeys.cs b/Swashbuckle.OData/ApiExplorer/RouteDataTokenKeys.cs new file mode 100644 index 0000000..fc16b84 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/RouteDataTokenKeys.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// Provides keys for looking up route values and data tokens. + /// + internal static class RouteDataTokenKeys + { + // Used to provide the action descriptors to consider for attribute routing + public const string Actions = "actions"; + + // Used to indicate that a route is a controller-level attribute route. + public const string Controller = "controller"; + + // Used to allow customer-provided disambiguation between multiple matching attribute routes + public const string Order = "order"; + + // Used to allow URI constraint-based disambiguation between multiple matching attribute routes + public const string Precedence = "precedence"; + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/RouteParser.cs b/Swashbuckle.OData/ApiExplorer/RouteParser.cs new file mode 100644 index 0000000..57cce58 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/RouteParser.cs @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +#if ASPNETWEBAPI +using ErrorResources = Swashbuckle.OData.ApiExplorer.SRResources; +using TParsedRoute = Swashbuckle.OData.ApiExplorer.HttpParsedRoute; + +#else +using ErrorResources = System.Web.Mvc.Properties.MvcResources; +using TParsedRoute = System.Web.Mvc.Routing.ParsedRoute; +#endif + +#if ASPNETWEBAPI + +namespace Swashbuckle.OData.ApiExplorer +#else +namespace System.Web.Mvc.Routing +#endif +{ + // in the MVC case, route parsing is done for AttributeRouting's sake, so that + // it could order the discovered routes before pushing them into the routeCollection, + // where, unfortunately, they would be parsed again. + internal static class RouteParser + { + private static string GetLiteral(string segmentLiteral) + { + // Scan for errant single { and } and convert double {{ to { and double }} to } + + // First we eliminate all escaped braces and then check if any other braces are remaining + var newLiteral = segmentLiteral.Replace("{{", string.Empty).Replace("}}", string.Empty); + if (newLiteral.Contains("{") || newLiteral.Contains("}")) + { + return null; + } + + // If it's a valid format, we unescape the braces + return segmentLiteral.Replace("{{", "{").Replace("}}", "}"); + } + + private static int IndexOfFirstOpenParameter(string segment, int startIndex) + { + // Find the first unescaped open brace + while (true) + { + startIndex = segment.IndexOf('{', startIndex); + if (startIndex == -1) + { + // If there are no more open braces, stop + return -1; + } + if ((startIndex + 1 == segment.Length) || ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{'))) + { + // If we found an open brace that is followed by a non-open brace, it's + // a parameter delimiter. + // It's also a delimiter if the open brace is the last character - though + // it ends up being being called out as invalid later on. + return startIndex; + } + // Increment by two since we want to skip both the open brace that + // we're on as well as the subsequent character since we know for + // sure that it is part of an escape sequence. + startIndex += 2; + } + } + + internal static bool IsSeparator(string s) + { + return string.Equals(s, "/", StringComparison.Ordinal); + } + + private static bool IsValidParameterName(string parameterName) + { + if (parameterName.Length == 0) + { + return false; + } + + for (var i = 0; i < parameterName.Length; i++) + { + var c = parameterName[i]; + if (c == '/' || c == '{' || c == '}') + { + return false; + } + } + + return true; + } + + internal static bool IsInvalidRouteTemplate(string routeTemplate) + { + return routeTemplate.StartsWith("~", StringComparison.Ordinal) || routeTemplate.StartsWith("/", StringComparison.Ordinal) || (routeTemplate.IndexOf('?') != -1); + } + + public static TParsedRoute Parse(string routeTemplate) + { + if (routeTemplate == null) + { + routeTemplate = string.Empty; + } + + if (IsInvalidRouteTemplate(routeTemplate)) + { + throw Error.Argument("routeTemplate", ErrorResources.Route_InvalidRouteTemplate); + } + + var uriParts = SplitUriToPathSegmentStrings(routeTemplate); + var ex = ValidateUriParts(uriParts); + if (ex != null) + { + throw ex; + } + + var pathSegments = SplitUriToPathSegments(uriParts); + + Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments"); + + return new TParsedRoute(pathSegments); + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static List ParseUriSegment(string segment, out Exception exception) + { + var startIndex = 0; + + var pathSubsegments = new List(); + + while (startIndex < segment.Length) + { + var nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex); + if (nextParameterStart == -1) + { + // If there are no more parameters in the segment, capture the remainder as a literal and stop + var lastLiteralPart = GetLiteral(segment.Substring(startIndex)); + if (lastLiteralPart == null) + { + exception = Error.Argument("routeTemplate", ErrorResources.Route_MismatchedParameter, segment); + return null; + } + + if (lastLiteralPart.Length > 0) + { + pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart)); + } + break; + } + + var nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1); + if (nextParameterEnd == -1) + { + exception = Error.Argument("routeTemplate", ErrorResources.Route_MismatchedParameter, segment); + return null; + } + + var literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex)); + if (literalPart == null) + { + exception = Error.Argument("routeTemplate", ErrorResources.Route_MismatchedParameter, segment); + return null; + } + + if (literalPart.Length > 0) + { + pathSubsegments.Add(new PathLiteralSubsegment(literalPart)); + } + + var parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1); + pathSubsegments.Add(new PathParameterSubsegment(parameterName)); + + startIndex = nextParameterEnd + 1; + } + + exception = null; + return pathSubsegments; + } + + private static List SplitUriToPathSegments(List uriParts) + { + var pathSegments = new List(); + + foreach (var pathSegment in uriParts) + { + var isCurrentPartSeparator = IsSeparator(pathSegment); + if (isCurrentPartSeparator) + { + pathSegments.Add(new PathSeparatorSegment()); + } + else + { + Exception exception; + var subsegments = ParseUriSegment(pathSegment, out exception); + Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here"); + pathSegments.Add(new PathContentSegment(subsegments)); + } + } + return pathSegments; + } + + internal static List SplitUriToPathSegmentStrings(string uri) + { + var parts = new List(); + + if (string.IsNullOrEmpty(uri)) + { + return parts; + } + + var currentIndex = 0; + + // Split the incoming URI into individual parts + while (currentIndex < uri.Length) + { + var indexOfNextSeparator = uri.IndexOf('/', currentIndex); + if (indexOfNextSeparator == -1) + { + // If there are no more separators, the rest of the string is the last part + var finalPart = uri.Substring(currentIndex); + if (finalPart.Length > 0) + { + parts.Add(finalPart); + } + break; + } + + var nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex); + if (nextPart.Length > 0) + { + parts.Add(nextPart); + } + + Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'."); + parts.Add("/"); + currentIndex = indexOfNextSeparator + 1; + } + + return parts; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static Exception ValidateUriParts(List pathSegments) + { + Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null."); + + var usedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + bool? isPreviousPartSeparator = null; + + var foundCatchAllParameter = false; + + foreach (var pathSegment in pathSegments) + { + if (foundCatchAllParameter) + { + // If we ever start an iteration of the loop and we've already found a + // catchall parameter then we have an invalid URI format. + return Error.Argument("routeTemplate", ErrorResources.Route_CatchAllMustBeLast, "routeTemplate"); + } + + bool isCurrentPartSeparator; + if (isPreviousPartSeparator == null) + { + // Prime the loop with the first value + isPreviousPartSeparator = IsSeparator(pathSegment); + isCurrentPartSeparator = isPreviousPartSeparator.Value; + } + else + { + isCurrentPartSeparator = IsSeparator(pathSegment); + + // If both the previous part and the current part are separators, it's invalid + if (isCurrentPartSeparator && isPreviousPartSeparator.Value) + { + return Error.Argument("routeTemplate", ErrorResources.Route_CannotHaveConsecutiveSeparators); + } + + Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part."); + isPreviousPartSeparator = isCurrentPartSeparator; + } + + // If it's not a separator, parse the segment for parameters and validate it + if (!isCurrentPartSeparator) + { + Exception exception; + var subsegments = ParseUriSegment(pathSegment, out exception); + if (exception != null) + { + return exception; + } + + exception = ValidateUriSegment(subsegments, usedParameterNames); + if (exception != null) + { + return exception; + } + + foundCatchAllParameter = subsegments.Any(seg => seg is PathParameterSubsegment && ((PathParameterSubsegment) seg).IsCatchAll); + } + } + return null; + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static Exception ValidateUriSegment(List pathSubsegments, HashSet usedParameterNames) + { + var segmentContainsCatchAll = false; + + Type previousSegmentType = null; + + foreach (var subsegment in pathSubsegments) + { + if (previousSegmentType != null) + { + if (previousSegmentType == subsegment.GetType()) + { + return Error.Argument("routeTemplate", ErrorResources.Route_CannotHaveConsecutiveParameters); + } + } + previousSegmentType = subsegment.GetType(); + + var literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // Nothing to validate for literals - everything is valid + } + else + { + var parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + var parameterName = parameterSubsegment.ParameterName; + + if (parameterSubsegment.IsCatchAll) + { + segmentContainsCatchAll = true; + } + + // Check for valid characters in the parameter name + if (!IsValidParameterName(parameterName)) + { + return Error.Argument("routeTemplate", ErrorResources.Route_InvalidParameterName, parameterName); + } + + if (usedParameterNames.Contains(parameterName)) + { + return Error.Argument("routeTemplate", ErrorResources.Route_RepeatedParameter, parameterName); + } + usedParameterNames.Add(parameterName); + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (segmentContainsCatchAll && (pathSubsegments.Count != 1)) + { + return Error.Argument("routeTemplate", ErrorResources.Route_CannotHaveCatchAllInMultiSegment); + } + + return null; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/RouteValueKeys.cs b/Swashbuckle.OData/ApiExplorer/RouteValueKeys.cs similarity index 54% rename from Swashbuckle.OData/RouteValueKeys.cs rename to Swashbuckle.OData/ApiExplorer/RouteValueKeys.cs index 6317a71..9dcf23c 100644 --- a/Swashbuckle.OData/RouteValueKeys.cs +++ b/Swashbuckle.OData/ApiExplorer/RouteValueKeys.cs @@ -1,4 +1,6 @@ -namespace Swashbuckle.OData +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Swashbuckle.OData.ApiExplorer { internal static class RouteValueKeys { diff --git a/Swashbuckle.OData/ApiExplorer/RoutingContext.cs b/Swashbuckle.OData/ApiExplorer/RoutingContext.cs new file mode 100644 index 0000000..7bc34f9 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/RoutingContext.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// Parameter class for URL matching in routing. + /// + internal class RoutingContext + { + private static readonly RoutingContext CachedInvalid = new RoutingContext + { + IsValid = false + }; + + private RoutingContext() + { + } + + public bool IsValid { get; private set; } + + public List PathSegments { get; private set; } + + public static RoutingContext Invalid() + { + return CachedInvalid; + } + + public static RoutingContext Valid(List pathSegments) + { + return new RoutingContext + { + PathSegments = pathSegments, + IsValid = true + }; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/SRResources.Designer.cs b/Swashbuckle.OData/ApiExplorer/SRResources.Designer.cs new file mode 100644 index 0000000..4894ad4 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/SRResources.Designer.cs @@ -0,0 +1,1282 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Swashbuckle.OData.ApiExplorer { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SRResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SRResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Swashbuckle.OData.ApiExplorer.SRResources", typeof(SRResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method.. + /// + internal static string ActionExecutor_UnexpectedTaskInstance { + get { + return ResourceManager.GetString("ActionExecutor_UnexpectedTaskInstance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task.. + /// + internal static string ActionExecutor_WrappedTaskInstance { + get { + return ResourceManager.GetString("ActionExecutor_WrappedTaskInstance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to After calling {0}.OnActionExecuted, the HttpActionExecutedContext properties Result and Exception were both null. At least one of these values must be non-null. To provide a new response, please set the Result object; to indicate an error, please throw an exception.. + /// + internal static string ActionFilterAttribute_MustSupplyResponseOrException { + get { + return ResourceManager.GetString("ActionFilterAttribute_MustSupplyResponseOrException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} on type {1}. + /// + internal static string ActionSelector_AmbiguousMatchType { + get { + return ResourceManager.GetString("ActionSelector_AmbiguousMatchType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ApiController.Request must not be null.. + /// + internal static string ApiController_RequestMustNotBeNull { + get { + return ResourceManager.GetString("ApiController_RequestMustNotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An object of type '{0}' was returned where an instance of IHttpActionResult was expected.. + /// + internal static string ApiControllerActionInvoker_InvalidHttpActionResult { + get { + return ResourceManager.GetString("ApiControllerActionInvoker_InvalidHttpActionResult", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A null value was returned where an instance of IHttpActionResult was expected.. + /// + internal static string ApiControllerActionInvoker_NullHttpActionResult { + get { + return ResourceManager.GetString("ApiControllerActionInvoker_NullHttpActionResult", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No action was found on the controller '{0}' that matches the name '{1}'.. + /// + internal static string ApiControllerActionSelector_ActionNameNotFound { + get { + return ResourceManager.GetString("ApiControllerActionSelector_ActionNameNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No action was found on the controller '{0}' that matches the request.. + /// + internal static string ApiControllerActionSelector_ActionNotFound { + get { + return ResourceManager.GetString("ApiControllerActionSelector_ActionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple actions were found that match the request: {0}. + /// + internal static string ApiControllerActionSelector_AmbiguousMatch { + get { + return ResourceManager.GetString("ApiControllerActionSelector_AmbiguousMatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The requested resource does not support http method '{0}'.. + /// + internal static string ApiControllerActionSelector_HttpMethodNotSupported { + get { + return ResourceManager.GetString("ApiControllerActionSelector_HttpMethodNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route prefix '{0}' on the controller named '{1}' cannot end with a '/' character.. + /// + internal static string AttributeRoutes_InvalidPrefix { + get { + return ResourceManager.GetString("AttributeRoutes_InvalidPrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template '{0}' on the action named '{1}' cannot start with a '/' character.. + /// + internal static string AttributeRoutes_InvalidTemplate { + get { + return ResourceManager.GetString("AttributeRoutes_InvalidTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authentication filter did not encounter an error or set a principal.. + /// + internal static string AuthenticationFilterDidNothing { + get { + return ResourceManager.GetString("AuthenticationFilterDidNothing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authentication filter encountered an error. ErrorResult='{0}'.. + /// + internal static string AuthenticationFilterErrorResult { + get { + return ResourceManager.GetString("AuthenticationFilterErrorResult", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authentication filter successfully set a principal to a known identity. Identity.Name='{0}'. Identity.AuthenticationType='{1}'.. + /// + internal static string AuthenticationFilterSetPrincipalToKnownIdentity { + get { + return ResourceManager.GetString("AuthenticationFilterSetPrincipalToKnownIdentity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The authentication filter set a principal to an unknown identity.. + /// + internal static string AuthenticationFilterSetPrincipalToUnknownIdentity { + get { + return ResourceManager.GetString("AuthenticationFilterSetPrincipalToUnknownIdentity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request is invalid.. + /// + internal static string BadRequest { + get { + return ResourceManager.GetString("BadRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The batch request must have a "Content-Type" header.. + /// + internal static string BatchContentTypeMissing { + get { + return ResourceManager.GetString("BatchContentTypeMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The batch request of media type '{0}' is not supported.. + /// + internal static string BatchMediaTypeNotSupported { + get { + return ResourceManager.GetString("BatchMediaTypeNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The 'Content' property on the batch request cannot be null.. + /// + internal static string BatchRequestMissingContent { + get { + return ResourceManager.GetString("BatchRequestMissingContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot reuse an '{0}' instance. '{0}' has to be constructed per incoming message. Check your custom '{1}' and make sure that it will not manufacture the same instance.. + /// + internal static string CannotSupportSingletonInstance { + get { + return ResourceManager.GetString("CannotSupportSingletonInstance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter '{0}' cannot contain a null element.. + /// + internal static string CollectionParameterContainsNullElement { + get { + return ResourceManager.GetString("CollectionParameterContainsNullElement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The property {0}.{1} could not be found.. + /// + internal static string Common_PropertyNotFound { + get { + return ResourceManager.GetString("Common_PropertyNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type {0} must derive from {1}.. + /// + internal static string Common_TypeMustDriveFromType { + get { + return ResourceManager.GetString("Common_TypeMustDriveFromType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No route providing a controller name was found to match request URI '{0}'. + /// + internal static string ControllerNameNotFound { + get { + return ResourceManager.GetString("ControllerNameNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}.. + /// + internal static string DataAnnotationsModelValidatorProvider_ConstructorRequirements { + get { + return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type {0} must have a public constructor which accepts two parameters of types {1} and {2}.. + /// + internal static string DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements { + get { + return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request ('{1}') found multiple controllers defined with the same name but differing namespaces, which is not supported.{3}{3}The request for '{0}' has found the following matching controllers:{2}. + /// + internal static string DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate { + get { + return ResourceManager.GetString("DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No type was found that matches the controller named '{0}'.. + /// + internal static string DefaultControllerFactory_ControllerNameNotFound { + get { + return ResourceManager.GetString("DefaultControllerFactory_ControllerNameNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred when trying to create a controller of type '{0}'. Make sure that the controller has a parameterless public constructor.. + /// + internal static string DefaultControllerFactory_ErrorCreatingController { + get { + return ResourceManager.GetString("DefaultControllerFactory_ErrorCreatingController", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.. + /// + internal static string DefaultInlineConstraintResolver_AmbiguousCtors { + get { + return ResourceManager.GetString("DefaultInlineConstraintResolver_AmbiguousCtors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.. + /// + internal static string DefaultInlineConstraintResolver_CouldNotFindCtor { + get { + return ResourceManager.GetString("DefaultInlineConstraintResolver_CouldNotFindCtor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The constraint type '{0}' which is mapped to constraint key '{1}' must implement the IHttpRouteConstraint interface.. + /// + internal static string DefaultInlineConstraintResolver_TypeNotConstraint { + get { + return ResourceManager.GetString("DefaultInlineConstraintResolver_TypeNotConstraint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The service type {0} is not supported.. + /// + internal static string DefaultServices_InvalidServiceType { + get { + return ResourceManager.GetString("DefaultServices_InvalidServiceType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A dependency resolver of type '{0}' returned an invalid value of null from its BeginScope method. If the container does not have a concept of scope, consider returning a scope that resolves in the root of the container instead.. + /// + internal static string DependencyResolver_BeginScopeReturnsNull { + get { + return ResourceManager.GetString("DependencyResolver_BeginScopeReturnsNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No service registered for type '{0}'.. + /// + internal static string DependencyResolverNoService { + get { + return ResourceManager.GetString("DependencyResolverNoService", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.{1}{1}The request has found the following matching controller types: {0}. + /// + internal static string DirectRoute_AmbiguousController { + get { + return ResourceManager.GetString("DirectRoute_AmbiguousController", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Direct routing does not support per-route message handlers.. + /// + internal static string DirectRoute_HandlerNotSupported { + get { + return ResourceManager.GetString("DirectRoute_HandlerNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A direct route for an action method cannot use the parameter 'action'. Specify a literal path in place of this parameter to create a route to the action.. + /// + internal static string DirectRoute_InvalidParameter_Action { + get { + return ResourceManager.GetString("DirectRoute_InvalidParameter_Action", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A direct route cannot use the parameter 'controller'. Specify a literal path in place of this parameter to create a route to a controller.. + /// + internal static string DirectRoute_InvalidParameter_Controller { + get { + return ResourceManager.GetString("DirectRoute_InvalidParameter_Controller", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route does not have any associated action descriptors. Routing requires that each direct route map to a non-empty set of actions.. + /// + internal static string DirectRoute_MissingActionDescriptors { + get { + return ResourceManager.GetString("DirectRoute_MissingActionDescriptors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error has occurred.. + /// + internal static string ErrorOccurred { + get { + return ResourceManager.GetString("ErrorOccurred", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No action result converter could be constructed for a generic parameter type '{0}'.. + /// + internal static string HttpActionDescriptor_NoConverterForGenericParamterTypeExists { + get { + return ResourceManager.GetString("HttpActionDescriptor_NoConverterForGenericParamterTypeExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to HttpControllerContext.Configuration must not be null.. + /// + internal static string HttpControllerContext_ConfigurationMustNotBeNull { + get { + return ResourceManager.GetString("HttpControllerContext_ConfigurationMustNotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request does not have an associated configuration object or the provided configuration was null.. + /// + internal static string HttpRequestMessageExtensions_NoConfiguration { + get { + return ResourceManager.GetString("HttpRequestMessageExtensions_NoConfiguration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The provided configuration does not have an instance of the '{0}' service registered.. + /// + internal static string HttpRequestMessageExtensions_NoContentNegotiator { + get { + return ResourceManager.GetString("HttpRequestMessageExtensions_NoContentNegotiator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'.. + /// + internal static string HttpRequestMessageExtensions_NoMatchingFormatter { + get { + return ResourceManager.GetString("HttpRequestMessageExtensions_NoMatchingFormatter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.. + /// + internal static string HttpResponseExceptionMessage { + get { + return ResourceManager.GetString("HttpResponseExceptionMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'.. + /// + internal static string HttpRouteBuilder_CouldNotResolveConstraint { + get { + return ResourceManager.GetString("HttpRouteBuilder_CouldNotResolveConstraint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The server is no longer available.. + /// + internal static string HttpServerDisposed { + get { + return ResourceManager.GetString("HttpServerDisposed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key is invalid JQuery syntax because it is missing a closing bracket. + /// + internal static string JQuerySyntaxMissingClosingBracket { + get { + return ResourceManager.GetString("JQuerySyntaxMissingClosingBracket", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class.. + /// + internal static string MaxHttpCollectionKeyLimitReached { + get { + return ResourceManager.GetString("MaxHttpCollectionKeyLimitReached", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)].. + /// + internal static string MissingDataMemberIsRequired { + get { + return ResourceManager.GetString("MissingDataMemberIsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} property is required.. + /// + internal static string MissingRequiredMember { + get { + return ResourceManager.GetString("MissingRequiredMember", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value '{0}' is not valid for {1}.. + /// + internal static string ModelBinderConfig_ValueInvalid { + get { + return ResourceManager.GetString("ModelBinderConfig_ValueInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A value is required.. + /// + internal static string ModelBinderConfig_ValueRequired { + get { + return ResourceManager.GetString("ModelBinderConfig_ValueRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type '{0}' does not subclass {1} or implement the interface {2}.. + /// + internal static string ModelBinderProviderCollection_InvalidBinderType { + get { + return ResourceManager.GetString("ModelBinderProviderCollection_InvalidBinderType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The binding context has a null Model, but this binder requires a non-null model of type '{0}'.. + /// + internal static string ModelBinderUtil_ModelCannotBeNull { + get { + return ResourceManager.GetString("ModelBinderUtil_ModelCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.. + /// + internal static string ModelBinderUtil_ModelInstanceIsWrong { + get { + return ResourceManager.GetString("ModelBinderUtil_ModelInstanceIsWrong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The binding context cannot have a null ModelMetadata.. + /// + internal static string ModelBinderUtil_ModelMetadataCannotBeNull { + get { + return ResourceManager.GetString("ModelBinderUtil_ModelMetadataCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.. + /// + internal static string ModelBinderUtil_ModelTypeIsWrong { + get { + return ResourceManager.GetString("ModelBinderUtil_ModelTypeIsWrong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ModelMetadata property must be set before accessing this property.. + /// + internal static string ModelBindingContext_ModelMetadataMustBeSet { + get { + return ResourceManager.GetString("ModelBindingContext_ModelMetadataMustBeSet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No controller was created to handle this request.. + /// + internal static string NoControllerCreated { + get { + return ResourceManager.GetString("NoControllerCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No controller was selected to handle this request.. + /// + internal static string NoControllerSelected { + get { + return ResourceManager.GetString("NoControllerSelected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No route data was found for this request.. + /// + internal static string NoRouteData { + get { + return ResourceManager.GetString("NoRouteData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The object has not yet been initialized. Ensure that HttpConfiguration.EnsureInitialized() is called in the application's startup code after all other initialization code.. + /// + internal static string Object_NotYetInitialized { + get { + return ResourceManager.GetString("Object_NotYetInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Optional parameter '{0}' is not supported by '{1}'.. + /// + internal static string OptionalBodyParameterNotSupported { + get { + return ResourceManager.GetString("OptionalBodyParameterNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't bind multiple parameters ('{0}' and '{1}') to the request's content.. + /// + internal static string ParameterBindingCantHaveMultipleBodyParameters { + get { + return ResourceManager.GetString("ParameterBindingCantHaveMultipleBodyParameters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't bind parameter '{0}' because it has conflicting attributes on it.. + /// + internal static string ParameterBindingConflictingAttributes { + get { + return ResourceManager.GetString("ParameterBindingConflictingAttributes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't bind parameter '{1}'. Must specify a custom model binder to bind parameters of type '{0}'.. + /// + internal static string ParameterBindingIllegalType { + get { + return ResourceManager.GetString("ParameterBindingIllegalType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters dictionary contains a null entry for parameter '{0}' of non-nullable type '{1}' for method '{2}' in '{3}'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.. + /// + internal static string ReflectedActionDescriptor_ParameterCannotBeNull { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_ParameterCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters dictionary does not contain an entry for parameter '{0}' of type '{1}' for method '{2}' in '{3}'. The dictionary must contain an entry for each parameter, including parameters that have null values.. + /// + internal static string ReflectedActionDescriptor_ParameterNotInDictionary { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_ParameterNotInDictionary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters dictionary contains an invalid entry for parameter '{0}' for method '{1}' in '{2}'. The dictionary contains a value of type '{3}', but the parameter requires a value of type '{4}'.. + /// + internal static string ReflectedActionDescriptor_ParameterValueHasWrongType { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_ParameterValueHasWrongType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot call action method '{0}' on controller '{1}' because the action method is a generic method.. + /// + internal static string ReflectedHttpActionDescriptor_CannotCallOpenGenericMethods { + get { + return ResourceManager.GetString("ReflectedHttpActionDescriptor_CannotCallOpenGenericMethods", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request must have a request context.. + /// + internal static string Request_RequestContextMustNotBeNull { + get { + return ResourceManager.GetString("Request_RequestContextMustNotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request context property on the request must be null or match ApiController.RequestContext.. + /// + internal static string RequestContextConflict { + get { + return ResourceManager.GetString("RequestContextConflict", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The property 'Request' on '{0}' is null. The property must be initialized with a non-null value.. + /// + internal static string RequestIsNull { + get { + return ResourceManager.GetString("RequestIsNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Authorization has been denied for this request.. + /// + internal static string RequestNotAuthorized { + get { + return ResourceManager.GetString("RequestNotAuthorized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No HTTP resource was found that matches the request URI '{0}'.. + /// + internal static string ResourceNotFound { + get { + return ResourceManager.GetString("ResourceNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A null value was returned where an instance of HttpResponseMessage was expected.. + /// + internal static string ResponseMessageResultConverter_NullHttpResponseMessage { + get { + return ResourceManager.GetString("ResponseMessageResultConverter_NullHttpResponseMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Adding or removing items from a '{0}' is not supported. Please use a key when adding and removing items.. + /// + internal static string Route_AddRemoveWithNoKeyNotSupported { + get { + return ResourceManager.GetString("Route_AddRemoveWithNoKeyNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.. + /// + internal static string Route_CannotHaveCatchAllInMultiSegment { + get { + return ResourceManager.GetString("Route_CannotHaveCatchAllInMultiSegment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.. + /// + internal static string Route_CannotHaveConsecutiveParameters { + get { + return ResourceManager.GetString("Route_CannotHaveConsecutiveParameters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.. + /// + internal static string Route_CannotHaveConsecutiveSeparators { + get { + return ResourceManager.GetString("Route_CannotHaveConsecutiveSeparators", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template.. + /// + internal static string Route_CatchAllMustBeLast { + get { + return ResourceManager.GetString("Route_CatchAllMustBeLast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?". + /// + internal static string Route_InvalidParameterName { + get { + return ResourceManager.GetString("Route_InvalidParameterName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.. + /// + internal static string Route_InvalidRouteTemplate { + get { + return ResourceManager.GetString("Route_InvalidRouteTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.. + /// + internal static string Route_MismatchedParameter { + get { + return ResourceManager.GetString("Route_MismatchedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template.. + /// + internal static string Route_RepeatedParameter { + get { + return ResourceManager.GetString("Route_RepeatedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.. + /// + internal static string Route_ValidationMustBeStringOrCustomConstraint { + get { + return ResourceManager.GetString("Route_ValidationMustBeStringOrCustomConstraint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A route named '{0}' could not be found in the route collection.. + /// + internal static string RouteCollection_NameNotFound { + get { + return ResourceManager.GetString("RouteCollection_NameNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one route prefix attribute is supported. Remove extra attributes from the controller of type '{0}'.. + /// + internal static string RoutePrefix_CannotSupportMultiRoutePrefix { + get { + return ResourceManager.GetString("RoutePrefix_CannotSupportMultiRoutePrefix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The property 'prefix' from route prefix attribute on controller of type '{0}' cannot be null.. + /// + internal static string RoutePrefix_PrefixCannotBeNull { + get { + return ResourceManager.GetString("RoutePrefix_PrefixCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A route named '{0}' is already in the route collection. Route names must be unique. + /// + ///Duplicates: + ///{1} + ///{2}. + /// + internal static string SubRouteCollection_DuplicateRouteName { + get { + return ResourceManager.GetString("SubRouteCollection_DuplicateRouteName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Action filter for '{0}'. + /// + internal static string TraceActionFilterMessage { + get { + return ResourceManager.GetString("TraceActionFilterMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Action='{0}'. + /// + internal static string TraceActionInvokeMessage { + get { + return ResourceManager.GetString("TraceActionInvokeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Action returned '{0}'. + /// + internal static string TraceActionReturnValue { + get { + return ResourceManager.GetString("TraceActionReturnValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Selected action '{0}'. + /// + internal static string TraceActionSelectedMessage { + get { + return ResourceManager.GetString("TraceActionSelectedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Binding parameter '{0}'. + /// + internal static string TraceBeginParameterBind { + get { + return ResourceManager.GetString("TraceBeginParameterBind", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancelled. + /// + internal static string TraceCancelledMessage { + get { + return ResourceManager.GetString("TraceCancelledMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter '{0}' bound to the value '{1}'. + /// + internal static string TraceEndParameterBind { + get { + return ResourceManager.GetString("TraceEndParameterBind", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter '{0}' failed to bind.. + /// + internal static string TraceEndParameterBindNoBind { + get { + return ResourceManager.GetString("TraceEndParameterBindNoBind", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Will use same '{0}' formatter. + /// + internal static string TraceGetPerRequestFormatterEndMessage { + get { + return ResourceManager.GetString("TraceGetPerRequestFormatterEndMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Will use new '{0}' formatter. + /// + internal static string TraceGetPerRequestFormatterEndMessageNew { + get { + return ResourceManager.GetString("TraceGetPerRequestFormatterEndMessageNew", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Obtaining formatter of type '{0}' for type='{1}', mediaType='{2}'. + /// + internal static string TraceGetPerRequestFormatterMessage { + get { + return ResourceManager.GetString("TraceGetPerRequestFormatterMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Null formatter. + /// + internal static string TraceGetPerRequestNullFormatterEndMessage { + get { + return ResourceManager.GetString("TraceGetPerRequestNullFormatterEndMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Exception thrown while getting types from '{0}'.. + /// + internal static string TraceHttpControllerTypeResolverError { + get { + return ResourceManager.GetString("TraceHttpControllerTypeResolverError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invoking action '{0}'. + /// + internal static string TraceInvokingAction { + get { + return ResourceManager.GetString("TraceInvokingAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}: {1}. + /// + internal static string TraceModelStateErrorMessage { + get { + return ResourceManager.GetString("TraceModelStateErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model state is invalid. {0}. + /// + internal static string TraceModelStateInvalidMessage { + get { + return ResourceManager.GetString("TraceModelStateInvalidMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type='{0}', formatters=[{1}]. + /// + internal static string TraceNegotiateFormatter { + get { + return ResourceManager.GetString("TraceNegotiateFormatter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to none. + /// + internal static string TraceNoneObjectMessage { + get { + return ResourceManager.GetString("TraceNoneObjectMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type='{0}', content-type='{1}'. + /// + internal static string TraceReadFromStreamMessage { + get { + return ResourceManager.GetString("TraceReadFromStreamMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value read='{0}'. + /// + internal static string TraceReadFromStreamValueMessage { + get { + return ResourceManager.GetString("TraceReadFromStreamValueMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Content-type='{0}', content-length={1}. + /// + internal static string TraceRequestCompleteMessage { + get { + return ResourceManager.GetString("TraceRequestCompleteMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Route='{0}'. + /// + internal static string TraceRouteMessage { + get { + return ResourceManager.GetString("TraceRouteMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Selected formatter='{0}', content-type='{1}'. + /// + internal static string TraceSelectedFormatter { + get { + return ResourceManager.GetString("TraceSelectedFormatter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown. + /// + internal static string TraceUnknownMessage { + get { + return ResourceManager.GetString("TraceUnknownMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Model state is valid. Values: {0}. + /// + internal static string TraceValidModelState { + get { + return ResourceManager.GetString("TraceValidModelState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value='{0}', type='{1}', content-type='{2}'. + /// + internal static string TraceWriteToStreamMessage { + get { + return ResourceManager.GetString("TraceWriteToStreamMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The {0} instance must not be null.. + /// + internal static string TypeInstanceMustNotBeNull { + get { + return ResourceManager.GetString("TypeInstanceMustNotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} must not return null.. + /// + internal static string TypeMethodMustNotReturnNull { + get { + return ResourceManager.GetString("TypeMethodMustNotReturnNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} must not be null.. + /// + internal static string TypePropertyMustNotBeNull { + get { + return ResourceManager.GetString("TypePropertyMustNotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request entity's media type '{0}' is not supported for this resource.. + /// + internal static string UnsupportedMediaType { + get { + return ResourceManager.GetString("UnsupportedMediaType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The request contains an entity body but no Content-Type header. The inferred media type '{0}' is not supported for this resource.. + /// + internal static string UnsupportedMediaTypeNoContentType { + get { + return ResourceManager.GetString("UnsupportedMediaTypeNoContentType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UrlHelper.Link must not return null.. + /// + internal static string UrlHelper_LinkMustNotReturnNull { + get { + return ResourceManager.GetString("UrlHelper_LinkMustNotReturnNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The model object inside the metadata claimed to be compatible with {0}, but was actually {1}.. + /// + internal static string ValidatableObjectAdapter_IncompatibleType { + get { + return ResourceManager.GetString("ValidatableObjectAdapter_IncompatibleType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A value is required but was not present in the request.. + /// + internal static string Validation_ValueNotFound { + get { + return ResourceManager.GetString("Validation_ValueNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead.. + /// + internal static string ValidationAttributeOnField { + get { + return ResourceManager.GetString("ValidationAttributeOnField", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Non-public property '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead.. + /// + internal static string ValidationAttributeOnNonPublicProperty { + get { + return ResourceManager.GetString("ValidationAttributeOnNonPublicProperty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The model state is valid.. + /// + internal static string ValidModelState { + get { + return ResourceManager.GetString("ValidModelState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not create a '{0}' from '{1}'. Please ensure it derives from '{0}' and has a public parameterless constructor.. + /// + internal static string ValueProviderFactory_Cannot_Create { + get { + return ResourceManager.GetString("ValueProviderFactory_Cannot_Create", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.. + /// + internal static string ValueProviderResult_ConversionThrew { + get { + return ResourceManager.GetString("ValueProviderResult_ConversionThrew", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.. + /// + internal static string ValueProviderResult_NoConverterExists { + get { + return ResourceManager.GetString("ValueProviderResult_NoConverterExists", resourceCulture); + } + } + } +} diff --git a/Swashbuckle.OData/ApiExplorer/SRResources.resx b/Swashbuckle.OData/ApiExplorer/SRResources.resx new file mode 100644 index 0000000..3efb96c --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/SRResources.resx @@ -0,0 +1,529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The binding context has a null Model, but this binder requires a non-null model of type '{0}'. + + + The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'. + + + The binding context cannot have a null ModelMetadata. + + + The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'. + + + The value '{0}' is not valid for {1}. + + + A value is required. + + + The ModelMetadata property must be set before accessing this property. + + + The property {0}.{1} could not be found. + + + The type '{0}' does not subclass {1} or implement the interface {2}. + + + The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information. + + + The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types. + + + The type {0} must derive from {1}. + + + The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}. + + + The type {0} must have a public constructor which accepts two parameters of types {1} and {2}. + + + The model object inside the metadata claimed to be compatible with {0}, but was actually {1}. + + + Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details. + + + Multiple actions were found that match the request: {0} + + + Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request ('{1}') found multiple controllers defined with the same name but differing namespaces, which is not supported.{3}{3}The request for '{0}' has found the following matching controllers:{2} + + + No type was found that matches the controller named '{0}'. + + + An error occurred when trying to create a controller of type '{0}'. Make sure that the controller has a parameterless public constructor. + + + No service registered for type '{0}'. + + + No action was found on the controller '{0}' that matches the request. + + + No action was found on the controller '{0}' that matches the name '{1}'. + + + The requested resource does not support http method '{0}'. + + + {0} on type {1} + + + The parameters dictionary contains a null entry for parameter '{0}' of non-nullable type '{1}' for method '{2}' in '{3}'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter. + + + The parameters dictionary does not contain an entry for parameter '{0}' of type '{1}' for method '{2}' in '{3}'. The dictionary must contain an entry for each parameter, including parameters that have null values. + + + The parameters dictionary contains an invalid entry for parameter '{0}' for method '{1}' in '{2}'. The dictionary contains a value of type '{3}', but the parameter requires a value of type '{4}'. + + + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + + + A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + + + The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + + + A catch-all parameter can only appear as the last segment of the route template. + + + The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?" + + + The route template cannot start with a '/' or '~' character and it cannot contain a '?' character. + + + There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character. + + + The route parameter name '{0}' appears more than one time in the route template. + + + The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. + + + Adding or removing items from a '{0}' is not supported. Please use a key when adding and removing items. + + + Could not create a '{0}' from '{1}'. Please ensure it derives from '{0}' and has a public parameterless constructor. + + + A value is required but was not present in the request. + + + Cannot reuse an '{0}' instance. '{0}' has to be constructed per incoming message. Check your custom '{1}' and make sure that it will not manufacture the same instance. + + + After calling {0}.OnActionExecuted, the HttpActionExecutedContext properties Result and Exception were both null. At least one of these values must be non-null. To provide a new response, please set the Result object; to indicate an error, please throw an exception. + + + Action='{0}' + + + Action filter for '{0}' + + + Selected action '{0}' + + + Cancelled + + + Route='{0}' + + + Model state is invalid. {0} + + + none + + + Type='{0}', content-type='{1}' + + + Value read='{0}' + + + Content-type='{0}', content-length={1} + + + Selected formatter='{0}', content-type='{1}' + + + Type='{0}', formatters=[{1}] + + + unknown + + + Value='{0}', type='{1}', content-type='{2}' + + + The request does not have an associated configuration object or the provided configuration was null. + + + Will use same '{0}' formatter + + + Obtaining formatter of type '{0}' for type='{1}', mediaType='{2}' + + + A route named '{0}' could not be found in the route collection. + + + Can't bind multiple parameters ('{0}' and '{1}') to the request's content. + + + Can't bind parameter '{0}' because it has conflicting attributes on it. + + + Can't bind parameter '{1}'. Must specify a custom model binder to bind parameters of type '{0}'. + + + Binding parameter '{0}' + + + Parameter '{0}' bound to the value '{1}' + + + Parameter '{0}' failed to bind. + + + Invoking action '{0}' + + + Action returned '{0}' + + + {0}: {1} + + + Null formatter + + + Model state is valid. Values: {0} + + + Will use new '{0}' formatter + + + The key is invalid JQuery syntax because it is missing a closing bracket + + + The provided configuration does not have an instance of the '{0}' service registered. + + + Cannot call action method '{0}' on controller '{1}' because the action method is a generic method. + + + A null value was returned where an instance of HttpResponseMessage was expected. + + + The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method. + + + The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task. + + + No action result converter could be constructed for a generic parameter type '{0}'. + + + The {0} property is required. + + + Could not find a formatter matching the media type '{0}' that can write an instance of '{1}'. + + + The service type {0} is not supported. + + + The number of keys in a NameValueCollection has exceeded the limit of '{0}'. You can adjust it by modifying the MaxHttpCollectionKeys property on the '{1}' class. + + + No route providing a controller name was found to match request URI '{0}' + + + The server is no longer available. + + + No controller was created to handle this request. + + + No controller was selected to handle this request. + + + No route data was found for this request. + + + Authorization has been denied for this request. + + + The model state is valid. + + + An error has occurred. + + + The request is invalid. + + + No HTTP resource was found that matches the request URI '{0}'. + + + Optional parameter '{0}' is not supported by '{1}'. + + + Property '{0}' on type '{1}' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]. + + + Field '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on fields are not supported. Consider using a public property for validation instead. + + + Non-public property '{0}' on type '{1}' is attributed with one or more validation attributes. Validation attributes on non-public properties are not supported. Consider using a public property for validation instead. + + + A dependency resolver of type '{0}' returned an invalid value of null from its BeginScope method. If the container does not have a concept of scope, consider returning a scope that resolves in the root of the container instead. + + + The request entity's media type '{0}' is not supported for this resource. + + + The request contains an entity body but no Content-Type header. The inferred media type '{0}' is not supported for this resource. + + + Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}. + + + The constraint type '{0}' which is mapped to constraint key '{1}' must implement the IHttpRouteConstraint interface. + + + The inline constraint resolver of type '{0}' was unable to resolve the following inline constraint: '{1}'. + + + The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}. + + + The route prefix '{0}' on the controller named '{1}' cannot end with a '/' character. + + + The route template '{0}' on the action named '{1}' cannot start with a '/' character. + + + The batch request of media type '{0}' is not supported. + + + The batch request must have a "Content-Type" header. + + + The 'Content' property on the batch request cannot be null. + + + An object of type '{0}' was returned where an instance of IHttpActionResult was expected. + + + A null value was returned where an instance of IHttpActionResult was expected. + + + ApiController.Request must not be null. + + + HttpControllerContext.Configuration must not be null. + + + UrlHelper.Link must not return null. + + + The property 'Request' on '{0}' is null. The property must be initialized with a non-null value. + + + The object has not yet been initialized. Ensure that HttpConfiguration.EnsureInitialized() is called in the application's startup code after all other initialization code. + + + The request context property on the request must be null or match ApiController.RequestContext. + + + The request must have a request context. + + + The {0} instance must not be null. + + + {0}.{1} must not be null. + + + {0}.{1} must not return null. + + + A route named '{0}' is already in the route collection. Route names must be unique. + +Duplicates: +{1} +{2} + + + The route does not have any associated action descriptors. Routing requires that each direct route map to a non-empty set of actions. + + + Only one route prefix attribute is supported. Remove extra attributes from the controller of type '{0}'. + + + The property 'prefix' from route prefix attribute on controller of type '{0}' cannot be null. + + + Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.{1}{1}The request has found the following matching controller types: {0} + + + Direct routing does not support per-route message handlers. + + + Exception thrown while getting types from '{0}'. + + + A direct route for an action method cannot use the parameter 'action'. Specify a literal path in place of this parameter to create a route to the action. + + + A direct route cannot use the parameter 'controller'. Specify a literal path in place of this parameter to create a route to a controller. + + + The parameter '{0}' cannot contain a null element. + + + The authentication filter encountered an error. ErrorResult='{0}'. + + + The authentication filter successfully set a principal to a known identity. Identity.Name='{0}'. Identity.AuthenticationType='{1}'. + + + The authentication filter did not encounter an error or set a principal. + + + The authentication filter set a principal to an unknown identity. + + \ No newline at end of file diff --git a/Swashbuckle.OData/ApiExplorer/TypeHelper.cs b/Swashbuckle.OData/ApiExplorer/TypeHelper.cs new file mode 100644 index 0000000..b0b0215 --- /dev/null +++ b/Swashbuckle.OData/ApiExplorer/TypeHelper.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics.Contracts; +using System.Threading.Tasks; +using System.Web.Http; + +namespace Swashbuckle.OData.ApiExplorer +{ + /// + /// A static class that provides various related helpers. + /// + internal static class TypeHelper + { + private static readonly Type TaskGenericType = typeof (Task<>); + + internal static readonly Type ApiControllerType = typeof (ApiController); + + internal static Type GetTaskInnerTypeOrNull(Type type) + { + Contract.Assert(type != null); + if (type.IsGenericType && !type.IsGenericTypeDefinition) + { + var genericTypeDefinition = type.GetGenericTypeDefinition(); + + if (TaskGenericType == genericTypeDefinition) + { + return type.GetGenericArguments()[0]; + } + } + + return null; + } + + internal static Type[] GetTypeArgumentsIfMatch(Type closedType, Type matchingOpenType) + { + if (!closedType.IsGenericType) + { + return null; + } + + var openType = closedType.GetGenericTypeDefinition(); + return matchingOpenType == openType ? closedType.GetGenericArguments() : null; + } + + internal static bool IsCompatibleObject(Type type, object value) + { + return (value == null && TypeAllowsNullValue(type)) || type.IsInstanceOfType(value); + } + + internal static bool IsNullableValueType(Type type) + { + return Nullable.GetUnderlyingType(type) != null; + } + + internal static bool TypeAllowsNullValue(Type type) + { + return !type.IsValueType || IsNullableValueType(type); + } + + internal static bool IsSimpleType(Type type) + { + return type.IsPrimitive || type.Equals(typeof (string)) || type.Equals(typeof (DateTime)) || type.Equals(typeof (decimal)) || type.Equals(typeof (Guid)) || type.Equals(typeof (DateTimeOffset)) || type.Equals(typeof (TimeSpan)); + } + + internal static bool IsSimpleUnderlyingType(Type type) + { + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType != null) + { + type = underlyingType; + } + + return IsSimpleType(type); + } + + internal static bool CanConvertFromString(Type type) + { + return IsSimpleUnderlyingType(type) || HasStringConverter(type); + } + + internal static bool HasStringConverter(Type type) + { + return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof (string)); + } + + /// + /// Fast implementation to get the subset of a given type. + /// + /// type to search for + /// subset of objects that can be assigned to T + internal static ReadOnlyCollection OfType(object[] objects) where T : class + { + var max = objects.Length; + var list = new List(max); + var idx = 0; + for (var i = 0; i < max; i++) + { + var attr = objects[i] as T; + if (attr != null) + { + list.Add(attr); + idx++; + } + } + list.Capacity = idx; + + return new ReadOnlyCollection(list); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/ODataApiExplorer.cs deleted file mode 100644 index 19dea26..0000000 --- a/Swashbuckle.OData/ODataApiExplorer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Text.RegularExpressions; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Description; -using System.Web.Http.Routing; - -namespace Swashbuckle.OData -{ - public class ODataApiExplorer : ApiExplorer - { - public ODataApiExplorer(HttpConfiguration configuration) : base(configuration) - { - } - - /// - /// Determines whether the controller should be considered for generation. - /// Called when initializing the . - /// - /// The controller variable value from the route. - /// The controller descriptor. - /// The route. - /// - /// true if the controller should be considered for generation, - /// false otherwise. - /// - public override bool ShouldExploreController(string controllerVariableValue, HttpControllerDescriptor controllerDescriptor, IHttpRoute route) - { - Contract.Requires(controllerDescriptor != null); - Contract.Requires(route != null); - - //var setting = controllerDescriptor.GetCustomAttributes().FirstOrDefault(); - - //return (setting == null || !setting.IgnoreApi) && MatchRegexConstraint(route, RouteValueKeys.Controller, controllerVariableValue); - return MatchRegexConstraint(route, RouteValueKeys.Controller, controllerVariableValue); - } - - private static bool MatchRegexConstraint(IHttpRoute route, string parameterName, string parameterValue) - { - var constraints = route.Constraints; - if (constraints != null) - { - object constraint; - if (constraints.TryGetValue(parameterName, out constraint)) - { - // treat the constraint as a string which represents a Regex. - // note that we don't support custom constraint (IHttpRouteConstraint) because it might rely on the request and some runtime states - var constraintsRule = constraint as string; - if (constraintsRule != null) - { - var constraintsRegEx = "^(" + constraintsRule + ")$"; - return parameterValue != null && Regex.IsMatch(parameterValue, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - } - } - } - - return true; - } - } -} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerConverter.cs b/Swashbuckle.OData/ODataSwaggerConverter.cs index 1fb124c..93eae11 100644 --- a/Swashbuckle.OData/ODataSwaggerConverter.cs +++ b/Swashbuckle.OData/ODataSwaggerConverter.cs @@ -137,10 +137,19 @@ protected virtual void InitializeDocument() version = "0.1.0" }, host = Host, - schemes = new List { "http" }, + schemes = new List + { + "http" + }, basePath = BasePath, - consumes = new List { "application/json" }, - produces = new List { "application/json" } + consumes = new List + { + "application/json" + }, + produces = new List + { + "application/json" + } }; } diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index 1b842ac..a19cca8 100644 --- a/Swashbuckle.OData/ODataSwaggerProvider.cs +++ b/Swashbuckle.OData/ODataSwaggerProvider.cs @@ -12,29 +12,31 @@ namespace Swashbuckle.OData public class ODataSwaggerProvider : ISwaggerProvider { private readonly ISwaggerProvider _defaultProvider; + private readonly Func _httpConfigurationProvider; // Here for future use against the OData API... private readonly SwaggerDocsConfig _swaggerDocsConfig; - private readonly Func _httpConfigurationProvider; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The default provider. /// The swagger docs configuration. - public ODataSwaggerProvider(ISwaggerProvider defaultProvider, SwaggerDocsConfig swaggerDocsConfig) - : this(defaultProvider, swaggerDocsConfig, () => GlobalConfiguration.Configuration) + public ODataSwaggerProvider(ISwaggerProvider defaultProvider, SwaggerDocsConfig swaggerDocsConfig) : this(defaultProvider, swaggerDocsConfig, () => GlobalConfiguration.Configuration) { Contract.Requires(defaultProvider != null); Contract.Requires(swaggerDocsConfig != null); } /// - /// Initializes a new instance of the class. - /// Use this constructor for self-hosted scenarios. + /// Initializes a new instance of the class. + /// Use this constructor for self-hosted scenarios. /// /// The default provider. /// The swagger docs configuration. - /// A function that will return the HttpConfiguration that contains the OData Edm Model. + /// + /// A function that will return the HttpConfiguration that contains the OData Edm + /// Model. + /// public ODataSwaggerProvider(ISwaggerProvider defaultProvider, SwaggerDocsConfig swaggerDocsConfig, Func httpConfigurationProvider) { Contract.Requires(defaultProvider != null); @@ -72,7 +74,10 @@ public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) var edmSwaggerDocument = oDataSwaggerConverter.ConvertToSwaggerModel(); edmSwaggerDocument.host = rootUri.Host + port; edmSwaggerDocument.basePath = basePath; - edmSwaggerDocument.schemes = new[] { rootUri.Scheme }.ToList(); + edmSwaggerDocument.schemes = new[] + { + rootUri.Scheme + }.ToList(); return edmSwaggerDocument; } diff --git a/Swashbuckle.OData/ODataSwaggerUtilities.cs b/Swashbuckle.OData/ODataSwaggerUtilities.cs index 7ddf96e..6affe69 100644 --- a/Swashbuckle.OData/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/ODataSwaggerUtilities.cs @@ -33,30 +33,8 @@ public static PathItem CreateSwaggerPathForEntitySet(IEdmNavigationSource naviga return new PathItem { - get = new Operation() - .Summary("Get EntitySet " + entitySet.Name) - .OperationId(entitySet.Name + "_Get") - .Description("Returns the EntitySet " + entitySet.Name) - .Tags(entitySet.Name) - .Parameters(new List() - .Parameter("$expand", "query", "Expands related entities inline.", "string") - .Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string") - .Parameter("$select", "query", "Selects which properties to include in the response.", "string") - .Parameter("$orderby", "query", "Sorts the results.", "string") - .Parameter("$top", "query", "Returns only the first n results.", "integer") - .Parameter("$skip", "query", "Skips the first n results.", "integer") - .Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean")) - .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()) - .DefaultErrorResponse()), - post = new Operation() - .Summary("Post a new entity to EntitySet " + entitySet.Name) - .OperationId(entitySet.Name + "_Post") - .Description("Post a new entity to EntitySet " + entitySet.Name) - .Tags(entitySet.Name) - .Parameters(new List().Parameter(entitySet.EntityType() - .Name, "body", "The entity to post", entitySet.EntityType())) - .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()) - .DefaultErrorResponse()) + get = new Operation().Summary("Get EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_Get").Description("Returns the EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string").Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string").Parameter("$select", "query", "Selects which properties to include in the response.", "string").Parameter("$orderby", "query", "Sorts the results.", "string").Parameter("$top", "query", "Returns only the first n results.", "integer").Parameter("$skip", "query", "Skips the first n results.", "integer").Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean")).Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), + post = new Operation().Summary("Post a new entity to EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_Post").Description("Post a new entity to EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(new List().Parameter(entitySet.EntityType().Name, "body", "The entity to post", entitySet.EntityType())).Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()) }; } @@ -74,8 +52,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio } var keyParameters = new List(); - foreach (var key in entitySet.EntityType() - .Key()) + foreach (var key in entitySet.EntityType().Key()) { string format; var type = GetPrimitiveTypeAndFormat(key.Type.Definition as IEdmPrimitiveType, out format); @@ -84,33 +61,9 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio return new PathItem { - get = new Operation() - .Summary("Get entity from " + entitySet.Name + " by key.") - .OperationId(entitySet.Name + "_GetById") - .Description("Returns the entity with the key from " + entitySet.Name) - .Tags(entitySet.Name) - .Parameters(keyParameters.DeepClone().Parameter("$expand", "query", "Expands related entities inline.", "string")) - .Parameters(keyParameters.DeepClone().Parameter("$select", "query", "Selects which properties to include in the response.", "string")) - .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()) - .DefaultErrorResponse()), - patch = new Operation() - .Summary("Update entity in EntitySet " + entitySet.Name) - .OperationId(entitySet.Name + "_PatchById") - .Description("Update entity in EntitySet " + entitySet.Name) - .Tags(entitySet.Name) - .Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType() - .Name, "body", "The entity to patch", entitySet.EntityType())) - .Responses(new Dictionary().Response("204", "Empty response") - .DefaultErrorResponse()), - delete = new Operation() - .Summary("Delete entity in EntitySet " + entitySet.Name) - .OperationId(entitySet.Name + "_DeleteById") - .Description("Delete entity in EntitySet " + entitySet.Name) - .Tags(entitySet.Name) - .Parameters(keyParameters.DeepClone().Parameter("If-Match", "header", "If-Match header", "string")) - .Responses(new Dictionary() - .Response("204", "Empty response") - .DefaultErrorResponse()) + get = new Operation().Summary("Get entity from " + entitySet.Name + " by key.").OperationId(entitySet.Name + "_GetById").Description("Returns the entity with the key from " + entitySet.Name).Tags(entitySet.Name).Parameters(keyParameters.DeepClone().Parameter("$expand", "query", "Expands related entities inline.", "string")).Parameters(keyParameters.DeepClone().Parameter("$select", "query", "Selects which properties to include in the response.", "string")).Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), + patch = new Operation().Summary("Update entity in EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_PatchById").Description("Update entity in EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType().Name, "body", "The entity to patch", entitySet.EntityType())).Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), + delete = new Operation().Summary("Delete entity in EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_DeleteById").Description("Delete entity in EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(keyParameters.DeepClone().Parameter("If-Match", "header", "If-Match header", "string")).Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()) }; } @@ -143,10 +96,7 @@ public static PathItem CreateSwaggerPathForOperationImport(IEdmOperationImport o swaggerResponses.Response("200", "Response from " + operationImport.Name, operationImport.Operation.ReturnType.Definition); } - var swaggerOperationImport = new Operation().Summary("Call operation import " + operationImport.Name) - .OperationId(operationImport.Name + (isFunctionImport ? "_FunctionImportGet" : "_ActionImportPost")) - .Description("Call operation import " + operationImport.Name) - .Tags(isFunctionImport ? "Function Import" : "Action Import"); + var swaggerOperationImport = new Operation().Summary("Call operation import " + operationImport.Name).OperationId(operationImport.Name + (isFunctionImport ? "_FunctionImportGet" : "_ActionImportPost")).Description("Call operation import " + operationImport.Name).Tags(isFunctionImport ? "Function Import" : "Action Import"); if (swaggerParameters.Count > 0) { @@ -154,15 +104,13 @@ public static PathItem CreateSwaggerPathForOperationImport(IEdmOperationImport o } swaggerOperationImport.Responses(swaggerResponses.DefaultErrorResponse()); - return isFunctionImport - ? new PathItem - { - get = swaggerOperationImport - } - : new PathItem - { - post = swaggerOperationImport - }; + return isFunctionImport ? new PathItem + { + get = swaggerOperationImport + } : new PathItem + { + post = swaggerOperationImport + }; } /// @@ -199,12 +147,7 @@ public static PathItem CreateSwaggerPathForOperationOfEntitySet(IEdmOperation op swaggerResponses.Response("200", "Response from " + operation.Name, operation.ReturnType.Definition); } - var swaggerOperation = new Operation() - .Summary("Call operation " + operation.Name) - .OperationId(operation.Name + (isFunction ? "_FunctionGet" : "_ActionPost")) - .Description("Call operation " + operation.Name) - .OperationId(operation.Name + (isFunction ? "_FunctionGetById" : "_ActionPostById")) - .Tags(entitySet.Name, isFunction ? "Function" : "Action"); + var swaggerOperation = new Operation().Summary("Call operation " + operation.Name).OperationId(operation.Name + (isFunction ? "_FunctionGet" : "_ActionPost")).Description("Call operation " + operation.Name).OperationId(operation.Name + (isFunction ? "_FunctionGetById" : "_ActionPostById")).Tags(entitySet.Name, isFunction ? "Function" : "Action"); if (swaggerParameters.Count > 0) { @@ -212,15 +155,13 @@ public static PathItem CreateSwaggerPathForOperationOfEntitySet(IEdmOperation op } swaggerOperation.Responses(swaggerResponses.DefaultErrorResponse()); - return isFunction - ? new PathItem - { - get = swaggerOperation - } - : new PathItem - { - post = swaggerOperation - }; + return isFunction ? new PathItem + { + get = swaggerOperation + } : new PathItem + { + post = swaggerOperation + }; } /// @@ -240,8 +181,7 @@ public static PathItem CreateSwaggerPathForOperationOfEntity(IEdmOperation opera var isFunction = operation is IEdmFunction; var swaggerParameters = new List(); - foreach (var key in entitySet.EntityType() - .Key()) + foreach (var key in entitySet.EntityType().Key()) { string format; var type = GetPrimitiveTypeAndFormat(key.Type.Definition as IEdmPrimitiveType, out format); @@ -263,9 +203,7 @@ public static PathItem CreateSwaggerPathForOperationOfEntity(IEdmOperation opera swaggerResponses.Response("200", "Response from " + operation.Name, operation.ReturnType.Definition); } - var swaggerOperation = new Operation().Summary("Call operation " + operation.Name) - .Description("Call operation " + operation.Name) - .Tags(entitySet.Name, isFunction ? "Function" : "Action"); + var swaggerOperation = new Operation().Summary("Call operation " + operation.Name).Description("Call operation " + operation.Name).Tags(entitySet.Name, isFunction ? "Function" : "Action"); if (swaggerParameters.Count > 0) { @@ -273,15 +211,13 @@ public static PathItem CreateSwaggerPathForOperationOfEntity(IEdmOperation opera } swaggerOperation.Responses(swaggerResponses.DefaultErrorResponse()); - return isFunction - ? new PathItem - { - get = swaggerOperation - } - : new PathItem - { - post = swaggerOperation - }; + return isFunction ? new PathItem + { + get = swaggerOperation + } : new PathItem + { + post = swaggerOperation + }; } /// @@ -298,8 +234,7 @@ public static string GetPathForEntity(IEdmNavigationSource navigationSource) } var singleEntityPath = "/" + entitySet.Name + "("; - foreach (var key in entitySet.EntityType() - .Key()) + foreach (var key in entitySet.EntityType().Key()) { if (key.Type.Definition.TypeKind == EdmTypeKind.Primitive && ((IEdmPrimitiveType) key.Type.Definition).PrimitiveKind == EdmPrimitiveTypeKind.String) { @@ -422,11 +357,11 @@ public static string GetPathForOperationOfEntity(IEdmOperation operation, IEdmNa } /// - /// Create the Swagger definition for the structure Edm type. + /// Create the Swagger definition for the structure Edm type. /// /// The structure Edm type. /// - /// The represents the related structure Edm type. + /// The represents the related structure Edm type. /// public static Schema CreateSwaggerDefinitionForStructureType(IEdmStructuredType edmType) { @@ -696,18 +631,16 @@ private static Operation OperationId(this Operation obj, string operationId) return obj; } - /// - /// Perform a deep Copy of the object, using Json as a serialisation method. - /// - /// The type of object being copied. - /// The object instance to copy. - /// The copied object. - public static T DeepClone(this T source) + /// + /// Perform a deep Copy of the object, using Json as a serialisation method. + /// + /// The type of object being copied. + /// The object instance to copy. + /// The copied object. + public static T DeepClone(this T source) { // Don't serialize a null object, simply return the default for that object - return ReferenceEquals(source, null) - ? default(T) - : JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); + return ReferenceEquals(source, null) ? default(T) : JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Properties/AssemblyInfo.cs b/Swashbuckle.OData/Properties/AssemblyInfo.cs index e2aeae0..593b2ae 100644 --- a/Swashbuckle.OData/Properties/AssemblyInfo.cs +++ b/Swashbuckle.OData/Properties/AssemblyInfo.cs @@ -4,6 +4,7 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. + [assembly: AssemblyTitle("Swashbuckle.OData")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] @@ -16,9 +17,11 @@ // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. + [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM + [assembly: Guid("aa7b6c37-092e-46ec-81a3-9865efd646f1")] // Version information for an assembly consists of the following four values: @@ -31,6 +34,7 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] + [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("2.0.0")] +[assembly: AssemblyInformationalVersion("2.0.0")] \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 0d58ef1..891f9fe 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -21,7 +21,7 @@ full false bin\Debug\ - TRACE;DEBUG;CONTRACTS_FULL + TRACE;DEBUG;CONTRACTS_FULL;ASPNETWEBAPI prompt 4 True @@ -73,7 +73,7 @@ pdbonly true bin\Release\ - TRACE;CONTRACTS_FULL + TRACE;CONTRACTS_FULL;ASPNETWEBAPI prompt 4 True @@ -169,16 +169,61 @@ - + + + + + + True + True + CommonWebApiResources.resx + + + + + + + + + + + + + + + + + - + + + + + + + True + True + SRResources.resx + + + + + ResXFileCodeGenerator + CommonWebApiResources.Designer.cs + + + ResXFileCodeGenerator + SRResources.Designer.cs + Designer + + diff --git a/Swashbuckle.OData/packages.config b/Swashbuckle.OData/packages.config index 584c32d..d7a9d59 100644 --- a/Swashbuckle.OData/packages.config +++ b/Swashbuckle.OData/packages.config @@ -1,15 +1,16 @@  + - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file From 370f1c6bdac3fda98cedf7ae19860f3cdb379cc6 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Thu, 3 Dec 2015 00:46:16 -0800 Subject: [PATCH 03/12] Initial implementation of ODataApiExplorer --- ...xplorer.cs => ApiExplorerFromMicrosoft.cs} | 10 +- Swashbuckle.OData/CollectionExtentions.cs | 44 +++ Swashbuckle.OData/ODataApiExplorer.cs | 261 ++++++++++++++++++ Swashbuckle.OData/Swashbuckle.OData.csproj | 4 +- 4 files changed, 312 insertions(+), 7 deletions(-) rename Swashbuckle.OData/ApiExplorer/{ODataApiExplorer.cs => ApiExplorerFromMicrosoft.cs} (98%) create mode 100644 Swashbuckle.OData/CollectionExtentions.cs create mode 100644 Swashbuckle.OData/ODataApiExplorer.cs diff --git a/Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs b/Swashbuckle.OData/ApiExplorer/ApiExplorerFromMicrosoft.cs similarity index 98% rename from Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs rename to Swashbuckle.OData/ApiExplorer/ApiExplorerFromMicrosoft.cs index 9196f16..c528e98 100644 --- a/Swashbuckle.OData/ApiExplorer/ODataApiExplorer.cs +++ b/Swashbuckle.OData/ApiExplorer/ApiExplorerFromMicrosoft.cs @@ -17,18 +17,16 @@ using System.Web.Http.ModelBinding.Binders; using System.Web.Http.Routing; using System.Web.Http.Services; -using System.Web.OData.Routing; namespace Swashbuckle.OData.ApiExplorer { /// - /// Explores the URI space of the service based on routes, controllers and actions available in the system. + /// This is here so I can debug and understand how ApiExploring is implemented. Will be removed after ODataApiExplorer is completed. /// - public class ODataApiExplorer : IApiExplorer + public class ApiExplorerFromMicrosoft : IApiExplorer { private static readonly Regex _actionVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", RouteValueKeys.Action), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - //private static readonly Regex _controllerVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", RouteValueKeys.Controller), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private static readonly Regex _controllerVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", ODataRouteConstants.ODataPath), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + private static readonly Regex _controllerVariableRegex = new Regex(string.Format(CultureInfo.CurrentCulture, "{{{0}}}", RouteValueKeys.Controller), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private readonly Lazy> _apiDescriptions; private readonly HttpConfiguration _config; @@ -36,7 +34,7 @@ public class ODataApiExplorer : IApiExplorer /// Initializes a new instance of the class. /// /// The configuration. - public ODataApiExplorer(HttpConfiguration configuration) + public ApiExplorerFromMicrosoft(HttpConfiguration configuration) { _config = configuration; _apiDescriptions = new Lazy>(InitializeApiDescriptions); diff --git a/Swashbuckle.OData/CollectionExtentions.cs b/Swashbuckle.OData/CollectionExtentions.cs new file mode 100644 index 0000000..4b6d858 --- /dev/null +++ b/Swashbuckle.OData/CollectionExtentions.cs @@ -0,0 +1,44 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; + +namespace Swashbuckle.OData +{ + public static class CollectionExtentions + { + /// + /// Adds the elements of the specified collection to the end of the . + /// + /// + /// The source collection. + /// + /// The collection whose elements should be added to the end of the . + /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type. + /// + public static void AddRange(this Collection source, Collection collection) + { + Contract.Requires(source != null); + Contract.Requires(collection != null); + + foreach (var item in collection) + { + source.Add(item); + } + } + + /// + /// Adds the given item to the collection if the item is not null. + /// + /// + /// The source. + /// The item. + public static void AddIfNotNull(this Collection source, T item) + { + Contract.Requires(source != null); + + if (item != null) + { + source.Add(item); + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/ODataApiExplorer.cs new file mode 100644 index 0000000..1dc3cdd --- /dev/null +++ b/Swashbuckle.OData/ODataApiExplorer.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Description; +using System.Web.Http.Routing; +using System.Web.OData.Routing; +using Microsoft.OData.Edm; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData +{ + public class ODataApiExplorer : IApiExplorer + { + private readonly HttpConfiguration _config; + private readonly Lazy> _apiDescriptions; + private const string ServiceRoot = "http://any/"; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public ODataApiExplorer(HttpConfiguration configuration) + { + _config = configuration; + _apiDescriptions = new Lazy>(InitializeApiDescriptions); + } + + /// + /// Gets the API descriptions. The descriptions are initialized on the first access. + /// + public Collection ApiDescriptions + { + get { return _apiDescriptions.Value; } + } + + private Collection InitializeApiDescriptions() + { + var apiDescriptions = new Collection(); + var controllerSelector = _config.Services.GetHttpControllerSelector(); + var controllerMappings = controllerSelector.GetControllerMapping(); + if (controllerMappings != null) + { + var descriptionComparer = new ApiDescriptionComparer(); + foreach (var route in FlattenRoutes(_config.Routes)) + { + var odataRoute = route as ODataRoute; + if (odataRoute != null) + { + var descriptionsFromRoute = ExploreRoute(controllerMappings, odataRoute); + + // Remove ApiDescription that will lead to ambiguous action matching. + // E.g. a controller with Post() and PostComment(). When the route template is {controller}, it produces POST /controller and POST /controller. + descriptionsFromRoute = RemoveInvalidApiDescriptions(descriptionsFromRoute); + + foreach (var description in descriptionsFromRoute) + { + // Do not add the description if the previous route has a matching description with the same HTTP method and relative path. + // E.g. having two routes with the templates "api/Values/{id}" and "api/{controller}/{id}" can potentially produce the same + // relative path "api/Values/{id}" but only the first one matters. + if (!apiDescriptions.Contains(description, descriptionComparer)) + { + apiDescriptions.Add(description); + } + } + } + } + } + + return apiDescriptions; + } + + /// + /// Explores the route. + /// + /// The controller mappings. + /// The route. + /// + private Collection ExploreRoute(IDictionary controllerMappings, ODataRoute oDataRoute) + { + var apiDescriptions = new Collection(); + + var edmSwaggerDocument = GetDefaultEdmSwaggerDocument(oDataRoute); + + foreach (var potentialPath in edmSwaggerDocument.paths) + { + apiDescriptions.AddRange(ExplorePath(controllerMappings, oDataRoute, potentialPath.Key, potentialPath.Value)); + } + + return apiDescriptions; + } + + private Collection ExplorePath(IDictionary controllerMappings, ODataRoute oDataRoute, string potentialPath, PathItem potentialOperations) + { + var apiDescriptions = new Collection(); + + var edmModel = GetEdmModel(oDataRoute); + + HttpControllerDescriptor controllerDescriptor; + if (TryGetControllerDesciptor(controllerMappings, oDataRoute, potentialPath, out controllerDescriptor)) + { + apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Delete, oDataRoute, potentialPath, potentialOperations.delete, controllerDescriptor, edmModel)); + apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Get, oDataRoute, potentialPath, potentialOperations.get, controllerDescriptor, edmModel)); + } + + return apiDescriptions; + } + + private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oDataRoute, string potentialPath, Operation operation, HttpControllerDescriptor controllerDescriptor, IEdmModel edmModel) + { + if (operation != null) + { + var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + + var entitySetPath = oDataPathRouteConstraint.PathHandler.Parse(edmModel, ServiceRoot, potentialPath); + + var controllerContext = new HttpControllerContext + { + Request = new HttpRequestMessage(httpMethod, ServiceRoot) + }; + + var actionSelector = _config.Services.GetActionSelector(); + var actionMappings = actionSelector.GetActionMapping(controllerDescriptor); + var action = SelectAction(oDataPathRouteConstraint, entitySetPath, controllerContext, actionMappings); + + return null; + } + return null; + } + + private static bool TryGetControllerDesciptor(IDictionary controllerMappings, ODataRoute oDataRoute, string potentialPath, out HttpControllerDescriptor controllerDescriptor) + { + var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + + var model = GetEdmModel(oDataRoute); + + + var oDataPathHandler = oDataPathRouteConstraint.PathHandler; + + var entitySetPath = oDataPathHandler.Parse(model, ServiceRoot, potentialPath); + + var controllerName = SelectControllerName(oDataPathRouteConstraint, entitySetPath); + + if (controllerName != null) + { + return controllerMappings.TryGetValue(controllerName, out controllerDescriptor); + } + controllerDescriptor = null; + return false; + } + + /// + /// Selects the name of the controller to dispatch the request to. + /// + /// The o data path route constraint. + /// The OData path of the request. + /// The request. + /// + /// The name of the controller to dispatch to, or null if one cannot be resolved. + /// + private static string SelectControllerName(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path) + { + return oDataPathRouteConstraint.RoutingConventions + .Select(routingConvention => routingConvention.SelectController(path, new HttpRequestMessage())) + .FirstOrDefault(controllerName => controllerName != null); + } + + private static string SelectAction(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path, HttpControllerContext controllerContext, ILookup actionMap) + { + return oDataPathRouteConstraint.RoutingConventions + .Select(routingConvention => routingConvention.SelectAction(path, controllerContext, actionMap)) + .FirstOrDefault(action => action != null); + } + + private static SwaggerDocument GetDefaultEdmSwaggerDocument(ODataRoute oDataRoute) + { + var edmModel = GetEdmModel(oDataRoute); + var oDataSwaggerConverter = new ODataSwaggerConverter(edmModel); + return oDataSwaggerConverter.ConvertToSwaggerModel(); + } + + private static IEdmModel GetEdmModel(ODataRoute oDataRoute) + { + var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + var edmModel = oDataPathRouteConstraint.EdmModel; + return edmModel; + } + + private static ODataPathRouteConstraint GetODataPathRouteConstraint(ODataRoute oDataRoute) + { + return oDataRoute.Constraints.Values.SingleOrDefault(value => value is ODataPathRouteConstraint) as ODataPathRouteConstraint; + } + + // remove ApiDescription that will lead to ambiguous action matching. + private static Collection RemoveInvalidApiDescriptions(Collection apiDescriptions) + { + var duplicateApiDescriptionIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var visitedApiDescriptionIds = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var description in apiDescriptions) + { + var apiDescriptionId = description.ID; + if (visitedApiDescriptionIds.Contains(apiDescriptionId)) + { + duplicateApiDescriptionIds.Add(apiDescriptionId); + } + else + { + visitedApiDescriptionIds.Add(apiDescriptionId); + } + } + + var filteredApiDescriptions = new Collection(); + foreach (var apiDescription in apiDescriptions) + { + var apiDescriptionId = apiDescription.ID; + if (!duplicateApiDescriptionIds.Contains(apiDescriptionId)) + { + filteredApiDescriptions.Add(apiDescription); + } + } + + return filteredApiDescriptions; + } + + private static IEnumerable FlattenRoutes(IEnumerable routes) + { + foreach (var route in routes) + { + var nested = route as IEnumerable; + if (nested != null) + { + foreach (var subRoute in FlattenRoutes(nested)) + { + yield return subRoute; + } + } + else + { + yield return route; + } + } + } + + private sealed class ApiDescriptionComparer : IEqualityComparer + { + public bool Equals(ApiDescription x, ApiDescription y) + { + return string.Equals(x.ID, y.ID, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(ApiDescription obj) + { + return obj.ID.ToUpperInvariant().GetHashCode(); + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 891f9fe..7e9480c 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -187,7 +187,9 @@ - + + + From 352b5bf62ba59367024beacdcf74b7720ad73a99 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Thu, 3 Dec 2015 13:16:11 -0800 Subject: [PATCH 04/12] Implement ODataApiExplorer --- Swashbuckle.OData/CollectionExtentions.cs | 5 +- Swashbuckle.OData/ODataApiExplorer.cs | 349 +++++++++++++-------- Swashbuckle.OData/Swashbuckle.OData.csproj | 1 + 3 files changed, 224 insertions(+), 131 deletions(-) diff --git a/Swashbuckle.OData/CollectionExtentions.cs b/Swashbuckle.OData/CollectionExtentions.cs index 4b6d858..39a9fe3 100644 --- a/Swashbuckle.OData/CollectionExtentions.cs +++ b/Swashbuckle.OData/CollectionExtentions.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; namespace Swashbuckle.OData @@ -14,7 +15,7 @@ public static class CollectionExtentions /// The collection whose elements should be added to the end of the . /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type. /// - public static void AddRange(this Collection source, Collection collection) + public static void AddRange(this Collection source, IEnumerable collection) { Contract.Requires(source != null); Contract.Requires(collection != null); diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/ODataApiExplorer.cs index 1dc3cdd..00047e3 100644 --- a/Swashbuckle.OData/ODataApiExplorer.cs +++ b/Swashbuckle.OData/ODataApiExplorer.cs @@ -3,10 +3,12 @@ using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; +using System.Net.Http.Formatting; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Description; using System.Web.Http.Routing; +using System.Web.Http.Services; using System.Web.OData.Routing; using Microsoft.OData.Edm; using Swashbuckle.Swagger; @@ -15,215 +17,317 @@ namespace Swashbuckle.OData { public class ODataApiExplorer : IApiExplorer { - private readonly HttpConfiguration _config; - private readonly Lazy> _apiDescriptions; private const string ServiceRoot = "http://any/"; + private readonly Lazy> _apiDescriptions; + private readonly HttpConfiguration _config; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. public ODataApiExplorer(HttpConfiguration configuration) { _config = configuration; - _apiDescriptions = new Lazy>(InitializeApiDescriptions); + _apiDescriptions = new Lazy>(GetApiDescriptions); } /// - /// Gets the API descriptions. The descriptions are initialized on the first access. + /// Gets the API descriptions. The descriptions are initialized on the first access. /// public Collection ApiDescriptions { get { return _apiDescriptions.Value; } } - private Collection InitializeApiDescriptions() + private Collection GetApiDescriptions() { var apiDescriptions = new Collection(); - var controllerSelector = _config.Services.GetHttpControllerSelector(); - var controllerMappings = controllerSelector.GetControllerMapping(); - if (controllerMappings != null) + + foreach (var odataRoute in FlattenRoutes(_config.Routes).OfType()) { - var descriptionComparer = new ApiDescriptionComparer(); - foreach (var route in FlattenRoutes(_config.Routes)) - { - var odataRoute = route as ODataRoute; - if (odataRoute != null) - { - var descriptionsFromRoute = ExploreRoute(controllerMappings, odataRoute); - - // Remove ApiDescription that will lead to ambiguous action matching. - // E.g. a controller with Post() and PostComment(). When the route template is {controller}, it produces POST /controller and POST /controller. - descriptionsFromRoute = RemoveInvalidApiDescriptions(descriptionsFromRoute); - - foreach (var description in descriptionsFromRoute) - { - // Do not add the description if the previous route has a matching description with the same HTTP method and relative path. - // E.g. having two routes with the templates "api/Values/{id}" and "api/{controller}/{id}" can potentially produce the same - // relative path "api/Values/{id}" but only the first one matters. - if (!apiDescriptions.Contains(description, descriptionComparer)) - { - apiDescriptions.Add(description); - } - } - } - } + apiDescriptions.AddRange(GetApiDescriptions(odataRoute)); } return apiDescriptions; } /// - /// Explores the route. + /// Explores the route. /// - /// The controller mappings. /// The route. /// - private Collection ExploreRoute(IDictionary controllerMappings, ODataRoute oDataRoute) + private Collection GetApiDescriptions(ODataRoute oDataRoute) { var apiDescriptions = new Collection(); - var edmSwaggerDocument = GetDefaultEdmSwaggerDocument(oDataRoute); - - foreach (var potentialPath in edmSwaggerDocument.paths) + foreach (var potentialPathTemplateAndOperations in GetDefaultEdmSwaggerDocument(oDataRoute).paths) { - apiDescriptions.AddRange(ExplorePath(controllerMappings, oDataRoute, potentialPath.Key, potentialPath.Value)); + apiDescriptions.AddRange(GetApiDescriptions(oDataRoute, potentialPathTemplateAndOperations.Key, potentialPathTemplateAndOperations.Value)); } return apiDescriptions; } - private Collection ExplorePath(IDictionary controllerMappings, ODataRoute oDataRoute, string potentialPath, PathItem potentialOperations) + private Collection GetApiDescriptions(ODataRoute oDataRoute, string potentialPathTemplate, PathItem potentialOperations) { var apiDescriptions = new Collection(); - var edmModel = GetEdmModel(oDataRoute); - - HttpControllerDescriptor controllerDescriptor; - if (TryGetControllerDesciptor(controllerMappings, oDataRoute, potentialPath, out controllerDescriptor)) - { - apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Delete, oDataRoute, potentialPath, potentialOperations.delete, controllerDescriptor, edmModel)); - apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Get, oDataRoute, potentialPath, potentialOperations.get, controllerDescriptor, edmModel)); - } + apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Delete, oDataRoute, potentialPathTemplate, potentialOperations.delete)); + apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Get, oDataRoute, potentialPathTemplate, potentialOperations.get)); return apiDescriptions; } - private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oDataRoute, string potentialPath, Operation operation, HttpControllerDescriptor controllerDescriptor, IEdmModel edmModel) + private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oDataRoute, string potentialPathTemplate, Operation potentialOperation) { - if (operation != null) + if (potentialOperation != null) { - var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + var odataPath = GenerateSampleODataPath(oDataRoute, potentialPathTemplate, potentialOperation); - var entitySetPath = oDataPathRouteConstraint.PathHandler.Parse(edmModel, ServiceRoot, potentialPath); + var httpControllerDescriptor = GetControllerDesciptor(oDataRoute, odataPath); - var controllerContext = new HttpControllerContext + if (httpControllerDescriptor != null) { - Request = new HttpRequestMessage(httpMethod, ServiceRoot) - }; + var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + + var controllerContext = new HttpControllerContext + { + Request = new HttpRequestMessage(httpMethod, ServiceRoot), + RouteData = new HttpRouteData(new HttpRoute()) + }; - var actionSelector = _config.Services.GetActionSelector(); - var actionMappings = actionSelector.GetActionMapping(controllerDescriptor); - var action = SelectAction(oDataPathRouteConstraint, entitySetPath, controllerContext, actionMappings); + var actionMappings = _config.Services.GetActionSelector().GetActionMapping(httpControllerDescriptor); - return null; + var action = GetActionName(oDataPathRouteConstraint, odataPath, controllerContext, actionMappings); + + if (action != null) + { + return GetApiDescription(actionMappings[action].First(), httpMethod, potentialOperation, oDataRoute, odataPath); + } + } } + return null; } - private static bool TryGetControllerDesciptor(IDictionary controllerMappings, ODataRoute oDataRoute, string potentialPath, out HttpControllerDescriptor controllerDescriptor) + private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, HttpMethod httpMethod, Operation operation, ODataRoute route, ODataPath path) { - var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + var apiDocumentation = GetApiDocumentation(actionDescriptor); - var model = GetEdmModel(oDataRoute); + var parameterDescriptions = CreateParameterDescriptions(operation); + // request formatters + var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.Source == ApiParameterSource.FromBody); + var supportedRequestBodyFormatters = bodyParameter != null + ? actionDescriptor.Configuration.Formatters.Where(f => f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) + : Enumerable.Empty(); - var oDataPathHandler = oDataPathRouteConstraint.PathHandler; + // response formatters + var responseDescription = CreateResponseDescription(actionDescriptor); + var returnType = responseDescription.ResponseType ?? responseDescription.DeclaredType; + var supportedResponseFormatters = returnType != null && returnType != typeof (void) + ? actionDescriptor.Configuration.Formatters.Where(f => f.CanWriteType(returnType)) + : Enumerable.Empty(); - var entitySetPath = oDataPathHandler.Parse(model, ServiceRoot, potentialPath); + // Replacing the formatter tracers with formatters if tracers are present. + supportedRequestBodyFormatters = GetInnerFormatters(supportedRequestBodyFormatters); + supportedResponseFormatters = GetInnerFormatters(supportedResponseFormatters); - var controllerName = SelectControllerName(oDataPathRouteConstraint, entitySetPath); + var apiDescription = new ApiDescription + { + Documentation = apiDocumentation, + HttpMethod = httpMethod, + RelativePath = path.PathTemplate, + ActionDescriptor = actionDescriptor, + Route = route + }; + + apiDescription.SupportedResponseFormatters.AddRange(supportedResponseFormatters); + apiDescription.SupportedRequestBodyFormatters.AddRange(supportedRequestBodyFormatters.ToList()); + apiDescription.ParameterDescriptions.AddRange(parameterDescriptions); - if (controllerName != null) + // Have to set ResponseDescription because it's internal!?? + apiDescription.GetType().GetProperty("ResponseDescription").SetValue(apiDescription, responseDescription); + + return apiDescription; + } + + private static IEnumerable GetInnerFormatters(IEnumerable mediaTypeFormatters) + { + return mediaTypeFormatters.Select(Decorator.GetInner); + } + + private static ResponseDescription CreateResponseDescription(HttpActionDescriptor actionDescriptor) + { + var responseTypeAttribute = actionDescriptor.GetCustomAttributes(); + var responseType = responseTypeAttribute.Select(attribute => attribute.ResponseType).FirstOrDefault(); + + return new ResponseDescription + { + DeclaredType = actionDescriptor.ReturnType, + ResponseType = responseType, + Documentation = GetApiResponseDocumentation(actionDescriptor) + }; + } + + private static string GetApiResponseDocumentation(HttpActionDescriptor actionDescriptor) + { + var documentationProvider = actionDescriptor.Configuration.Services.GetDocumentationProvider(); + if (documentationProvider != null) { - return controllerMappings.TryGetValue(controllerName, out controllerDescriptor); + return documentationProvider.GetResponseDocumentation(actionDescriptor); } - controllerDescriptor = null; - return false; + + return null; } - /// - /// Selects the name of the controller to dispatch the request to. - /// - /// The o data path route constraint. - /// The OData path of the request. - /// The request. - /// - /// The name of the controller to dispatch to, or null if one cannot be resolved. - /// - private static string SelectControllerName(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path) + private IList CreateParameterDescriptions(Operation operation) { - return oDataPathRouteConstraint.RoutingConventions - .Select(routingConvention => routingConvention.SelectController(path, new HttpRequestMessage())) - .FirstOrDefault(controllerName => controllerName != null); + return operation.parameters.Select(GetParameterDescription).ToList(); } - private static string SelectAction(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path, HttpControllerContext controllerContext, ILookup actionMap) + private ApiParameterDescription GetParameterDescription(Parameter parameter) { - return oDataPathRouteConstraint.RoutingConventions - .Select(routingConvention => routingConvention.SelectAction(path, controllerContext, actionMap)) - .FirstOrDefault(action => action != null); + //return new ApiParameterDescription + //{ + // ParameterDescriptor = new + //}; + throw new NotImplementedException(); } - private static SwaggerDocument GetDefaultEdmSwaggerDocument(ODataRoute oDataRoute) + private static string GetApiDocumentation(HttpActionDescriptor actionDescriptor) { - var edmModel = GetEdmModel(oDataRoute); - var oDataSwaggerConverter = new ODataSwaggerConverter(edmModel); - return oDataSwaggerConverter.ConvertToSwaggerModel(); + var documentationProvider = actionDescriptor.Configuration.Services.GetDocumentationProvider(); + if (documentationProvider != null) + { + return documentationProvider.GetDocumentation(actionDescriptor); + } + + return null; } - private static IEdmModel GetEdmModel(ODataRoute oDataRoute) + private static ODataPath GenerateSampleODataPath(ODataRoute oDataRoute, string pathTemplate, Operation operation) { + var sampleODataPathString = GenerateSampleODataPathString(pathTemplate, operation); + var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); - var edmModel = oDataPathRouteConstraint.EdmModel; - return edmModel; + + var model = GetEdmModel(oDataRoute); + + return oDataPathRouteConstraint.PathHandler.Parse(model, ServiceRoot, sampleODataPathString); } - private static ODataPathRouteConstraint GetODataPathRouteConstraint(ODataRoute oDataRoute) + private static string GenerateSampleODataPathString(string pathTemplate, Operation operation) { - return oDataRoute.Constraints.Values.SingleOrDefault(value => value is ODataPathRouteConstraint) as ODataPathRouteConstraint; + var uriTemplate = new UriTemplate(pathTemplate); + + var parameters = GenerateSampleQueryParameterValues(operation); + + var prefix = new Uri(ServiceRoot); + + return uriTemplate.BindByName(prefix, parameters).ToString(); + } + + private static IDictionary GenerateSampleQueryParameterValues(Operation operation) + { + return operation.parameters.Where(parameter => parameter.@in == "path").ToDictionary(queryParameter => queryParameter.name, GenerateSampleQueryParameterValue); } - // remove ApiDescription that will lead to ambiguous action matching. - private static Collection RemoveInvalidApiDescriptions(Collection apiDescriptions) + private static string GenerateSampleQueryParameterValue(Parameter queryParameter) { - var duplicateApiDescriptionIds = new HashSet(StringComparer.OrdinalIgnoreCase); - var visitedApiDescriptionIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var type = queryParameter.type; + var format = queryParameter.format; - foreach (var description in apiDescriptions) + switch (format) { - var apiDescriptionId = description.ID; - if (visitedApiDescriptionIds.Contains(apiDescriptionId)) - { - duplicateApiDescriptionIds.Add(apiDescriptionId); - } - else - { - visitedApiDescriptionIds.Add(apiDescriptionId); - } + case null: + switch (type) + { + case "string": + return "SampleString"; + case "boolean": + return "true"; + default: + throw new Exception(string.Format("Could not generate sample value for query parameter type {0} and format {1}", type, format)); + } + case "int32": + case "int64": + return "42"; + case "byte": + return "1"; + case "date": + return "2015-12-12T12:00"; + case "date-time": + return "2015-10-10T17:00:00Z"; + case "double": + return "2.34d"; + case "float": + return "2.0f"; + default: + throw new Exception(string.Format("Could not generate sample value for query parameter type {0} and format {1}", type, format)); } + } - var filteredApiDescriptions = new Collection(); - foreach (var apiDescription in apiDescriptions) + private HttpControllerDescriptor GetControllerDesciptor(ODataRoute oDataRoute, ODataPath potentialPath) + { + var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); + + var controllerName = GetControllerName(oDataPathRouteConstraint, potentialPath); + + var controllerMappings = _config.Services.GetHttpControllerSelector().GetControllerMapping(); + + HttpControllerDescriptor controllerDescriptor = null; + if (controllerName != null && controllerMappings != null) { - var apiDescriptionId = apiDescription.ID; - if (!duplicateApiDescriptionIds.Contains(apiDescriptionId)) + controllerMappings.TryGetValue(controllerName, out controllerDescriptor); + } + return controllerDescriptor; + } + + /// + /// Selects the name of the controller to dispatch the request to. + /// + /// The o data path route constraint. + /// The OData path of the request. + /// + /// The name of the controller to dispatch to, or null if one cannot be resolved. + /// + private static string GetControllerName(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path) + { + return oDataPathRouteConstraint.RoutingConventions.Select(routingConvention => routingConvention.SelectController(path, new HttpRequestMessage())).FirstOrDefault(controllerName => controllerName != null); + } + + private static string GetActionName(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path, HttpControllerContext controllerContext, ILookup actionMap) + { + return oDataPathRouteConstraint.RoutingConventions.Select(routingConvention => routingConvention.SelectAction(path, controllerContext, actionMap)).FirstOrDefault(action => action != null); + } + + public static string FindMatchingAction(ILookup actionMap, params string[] targetActionNames) + { + foreach (var targetActionName in targetActionNames) + { + if (actionMap.Contains(targetActionName)) { - filteredApiDescriptions.Add(apiDescription); + return targetActionName; } } - return filteredApiDescriptions; + return null; + } + + private static SwaggerDocument GetDefaultEdmSwaggerDocument(ODataRoute oDataRoute) + { + return new ODataSwaggerConverter(GetEdmModel(oDataRoute)).ConvertToSwaggerModel(); + } + + private static IEdmModel GetEdmModel(ODataRoute oDataRoute) + { + return GetODataPathRouteConstraint(oDataRoute).EdmModel; + } + + private static ODataPathRouteConstraint GetODataPathRouteConstraint(ODataRoute oDataRoute) + { + return oDataRoute.Constraints.Values.SingleOrDefault(value => value is ODataPathRouteConstraint) as ODataPathRouteConstraint; } private static IEnumerable FlattenRoutes(IEnumerable routes) @@ -244,18 +348,5 @@ private static IEnumerable FlattenRoutes(IEnumerable rou } } } - - private sealed class ApiDescriptionComparer : IEqualityComparer - { - public bool Equals(ApiDescription x, ApiDescription y) - { - return string.Equals(x.ID, y.ID, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(ApiDescription obj) - { - return obj.ID.ToUpperInvariant().GetHashCode(); - } - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 7e9480c..b22da9b 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -154,6 +154,7 @@ True + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll From e70d2eed90ef98153a91e6717ae92ccb36dd873f Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Thu, 3 Dec 2015 17:01:14 -0800 Subject: [PATCH 05/12] Implementing ODataApiExplorer --- .../App_Start/SwaggerConfig.cs | 2 +- Swashbuckle.OData/ODataApiExplorer.cs | 118 ++++++++++++++---- Swashbuckle.OData/ODataSwaggerUtilities.cs | 59 +++++++-- 3 files changed, 141 insertions(+), 38 deletions(-) diff --git a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs index ed84b71..f27a8a5 100644 --- a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs @@ -167,7 +167,7 @@ public static void Register() // Wrap the default SwaggerGenerator with additional behavior (e.g. caching) or provide an // alternative implementation for ISwaggerProvider with the CustomProvider option. // - c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c)); + //c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c)); }) .EnableSwaggerUi(c => { diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/ODataApiExplorer.cs index 00047e3..5405590 100644 --- a/Swashbuckle.OData/ODataApiExplorer.cs +++ b/Swashbuckle.OData/ODataApiExplorer.cs @@ -102,7 +102,7 @@ private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oData if (action != null) { - return GetApiDescription(actionMappings[action].First(), httpMethod, potentialOperation, oDataRoute, odataPath); + return GetApiDescription(actionMappings[action].First(), httpMethod, potentialOperation, oDataRoute, odataPath, potentialPathTemplate); } } } @@ -110,11 +110,11 @@ private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oData return null; } - private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, HttpMethod httpMethod, Operation operation, ODataRoute route, ODataPath path) + private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, HttpMethod httpMethod, Operation operation, ODataRoute route, ODataPath path, string potentialPathTemplate) { var apiDocumentation = GetApiDocumentation(actionDescriptor); - var parameterDescriptions = CreateParameterDescriptions(operation); + var parameterDescriptions = CreateParameterDescriptions(operation, actionDescriptor); // request formatters var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.Source == ApiParameterSource.FromBody); @@ -137,7 +137,7 @@ private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, { Documentation = apiDocumentation, HttpMethod = httpMethod, - RelativePath = path.PathTemplate, + RelativePath = potentialPathTemplate.TrimStart('/'), ActionDescriptor = actionDescriptor, Route = route }; @@ -173,26 +173,49 @@ private static ResponseDescription CreateResponseDescription(HttpActionDescripto private static string GetApiResponseDocumentation(HttpActionDescriptor actionDescriptor) { var documentationProvider = actionDescriptor.Configuration.Services.GetDocumentationProvider(); - if (documentationProvider != null) - { - return documentationProvider.GetResponseDocumentation(actionDescriptor); - } + return documentationProvider != null + ? documentationProvider.GetResponseDocumentation(actionDescriptor) + : null; + } - return null; + private List CreateParameterDescriptions(Operation operation, HttpActionDescriptor actionDescriptor) + { + return operation.parameters.Select(parameter => GetParameterDescription(parameter, actionDescriptor)).ToList(); } - private IList CreateParameterDescriptions(Operation operation) + private ApiParameterDescription GetParameterDescription(Parameter parameter, HttpActionDescriptor actionDescriptor) { - return operation.parameters.Select(GetParameterDescription).ToList(); + var httpParameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == parameter.name); + if (httpParameterDescriptor != null) + { + return new ApiParameterDescription + { + ParameterDescriptor = httpParameterDescriptor, + Name = httpParameterDescriptor.Prefix ?? httpParameterDescriptor.ParameterName, + Documentation = GetApiParameterDocumentation(httpParameterDescriptor), + Source = parameter.@in == "path" || parameter.@in == "query" ? ApiParameterSource.FromUri : ApiParameterSource.FromBody + }; + } + return new ApiParameterDescription + { + ParameterDescriptor = new ODataParameterDescriptor(parameter.name, GetType(parameter)) + { + Configuration = _config, + ActionDescriptor = actionDescriptor + }, + Name = parameter.name, + Documentation = parameter.description, + Source = parameter.@in == "path" || parameter.@in == "query" ? ApiParameterSource.FromUri : ApiParameterSource.FromBody + }; } - private ApiParameterDescription GetParameterDescription(Parameter parameter) + private static string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor) { - //return new ApiParameterDescription - //{ - // ParameterDescriptor = new - //}; - throw new NotImplementedException(); + var documentationProvider = parameterDescriptor.Configuration.Services.GetDocumentationProvider(); + + return documentationProvider != null + ? documentationProvider.GetDocumentation(parameterDescriptor) + : null; } private static string GetApiDocumentation(HttpActionDescriptor actionDescriptor) @@ -248,7 +271,7 @@ private static string GenerateSampleQueryParameterValue(Parameter queryParameter case "boolean": return "true"; default: - throw new Exception(string.Format("Could not generate sample value for query parameter type {0} and format {1}", type, format)); + throw new Exception(string.Format("Could not generate sample value for query parameter type {0} and format {1}", type, "null")); } case "int32": case "int64": @@ -268,6 +291,42 @@ private static string GenerateSampleQueryParameterValue(Parameter queryParameter } } + private static Type GetType(Parameter queryParameter) + { + var type = queryParameter.type; + var format = queryParameter.format; + + switch (format) + { + case null: + switch (type) + { + case "string": + return typeof(string); + case "boolean": + return typeof(bool); + default: + throw new Exception(string.Format("Could not determine .NET type for parameter type {0} and format {1}", type, "null")); + } + case "int32": + return typeof(int); + case "int64": + return typeof(long); + case "byte": + return typeof(byte); + case "date": + return typeof(DateTime); + case "date-time": + return typeof(DateTimeOffset); + case "double": + return typeof(double); + case "float": + return typeof(float); + default: + throw new Exception(string.Format("Could not determine .NET type for parameter type {0} and format {1}", type, format)); + } + } + private HttpControllerDescriptor GetControllerDesciptor(ODataRoute oDataRoute, ODataPath potentialPath) { var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); @@ -304,15 +363,7 @@ private static string GetActionName(ODataPathRouteConstraint oDataPathRouteConst public static string FindMatchingAction(ILookup actionMap, params string[] targetActionNames) { - foreach (var targetActionName in targetActionNames) - { - if (actionMap.Contains(targetActionName)) - { - return targetActionName; - } - } - - return null; + return targetActionNames.FirstOrDefault(actionMap.Contains); } private static SwaggerDocument GetDefaultEdmSwaggerDocument(ODataRoute oDataRoute) @@ -349,4 +400,17 @@ private static IEnumerable FlattenRoutes(IEnumerable rou } } } + + internal class ODataParameterDescriptor : HttpParameterDescriptor + { + public ODataParameterDescriptor(string parameterName, Type parameterType) + { + ParameterName = parameterName; + ParameterType = parameterType; + } + + public override string ParameterName { get; } + + public override Type ParameterType { get; } + } } \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerUtilities.cs b/Swashbuckle.OData/ODataSwaggerUtilities.cs index 6affe69..1b71038 100644 --- a/Swashbuckle.OData/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/ODataSwaggerUtilities.cs @@ -33,8 +33,26 @@ public static PathItem CreateSwaggerPathForEntitySet(IEdmNavigationSource naviga return new PathItem { - get = new Operation().Summary("Get EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_Get").Description("Returns the EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string").Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string").Parameter("$select", "query", "Selects which properties to include in the response.", "string").Parameter("$orderby", "query", "Sorts the results.", "string").Parameter("$top", "query", "Returns only the first n results.", "integer").Parameter("$skip", "query", "Skips the first n results.", "integer").Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean")).Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), - post = new Operation().Summary("Post a new entity to EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_Post").Description("Post a new entity to EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(new List().Parameter(entitySet.EntityType().Name, "body", "The entity to post", entitySet.EntityType())).Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()) + get = new Operation() + .Summary("Get EntitySet " + entitySet.Name) + .OperationId(entitySet.Name + "_Get") + .Description("Returns the EntitySet " + entitySet.Name) + .Tags(entitySet.Name) + .Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string") + .Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string") + .Parameter("$select", "query", "Selects which properties to include in the response.", "string") + .Parameter("$orderby", "query", "Sorts the results.", "string") + .Parameter("$top", "query", "Returns only the first n results.", "integer", "int32") + .Parameter("$skip", "query", "Skips the first n results.", "integer", "int32") + .Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean")) + .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), + post = new Operation() + .Summary("Post a new entity to EntitySet " + entitySet.Name) + .OperationId(entitySet.Name + "_Post") + .Description("Post a new entity to EntitySet " + entitySet.Name) + .Tags(entitySet.Name).Parameters(new List() + .Parameter(entitySet.EntityType().Name, "body", "The entity to post", entitySet.EntityType())) + .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()) }; } @@ -61,9 +79,29 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio return new PathItem { - get = new Operation().Summary("Get entity from " + entitySet.Name + " by key.").OperationId(entitySet.Name + "_GetById").Description("Returns the entity with the key from " + entitySet.Name).Tags(entitySet.Name).Parameters(keyParameters.DeepClone().Parameter("$expand", "query", "Expands related entities inline.", "string")).Parameters(keyParameters.DeepClone().Parameter("$select", "query", "Selects which properties to include in the response.", "string")).Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), - patch = new Operation().Summary("Update entity in EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_PatchById").Description("Update entity in EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType().Name, "body", "The entity to patch", entitySet.EntityType())).Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), - delete = new Operation().Summary("Delete entity in EntitySet " + entitySet.Name).OperationId(entitySet.Name + "_DeleteById").Description("Delete entity in EntitySet " + entitySet.Name).Tags(entitySet.Name).Parameters(keyParameters.DeepClone().Parameter("If-Match", "header", "If-Match header", "string")).Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()) + get = new Operation() + .Summary("Get entity from " + entitySet.Name + " by key.") + .OperationId(entitySet.Name + "_GetById") + .Description("Returns the entity with the key from " + entitySet.Name) + .Tags(entitySet.Name).Parameters(keyParameters.DeepClone() + .Parameter("$expand", "query", "Expands related entities inline.", "string")) + .Parameters(keyParameters.DeepClone().Parameter("$select", "query", "Selects which properties to include in the response.", "string")) + .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), + + patch = new Operation() + .Summary("Update entity in EntitySet " + entitySet.Name) + .OperationId(entitySet.Name + "_PatchById") + .Description("Update entity in EntitySet " + entitySet.Name) + .Tags(entitySet.Name) + .Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType().Name, "body", "The entity to patch", entitySet.EntityType())) + .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), + + delete = new Operation().Summary("Delete entity in EntitySet " + entitySet.Name) + .OperationId(entitySet.Name + "_DeleteById") + .Description("Delete entity in EntitySet " + entitySet.Name) + .Tags(entitySet.Name) + .Parameters(keyParameters.DeepClone().Parameter("If-Match", "header", "If-Match header", "string")) + .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()) }; } @@ -566,13 +604,14 @@ private static IList Parameter(this IList parameters, stri name = name, @in = kind, description = description, - type = type + type = type, + format = format }); - if (!string.IsNullOrEmpty(format)) - { - parameters.First().format = format; - } + //if (!string.IsNullOrEmpty(format)) + //{ + // parameters.First().format = format; + //} return parameters; } From b188e62df06555f4e1ec0a2d9654b4b1f13b9cd8 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 4 Dec 2015 12:07:35 -0800 Subject: [PATCH 06/12] Implementing custom ApiExplorer --- .../App_Start/SwaggerConfig.cs | 5 +- Swashbuckle.OData.Tests/Customer.cs | 13 - .../{ => Fixtures}/GetQueryParametersTests.cs | 18 ++ .../HttpConfigurationRoutesTests.cs | 0 Swashbuckle.OData.Tests/Order.cs | 21 -- .../Swashbuckle.OData.Tests.csproj | 6 +- .../HttpConfigurationExtensions.cs | 16 ++ Swashbuckle.OData/ODataApiExplorer.cs | 42 ++-- Swashbuckle.OData/ODataSwaggerProvider.cs | 231 ++++++++++++++++-- Swashbuckle.OData/ODataSwaggerUtilities.cs | 29 +-- Swashbuckle.OData/ReflectionExtensions.cs | 22 ++ .../SwaggerApiParameterSource.cs | 11 + Swashbuckle.OData/Swashbuckle.OData.csproj | 50 +--- 13 files changed, 327 insertions(+), 137 deletions(-) delete mode 100644 Swashbuckle.OData.Tests/Customer.cs rename Swashbuckle.OData.Tests/{ => Fixtures}/GetQueryParametersTests.cs (63%) rename Swashbuckle.OData.Tests/{ => Fixtures}/HttpConfigurationRoutesTests.cs (100%) delete mode 100644 Swashbuckle.OData.Tests/Order.cs create mode 100644 Swashbuckle.OData/HttpConfigurationExtensions.cs create mode 100644 Swashbuckle.OData/ReflectionExtensions.cs create mode 100644 Swashbuckle.OData/SwaggerApiParameterSource.cs diff --git a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs index f27a8a5..4674416 100644 --- a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs @@ -2,7 +2,6 @@ using System.Web.Http.Description; using Swashbuckle.Application; using Swashbuckle.OData; -using Swashbuckle.OData.ApiExplorer; using SwashbuckleODataSample; using WebActivatorEx; @@ -14,8 +13,6 @@ public class SwaggerConfig { public static void Register() { - GlobalConfiguration.Configuration.Services.Replace(typeof(IApiExplorer), new ODataApiExplorer(GlobalConfiguration.Configuration)); - GlobalConfiguration.Configuration.EnableSwagger(c => { // By default, the service root url is inferred from the request used to access the docs. @@ -167,7 +164,7 @@ public static void Register() // Wrap the default SwaggerGenerator with additional behavior (e.g. caching) or provide an // alternative implementation for ISwaggerProvider with the CustomProvider option. // - //c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c)); + c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c)); }) .EnableSwaggerUi(c => { diff --git a/Swashbuckle.OData.Tests/Customer.cs b/Swashbuckle.OData.Tests/Customer.cs deleted file mode 100644 index 54a36bc..0000000 --- a/Swashbuckle.OData.Tests/Customer.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace Swashbuckle.OData.Tests -{ - public class Customer - { - public int Id { get; set; } - - public string Name { get; set; } - - public IList Orders { get; set; } - } -} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/GetQueryParametersTests.cs b/Swashbuckle.OData.Tests/Fixtures/GetQueryParametersTests.cs similarity index 63% rename from Swashbuckle.OData.Tests/GetQueryParametersTests.cs rename to Swashbuckle.OData.Tests/Fixtures/GetQueryParametersTests.cs index a18944e..2f04304 100644 --- a/Swashbuckle.OData.Tests/GetQueryParametersTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/GetQueryParametersTests.cs @@ -33,5 +33,23 @@ public async Task It_includes_the_filter_parameter() filterParameter.@in.ShouldBeEquivalentTo("query"); } } + + [Test] + public async Task It_has_all_optional_odata_query_parameters() + { + using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(); + + // Act + var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/Customers", out pathItem); + pathItem.get.parameters.Where(parameter => parameter.name.StartsWith("$")).Should().OnlyContain(parameter => parameter.required == false); + } + } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/HttpConfigurationRoutesTests.cs b/Swashbuckle.OData.Tests/Fixtures/HttpConfigurationRoutesTests.cs similarity index 100% rename from Swashbuckle.OData.Tests/HttpConfigurationRoutesTests.cs rename to Swashbuckle.OData.Tests/Fixtures/HttpConfigurationRoutesTests.cs diff --git a/Swashbuckle.OData.Tests/Order.cs b/Swashbuckle.OData.Tests/Order.cs deleted file mode 100644 index 3aacd0d..0000000 --- a/Swashbuckle.OData.Tests/Order.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Web.OData.Builder; -using Microsoft.OData.Edm; - -namespace Swashbuckle.OData.Tests -{ - public class Order - { - [Key] - public int OrderId { get; set; } - - public string OrderName { get; set; } - - public int CustomerId { get; set; } - - [ForeignKey("CustomerId")] - [ActionOnDelete(EdmOnDeleteAction.Cascade)] - public Customer Customer { get; set; } - } -} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index 3a37b1d..b84a9a7 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -112,11 +112,9 @@ - - - - + + diff --git a/Swashbuckle.OData/HttpConfigurationExtensions.cs b/Swashbuckle.OData/HttpConfigurationExtensions.cs new file mode 100644 index 0000000..91fecbb --- /dev/null +++ b/Swashbuckle.OData/HttpConfigurationExtensions.cs @@ -0,0 +1,16 @@ +using System.Web.Http; +using Newtonsoft.Json; + +namespace Swashbuckle.OData +{ + public static class HttpConfigurationExtensions + { + internal static JsonSerializerSettings SerializerSettingsOrDefault(this HttpConfiguration httpConfig) + { + var formatter = httpConfig.Formatters.JsonFormatter; + return formatter != null + ? formatter.SerializerSettings + : new JsonSerializerSettings(); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/ODataApiExplorer.cs index 5405590..35c3721 100644 --- a/Swashbuckle.OData/ODataApiExplorer.cs +++ b/Swashbuckle.OData/ODataApiExplorer.cs @@ -19,15 +19,15 @@ public class ODataApiExplorer : IApiExplorer { private const string ServiceRoot = "http://any/"; private readonly Lazy> _apiDescriptions; - private readonly HttpConfiguration _config; + private readonly Func _config; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The configuration. - public ODataApiExplorer(HttpConfiguration configuration) + /// The HTTP configuration provider. + public ODataApiExplorer(Func httpConfigurationProvider) { - _config = configuration; + _config = httpConfigurationProvider; _apiDescriptions = new Lazy>(GetApiDescriptions); } @@ -43,7 +43,7 @@ private Collection GetApiDescriptions() { var apiDescriptions = new Collection(); - foreach (var odataRoute in FlattenRoutes(_config.Routes).OfType()) + foreach (var odataRoute in FlattenRoutes(_config().Routes).OfType()) { apiDescriptions.AddRange(GetApiDescriptions(odataRoute)); } @@ -96,13 +96,13 @@ private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oData RouteData = new HttpRouteData(new HttpRoute()) }; - var actionMappings = _config.Services.GetActionSelector().GetActionMapping(httpControllerDescriptor); + var actionMappings = _config().Services.GetActionSelector().GetActionMapping(httpControllerDescriptor); var action = GetActionName(oDataPathRouteConstraint, odataPath, controllerContext, actionMappings); if (action != null) { - return GetApiDescription(actionMappings[action].First(), httpMethod, potentialOperation, oDataRoute, odataPath, potentialPathTemplate); + return GetApiDescription(actionMappings[action].First(), httpMethod, potentialOperation, oDataRoute, potentialPathTemplate); } } } @@ -110,7 +110,7 @@ private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oData return null; } - private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, HttpMethod httpMethod, Operation operation, ODataRoute route, ODataPath path, string potentialPathTemplate) + private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, HttpMethod httpMethod, Operation operation, ODataRoute route, string potentialPathTemplate) { var apiDocumentation = GetApiDocumentation(actionDescriptor); @@ -185,7 +185,7 @@ private List CreateParameterDescriptions(Operation oper private ApiParameterDescription GetParameterDescription(Parameter parameter, HttpActionDescriptor actionDescriptor) { - var httpParameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == parameter.name); + var httpParameterDescriptor = GetHttpParameterDescriptor(parameter, actionDescriptor); if (httpParameterDescriptor != null) { return new ApiParameterDescription @@ -198,9 +198,9 @@ private ApiParameterDescription GetParameterDescription(Parameter parameter, Htt } return new ApiParameterDescription { - ParameterDescriptor = new ODataParameterDescriptor(parameter.name, GetType(parameter)) + ParameterDescriptor = new ODataParameterDescriptor(parameter.name, GetType(parameter), parameter.required.Value) { - Configuration = _config, + Configuration = _config(), ActionDescriptor = actionDescriptor }, Name = parameter.name, @@ -209,6 +209,17 @@ private ApiParameterDescription GetParameterDescription(Parameter parameter, Htt }; } + private static HttpParameterDescriptor GetHttpParameterDescriptor(Parameter parameter, HttpActionDescriptor actionDescriptor) + { + var httpParameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == parameter.name); + // Maybe the parameter is a key parameter, e.g., where Id in the URI path maps to a parameter named 'key' + if (httpParameterDescriptor == null && parameter.description.StartsWith("key:")) + { + httpParameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == "key"); + } + return httpParameterDescriptor; + } + private static string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor) { var documentationProvider = parameterDescriptor.Configuration.Services.GetDocumentationProvider(); @@ -333,7 +344,7 @@ private HttpControllerDescriptor GetControllerDesciptor(ODataRoute oDataRoute, O var controllerName = GetControllerName(oDataPathRouteConstraint, potentialPath); - var controllerMappings = _config.Services.GetHttpControllerSelector().GetControllerMapping(); + var controllerMappings = _config().Services.GetHttpControllerSelector().GetControllerMapping(); HttpControllerDescriptor controllerDescriptor = null; if (controllerName != null && controllerMappings != null) @@ -403,14 +414,17 @@ private static IEnumerable FlattenRoutes(IEnumerable rou internal class ODataParameterDescriptor : HttpParameterDescriptor { - public ODataParameterDescriptor(string parameterName, Type parameterType) + public ODataParameterDescriptor(string parameterName, Type parameterType, bool isOptional) { ParameterName = parameterName; ParameterType = parameterType; + IsOptional = isOptional; } public override string ParameterName { get; } public override Type ParameterType { get; } + + public override bool IsOptional { get; } } } \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index a19cca8..663361c 100644 --- a/Swashbuckle.OData/ODataSwaggerProvider.cs +++ b/Swashbuckle.OData/ODataSwaggerProvider.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Web.Http; +using System.Web.Http.Description; using System.Web.OData.Routing; -using Flurl; using Swashbuckle.Application; using Swashbuckle.Swagger; @@ -11,10 +12,12 @@ namespace Swashbuckle.OData { public class ODataSwaggerProvider : ISwaggerProvider { - private readonly ISwaggerProvider _defaultProvider; + private readonly IApiExplorer _apiExplorer; + private readonly IDictionary _apiVersions; + private readonly SwaggerGeneratorOptions _options; + private readonly Func _httpConfigurationProvider; - // Here for future use against the OData API... - private readonly SwaggerDocsConfig _swaggerDocsConfig; + private readonly ISwaggerProvider _defaultProvider; /// /// Initializes a new instance of the class. @@ -43,46 +46,234 @@ public ODataSwaggerProvider(ISwaggerProvider defaultProvider, SwaggerDocsConfig Contract.Requires(swaggerDocsConfig != null); Contract.Requires(httpConfigurationProvider != null); + _apiExplorer = new ODataApiExplorer(httpConfigurationProvider); + _apiVersions = GetApiVersions(defaultProvider); _defaultProvider = defaultProvider; - _swaggerDocsConfig = swaggerDocsConfig; _httpConfigurationProvider = httpConfigurationProvider; + _options = GetSwaggerGeneratorOptions(defaultProvider); + } + + /// + /// Gets the API versions. I'd rather not use reflection because the implementation may change, but can't find a better way. + /// + /// The default provider. + /// + private static IDictionary GetApiVersions(ISwaggerProvider defaultProvider) + { + var swaggerGenerator = defaultProvider as SwaggerGenerator; + Contract.Assert(swaggerGenerator != null, "The ODataSwaggerProvider currently requires a defaultProvider of type SwaggerGenerator"); + var apiVersions = typeof(SwaggerGenerator).GetInstanceField(swaggerGenerator, "_apiVersions") as IDictionary; + Contract.Assert(apiVersions != null, "The ODataSwaggerProvider currently requires that the SwaggerGenerator has a non-null field '_apiVersions' of type SwaggerGeneratorOptions"); + return apiVersions; + } + + /// + /// Gets the swagger generator options via Reflection. I'd rather not use reflection because the implementation may change, but can't find a better way. + /// + /// The default provider. + private static SwaggerGeneratorOptions GetSwaggerGeneratorOptions(ISwaggerProvider defaultProvider) + { + var swaggerGenerator = defaultProvider as SwaggerGenerator; + Contract.Assert(swaggerGenerator != null, "The ODataSwaggerProvider currently requires a defaultProvider of type SwaggerGenerator"); + var options = typeof (SwaggerGenerator).GetInstanceField(swaggerGenerator, "_options") as SwaggerGeneratorOptions; + Contract.Assert(options != null, "The ODataSwaggerProvider currently requires that the SwaggerGenerator has a non-null field '_options' of type SwaggerGeneratorOptions"); + return options; } public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) { - var apiDescriptions = _httpConfigurationProvider().Services.GetApiExplorer().ApiDescriptions; + var schemaRegistry = new SchemaRegistry( + _httpConfigurationProvider().SerializerSettingsOrDefault(), + _options.CustomSchemaMappings, + _options.SchemaFilters, + _options.ModelFilters, + _options.IgnoreObsoleteProperties, + _options.SchemaIdSelector, + _options.DescribeAllEnumsAsStrings, + _options.DescribeStringEnumsInCamelCase); + + Info info; + _apiVersions.TryGetValue(apiVersion, out info); + if (info == null) + throw new UnknownApiVersion(apiVersion); + + var paths = GetApiDescriptionsFor(apiVersion) + .Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.IsObsolete())) + .OrderBy(_options.GroupingKeySelector, _options.GroupingKeyComparer) + .GroupBy(apiDesc => apiDesc.RelativePathSansQueryString()) + .ToDictionary(group => "/" + group.Key, group => CreatePathItem(group, schemaRegistry)); var oDataRoute = _httpConfigurationProvider().Routes.SingleOrDefault(route => route is ODataRoute) as ODataRoute; if (oDataRoute != null) { - var oDataPathRouteConstraint = oDataRoute.Constraints.Values.SingleOrDefault(value => value is ODataPathRouteConstraint) as ODataPathRouteConstraint; + //var oDataPathRouteConstraint = oDataRoute.Constraints.Values.SingleOrDefault(value => value is ODataPathRouteConstraint) as ODataPathRouteConstraint; - var edmModel = oDataPathRouteConstraint.EdmModel; - var routePrefix = oDataRoute.RoutePrefix; + //var edmModel = oDataPathRouteConstraint.EdmModel; + //var routePrefix = oDataRoute.RoutePrefix; - var oDataSwaggerConverter = new ODataSwaggerConverter(edmModel); + //var oDataSwaggerConverter = new ODataSwaggerConverter(edmModel); - var rootUri = new Uri(rootUrl); + //var rootUri = new Uri(rootUrl); + + //var basePath = rootUri.AbsolutePath != "/" ? rootUri.AbsolutePath : "/" + routePrefix; - var basePath = rootUri.AbsolutePath != "/" ? rootUri.AbsolutePath : "/" + routePrefix; + //oDataSwaggerConverter.MetadataUri = new Uri(rootUrl.AppendPathSegments(basePath, "$metadata")); - oDataSwaggerConverter.MetadataUri = new Uri(rootUrl.AppendPathSegments(basePath, "$metadata")); + //var port = !rootUri.IsDefaultPort ? ":" + rootUri.Port : string.Empty; + //var edmSwaggerDocument = oDataSwaggerConverter.ConvertToSwaggerModel(); + //edmSwaggerDocument.host = rootUri.Host + port; + //edmSwaggerDocument.basePath = basePath; + //edmSwaggerDocument.schemes = new[] + //{ + // rootUri.Scheme + //}.ToList(); + + //return edmSwaggerDocument; + + var rootUri = new Uri(rootUrl); var port = !rootUri.IsDefaultPort ? ":" + rootUri.Port : string.Empty; - var edmSwaggerDocument = oDataSwaggerConverter.ConvertToSwaggerModel(); - edmSwaggerDocument.host = rootUri.Host + port; - edmSwaggerDocument.basePath = basePath; - edmSwaggerDocument.schemes = new[] + var swaggerDoc = new SwaggerDocument { - rootUri.Scheme - }.ToList(); + info = info, + host = rootUri.Host + port, + basePath = rootUri.AbsolutePath != "/" ? rootUri.AbsolutePath : null, + schemes = _options.Schemes != null ? _options.Schemes.ToList() : new[] { rootUri.Scheme }.ToList(), + paths = paths, + definitions = schemaRegistry.Definitions, + securityDefinitions = _options.SecurityDefinitions + }; - return edmSwaggerDocument; + foreach (var filter in _options.DocumentFilters) + { + filter.Apply(swaggerDoc, schemaRegistry, _apiExplorer); + } + + return swaggerDoc; } return _defaultProvider.GetSwagger(rootUrl, apiVersion); } + + private PathItem CreatePathItem(IEnumerable apiDescriptions, SchemaRegistry schemaRegistry) + { + var pathItem = new PathItem(); + + // Group further by http method + var perMethodGrouping = apiDescriptions + .GroupBy(apiDesc => apiDesc.HttpMethod.Method.ToLower()); + + foreach (var group in perMethodGrouping) + { + var httpMethod = group.Key; + + var apiDescription = (group.Count() == 1) + ? group.First() + : _options.ConflictingActionsResolver(group); + + switch (httpMethod) + { + case "get": + pathItem.get = CreateOperation(apiDescription, schemaRegistry); + break; + case "put": + pathItem.put = CreateOperation(apiDescription, schemaRegistry); + break; + case "post": + pathItem.post = CreateOperation(apiDescription, schemaRegistry); + break; + case "delete": + pathItem.delete = CreateOperation(apiDescription, schemaRegistry); + break; + case "options": + pathItem.options = CreateOperation(apiDescription, schemaRegistry); + break; + case "head": + pathItem.head = CreateOperation(apiDescription, schemaRegistry); + break; + case "patch": + pathItem.patch = CreateOperation(apiDescription, schemaRegistry); + break; + } + } + + return pathItem; + } + + private Operation CreateOperation(ApiDescription apiDescription, SchemaRegistry schemaRegistry) + { + var parameters = apiDescription.ParameterDescriptions + .Select(paramDesc => + { + var inPath = apiDescription.RelativePathSansQueryString().Contains("{" + paramDesc.Name + "}"); + return CreateParameter(paramDesc, inPath, schemaRegistry); + }) + .ToList(); + + var responses = new Dictionary(); + var responseType = apiDescription.ResponseType(); + if (responseType == null || responseType == typeof(void)) + responses.Add("204", new Response { description = "No Content" }); + else + responses.Add("200", new Response { description = "OK", schema = schemaRegistry.GetOrRegister(responseType) }); + + var operation = new Operation + { + tags = new[] { _options.GroupingKeySelector(apiDescription) }, + operationId = apiDescription.FriendlyId(), + produces = apiDescription.Produces().ToList(), + consumes = apiDescription.Consumes().ToList(), + parameters = parameters.Any() ? parameters : null, // parameters can be null but not empty + responses = responses, + deprecated = apiDescription.IsObsolete() + }; + + foreach (var filter in _options.OperationFilters) + { + filter.Apply(operation, schemaRegistry, apiDescription); + } + + return operation; + } + + private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry) + { + var @in = inPath + ? "path" + : paramDesc.Source == ApiParameterSource.FromUri ? "query" : "body"; + + var parameter = new Parameter + { + name = paramDesc.Name, + @in = @in + }; + + if (paramDesc.ParameterDescriptor == null) + { + parameter.type = "string"; + parameter.required = true; + return parameter; + } + + parameter.required = inPath || !paramDesc.ParameterDescriptor.IsOptional; + parameter.@default = paramDesc.ParameterDescriptor.DefaultValue; + + var schema = schemaRegistry.GetOrRegister(paramDesc.ParameterDescriptor.ParameterType); + if (parameter.@in == "body") + parameter.schema = schema; + else + parameter.PopulateFrom(schema); + + return parameter; + } + + private IEnumerable GetApiDescriptionsFor(string apiVersion) + { + return _options.VersionSupportResolver == null + ? _apiExplorer.ApiDescriptions + : _apiExplorer.ApiDescriptions.Where(apiDesc => _options.VersionSupportResolver(apiDesc, apiVersion)); + } } } \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerUtilities.cs b/Swashbuckle.OData/ODataSwaggerUtilities.cs index 1b71038..667e611 100644 --- a/Swashbuckle.OData/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/ODataSwaggerUtilities.cs @@ -38,13 +38,13 @@ public static PathItem CreateSwaggerPathForEntitySet(IEdmNavigationSource naviga .OperationId(entitySet.Name + "_Get") .Description("Returns the EntitySet " + entitySet.Name) .Tags(entitySet.Name) - .Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string") - .Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string") - .Parameter("$select", "query", "Selects which properties to include in the response.", "string") - .Parameter("$orderby", "query", "Sorts the results.", "string") - .Parameter("$top", "query", "Returns only the first n results.", "integer", "int32") - .Parameter("$skip", "query", "Skips the first n results.", "integer", "int32") - .Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean")) + .Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string", required: false) + .Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string", required: false) + .Parameter("$select", "query", "Selects which properties to include in the response.", "string", required: false) + .Parameter("$orderby", "query", "Sorts the results.", "string", required: false) + .Parameter("$top", "query", "Returns only the first n results.", "integer", false, "int32") + .Parameter("$skip", "query", "Skips the first n results.", "integer", false, "int32") + .Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean", required: false)) .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), post = new Operation() .Summary("Post a new entity to EntitySet " + entitySet.Name) @@ -74,7 +74,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio { string format; var type = GetPrimitiveTypeAndFormat(key.Type.Definition as IEdmPrimitiveType, out format); - keyParameters.Parameter(key.Name, "path", "key: " + key.Name, type, format); + keyParameters.Parameter(key.Name, "path", "key: " + key.Name, type, true, format); } return new PathItem @@ -84,8 +84,8 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio .OperationId(entitySet.Name + "_GetById") .Description("Returns the entity with the key from " + entitySet.Name) .Tags(entitySet.Name).Parameters(keyParameters.DeepClone() - .Parameter("$expand", "query", "Expands related entities inline.", "string")) - .Parameters(keyParameters.DeepClone().Parameter("$select", "query", "Selects which properties to include in the response.", "string")) + .Parameter("$expand", "query", "Expands related entities inline.", "string", false)) + .Parameters(keyParameters.DeepClone().Parameter("$select", "query", "Selects which properties to include in the response.", "string", false)) .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), patch = new Operation() @@ -100,7 +100,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio .OperationId(entitySet.Name + "_DeleteById") .Description("Delete entity in EntitySet " + entitySet.Name) .Tags(entitySet.Name) - .Parameters(keyParameters.DeepClone().Parameter("If-Match", "header", "If-Match header", "string")) + .Parameters(keyParameters.DeepClone().Parameter("If-Match", "header", "If-Match header", "string", false)) .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()) }; } @@ -223,7 +223,7 @@ public static PathItem CreateSwaggerPathForOperationOfEntity(IEdmOperation opera { string format; var type = GetPrimitiveTypeAndFormat(key.Type.Definition as IEdmPrimitiveType, out format); - swaggerParameters.Parameter(key.Name, "path", "key: " + key.Name, type, format); + swaggerParameters.Parameter(key.Name, "path", "key: " + key.Name, type, true, format); } foreach (var parameter in operation.Parameters.Skip(1)) @@ -597,7 +597,7 @@ private static Operation Parameters(this Operation obj, IList paramet return obj; } - private static IList Parameter(this IList parameters, string name, string kind, string description, string type, string format = null) + private static IList Parameter(this IList parameters, string name, string kind, string description, string type, bool required, string format = null) { parameters.Add(new Parameter { @@ -605,7 +605,8 @@ private static IList Parameter(this IList parameters, stri @in = kind, description = description, type = type, - format = format + format = format, + required = required }); //if (!string.IsNullOrEmpty(format)) diff --git a/Swashbuckle.OData/ReflectionExtensions.cs b/Swashbuckle.OData/ReflectionExtensions.cs new file mode 100644 index 0000000..d49f371 --- /dev/null +++ b/Swashbuckle.OData/ReflectionExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Reflection; + +namespace Swashbuckle.OData +{ + public static class ReflectionExtensions + { + /// + /// Uses reflection to get the field value from an object. + /// + /// The instance type. + /// The instance object. + /// The field's name which is to be fetched. + /// The field value from the object. + internal static object GetInstanceField(this Type type, object instance, string fieldName) + { + const BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; + var field = type.GetField(fieldName, bindFlags); + return field.GetValue(instance); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/SwaggerApiParameterSource.cs b/Swashbuckle.OData/SwaggerApiParameterSource.cs new file mode 100644 index 0000000..7bd8075 --- /dev/null +++ b/Swashbuckle.OData/SwaggerApiParameterSource.cs @@ -0,0 +1,11 @@ +namespace Swashbuckle.OData +{ + public enum SwaggerApiParameterSource + { + Query, + Header, + Path, + FormData, + Body + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index b22da9b..51b763a 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -170,63 +170,19 @@ - - - - - - True - True - CommonWebApiResources.resx - - - - - - - - - - - + - - - - - - - - - - - - - True - True - SRResources.resx - - + + - - - ResXFileCodeGenerator - CommonWebApiResources.Designer.cs - - - ResXFileCodeGenerator - SRResources.Designer.cs - Designer - - From 012a5da7f6d3f8261f52708e99756723d69d19ed Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 4 Dec 2015 15:22:15 -0800 Subject: [PATCH 07/12] Implementing custom ApiExplorer --- Swashbuckle.OData.Tests/Fixtures/PutTests.cs | 32 ++++ .../Swashbuckle.OData.Tests.csproj | 1 + .../Descriptions/IParameterMapper.cs | 94 +++++++++++ .../{ => Descriptions}/ODataApiExplorer.cs | 151 +++++++++--------- .../SwaggerToApiExplorerMapper.cs | 27 ++++ Swashbuckle.OData/ODataSwaggerProvider.cs | 34 +++- Swashbuckle.OData/ODataSwaggerUtilities.cs | 8 + .../SwaggerApiParameterDescription.cs | 9 ++ Swashbuckle.OData/Swashbuckle.OData.csproj | 6 +- 9 files changed, 275 insertions(+), 87 deletions(-) create mode 100644 Swashbuckle.OData.Tests/Fixtures/PutTests.cs create mode 100644 Swashbuckle.OData/Descriptions/IParameterMapper.cs rename Swashbuckle.OData/{ => Descriptions}/ODataApiExplorer.cs (76%) create mode 100644 Swashbuckle.OData/Descriptions/SwaggerToApiExplorerMapper.cs create mode 100644 Swashbuckle.OData/SwaggerApiParameterDescription.cs diff --git a/Swashbuckle.OData.Tests/Fixtures/PutTests.cs b/Swashbuckle.OData.Tests/Fixtures/PutTests.cs new file mode 100644 index 0000000..db6141f --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/PutTests.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Owin.Hosting; +using NUnit.Framework; +using Swashbuckle.OData.Tests.WebHost; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData.Tests +{ + [TestFixture] + public class PutTests + { + [Test] + public async Task It_includes_a_put_operation() + { + using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(); + + // Act + var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/Customers({Id})", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.put.Should().NotBeNull(); + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index b84a9a7..e1f45a8 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -112,6 +112,7 @@ + diff --git a/Swashbuckle.OData/Descriptions/IParameterMapper.cs b/Swashbuckle.OData/Descriptions/IParameterMapper.cs new file mode 100644 index 0000000..1f70632 --- /dev/null +++ b/Swashbuckle.OData/Descriptions/IParameterMapper.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using System.Web.Http.Controllers; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData +{ + public interface IParameterMapper + { + HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDescriptor actionDescriptor); + } + + public class MapByParameterName : IParameterMapper + { + public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) + { + return actionDescriptor.GetParameters() + .SingleOrDefault(descriptor => string.Equals(descriptor.ParameterName, parameter.name, StringComparison.CurrentCultureIgnoreCase)); + } + } + + public class MapByDescription : IParameterMapper + { + public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) + { + // Maybe the parameter is a key parameter, e.g., where Id in the URI path maps to a parameter named 'key' + if (parameter.description.StartsWith("key:")) + { + return actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == "key"); + } + return null; + } + } + + public class MapByIndex : IParameterMapper + { + public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) + { + if (parameter.@in != "query" && index < actionDescriptor.GetParameters().Count) + { + return actionDescriptor.GetParameters()[index]; + } + return null; + } + } + + public class MapToDefault : IParameterMapper + { + public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) + { + return new ODataParameterDescriptor(parameter.name, GetType(parameter), !parameter.required.Value) + { + Configuration = actionDescriptor.ControllerDescriptor.Configuration, + ActionDescriptor = actionDescriptor + }; + } + + private static Type GetType(Parameter queryParameter) + { + var type = queryParameter.type; + var format = queryParameter.format; + + switch (format) + { + case null: + switch (type) + { + case "string": + return typeof(string); + case "boolean": + return typeof(bool); + default: + throw new Exception(string.Format("Could not determine .NET type for parameter type {0} and format {1}", type, "null")); + } + case "int32": + return typeof(int); + case "int64": + return typeof(long); + case "byte": + return typeof(byte); + case "date": + return typeof(DateTime); + case "date-time": + return typeof(DateTimeOffset); + case "double": + return typeof(double); + case "float": + return typeof(float); + default: + throw new Exception(string.Format("Could not determine .NET type for parameter type {0} and format {1}", type, format)); + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataApiExplorer.cs b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs similarity index 76% rename from Swashbuckle.OData/ODataApiExplorer.cs rename to Swashbuckle.OData/Descriptions/ODataApiExplorer.cs index 35c3721..7139cac 100644 --- a/Swashbuckle.OData/ODataApiExplorer.cs +++ b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs @@ -20,6 +20,7 @@ public class ODataApiExplorer : IApiExplorer private const string ServiceRoot = "http://any/"; private readonly Lazy> _apiDescriptions; private readonly Func _config; + private readonly SwaggerToApiExplorerMapper _swaggerToApiExplorerMapper; /// /// Initializes a new instance of the class. @@ -29,6 +30,20 @@ public ODataApiExplorer(Func httpConfigurationProvider) { _config = httpConfigurationProvider; _apiDescriptions = new Lazy>(GetApiDescriptions); + _swaggerToApiExplorerMapper = GetSwaggerToApiExplorerMapper(); + } + + private static SwaggerToApiExplorerMapper GetSwaggerToApiExplorerMapper() + { + var parameterMappers = new List + { + new MapByParameterName(), + new MapByDescription(), + new MapByIndex(), + new MapToDefault() + }; + + return new SwaggerToApiExplorerMapper(parameterMappers); } /// @@ -72,8 +87,11 @@ private Collection GetApiDescriptions(ODataRoute oDataRoute, str { var apiDescriptions = new Collection(); - apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Delete, oDataRoute, potentialPathTemplate, potentialOperations.delete)); - apiDescriptions.AddIfNotNull(GetApiDescription(HttpMethod.Get, oDataRoute, potentialPathTemplate, potentialOperations.get)); + apiDescriptions.AddIfNotNull(GetApiDescription(new HttpMethod("DELETE"), oDataRoute, potentialPathTemplate, potentialOperations.delete)); + apiDescriptions.AddIfNotNull(GetApiDescription(new HttpMethod("GET"), oDataRoute, potentialPathTemplate, potentialOperations.get)); + apiDescriptions.AddIfNotNull(GetApiDescription(new HttpMethod("POST"), oDataRoute, potentialPathTemplate, potentialOperations.post)); + apiDescriptions.AddIfNotNull(GetApiDescription(new HttpMethod("PUT"), oDataRoute, potentialPathTemplate, potentialOperations.put)); + apiDescriptions.AddIfNotNull(GetApiDescription(new HttpMethod("PATCH"), oDataRoute, potentialPathTemplate, potentialOperations.patch)); return apiDescriptions; } @@ -112,12 +130,12 @@ private ApiDescription GetApiDescription(HttpMethod httpMethod, ODataRoute oData private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, HttpMethod httpMethod, Operation operation, ODataRoute route, string potentialPathTemplate) { - var apiDocumentation = GetApiDocumentation(actionDescriptor); + var apiDocumentation = GetApiDocumentation(actionDescriptor, operation); var parameterDescriptions = CreateParameterDescriptions(operation, actionDescriptor); // request formatters - var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.Source == ApiParameterSource.FromBody); + var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.SwaggerSource == SwaggerApiParameterSource.Body); var supportedRequestBodyFormatters = bodyParameter != null ? actionDescriptor.Configuration.Formatters.Where(f => f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) : Enumerable.Empty(); @@ -178,66 +196,75 @@ private static string GetApiResponseDocumentation(HttpActionDescriptor actionDes : null; } - private List CreateParameterDescriptions(Operation operation, HttpActionDescriptor actionDescriptor) + private List CreateParameterDescriptions(Operation operation, HttpActionDescriptor actionDescriptor) { - return operation.parameters.Select(parameter => GetParameterDescription(parameter, actionDescriptor)).ToList(); + return operation.parameters.Select((parameter, index) => GetParameterDescription(parameter, index, actionDescriptor)).ToList(); } - private ApiParameterDescription GetParameterDescription(Parameter parameter, HttpActionDescriptor actionDescriptor) + private SwaggerApiParameterDescription GetParameterDescription(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) { - var httpParameterDescriptor = GetHttpParameterDescriptor(parameter, actionDescriptor); - if (httpParameterDescriptor != null) - { - return new ApiParameterDescription - { - ParameterDescriptor = httpParameterDescriptor, - Name = httpParameterDescriptor.Prefix ?? httpParameterDescriptor.ParameterName, - Documentation = GetApiParameterDocumentation(httpParameterDescriptor), - Source = parameter.@in == "path" || parameter.@in == "query" ? ApiParameterSource.FromUri : ApiParameterSource.FromBody - }; - } - return new ApiParameterDescription + var httpParameterDescriptor = GetHttpParameterDescriptor(parameter, index, actionDescriptor); + + return new SwaggerApiParameterDescription { - ParameterDescriptor = new ODataParameterDescriptor(parameter.name, GetType(parameter), parameter.required.Value) - { - Configuration = _config(), - ActionDescriptor = actionDescriptor - }, - Name = parameter.name, - Documentation = parameter.description, - Source = parameter.@in == "path" || parameter.@in == "query" ? ApiParameterSource.FromUri : ApiParameterSource.FromBody + ParameterDescriptor = GetHttpParameterDescriptor(parameter, index, actionDescriptor), + Name = httpParameterDescriptor.Prefix ?? httpParameterDescriptor.ParameterName, + Documentation = GetApiParameterDocumentation(parameter, httpParameterDescriptor), + SwaggerSource = MapSource(parameter) }; + + //return new SwaggerApiParameterDescription + //{ + // ParameterDescriptor = new ODataParameterDescriptor(parameter.name, GetType(parameter), !parameter.required.Value) + // { + // Configuration = _config(), + // ActionDescriptor = actionDescriptor + // }, + // Name = parameter.name, + // Documentation = parameter.description, + // SwaggerSource = MapSource(parameter) + //}; } - private static HttpParameterDescriptor GetHttpParameterDescriptor(Parameter parameter, HttpActionDescriptor actionDescriptor) + private static SwaggerApiParameterSource MapSource(Parameter parameter) { - var httpParameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == parameter.name); - // Maybe the parameter is a key parameter, e.g., where Id in the URI path maps to a parameter named 'key' - if (httpParameterDescriptor == null && parameter.description.StartsWith("key:")) + switch (parameter.@in) { - httpParameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == "key"); + case "query": + return SwaggerApiParameterSource.Query; + case "header": + return SwaggerApiParameterSource.Header; + case "path": + return SwaggerApiParameterSource.Path; + case "formData": + return SwaggerApiParameterSource.FormData; + case "body": + return SwaggerApiParameterSource.Body; + default: + throw new ArgumentOutOfRangeException("parameter"); } - return httpParameterDescriptor; } - private static string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor) + private HttpParameterDescriptor GetHttpParameterDescriptor(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) + { + return _swaggerToApiExplorerMapper.Map(parameter, index, actionDescriptor); + } + + private static string GetApiParameterDocumentation(Parameter parameter, HttpParameterDescriptor parameterDescriptor) { var documentationProvider = parameterDescriptor.Configuration.Services.GetDocumentationProvider(); return documentationProvider != null ? documentationProvider.GetDocumentation(parameterDescriptor) - : null; + : parameter.description; } - private static string GetApiDocumentation(HttpActionDescriptor actionDescriptor) + private static string GetApiDocumentation(HttpActionDescriptor actionDescriptor, Operation operation) { var documentationProvider = actionDescriptor.Configuration.Services.GetDocumentationProvider(); - if (documentationProvider != null) - { - return documentationProvider.GetDocumentation(actionDescriptor); - } - - return null; + return documentationProvider != null + ? documentationProvider.GetDocumentation(actionDescriptor) + : operation.description; } private static ODataPath GenerateSampleODataPath(ODataRoute oDataRoute, string pathTemplate, Operation operation) @@ -302,42 +329,6 @@ private static string GenerateSampleQueryParameterValue(Parameter queryParameter } } - private static Type GetType(Parameter queryParameter) - { - var type = queryParameter.type; - var format = queryParameter.format; - - switch (format) - { - case null: - switch (type) - { - case "string": - return typeof(string); - case "boolean": - return typeof(bool); - default: - throw new Exception(string.Format("Could not determine .NET type for parameter type {0} and format {1}", type, "null")); - } - case "int32": - return typeof(int); - case "int64": - return typeof(long); - case "byte": - return typeof(byte); - case "date": - return typeof(DateTime); - case "date-time": - return typeof(DateTimeOffset); - case "double": - return typeof(double); - case "float": - return typeof(float); - default: - throw new Exception(string.Format("Could not determine .NET type for parameter type {0} and format {1}", type, format)); - } - } - private HttpControllerDescriptor GetControllerDesciptor(ODataRoute oDataRoute, ODataPath potentialPath) { var oDataPathRouteConstraint = GetODataPathRouteConstraint(oDataRoute); @@ -364,7 +355,9 @@ private HttpControllerDescriptor GetControllerDesciptor(ODataRoute oDataRoute, O /// private static string GetControllerName(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path) { - return oDataPathRouteConstraint.RoutingConventions.Select(routingConvention => routingConvention.SelectController(path, new HttpRequestMessage())).FirstOrDefault(controllerName => controllerName != null); + return oDataPathRouteConstraint.RoutingConventions + .Select(routingConvention => routingConvention.SelectController(path, new HttpRequestMessage())) + .FirstOrDefault(controllerName => controllerName != null); } private static string GetActionName(ODataPathRouteConstraint oDataPathRouteConstraint, ODataPath path, HttpControllerContext controllerContext, ILookup actionMap) diff --git a/Swashbuckle.OData/Descriptions/SwaggerToApiExplorerMapper.cs b/Swashbuckle.OData/Descriptions/SwaggerToApiExplorerMapper.cs new file mode 100644 index 0000000..443e754 --- /dev/null +++ b/Swashbuckle.OData/Descriptions/SwaggerToApiExplorerMapper.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Web.Http.Controllers; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData +{ + public class SwaggerToApiExplorerMapper + { + private readonly IEnumerable _parameterMappers; + + public SwaggerToApiExplorerMapper(IEnumerable parameterMappers) + { + Contract.Requires(parameterMappers != null); + + _parameterMappers = parameterMappers; + } + + public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) + { + return _parameterMappers + .Select(mapper => mapper.Map(parameter, index, actionDescriptor)) + .FirstOrDefault(httpParameterDescriptor => httpParameterDescriptor != null); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index 663361c..ecfe15a 100644 --- a/Swashbuckle.OData/ODataSwaggerProvider.cs +++ b/Swashbuckle.OData/ODataSwaggerProvider.cs @@ -132,6 +132,8 @@ public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) //return edmSwaggerDocument; + var routePrefix = oDataRoute.RoutePrefix; + var rootUri = new Uri(rootUrl); var port = !rootUri.IsDefaultPort ? ":" + rootUri.Port : string.Empty; @@ -139,7 +141,7 @@ public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) { info = info, host = rootUri.Host + port, - basePath = rootUri.AbsolutePath != "/" ? rootUri.AbsolutePath : null, + basePath = rootUri.AbsolutePath != "/" ? rootUri.AbsolutePath : "/" + routePrefix, schemes = _options.Schemes != null ? _options.Schemes.ToList() : new[] { rootUri.Scheme }.ToList(), paths = paths, definitions = schemaRegistry.Definitions, @@ -208,7 +210,7 @@ private Operation CreateOperation(ApiDescription apiDescription, SchemaRegistry .Select(paramDesc => { var inPath = apiDescription.RelativePathSansQueryString().Contains("{" + paramDesc.Name + "}"); - return CreateParameter(paramDesc, inPath, schemaRegistry); + return CreateParameter(paramDesc as SwaggerApiParameterDescription, inPath, schemaRegistry); }) .ToList(); @@ -238,15 +240,16 @@ private Operation CreateOperation(ApiDescription apiDescription, SchemaRegistry return operation; } - private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry) + private static Parameter CreateParameter(SwaggerApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry) { var @in = inPath ? "path" - : paramDesc.Source == ApiParameterSource.FromUri ? "query" : "body"; + : MapToSwaggerParameterLocation(paramDesc.SwaggerSource); var parameter = new Parameter { name = paramDesc.Name, + description = paramDesc.Documentation, @in = @in }; @@ -269,11 +272,28 @@ private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool return parameter; } + private static string MapToSwaggerParameterLocation(SwaggerApiParameterSource swaggerSource) + { + switch (swaggerSource) + { + case SwaggerApiParameterSource.Query: + return "query"; + case SwaggerApiParameterSource.Header: + return "header"; + case SwaggerApiParameterSource.Path: + return "path"; + case SwaggerApiParameterSource.FormData: + return "formData"; + case SwaggerApiParameterSource.Body: + return "body"; + default: + throw new ArgumentOutOfRangeException(nameof(swaggerSource), swaggerSource, null); + } + } + private IEnumerable GetApiDescriptionsFor(string apiVersion) { - return _options.VersionSupportResolver == null - ? _apiExplorer.ApiDescriptions - : _apiExplorer.ApiDescriptions.Where(apiDesc => _options.VersionSupportResolver(apiDesc, apiVersion)); + return _options.VersionSupportResolver == null ? _apiExplorer.ApiDescriptions : _apiExplorer.ApiDescriptions.Where(apiDesc => _options.VersionSupportResolver(apiDesc, apiVersion)); } } } \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerUtilities.cs b/Swashbuckle.OData/ODataSwaggerUtilities.cs index 667e611..faebf08 100644 --- a/Swashbuckle.OData/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/ODataSwaggerUtilities.cs @@ -96,6 +96,14 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio .Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType().Name, "body", "The entity to patch", entitySet.EntityType())) .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), + put = new Operation() + .Summary("Replace entity in EntitySet " + entitySet.Name) + .OperationId(entitySet.Name + "_PutById") + .Description("Replace entity in EntitySet " + entitySet.Name) + .Tags(entitySet.Name) + .Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType().Name, "body", "The entity to put", entitySet.EntityType())) + .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), + delete = new Operation().Summary("Delete entity in EntitySet " + entitySet.Name) .OperationId(entitySet.Name + "_DeleteById") .Description("Delete entity in EntitySet " + entitySet.Name) diff --git a/Swashbuckle.OData/SwaggerApiParameterDescription.cs b/Swashbuckle.OData/SwaggerApiParameterDescription.cs new file mode 100644 index 0000000..ec27704 --- /dev/null +++ b/Swashbuckle.OData/SwaggerApiParameterDescription.cs @@ -0,0 +1,9 @@ +using System.Web.Http.Description; + +namespace Swashbuckle.OData +{ + public class SwaggerApiParameterDescription : ApiParameterDescription + { + public SwaggerApiParameterSource SwaggerSource { get; set; } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 51b763a..2068762 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -171,18 +171,22 @@ + - + + + + From b77f50f1f79db2ec9c30be2e1c50b7c80fd2326e Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 4 Dec 2015 17:02:35 -0800 Subject: [PATCH 08/12] Validate posts are working --- .../SwashbuckleODataContext.cs | 33 +++++++++--- Swashbuckle.OData.Tests/Fixtures/PostTests.cs | 53 +++++++++++++++++++ .../Swashbuckle.OData.Tests.csproj | 1 + .../Descriptions/IParameterMapper.cs | 24 ++++++++- .../Descriptions/ODataApiExplorer.cs | 21 +++----- Swashbuckle.OData/ODataSwaggerUtilities.cs | 10 ++-- 6 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 Swashbuckle.OData.Tests/Fixtures/PostTests.cs diff --git a/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs b/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs index 95bd4b8..1c97840 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs @@ -4,12 +4,11 @@ namespace SwashbuckleODataSample.Models { public class SwashbuckleODataContext : DbContext { - // You can add custom code to this file. Changes will not be overwritten. - // - // If you want Entity Framework to drop and regenerate your database - // automatically whenever you change your model schema, please use data migrations. - // For more information refer to the documentation: - // http://msdn.microsoft.com/en-us/data/jj591621.aspx + static SwashbuckleODataContext() + { + Database.SetInitializer(new SwashbuckleODataInitializer()); + } + public SwashbuckleODataContext() : base("name=SwashbuckleODataContext") { @@ -23,4 +22,26 @@ public SwashbuckleODataContext() : base("name=SwashbuckleODataContext") public DbSet Projects { get; set; } } + + public class SwashbuckleODataInitializer : DropCreateDatabaseAlways + { + protected override void Seed(SwashbuckleODataContext context) + { + var clientOne = new Client { Name = "ClientOne" }; + context.Clients.Add(clientOne); + context.Clients.Add(new Client { Name = "ClientTwo" }); + + context.Projects.Add(new Project { ProjectName = "ProjectOne", Client = clientOne}); + context.Projects.Add(new Project { ProjectName = "ProjectTwo", Client = clientOne}); + + var customerOne = new Customer { Name = "CustomerOne" }; + context.Customers.Add(customerOne); + context.Customers.Add(new Customer { Name = "CustomerTwo" }); + + context.Orders.Add(new Order { OrderName = "OrderOne", Customer = customerOne }); + context.Orders.Add(new Order { OrderName = "OrderTwo", Customer = customerOne }); + + base.Seed(context); + } + } } \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs new file mode 100644 index 0000000..efd3d88 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Owin.Hosting; +using NUnit.Framework; +using Swashbuckle.OData.Tests.WebHost; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData.Tests +{ + [TestFixture] + public class PostTests + { + [Test] + public async Task It_has_a_body_content_type_of_application_json() + { + using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(); + + // Act + var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/Customers", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.post.Should().NotBeNull(); + pathItem.post.consumes.Should().Contain("application/json"); + } + } + + [Test] + public async Task It_has_a_parameter_with_a_name_equal_to_the_path_name() + { + using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(); + + // Act + var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/Customers({Id})", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.get.Should().NotBeNull(); + pathItem.get.parameters.Should().Contain(parameter => parameter.name == "Id"); + } + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index e1f45a8..c258045 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -112,6 +112,7 @@ + diff --git a/Swashbuckle.OData/Descriptions/IParameterMapper.cs b/Swashbuckle.OData/Descriptions/IParameterMapper.cs index 1f70632..cf750fb 100644 --- a/Swashbuckle.OData/Descriptions/IParameterMapper.cs +++ b/Swashbuckle.OData/Descriptions/IParameterMapper.cs @@ -26,7 +26,17 @@ public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDes // Maybe the parameter is a key parameter, e.g., where Id in the URI path maps to a parameter named 'key' if (parameter.description.StartsWith("key:")) { - return actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == "key"); + var parameterDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterName == "key"); + if (parameterDescriptor != null) + { + // Need to assign the correct name expected by OData + return new ODataParameterDescriptor(parameter.name, parameterDescriptor.ParameterType, parameterDescriptor.IsOptional) + { + Configuration = actionDescriptor.ControllerDescriptor.Configuration, + ActionDescriptor = actionDescriptor, + ParameterBinderAttribute = parameterDescriptor.ParameterBinderAttribute + }; + } } return null; } @@ -38,7 +48,17 @@ public HttpParameterDescriptor Map(Parameter parameter, int index, HttpActionDes { if (parameter.@in != "query" && index < actionDescriptor.GetParameters().Count) { - return actionDescriptor.GetParameters()[index]; + var parameterDescriptor = actionDescriptor.GetParameters()[index]; + if (parameterDescriptor != null) + { + // Need to assign the correct name expected by OData + return new ODataParameterDescriptor(parameter.name, parameterDescriptor.ParameterType, parameterDescriptor.IsOptional) + { + Configuration = actionDescriptor.ControllerDescriptor.Configuration, + ActionDescriptor = actionDescriptor, + ParameterBinderAttribute = parameterDescriptor.ParameterBinderAttribute + }; + } } return null; } diff --git a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs index 7139cac..f0160e1 100644 --- a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs +++ b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs @@ -9,6 +9,7 @@ using System.Web.Http.Description; using System.Web.Http.Routing; using System.Web.Http.Services; +using System.Web.OData.Formatter; using System.Web.OData.Routing; using Microsoft.OData.Edm; using Swashbuckle.Swagger; @@ -137,14 +138,14 @@ private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, // request formatters var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.SwaggerSource == SwaggerApiParameterSource.Body); var supportedRequestBodyFormatters = bodyParameter != null - ? actionDescriptor.Configuration.Formatters.Where(f => f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) + ? actionDescriptor.Configuration.Formatters.Where(f => f is ODataMediaTypeFormatter) : Enumerable.Empty(); // response formatters var responseDescription = CreateResponseDescription(actionDescriptor); var returnType = responseDescription.ResponseType ?? responseDescription.DeclaredType; var supportedResponseFormatters = returnType != null && returnType != typeof (void) - ? actionDescriptor.Configuration.Formatters.Where(f => f.CanWriteType(returnType)) + ? actionDescriptor.Configuration.Formatters.Where(f => f is ODataMediaTypeFormatter && f.CanWriteType(returnType)) : Enumerable.Empty(); // Replacing the formatter tracers with formatters if tracers are present. @@ -212,18 +213,6 @@ private SwaggerApiParameterDescription GetParameterDescription(Parameter paramet Documentation = GetApiParameterDocumentation(parameter, httpParameterDescriptor), SwaggerSource = MapSource(parameter) }; - - //return new SwaggerApiParameterDescription - //{ - // ParameterDescriptor = new ODataParameterDescriptor(parameter.name, GetType(parameter), !parameter.required.Value) - // { - // Configuration = _config(), - // ActionDescriptor = actionDescriptor - // }, - // Name = parameter.name, - // Documentation = parameter.description, - // SwaggerSource = MapSource(parameter) - //}; } private static SwaggerApiParameterSource MapSource(Parameter parameter) @@ -241,7 +230,7 @@ private static SwaggerApiParameterSource MapSource(Parameter parameter) case "body": return SwaggerApiParameterSource.Body; default: - throw new ArgumentOutOfRangeException("parameter"); + throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null); } } @@ -419,5 +408,7 @@ public ODataParameterDescriptor(string parameterName, Type parameterType, bool i public override Type ParameterType { get; } public override bool IsOptional { get; } + + } } \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerUtilities.cs b/Swashbuckle.OData/ODataSwaggerUtilities.cs index faebf08..80517d9 100644 --- a/Swashbuckle.OData/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/ODataSwaggerUtilities.cs @@ -38,13 +38,13 @@ public static PathItem CreateSwaggerPathForEntitySet(IEdmNavigationSource naviga .OperationId(entitySet.Name + "_Get") .Description("Returns the EntitySet " + entitySet.Name) .Tags(entitySet.Name) - .Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string", required: false) - .Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string", required: false) - .Parameter("$select", "query", "Selects which properties to include in the response.", "string", required: false) - .Parameter("$orderby", "query", "Sorts the results.", "string", required: false) + .Parameters(new List().Parameter("$expand", "query", "Expands related entities inline.", "string", false) + .Parameter("$filter", "query", "Filters the results, based on a Boolean condition.", "string", false) + .Parameter("$select", "query", "Selects which properties to include in the response.", "string", false) + .Parameter("$orderby", "query", "Sorts the results.", "string", false) .Parameter("$top", "query", "Returns only the first n results.", "integer", false, "int32") .Parameter("$skip", "query", "Skips the first n results.", "integer", false, "int32") - .Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean", required: false)) + .Parameter("$count", "query", "Includes a count of the matching results in the reponse.", "boolean", false)) .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.EntityType()).DefaultErrorResponse()), post = new Operation() .Summary("Post a new entity to EntitySet " + entitySet.Name) From b6a7e517db943159115c1c996fa5e870dade0a16 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 4 Dec 2015 18:17:14 -0800 Subject: [PATCH 09/12] Fix possible set of parameter content types. --- .../ODataControllers/CustomersController.cs | 3 ++ .../ODataControllers/OrdersController.cs | 37 ++----------------- ...GetQueryParametersTests.cs => GetTests.cs} | 22 ++++++++++- Swashbuckle.OData.Tests/Fixtures/PostTests.cs | 8 ++-- Swashbuckle.OData.Tests/Fixtures/PutTests.cs | 19 ++++++++++ .../Swashbuckle.OData.Tests.csproj | 2 +- .../Descriptions/ODataApiExplorer.cs | 2 +- Swashbuckle.OData/ODataSwaggerProvider.cs | 26 +------------ Swashbuckle.OData/ODataSwaggerUtilities.cs | 3 +- 9 files changed, 55 insertions(+), 67 deletions(-) rename Swashbuckle.OData.Tests/Fixtures/{GetQueryParametersTests.cs => GetTests.cs} (70%) diff --git a/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs index 9cb9c95..6152037 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using System.Web.Http; +using System.Web.Http.Description; using System.Web.OData; using SwashbuckleODataSample.Models; @@ -20,6 +21,7 @@ public IQueryable GetCustomers() } // GET: odata/Customers(5) + [ResponseType(typeof(Customer))] [EnableQuery] public SingleResult GetCustomer([FromODataUri] int key) { @@ -61,6 +63,7 @@ public async Task Put([FromODataUri] int key, Delta } // POST: odata/Customers + [ResponseType(typeof(Customer))] public async Task Post(Customer customer) { if (!ModelState.IsValid) diff --git a/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs index 33fe67b..8dda64a 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using System.Web.Http; +using System.Web.Http.Description; using System.Web.OData; using SwashbuckleODataSample.Models; @@ -20,47 +21,15 @@ public IQueryable GetOrders() } // GET: odata/Orders(5) + [ResponseType(typeof(Order))] [EnableQuery] public SingleResult GetOrder([FromODataUri] int key) { return SingleResult.Create(_db.Orders.Where(order => order.OrderId == key)); } - // PUT: odata/Orders(5) - public async Task Put([FromODataUri] int key, Delta patch) - { - Validate(patch.GetEntity()); - - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var order = await _db.Orders.FindAsync(key); - if (order == null) - { - return NotFound(); - } - - patch.Put(order); - - try - { - await _db.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) - { - if (!OrderExists(key)) - { - return NotFound(); - } - throw; - } - - return Updated(order); - } - // POST: odata/Orders + [ResponseType(typeof(Order))] public async Task Post(Order order) { if (!ModelState.IsValid) diff --git a/Swashbuckle.OData.Tests/Fixtures/GetQueryParametersTests.cs b/Swashbuckle.OData.Tests/Fixtures/GetTests.cs similarity index 70% rename from Swashbuckle.OData.Tests/Fixtures/GetQueryParametersTests.cs rename to Swashbuckle.OData.Tests/Fixtures/GetTests.cs index 2f04304..c3a3c47 100644 --- a/Swashbuckle.OData.Tests/Fixtures/GetQueryParametersTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/GetTests.cs @@ -9,7 +9,7 @@ namespace Swashbuckle.OData.Tests { [TestFixture] - public class GetQueryParametersTests + public class GetTests { [Test] public async Task It_includes_the_filter_parameter() @@ -51,5 +51,25 @@ public async Task It_has_all_optional_odata_query_parameters() pathItem.get.parameters.Where(parameter => parameter.name.StartsWith("$")).Should().OnlyContain(parameter => parameter.required == false); } } + + [Test] + public async Task It_has_a_parameter_with_a_name_equal_to_the_path_name() + { + using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(); + + // Act + var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/Customers({Id})", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.get.Should().NotBeNull(); + pathItem.get.parameters.Should().Contain(parameter => parameter.name == "Id"); + } + } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs index efd3d88..f0f137f 100644 --- a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs @@ -31,7 +31,7 @@ public async Task It_has_a_body_content_type_of_application_json() } [Test] - public async Task It_has_a_parameter_with_a_name_equal_to_the_path_name() + public async Task It_has_a_summary() { using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) { @@ -43,10 +43,10 @@ public async Task It_has_a_parameter_with_a_name_equal_to_the_path_name() // Assert PathItem pathItem; - swaggerDocument.paths.TryGetValue("/Customers({Id})", out pathItem); + swaggerDocument.paths.TryGetValue("/Customers", out pathItem); pathItem.Should().NotBeNull(); - pathItem.get.Should().NotBeNull(); - pathItem.get.parameters.Should().Contain(parameter => parameter.name == "Id"); + pathItem.post.Should().NotBeNull(); + pathItem.post.summary.Should().NotBeNullOrWhiteSpace(); } } } diff --git a/Swashbuckle.OData.Tests/Fixtures/PutTests.cs b/Swashbuckle.OData.Tests/Fixtures/PutTests.cs index db6141f..15b7cd9 100644 --- a/Swashbuckle.OData.Tests/Fixtures/PutTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/PutTests.cs @@ -28,5 +28,24 @@ public async Task It_includes_a_put_operation() pathItem.put.Should().NotBeNull(); } } + + [Test] + public async Task It_does_not_exist_if_not_in_the_controller() + { + using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(); + + // Act + var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/Orders({OrderId})", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.put.Should().BeNull(); + } + } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index c258045..3137e90 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -116,7 +116,7 @@ - + diff --git a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs index f0160e1..dea8d5c 100644 --- a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs +++ b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs @@ -138,7 +138,7 @@ private ApiDescription GetApiDescription(HttpActionDescriptor actionDescriptor, // request formatters var bodyParameter = parameterDescriptions.FirstOrDefault(description => description.SwaggerSource == SwaggerApiParameterSource.Body); var supportedRequestBodyFormatters = bodyParameter != null - ? actionDescriptor.Configuration.Formatters.Where(f => f is ODataMediaTypeFormatter) + ? actionDescriptor.Configuration.Formatters.Where(f => f is ODataMediaTypeFormatter && f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) : Enumerable.Empty(); // response formatters diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index ecfe15a..27603e8 100644 --- a/Swashbuckle.OData/ODataSwaggerProvider.cs +++ b/Swashbuckle.OData/ODataSwaggerProvider.cs @@ -107,31 +107,6 @@ public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) if (oDataRoute != null) { - //var oDataPathRouteConstraint = oDataRoute.Constraints.Values.SingleOrDefault(value => value is ODataPathRouteConstraint) as ODataPathRouteConstraint; - - //var edmModel = oDataPathRouteConstraint.EdmModel; - //var routePrefix = oDataRoute.RoutePrefix; - - //var oDataSwaggerConverter = new ODataSwaggerConverter(edmModel); - - //var rootUri = new Uri(rootUrl); - - //var basePath = rootUri.AbsolutePath != "/" ? rootUri.AbsolutePath : "/" + routePrefix; - - //oDataSwaggerConverter.MetadataUri = new Uri(rootUrl.AppendPathSegments(basePath, "$metadata")); - - //var port = !rootUri.IsDefaultPort ? ":" + rootUri.Port : string.Empty; - - //var edmSwaggerDocument = oDataSwaggerConverter.ConvertToSwaggerModel(); - //edmSwaggerDocument.host = rootUri.Host + port; - //edmSwaggerDocument.basePath = basePath; - //edmSwaggerDocument.schemes = new[] - //{ - // rootUri.Scheme - //}.ToList(); - - //return edmSwaggerDocument; - var routePrefix = oDataRoute.RoutePrefix; var rootUri = new Uri(rootUrl); @@ -223,6 +198,7 @@ private Operation CreateOperation(ApiDescription apiDescription, SchemaRegistry var operation = new Operation { + summary = apiDescription.Documentation, tags = new[] { _options.GroupingKeySelector(apiDescription) }, operationId = apiDescription.FriendlyId(), produces = apiDescription.Produces().ToList(), diff --git a/Swashbuckle.OData/ODataSwaggerUtilities.cs b/Swashbuckle.OData/ODataSwaggerUtilities.cs index 80517d9..434b23f 100644 --- a/Swashbuckle.OData/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/ODataSwaggerUtilities.cs @@ -94,7 +94,8 @@ public static PathItem CreateSwaggerPathForEntity(IEdmNavigationSource navigatio .Description("Update entity in EntitySet " + entitySet.Name) .Tags(entitySet.Name) .Parameters(keyParameters.DeepClone().Parameter(entitySet.EntityType().Name, "body", "The entity to patch", entitySet.EntityType())) - .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), + .Responses(new Dictionary() + .Response("204", "Empty response").DefaultErrorResponse()), put = new Operation() .Summary("Replace entity in EntitySet " + entitySet.Name) From 330478cf2e86b479a10a4be4fdc07244b57d26d1 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 4 Dec 2015 18:42:49 -0800 Subject: [PATCH 10/12] Remove unneeded unit test --- Swashbuckle.OData.Tests/Fixtures/PostTests.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs index f0f137f..f6dc179 100644 --- a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs @@ -10,26 +10,6 @@ namespace Swashbuckle.OData.Tests [TestFixture] public class PostTests { - [Test] - public async Task It_has_a_body_content_type_of_application_json() - { - using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder))) - { - // Arrange - var httpClient = HttpClientUtils.GetHttpClient(); - - // Act - var swaggerDocument = await httpClient.GetAsync("swagger/docs/v1"); - - // Assert - PathItem pathItem; - swaggerDocument.paths.TryGetValue("/Customers", out pathItem); - pathItem.Should().NotBeNull(); - pathItem.post.Should().NotBeNull(); - pathItem.post.consumes.Should().Contain("application/json"); - } - } - [Test] public async Task It_has_a_summary() { From 801f5c2b3c7be409ada4cc0da9e0cd2aca7f7392 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Sun, 6 Dec 2015 22:29:17 -0800 Subject: [PATCH 11/12] Fix response and body models. --- .../App_Start/FormatterConfig.cs | 31 ++ Swashbuckle.OData.Sample/Global.asax.cs | 1 + .../ODataControllers/CustomersController.cs | 5 +- .../ODataControllers/OrdersController.cs | 3 +- .../Swashbuckle.OData.Sample.csproj | 1 + Swashbuckle.OData.Tests/ContentType.cs | 328 ++++++++++++++++++ Swashbuckle.OData.Tests/Fixtures/GetTests.cs | 8 +- .../Fixtures/HttpConfigurationRoutesTests.cs | 2 +- .../Fixtures/PatchTests.cs | 33 ++ Swashbuckle.OData.Tests/Fixtures/PostTests.cs | 2 +- Swashbuckle.OData.Tests/Fixtures/PutTests.cs | 7 +- Swashbuckle.OData.Tests/HttpClientUtils.cs | 8 +- Swashbuckle.OData.Tests/HttpExtensions.cs | 17 +- .../Swashbuckle.OData.Tests.csproj | 2 + .../WebHost/TestWebApiStartup.cs | 3 +- .../Descriptions/ODataApiExplorer.cs | 6 +- Swashbuckle.OData/ODataSwaggerProvider.cs | 29 +- Swashbuckle.OData/ODataSwaggerUtilities.cs | 11 +- Swashbuckle.OData/SchemaRegistryExtensions.cs | 26 ++ Swashbuckle.OData/Swashbuckle.OData.csproj | 1 + 20 files changed, 492 insertions(+), 32 deletions(-) create mode 100644 Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs create mode 100644 Swashbuckle.OData.Tests/ContentType.cs create mode 100644 Swashbuckle.OData.Tests/Fixtures/PatchTests.cs create mode 100644 Swashbuckle.OData/SchemaRegistryExtensions.cs diff --git a/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs b/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs new file mode 100644 index 0000000..8236a01 --- /dev/null +++ b/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs @@ -0,0 +1,31 @@ +using System.Web.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace SwashbuckleODataSample +{ + /// + /// + public static class FormatterConfig + { + /// + /// Registers the specified configuration. + /// + /// The configuration. + public static void Register(HttpConfiguration config) + { + var formatters = config.Formatters; + + formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore + }; + + formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter()); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/Global.asax.cs b/Swashbuckle.OData.Sample/Global.asax.cs index ee5ee60..f14d434 100644 --- a/Swashbuckle.OData.Sample/Global.asax.cs +++ b/Swashbuckle.OData.Sample/Global.asax.cs @@ -8,6 +8,7 @@ public class WebApiApplication : HttpApplication protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); + GlobalConfiguration.Configure(FormatterConfig.Register); } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs index 6152037..d7f55db 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs @@ -5,6 +5,7 @@ using System.Web.Http; using System.Web.Http.Description; using System.Web.OData; +using System.Web.OData.Results; using SwashbuckleODataSample.Models; namespace SwashbuckleODataSample.Controllers @@ -21,7 +22,6 @@ public IQueryable GetCustomers() } // GET: odata/Customers(5) - [ResponseType(typeof(Customer))] [EnableQuery] public SingleResult GetCustomer([FromODataUri] int key) { @@ -29,6 +29,7 @@ public SingleResult GetCustomer([FromODataUri] int key) } // PUT: odata/Customers(5) + [ResponseType(typeof(void))] public async Task Put([FromODataUri] int key, Delta patch) { Validate(patch.GetEntity()); @@ -78,6 +79,7 @@ public async Task Post(Customer customer) } // PATCH: odata/Customers(5) + [ResponseType(typeof(void))] [AcceptVerbs("PATCH", "MERGE")] public async Task Patch([FromODataUri] int key, Delta patch) { @@ -113,6 +115,7 @@ public async Task Patch([FromODataUri] int key, Delta Delete([FromODataUri] int key) { var customer = await _db.Customers.FindAsync(key); diff --git a/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs index 8dda64a..ed27cb2 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs @@ -21,7 +21,6 @@ public IQueryable GetOrders() } // GET: odata/Orders(5) - [ResponseType(typeof(Order))] [EnableQuery] public SingleResult GetOrder([FromODataUri] int key) { @@ -44,6 +43,7 @@ public async Task Post(Order order) } // PATCH: odata/Orders(5) + [ResponseType(typeof(void))] [AcceptVerbs("PATCH", "MERGE")] public async Task Patch([FromODataUri] int key, Delta patch) { @@ -79,6 +79,7 @@ public async Task Patch([FromODataUri] int key, Delta } // DELETE: odata/Orders(5) + [ResponseType(typeof(void))] public async Task Delete([FromODataUri] int key) { var order = await _db.Orders.FindAsync(key); diff --git a/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj b/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj index 4d02b0a..2994e4d 100644 --- a/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj +++ b/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj @@ -123,6 +123,7 @@ + diff --git a/Swashbuckle.OData.Tests/ContentType.cs b/Swashbuckle.OData.Tests/ContentType.cs new file mode 100644 index 0000000..5df220f --- /dev/null +++ b/Swashbuckle.OData.Tests/ContentType.cs @@ -0,0 +1,328 @@ +using System; + +namespace Swashbuckle.OData.Tests +{ + public static class ContentType + { + /// + /// Used to denote the encoding necessary for files containing JavaScript source code. The alternative MIME type for this file type is text/javascript. + /// + public const string ApplicationXJavascript = "application/x-javascript"; + + ///24bit Linear PCM audio at 8-48kHz, 1-N channels; Defined in RFC 3190 + public const string AudioL24 = "audio/L24"; + + ///Adobe Flash files for example with the extension .swf + public const string ApplicationXShockwaveFlash = "application/x-shockwave-flash"; + + ///Arbitrary binary data.[5] Generally speaking this type identifies files that are not associated with a specific application. Contrary to past assumptions by software packages such as Apache this is not a type that should be applied to unknown files. In such a case, a server or application should not indicate a content type, as it may be incorrect, but rather, should omit the type in order to allow the recipient to guess the type.[6] + public const string ApplicationOctetStream = "application/octet-stream"; + + ///Atom feeds + public const string ApplicationAtomXml = "application/atom+xml"; + + ///Cascading Style Sheets; Defined in RFC 2318 + public const string TextCss = "text/css"; + + ///commands; subtype resident in Gecko browsers like Firefox 3.5 + public const string TextCmd = "text/cmd"; + + ///Comma-separated values; Defined in RFC 4180 + public const string TextCsv = "text/csv"; + + ///deb (file format), a software package format used by the Debian project + public const string ApplicationXDeb = "application/x-deb"; + + ///Defined in RFC 1847 + public const string MultipartEncrypted = "multipart/encrypted"; + + ///Defined in RFC 1847 + public const string MultipartSigned = "multipart/signed"; + + ///Defined in RFC 2616 + public const string MessageHttp = "message/http"; + + ///Defined in RFC 4735 + public const string ModelExample = "model/example"; + + ///device-independent document in DVI format + public const string ApplicationXDvi = "application/x-dvi"; + + ///DTD files; Defined by RFC 3023 + public const string ApplicationXmlDtd = "application/xml-dtd"; + + ///ECMAScript/JavaScript; Defined in RFC 4329 (equivalent to application/ecmascript but with looser processing rules) It is not accepted in IE 8 or earlier - text/javascript is accepted but it is defined as obsolete in RFC 4329. The "type" attribute of the