From 8366dc8e5aa3dfc0b57093f3807d0d3450e16b45 Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 29 Jan 2016 19:42:28 -0800 Subject: [PATCH] Displays query string parameters not described by the EDM model or in the ODataRoute. Fixes #62. --- .../Fixtures/QueryStringParameterTests.cs | 181 ++++++++++++++++++ .../Swashbuckle.OData.Tests.csproj | 1 + ...ApiParameterDescriptionEqualityComparer.cs | 41 ++++ .../Descriptions/MapByDescription.cs | 2 +- Swashbuckle.OData/Descriptions/MapByIndex.cs | 2 +- .../Descriptions/MapToDefault.cs | 2 +- .../Descriptions/MapToODataActionParameter.cs | 2 +- .../ODataActionDescriptorEqualityComparer.cs | 35 ++++ .../ODataActionDescriptorMapper.cs | 82 -------- .../ODataActionDescriptorMapperBase.cs | 83 ++++++++ .../Descriptions/ODataApiExplorer.cs | 1 + .../Descriptions/ODataParameterDescriptor.cs | 7 +- .../Descriptions/SwaggerOperationMapper.cs | 9 +- Swashbuckle.OData/Swashbuckle.OData.csproj | 2 + 14 files changed, 360 insertions(+), 90 deletions(-) create mode 100644 Swashbuckle.OData.Tests/Fixtures/QueryStringParameterTests.cs create mode 100644 Swashbuckle.OData/Descriptions/ApiParameterDescriptionEqualityComparer.cs create mode 100644 Swashbuckle.OData/Descriptions/ODataActionDescriptorEqualityComparer.cs diff --git a/Swashbuckle.OData.Tests/Fixtures/QueryStringParameterTests.cs b/Swashbuckle.OData.Tests/Fixtures/QueryStringParameterTests.cs new file mode 100644 index 0000000..30b3f83 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/QueryStringParameterTests.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using System.Web.OData; +using System.Web.OData.Builder; +using System.Web.OData.Extensions; +using System.Web.OData.Routing; +using FluentAssertions; +using Microsoft.OData.Edm; +using Microsoft.Owin.Hosting; +using NUnit.Framework; +using Owin; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData.Tests +{ + [TestFixture] + public class QueryStringParameterTests + { + [Test] + public async Task It_displays_parameters_not_described_in_the_edm_model() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => FoobarsSetup.Configuration(appBuilder, typeof(FoobarsSetup.FoobarsController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var results = await httpClient.GetJsonAsync>>("odata/Foobars?bar=true"); + results.Should().NotBeNull(); + results.Value.Count.Should().Be(2); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/Foobars", out pathItem); + pathItem.Should().NotBeNull(); + var barParameter = pathItem.get.parameters.SingleOrDefault(parameter => parameter.name == "bar"); + barParameter.Should().NotBeNull(); + barParameter.required.Should().BeFalse(); + barParameter.type.ShouldBeEquivalentTo("boolean"); + barParameter.@in.ShouldBeEquivalentTo("query"); + var filterParameter = pathItem.get.parameters.SingleOrDefault(parameter => parameter.name == "$filter"); + filterParameter.Should().NotBeNull(); + filterParameter.description.Should().NotBeNullOrWhiteSpace(); + filterParameter.type.ShouldBeEquivalentTo("string"); + filterParameter.@in.ShouldBeEquivalentTo("query"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_displays_parameters_not_defined_in_the_odata_route() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => WombatsSetup.Configuration(appBuilder, typeof(WombatsSetup.WombatsController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var results = await httpClient.GetJsonAsync>>("odata/Wombats?bat=true"); + results.Should().NotBeNull(); + results.Value.Count.Should().Be(2); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/Wombats", out pathItem); + pathItem.Should().NotBeNull(); + var barParameter = pathItem.get.parameters.SingleOrDefault(parameter => parameter.name == "bat"); + barParameter.Should().NotBeNull(); + barParameter.required.Should().BeFalse(); + barParameter.type.ShouldBeEquivalentTo("boolean"); + barParameter.@in.ShouldBeEquivalentTo("query"); + var filterParameter = pathItem.get.parameters.SingleOrDefault(parameter => parameter.name == "$filter"); + filterParameter.Should().NotBeNull(); + filterParameter.description.Should().NotBeNullOrWhiteSpace(); + filterParameter.type.ShouldBeEquivalentTo("string"); + filterParameter.@in.ShouldBeEquivalentTo("query"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + } + + public class FoobarsSetup + { + public static void Configuration(IAppBuilder appBuilder, Type targetController) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + config.MapODataServiceRoute("odata", "odata", GetEdmModel()); + + config.EnsureInitialized(); + } + + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Foobars"); + + return builder.GetEdmModel(); + } + + public class Foobar + { + [Key] + public long Id { get; set; } + public string Variation { get; set; } + } + + public class FoobarsController : ODataController + { + [EnableQuery] + public IQueryable GetFoobars([FromODataUri] bool? bar = null) + { + IEnumerable foobars = new[] + { + new Foobar { Id=1, Variation = "a"}, + new Foobar { Id=2, Variation = "b"}, + new Foobar { Id=3, Variation = "c"}, + new Foobar { Id=4, Variation = "d"} + }; + if (bar != null && bar.Value) foobars = foobars.Where(fb => fb.Id >= 3); + return foobars.AsQueryable(); + } + } + } + + public class WombatsSetup + { + public static void Configuration(IAppBuilder appBuilder, Type targetController) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + config.MapODataServiceRoute("odata", "odata", GetEdmModel()); + + config.EnsureInitialized(); + } + + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Wombats"); + + return builder.GetEdmModel(); + } + + public class Wombat + { + [Key] + public long Id { get; set; } + public string Variation { get; set; } + } + + public class WombatsController : ODataController + { + [EnableQuery] + [ODataRoute("Wombats")] + public IQueryable GetWombats([FromODataUri] bool? bat = null) + { + IEnumerable wombats = new[] + { + new Wombat { Id=1, Variation = "a"}, + new Wombat { Id=2, Variation = "b"}, + new Wombat { Id=3, Variation = "c"}, + new Wombat { Id=4, Variation = "d"} + }; + if (bat != null && bat.Value) wombats = wombats.Where(wb => wb.Id >= 3); + return wombats.AsQueryable(); + } + } + } +} \ 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 193c51b..276bc19 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -144,6 +144,7 @@ + diff --git a/Swashbuckle.OData/Descriptions/ApiParameterDescriptionEqualityComparer.cs b/Swashbuckle.OData/Descriptions/ApiParameterDescriptionEqualityComparer.cs new file mode 100644 index 0000000..2c514d4 --- /dev/null +++ b/Swashbuckle.OData/Descriptions/ApiParameterDescriptionEqualityComparer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Web.Http.Controllers; +using System.Web.Http.Description; + +namespace Swashbuckle.OData.Descriptions +{ + internal class ApiParameterDescriptionEqualityComparer : IEqualityComparer + { + public bool Equals(ApiParameterDescription x, ApiParameterDescription y) + { + return ReferenceEquals(x, y) + || RootParameterDescriptorsAreEqual(x, y) + || string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); + } + + private static bool RootParameterDescriptorsAreEqual(ApiParameterDescription x, ApiParameterDescription y) + { + var xParameterDescriptor = GetRootParameterDescriptor(x); + var yParameterDescriptor = GetRootParameterDescriptor(y); + + return xParameterDescriptor != null && yParameterDescriptor != null && ReferenceEquals(xParameterDescriptor, yParameterDescriptor); + } + + private static HttpParameterDescriptor GetRootParameterDescriptor(ApiParameterDescription apiParameterDescription) + { + var oDataParameterDescriptor = apiParameterDescription.ParameterDescriptor as ODataParameterDescriptor; + + return oDataParameterDescriptor != null + ? oDataParameterDescriptor.ReflectedHttpParameterDescriptor + : apiParameterDescription.ParameterDescriptor; + } + + public int GetHashCode(ApiParameterDescription obj) + { + // Make all HashCodes the same to force + // an Equals(ApiParameterDescription x, ApiParameterDescription y) check + return 1; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/MapByDescription.cs b/Swashbuckle.OData/Descriptions/MapByDescription.cs index 711da4b..2fd4f45 100644 --- a/Swashbuckle.OData/Descriptions/MapByDescription.cs +++ b/Swashbuckle.OData/Descriptions/MapByDescription.cs @@ -17,7 +17,7 @@ public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterInde { var httpControllerDescriptor = actionDescriptor.ControllerDescriptor; Contract.Assume(httpControllerDescriptor != null); - return new ODataParameterDescriptor(swaggerParameter.name, parameterDescriptor.ParameterType, parameterDescriptor.IsOptional) + return new ODataParameterDescriptor(swaggerParameter.name, parameterDescriptor.ParameterType, parameterDescriptor.IsOptional, parameterDescriptor) { Configuration = httpControllerDescriptor.Configuration, ActionDescriptor = actionDescriptor, diff --git a/Swashbuckle.OData/Descriptions/MapByIndex.cs b/Swashbuckle.OData/Descriptions/MapByIndex.cs index 66bb179..e789588 100644 --- a/Swashbuckle.OData/Descriptions/MapByIndex.cs +++ b/Swashbuckle.OData/Descriptions/MapByIndex.cs @@ -15,7 +15,7 @@ public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterInde { var httpControllerDescriptor = actionDescriptor.ControllerDescriptor; Contract.Assume(httpControllerDescriptor != null); - return new ODataParameterDescriptor(swaggerParameter.name, parameterDescriptor.ParameterType, parameterDescriptor.IsOptional) + return new ODataParameterDescriptor(swaggerParameter.name, parameterDescriptor.ParameterType, parameterDescriptor.IsOptional, parameterDescriptor) { Configuration = httpControllerDescriptor.Configuration, ActionDescriptor = actionDescriptor, diff --git a/Swashbuckle.OData/Descriptions/MapToDefault.cs b/Swashbuckle.OData/Descriptions/MapToDefault.cs index e1d4e05..e671e23 100644 --- a/Swashbuckle.OData/Descriptions/MapToDefault.cs +++ b/Swashbuckle.OData/Descriptions/MapToDefault.cs @@ -11,7 +11,7 @@ public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterInde var required = swaggerParameter.required; Contract.Assume(required != null); - return new ODataParameterDescriptor(swaggerParameter.name, swaggerParameter.GetClrType(), !required.Value) + return new ODataParameterDescriptor(swaggerParameter.name, swaggerParameter.GetClrType(), !required.Value, null) { Configuration = actionDescriptor.ControllerDescriptor.Configuration, ActionDescriptor = actionDescriptor diff --git a/Swashbuckle.OData/Descriptions/MapToODataActionParameter.cs b/Swashbuckle.OData/Descriptions/MapToODataActionParameter.cs index dfd30b8..9a17db9 100644 --- a/Swashbuckle.OData/Descriptions/MapToODataActionParameter.cs +++ b/Swashbuckle.OData/Descriptions/MapToODataActionParameter.cs @@ -17,7 +17,7 @@ public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterInde { var odataActionParametersDescriptor = actionDescriptor.GetParameters().SingleOrDefault(descriptor => descriptor.ParameterType == typeof (ODataActionParameters)); Contract.Assume(odataActionParametersDescriptor != null); - return new ODataActionParameterDescriptor(odataActionParametersDescriptor.ParameterName, typeof(ODataActionParameters), !required.Value, swaggerParameter.schema) + return new ODataActionParameterDescriptor(odataActionParametersDescriptor.ParameterName, typeof(ODataActionParameters), !required.Value, swaggerParameter.schema, odataActionParametersDescriptor) { Configuration = actionDescriptor.ControllerDescriptor.Configuration, ActionDescriptor = actionDescriptor diff --git a/Swashbuckle.OData/Descriptions/ODataActionDescriptorEqualityComparer.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptorEqualityComparer.cs new file mode 100644 index 0000000..ea7db58 --- /dev/null +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptorEqualityComparer.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; + +namespace Swashbuckle.OData.Descriptions +{ + internal class ODataActionDescriptorEqualityComparer : IEqualityComparer + { + public bool Equals(ODataActionDescriptor x, ODataActionDescriptor y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + return x.Request.Method.Equals(y.Request.Method) + && string.Equals(NormalizeRelativePath(x.RelativePathTemplate), NormalizeRelativePath(y.RelativePathTemplate), StringComparison.OrdinalIgnoreCase) + && x.ActionDescriptor.Equals(y.ActionDescriptor); + } + + public int GetHashCode(ODataActionDescriptor obj) + { + var hashCode = obj.Request.Method.GetHashCode(); + hashCode = (hashCode * 397) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(NormalizeRelativePath(obj.RelativePathTemplate)); + hashCode = (hashCode * 397) ^ obj.ActionDescriptor.GetHashCode(); + return hashCode; + } + + private static string NormalizeRelativePath(string path) + { + Contract.Requires(path != null); + + return path.Replace("()", string.Empty); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs index f2cc225..1bf7316 100644 --- a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs @@ -21,88 +21,6 @@ public IEnumerable Map(ODataActionDescriptor oDataActionDescript return apiDescriptions; } - private static List CreateParameterDescriptions(HttpActionDescriptor actionDescriptor) - { - Contract.Requires(actionDescriptor != null); - - Contract.Assume(actionDescriptor.ControllerDescriptor == null || actionDescriptor.ControllerDescriptor.Configuration != null); - - var parameterDescriptions = new List(); - var actionBinding = GetActionBinding(actionDescriptor); - - var parameterBindings = actionBinding.ParameterBindings; - if (parameterBindings != null) - { - foreach (var parameterBinding in parameterBindings) - { - Contract.Assume(parameterBinding != null); - parameterDescriptions.Add(CreateParameterDescriptionFromBinding(parameterBinding)); - } - } - - return parameterDescriptions; - } - - private static HttpActionBinding GetActionBinding(HttpActionDescriptor actionDescriptor) - { - Contract.Requires(actionDescriptor != null); - Contract.Ensures(Contract.Result() != null); - - Contract.Assume(actionDescriptor.ControllerDescriptor?.Configuration != null); - - var controllerDescriptor = actionDescriptor.ControllerDescriptor; - var controllerServices = controllerDescriptor.Configuration.Services; - var actionValueBinder = controllerServices.GetActionValueBinder(); - Contract.Assume(actionValueBinder != null); - var actionBinding = actionValueBinder.GetBinding(actionDescriptor); - Contract.Assume(actionBinding != null); - return actionBinding; - } - - private static ApiParameterDescription CreateParameterDescriptionFromBinding(HttpParameterBinding parameterBinding) - { - Contract.Requires(parameterBinding != null); - - Contract.Assume(parameterBinding.Descriptor?.Configuration != null); - - var parameterDescription = CreateParameterDescriptionFromDescriptor(parameterBinding.Descriptor); - if (parameterBinding.WillReadBody) - { - parameterDescription.Source = ApiParameterSource.FromBody; - } - else if (parameterBinding.WillReadUri()) - { - parameterDescription.Source = ApiParameterSource.FromUri; - } - - return parameterDescription; - } - - private static ApiParameterDescription CreateParameterDescriptionFromDescriptor(HttpParameterDescriptor parameter) - { - Contract.Requires(parameter != null); - - Contract.Assume(parameter.Configuration != null); - - return new ApiParameterDescription - { - ParameterDescriptor = parameter, - Name = parameter.Prefix ?? parameter.ParameterName, - Documentation = GetApiParameterDocumentation(parameter), - Source = ApiParameterSource.Unknown - }; - } - - private static string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor) - { - Contract.Requires(parameterDescriptor != null); - Contract.Requires(parameterDescriptor.Configuration != null); - - var documentationProvider = parameterDescriptor.Configuration.Services.GetDocumentationProvider(); - - return documentationProvider?.GetDocumentation(parameterDescriptor); - } - private static string GetApiDocumentation(HttpActionDescriptor actionDescriptor) { Contract.Requires(actionDescriptor != null); diff --git a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs index 894884e..dad1bc3 100644 --- a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs @@ -4,6 +4,7 @@ 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; @@ -142,5 +143,87 @@ private static IEnumerable GetInnerFormatters(IEnumerable CreateParameterDescriptions(HttpActionDescriptor actionDescriptor) + { + Contract.Requires(actionDescriptor != null); + + Contract.Assume(actionDescriptor.ControllerDescriptor == null || actionDescriptor.ControllerDescriptor.Configuration != null); + + var parameterDescriptions = new List(); + var actionBinding = GetActionBinding(actionDescriptor); + + var parameterBindings = actionBinding.ParameterBindings; + if (parameterBindings != null) + { + foreach (var parameterBinding in parameterBindings) + { + Contract.Assume(parameterBinding != null); + parameterDescriptions.Add(CreateParameterDescriptionFromBinding(parameterBinding)); + } + } + + return parameterDescriptions; + } + + private static HttpActionBinding GetActionBinding(HttpActionDescriptor actionDescriptor) + { + Contract.Requires(actionDescriptor != null); + Contract.Ensures(Contract.Result() != null); + + Contract.Assume(actionDescriptor.ControllerDescriptor?.Configuration != null); + + var controllerDescriptor = actionDescriptor.ControllerDescriptor; + var controllerServices = controllerDescriptor.Configuration.Services; + var actionValueBinder = controllerServices.GetActionValueBinder(); + Contract.Assume(actionValueBinder != null); + var actionBinding = actionValueBinder.GetBinding(actionDescriptor); + Contract.Assume(actionBinding != null); + return actionBinding; + } + + private static ApiParameterDescription CreateParameterDescriptionFromBinding(HttpParameterBinding parameterBinding) + { + Contract.Requires(parameterBinding != null); + + Contract.Assume(parameterBinding.Descriptor?.Configuration != null); + + var parameterDescription = CreateParameterDescriptionFromDescriptor(parameterBinding.Descriptor); + if (parameterBinding.WillReadBody) + { + parameterDescription.Source = ApiParameterSource.FromBody; + } + else if (parameterBinding.WillReadUri()) + { + parameterDescription.Source = ApiParameterSource.FromUri; + } + + return parameterDescription; + } + + private static ApiParameterDescription CreateParameterDescriptionFromDescriptor(HttpParameterDescriptor parameter) + { + Contract.Requires(parameter != null); + + Contract.Assume(parameter.Configuration != null); + + return new ApiParameterDescription + { + ParameterDescriptor = parameter, + Name = parameter.Prefix ?? parameter.ParameterName, + Documentation = GetApiParameterDocumentation(parameter), + Source = ApiParameterSource.Unknown + }; + } + + private static string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor) + { + Contract.Requires(parameterDescriptor != null); + Contract.Requires(parameterDescriptor.Configuration != null); + + var documentationProvider = parameterDescriptor.Configuration.Services.GetDocumentationProvider(); + + return documentationProvider?.GetDocumentation(parameterDescriptor); + } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs index ab29194..158411d 100644 --- a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs +++ b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs @@ -43,6 +43,7 @@ private Collection GetApiDescriptions() return _actionDescriptorExplorers // Gather ODataActionDescriptors from the API .SelectMany(explorer => explorer.Generate(_httpConfig)) + .Distinct(new ODataActionDescriptorEqualityComparer()) // Map them to ApiDescriptors .SelectMany(oDataActionDescriptor => _actionDescriptorMappers.Select(mapper => mapper.Map(oDataActionDescriptor)) .FirstOrDefault(apiDescriptions => apiDescriptions.Any()) ?? new List()) diff --git a/Swashbuckle.OData/Descriptions/ODataParameterDescriptor.cs b/Swashbuckle.OData/Descriptions/ODataParameterDescriptor.cs index 6a78669..9bf7508 100644 --- a/Swashbuckle.OData/Descriptions/ODataParameterDescriptor.cs +++ b/Swashbuckle.OData/Descriptions/ODataParameterDescriptor.cs @@ -7,11 +7,12 @@ namespace Swashbuckle.OData.Descriptions { internal class ODataParameterDescriptor : HttpParameterDescriptor { - public ODataParameterDescriptor(string parameterName, Type parameterType, bool isOptional) + public ODataParameterDescriptor(string parameterName, Type parameterType, bool isOptional, HttpParameterDescriptor reflectedHttpParameterDescriptor) { ParameterName = parameterName; ParameterType = parameterType; IsOptional = isOptional; + ReflectedHttpParameterDescriptor = reflectedHttpParameterDescriptor; } public override string ParameterName { get; } @@ -20,12 +21,12 @@ public ODataParameterDescriptor(string parameterName, Type parameterType, bool i public override bool IsOptional { get; } - + public HttpParameterDescriptor ReflectedHttpParameterDescriptor { get; } } internal class ODataActionParameterDescriptor : ODataParameterDescriptor { - public ODataActionParameterDescriptor(string parameterName, Type parameterType, bool isOptional, Schema schema) : base(parameterName, parameterType, isOptional) + public ODataActionParameterDescriptor(string parameterName, Type parameterType, bool isOptional, Schema schema, HttpParameterDescriptor reflectedHttpParameterDescriptor) : base(parameterName, parameterType, isOptional, reflectedHttpParameterDescriptor) { Contract.Requires(schema != null); diff --git a/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs b/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs index 07e59fd..2682793 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs @@ -37,8 +37,15 @@ public IEnumerable Map(ODataActionDescriptor oDataActionDescript private List CreateParameterDescriptions(Operation operation, HttpActionDescriptor actionDescriptor) { Contract.Requires(operation != null); + Contract.Requires(actionDescriptor != null); - return operation.parameters?.Select((parameter, index) => GetParameterDescription(parameter, index, actionDescriptor)).ToList(); + return operation.parameters? + .Select((parameter, index) => GetParameterDescription(parameter, index, actionDescriptor)) + // Concat reflected parameter descriptors to ensure that parameters are not missed + // e.g., parameters not described by or derived from the EDM model. + .Concat(CreateParameterDescriptions(actionDescriptor)) + .Distinct(new ApiParameterDescriptionEqualityComparer()) + .ToList(); } private ApiParameterDescription GetParameterDescription(Parameter parameter, int index, HttpActionDescriptor actionDescriptor) diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 87a6e7e..4b1befe 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -174,6 +174,7 @@ + @@ -182,6 +183,7 @@ +