From dc42161b7e120079419a13051df1e8882be1510e Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 8 Jan 2016 18:58:52 -0800 Subject: [PATCH 1/2] Return correct OData response model for collections. Fixes #47. Closes #33. --- .../Containment/AccountsController.cs | 7 + .../Containment/ContainmentTests.cs | 5 + Swashbuckle.OData.Tests/Fixtures/PostTests.cs | 25 +++ .../Fixtures/ResponseModelTests.cs | 174 ++++++++++++++++++ .../Fixtures/RestierTests.cs | 13 +- Swashbuckle.OData.Tests/ODataResponse.cs | 9 - .../Swashbuckle.OData.Tests.csproj | 2 +- .../Descriptions/AttributeRouteStrategy.cs | 39 +++- .../Descriptions/ODataActionDescriptor.cs | 15 +- .../ODataActionDescriptorMapperBase.cs | 35 +++- .../Descriptions/ODataSwaggerUtilities.cs | 2 +- .../Descriptions/ParameterExtensions.cs | 24 ++- .../Descriptions/SwaggerOperationMapper.cs | 3 +- .../Descriptions/SwaggerRouteStrategy.cs | 2 +- .../LimitSchemaGraphToTopLevelEntity.cs | 24 ++- Swashbuckle.OData/ODataResponse.cs | 14 ++ Swashbuckle.OData/SchemaRegistryExtensions.cs | 13 +- Swashbuckle.OData/Swashbuckle.OData.csproj | 1 + 18 files changed, 370 insertions(+), 37 deletions(-) create mode 100644 Swashbuckle.OData.Tests/Fixtures/ResponseModelTests.cs delete mode 100644 Swashbuckle.OData.Tests/ODataResponse.cs create mode 100644 Swashbuckle.OData/ODataResponse.cs diff --git a/Swashbuckle.OData.Tests/Containment/AccountsController.cs b/Swashbuckle.OData.Tests/Containment/AccountsController.cs index 3fe1553..022b549 100644 --- a/Swashbuckle.OData.Tests/Containment/AccountsController.cs +++ b/Swashbuckle.OData.Tests/Containment/AccountsController.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Web.Http; +using System.Web.Http.Description; using System.Web.OData; using System.Web.OData.Extensions; using System.Web.OData.Routing; @@ -28,6 +29,7 @@ public IHttpActionResult Get() } [EnableQuery] + [ResponseType(typeof(List))] public IHttpActionResult GetPayinPIs(int key) { var payinPIs = _accounts.Single(a => a.AccountID == key).PayinPIs; @@ -36,6 +38,7 @@ public IHttpActionResult GetPayinPIs(int key) [EnableQuery] [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")] + [ResponseType(typeof(PaymentInstrument))] public IHttpActionResult GetSinglePayinPI(int accountId, int paymentInstrumentId) { var payinPIs = _accounts.Single(a => a.AccountID == accountId).PayinPIs; @@ -44,6 +47,7 @@ public IHttpActionResult GetSinglePayinPI(int accountId, int paymentInstrumentId } [EnableQuery] + [ResponseType(typeof(PaymentInstrument))] public IHttpActionResult GetPayoutPI(int key, int piKey) { var payoutPI = _accounts.Single(a => a.AccountID == key).PayoutPI; @@ -51,6 +55,7 @@ public IHttpActionResult GetPayoutPI(int key, int piKey) } // POST ~/Accounts(100)/PayinPIs + [ResponseType(typeof(PaymentInstrument))] public IHttpActionResult PostToPayinPIsFromAccount(int key, PaymentInstrument pi) { var account = _accounts.Single(a => a.AccountID == key); @@ -61,6 +66,7 @@ public IHttpActionResult PostToPayinPIsFromAccount(int key, PaymentInstrument pi // PUT ~/Accounts(100)/PayoutPI [ODataRoute("Accounts({accountId})/PayoutPI")] + [ResponseType(typeof(PaymentInstrument))] public IHttpActionResult PutToPayoutPIFromAccount(int accountId, [FromBody] PaymentInstrument paymentInstrument) { var account = _accounts.Single(a => a.AccountID == accountId); @@ -70,6 +76,7 @@ public IHttpActionResult PutToPayoutPIFromAccount(int accountId, [FromBody] Paym // PUT ~/Accounts(100)/PayinPIs(101) [ODataRoute("Accounts({accountId})/PayinPIs({paymentInstrumentId})")] + [ResponseType(typeof(PaymentInstrument))] public IHttpActionResult PutToPayinPI(int accountId, int paymentInstrumentId, [FromBody] PaymentInstrument paymentInstrument) { var account = _accounts.Single(a => a.AccountID == accountId); diff --git a/Swashbuckle.OData.Tests/Containment/ContainmentTests.cs b/Swashbuckle.OData.Tests/Containment/ContainmentTests.cs index f3c0b51..6be1595 100644 --- a/Swashbuckle.OData.Tests/Containment/ContainmentTests.cs +++ b/Swashbuckle.OData.Tests/Containment/ContainmentTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using System.Web.OData.Extensions; using FluentAssertions; @@ -47,6 +48,10 @@ public async Task It_supports_odata_attribute_routing() PathItem pathItem; swaggerDocument.paths.TryGetValue("/odata/Accounts({accountId})/PayinPIs({paymentInstrumentId})", out pathItem); pathItem.Should().NotBeNull(); + pathItem.get.Should().NotBeNull(); + pathItem.get.produces.Should().NotBeNull(); + pathItem.get.produces.Count.Should().Be(1); + pathItem.get.produces.First().Should().Be("application/json"); await ValidationUtils.ValidateSwaggerJson(); } diff --git a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs index b0d12a4..3fa7524 100644 --- a/Swashbuckle.OData.Tests/Fixtures/PostTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/PostTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using System.Web.OData.Builder; using System.Web.OData.Extensions; @@ -38,6 +39,30 @@ public async Task It_has_a_summary() } } + [Test] + public async Task It_consumes_application_json() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(CustomersController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/Customers", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.post.Should().NotBeNull(); + pathItem.post.consumes.Should().NotBeNull(); + pathItem.post.consumes.Count.Should().Be(1); + pathItem.post.consumes.First().Should().Be("application/json"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + private static void Configuration(IAppBuilder appBuilder, Type targetController) { var config = appBuilder.GetStandardHttpConfig(targetController); diff --git a/Swashbuckle.OData.Tests/Fixtures/ResponseModelTests.cs b/Swashbuckle.OData.Tests/Fixtures/ResponseModelTests.cs new file mode 100644 index 0000000..d3d4279 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/ResponseModelTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using System.Web.OData; +using System.Web.OData.Builder; +using System.Web.OData.Extensions; +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 ResponseModelTests + { + [Test] + public async Task It_produces_an_accurate_odata_response_model_for_iqueryable_return_type() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(ProductResponsesController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>("/odata/ProductResponses"); + oDataResponse.Value.Should().NotBeNull(); + oDataResponse.Value.Count.Should().Be(20); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/ProductResponses", out pathItem); + pathItem.get.Should().NotBeNull(); + pathItem.get.produces.Should().NotBeNull(); + pathItem.get.produces.Count.Should().Be(1); + pathItem.get.produces.First().Should().Be("application/json"); + var getResponse = pathItem.get.responses.SingleOrDefault(response => response.Key == "200"); + getResponse.Should().NotBeNull(); + getResponse.Value.schema.@ref.Should().Be("#/definitions/ODataResponse[ProductResponse]"); + swaggerDocument.definitions.Should().ContainKey("ODataResponse[ProductResponse]"); + var responseSchema = swaggerDocument.definitions["ODataResponse[ProductResponse]"]; + responseSchema.Should().NotBeNull(); + responseSchema.properties.Should().NotBeNull(); + responseSchema.properties.Should().ContainKey("@odata.context"); + responseSchema.properties["@odata.context"].type.Should().Be("string"); + responseSchema.properties["value"].type.Should().Be("array"); + responseSchema.properties["value"].items.Should().NotBeNull(); + responseSchema.properties["value"].items.@ref.Should().Be("#/definitions/ProductResponse"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_produces_an_accurate_odata_response_model_for_list_return_type() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(ProductResponsesController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var top10Response = await httpClient.GetJsonAsync>("/odata/ProductResponses/Default.Top10()"); + top10Response.Value.Should().NotBeNull(); + top10Response.Value.Count.Should().Be(10); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/ProductResponses/Default.Top10()", out pathItem); + var getResponse = pathItem.get.responses.SingleOrDefault(response => response.Key == "200"); + getResponse.Should().NotBeNull(); + getResponse.Value.schema.@ref.Should().Be("#/definitions/ODataResponse[ProductResponse]"); + swaggerDocument.definitions.Should().ContainKey("ODataResponse[ProductResponse]"); + var responseSchema = swaggerDocument.definitions["ODataResponse[ProductResponse]"]; + responseSchema.Should().NotBeNull(); + responseSchema.properties.Should().NotBeNull(); + responseSchema.properties.Should().ContainKey("@odata.context"); + responseSchema.properties["@odata.context"].type.Should().Be("string"); + responseSchema.properties["value"].type.Should().Be("array"); + responseSchema.properties["value"].items.Should().NotBeNull(); + responseSchema.properties["value"].items.@ref.Should().Be("#/definitions/ProductResponse"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + private static void Configuration(IAppBuilder appBuilder, Type targetController) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + // Define a route to a controller class that contains functions + config.MapODataServiceRoute("StringKeyTestsRoute", "odata", GetEdmModel()); + + config.EnsureInitialized(); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet("ProductResponses"); + + var productType = builder.EntityType(); + + // A function that returns a list + productType.Collection + .Function("Top10") + .ReturnsCollectionFromEntitySet("ProductResponses"); + + return builder.GetEdmModel(); + } + } + + public class ProductResponse + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public double Price { get; set; } + } + + public class ProductResponsesController : ODataController + { + private static readonly ConcurrentDictionary Data; + + static ProductResponsesController() + { + Data = new ConcurrentDictionary(); + var rand = new Random(); + + Enumerable.Range(0, 20).Select(i => new ProductResponse + { + Id = Guid.NewGuid().ToString(), + Name = "Product " + i, + Price = rand.NextDouble() * 1000 + }).ToList().ForEach(p => Data.TryAdd(p.Id, p)); + } + + /// + /// An action that returns an IQueryable[] + /// + [EnableQuery] + public IQueryable Get() + { + return Data.Values.AsQueryable(); + } + + /// + /// A function that returns a List[] + /// + [HttpGet] + [EnableQuery] + [ResponseType(typeof(List))] + public IHttpActionResult Top10() + { + var retval = Data.Values.OrderByDescending(p => p.Price).Take(10).ToList(); + + return Ok(retval); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/RestierTests.cs b/Swashbuckle.OData.Tests/Fixtures/RestierTests.cs index 07379c2..d1d0f05 100644 --- a/Swashbuckle.OData.Tests/Fixtures/RestierTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/RestierTests.cs @@ -144,9 +144,16 @@ public async Task It_has_a_restier_get_users_response_of_type_array() swaggerDocument.paths.TryGetValue("/restier/Users", out pathItem); var getUsersResponse = pathItem.get.responses.SingleOrDefault(response => response.Key == "200"); getUsersResponse.Should().NotBeNull(); - getUsersResponse.Value.schema.@ref.Should().BeNull(); - getUsersResponse.Value.schema.items.@ref.Should().Be("#/definitions/User"); - getUsersResponse.Value.schema.type.Should().Be("array"); + getUsersResponse.Value.schema.@ref.Should().Be("#/definitions/ODataResponse[User]"); + swaggerDocument.definitions.Should().ContainKey("ODataResponse[User]"); + var responseSchema = swaggerDocument.definitions["ODataResponse[User]"]; + responseSchema.Should().NotBeNull(); + responseSchema.properties.Should().NotBeNull(); + responseSchema.properties.Should().ContainKey("@odata.context"); + responseSchema.properties["@odata.context"].type.Should().Be("string"); + responseSchema.properties["value"].type.Should().Be("array"); + responseSchema.properties["value"].items.Should().NotBeNull(); + responseSchema.properties["value"].items.@ref.Should().Be("#/definitions/User"); await ValidationUtils.ValidateSwaggerJson(); } diff --git a/Swashbuckle.OData.Tests/ODataResponse.cs b/Swashbuckle.OData.Tests/ODataResponse.cs deleted file mode 100644 index 32da0ef..0000000 --- a/Swashbuckle.OData.Tests/ODataResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Swashbuckle.OData.Tests -{ - internal class ODataResponse - { - public List Value { 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 166af85..d7737a1 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -155,6 +155,7 @@ + @@ -171,7 +172,6 @@ - diff --git a/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs b/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs index 9589b91..f7479e0 100644 --- a/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs +++ b/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; +using System.Net.Http; using System.Web; using System.Web.Http; using System.Web.Http.Controllers; +using System.Web.OData.Extensions; using System.Web.OData.Routing; using System.Web.OData.Routing.Conventions; using Flurl; @@ -17,10 +19,10 @@ internal class AttributeRouteStrategy : IODataActionDescriptorExplorer { public IEnumerable Generate(HttpConfiguration httpConfig) { - return httpConfig.GetODataRoutes().SelectMany(GetODataActionDescriptorsFromAttributeRoutes); + return httpConfig.GetODataRoutes().SelectMany(oDataRoute => GetODataActionDescriptorsFromAttributeRoutes(oDataRoute, httpConfig)); } - private static IEnumerable GetODataActionDescriptorsFromAttributeRoutes(ODataRoute oDataRoute) + private static IEnumerable GetODataActionDescriptorsFromAttributeRoutes(ODataRoute oDataRoute, HttpConfiguration httpConfig) { Contract.Requires(oDataRoute != null); Contract.Requires(oDataRoute.Constraints != null); @@ -34,14 +36,14 @@ private static IEnumerable GetODataActionDescriptorsFromA { return attributeRoutingConvention .GetInstanceField>("_attributeMappings", true) - .Select(pair => GetODataActionDescriptorFromAttributeRoute(pair.Value, oDataRoute)) + .Select(pair => GetODataActionDescriptorFromAttributeRoute(pair.Value, oDataRoute, httpConfig)) .Where(descriptor => descriptor != null); } return new List(); } - private static ODataActionDescriptor GetODataActionDescriptorFromAttributeRoute(HttpActionDescriptor actionDescriptor, ODataRoute oDataRoute) + private static ODataActionDescriptor GetODataActionDescriptorFromAttributeRoute(HttpActionDescriptor actionDescriptor, ODataRoute oDataRoute, HttpConfiguration httpConfig) { Contract.Requires(actionDescriptor != null); Contract.Requires(oDataRoute != null); @@ -51,7 +53,34 @@ private static ODataActionDescriptor GetODataActionDescriptorFromAttributeRoute( Contract.Assume(odataRouteAttribute != null); var pathTemplate = HttpUtility.UrlDecode(oDataRoute.RoutePrefix.AppendPathSegment(odataRouteAttribute.PathTemplate)); Contract.Assume(pathTemplate != null); - return new ODataActionDescriptor(actionDescriptor, oDataRoute, pathTemplate); + return new ODataActionDescriptor(actionDescriptor, oDataRoute, pathTemplate, CreateHttpRequestMessage(actionDescriptor, oDataRoute, httpConfig)); + } + + private static HttpRequestMessage CreateHttpRequestMessage(HttpActionDescriptor actionDescriptor, ODataRoute oDataRoute, HttpConfiguration httpConfig) + { + Contract.Requires(httpConfig != null); + Contract.Requires(oDataRoute != null); + Contract.Requires(httpConfig != null); + Contract.Ensures(Contract.Result() != null); + + Contract.Assume(oDataRoute.Constraints != null); + + var httpRequestMessage = new HttpRequestMessage(actionDescriptor.SupportedHttpMethods.First(), "http://any/"); + + var requestContext = new HttpRequestContext + { + Configuration = httpConfig + }; + httpRequestMessage.SetConfiguration(httpConfig); + httpRequestMessage.SetRequestContext(requestContext); + + var httpRequestMessageProperties = httpRequestMessage.ODataProperties(); + Contract.Assume(httpRequestMessageProperties != null); + httpRequestMessageProperties.Model = oDataRoute.GetEdmModel(); + httpRequestMessageProperties.RouteName = oDataRoute.GetODataPathRouteConstraint().RouteName; + httpRequestMessageProperties.RoutingConventions = oDataRoute.GetODataPathRouteConstraint().RoutingConventions; + httpRequestMessageProperties.PathHandler = oDataRoute.GetODataPathRouteConstraint().PathHandler; + return httpRequestMessage; } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs index 139dad7..6a57746 100644 --- a/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs @@ -1,4 +1,5 @@ using System.Diagnostics.Contracts; +using System.Net.Http; using System.Web.Http.Controllers; using System.Web.OData.Routing; using Swashbuckle.Swagger; @@ -8,26 +9,30 @@ namespace Swashbuckle.OData.Descriptions internal class ODataActionDescriptor { private readonly string _relativePathTemplate; + private readonly HttpRequestMessage _request; private readonly ODataRoute _route; private readonly HttpActionDescriptor _actionDescriptor; private readonly Operation _operation; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The HTTP action descriptor. /// The OData route. /// The relative path template. + /// The request. /// Additional metadata based about the action. - public ODataActionDescriptor(HttpActionDescriptor actionDescriptor, ODataRoute route, string relativePathTemplate, Operation operation = null) + public ODataActionDescriptor(HttpActionDescriptor actionDescriptor, ODataRoute route, string relativePathTemplate, HttpRequestMessage request, Operation operation = null) { Contract.Requires(actionDescriptor != null); Contract.Requires(route != null); Contract.Requires(relativePathTemplate != null); + Contract.Requires(request != null); _actionDescriptor = actionDescriptor; _route = route; _relativePathTemplate = relativePathTemplate; + _request = request; _operation = operation; } @@ -51,12 +56,18 @@ public Operation Operation get { return _operation; } } + public HttpRequestMessage Request + { + get { return _request; } + } + [ContractInvariantMethod] private void ObjectInvariants() { Contract.Invariant(ActionDescriptor != null); Contract.Invariant(Route != null); Contract.Invariant(RelativePathTemplate != null); + Contract.Invariant(Request != null); } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs index dcf3d33..894884e 100644 --- a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapperBase.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; @@ -33,11 +34,11 @@ protected void PopulateApiDescriptions(ODataActionDescriptor oDataActionDescript IEnumerable supportedResponseFormatters = new List(); if (mediaTypeFormatterCollection != null) { - supportedRequestBodyFormatters = bodyParameter != null ? mediaTypeFormatterCollection.Where(f => f is ODataMediaTypeFormatter && f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) : Enumerable.Empty(); + supportedRequestBodyFormatters = bodyParameter != null ? mediaTypeFormatterCollection.Where(CanReadODataType(oDataActionDescriptor, bodyParameter)) : Enumerable.Empty(); // response formatters var returnType = responseDescription.ResponseType ?? responseDescription.DeclaredType; - supportedResponseFormatters = returnType != null && returnType != typeof (void) ? mediaTypeFormatterCollection.Where(f => f is ODataMediaTypeFormatter && f.CanWriteType(returnType)) : Enumerable.Empty(); + supportedResponseFormatters = returnType != null && returnType != typeof (void) ? mediaTypeFormatterCollection.Where(CanWriteODataType(oDataActionDescriptor, returnType)) : Enumerable.Empty(); // Replacing the formatter tracers with formatters if tracers are present. @@ -84,6 +85,36 @@ protected void PopulateApiDescriptions(ODataActionDescriptor oDataActionDescript } } + private static Func CanWriteODataType(ODataActionDescriptor oDataActionDescriptor, Type returnType) + { + return mediaTypeFormatter => + { + var oDataMediaTypeFormatter = mediaTypeFormatter as ODataMediaTypeFormatter; + + if (oDataMediaTypeFormatter != null) + { + oDataMediaTypeFormatter.SetInstanceProperty("Request", oDataActionDescriptor.Request); + return mediaTypeFormatter.CanWriteType(returnType); + } + return false; + }; + } + + private static Func CanReadODataType(ODataActionDescriptor oDataActionDescriptor, ApiParameterDescription bodyParameter) + { + return mediaTypeFormatter => + { + var oDataMediaTypeFormatter = mediaTypeFormatter as ODataMediaTypeFormatter; + + if (oDataMediaTypeFormatter != null) + { + oDataMediaTypeFormatter.SetInstanceProperty("Request", oDataActionDescriptor.Request); + return mediaTypeFormatter.CanReadType(bodyParameter.ParameterDescriptor.ParameterType); + } + return false; + }; + } + /// /// Gets a collection of HttpMethods supported by the action. Called when initializing the /// . diff --git a/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs b/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs index 8a87a98..d68e69e 100644 --- a/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs @@ -57,7 +57,7 @@ public static IList AddQueryOptionParameters(IList paramet .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", false); + .Parameter("$count", "query", "Includes a count of the matching results in the response.", "boolean", false); } /// diff --git a/Swashbuckle.OData/Descriptions/ParameterExtensions.cs b/Swashbuckle.OData/Descriptions/ParameterExtensions.cs index 8a5407f..23a9465 100644 --- a/Swashbuckle.OData/Descriptions/ParameterExtensions.cs +++ b/Swashbuckle.OData/Descriptions/ParameterExtensions.cs @@ -1,13 +1,14 @@ using System; using System.Diagnostics.Contracts; using System.Linq; +using System.Web.Http.Description; using Swashbuckle.Swagger; namespace Swashbuckle.OData.Descriptions { internal static class ParameterExtensions { - public static ParameterSource MapSource(this Parameter parameter) + public static ParameterSource MapToSwaggerSource(this Parameter parameter) { Contract.Requires(parameter != null); @@ -28,6 +29,27 @@ public static ParameterSource MapSource(this Parameter parameter) } } + public static ApiParameterSource MapToApiParameterSource(this Parameter parameter) + { + Contract.Requires(parameter != null); + + switch (parameter.@in) + { + case "query": + return ApiParameterSource.FromUri; + case "header": + return ApiParameterSource.Unknown; + case "path": + return ApiParameterSource.FromUri; + case "formData": + return ApiParameterSource.FromBody; + case "body": + return ApiParameterSource.FromBody; + default: + throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null); + } + } + public static Type GetClrType(this Parameter parameter) { Contract.Requires(parameter != null); diff --git a/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs b/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs index c901d4a..07e59fd 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs @@ -50,7 +50,8 @@ private ApiParameterDescription GetParameterDescription(Parameter parameter, int ParameterDescriptor = httpParameterDescriptor, Name = httpParameterDescriptor.Prefix ?? httpParameterDescriptor.ParameterName, Documentation = GetApiParameterDocumentation(parameter, httpParameterDescriptor), - SwaggerSource = parameter.MapSource() + SwaggerSource = parameter.MapToSwaggerSource(), + Source = parameter.MapToApiParameterSource() }; } diff --git a/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs b/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs index 5958f29..f0e2732 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs @@ -69,7 +69,7 @@ private static ODataActionDescriptor GetActionDescriptors(HttpMethod httpMethod, { actionDescriptor = MapForRestierIfNecessary(actionDescriptor, potentialOperation); - return new ODataActionDescriptor(actionDescriptor, oDataRoute, potentialPathTemplate, potentialOperation); + return new ODataActionDescriptor(actionDescriptor, oDataRoute, potentialPathTemplate, request, potentialOperation); } } diff --git a/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs b/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs index bcf4175..de7833e 100644 --- a/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs +++ b/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs @@ -15,19 +15,27 @@ public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IAp { foreach (var definition in swaggerDoc.definitions) { - var schema = definition.Value; - Contract.Assume(schema != null); - - var properties = schema.properties.ToList(); - foreach (var property in schema.properties) + if (IsEntityType(definition)) { - RemoveCollectionTypeProperty(property, properties); - RemoveReferenceTypeProperty(property, properties); + var schema = definition.Value; + Contract.Assume(schema != null); + + var properties = schema.properties.ToList(); + foreach (var property in schema.properties) + { + RemoveCollectionTypeProperty(property, properties); + RemoveReferenceTypeProperty(property, properties); + } + schema.properties = properties.ToDictionary(property => property.Key, property => property.Value); } - schema.properties = properties.ToDictionary(property => property.Key, property => property.Value); } } + private static bool IsEntityType(KeyValuePair definition) + { + return !definition.Key.StartsWith("ODataResponse["); + } + private static void RemoveCollectionTypeProperty(KeyValuePair property, ICollection> properties) { Contract.Requires(properties != null); diff --git a/Swashbuckle.OData/ODataResponse.cs b/Swashbuckle.OData/ODataResponse.cs new file mode 100644 index 0000000..3e62b95 --- /dev/null +++ b/Swashbuckle.OData/ODataResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Swashbuckle.OData +{ + public class ODataResponse + { + [JsonProperty("@odata.context")] + public string ODataContext { get; set; } + + [JsonProperty("value")] + public List Value { get; set; } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/SchemaRegistryExtensions.cs b/Swashbuckle.OData/SchemaRegistryExtensions.cs index eb0742d..987ee45 100644 --- a/Swashbuckle.OData/SchemaRegistryExtensions.cs +++ b/Swashbuckle.OData/SchemaRegistryExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.Contracts; +using System.Linq; using System.Web.Http; using System.Web.OData; using Swashbuckle.Swagger; @@ -13,17 +14,23 @@ public static Schema GetOrRegisterODataType(this SchemaRegistry registry, Type t Contract.Requires(registry != null); Contract.Requires(type != null); - var isAGenericODataType = IsAGenericODataType(type); - if (isAGenericODataType) + if (IsAGenericODataTypeThatShouldBeUnwrapped(type)) { var genericArguments = type.GetGenericArguments(); Contract.Assume(genericArguments != null); return registry.GetOrRegister(genericArguments[0]); } + Type elementType; + if (type.IsCollection(out elementType) && !elementType.IsPrimitive) + { + var openOdataType = typeof (ODataResponse<>); + var odataType = openOdataType.MakeGenericType(elementType); + return registry.GetOrRegister(odataType); + } return registry.GetOrRegister(type); } - private static bool IsAGenericODataType(Type type) + private static bool IsAGenericODataTypeThatShouldBeUnwrapped(Type type) { Contract.Requires(type != null); diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index e4067b4..39ad8c0 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -211,6 +211,7 @@ + From df45a0570e66e2186eb5c16df9eec4b5e29ff36f Mon Sep 17 00:00:00 2001 From: Richard Beauchamp Date: Fri, 8 Jan 2016 19:03:33 -0800 Subject: [PATCH 2/2] Prepare release v2.10.1 --- Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj | 4 ++-- Swashbuckle.OData/Properties/AssemblyInfo.cs | 2 +- appveyor.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj index 7609a98..e14700f 100644 --- a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj +++ b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj @@ -25,12 +25,12 @@ Richard Beauchamp Extends Swashbuckle with OData v4 support! Extends Swashbuckle with OData v4 support! - Supports functions that accept an enum parameter. + Response model for collections is more accurate. Fixes #47, #33. https://github.com/rbeauchamp/Swashbuckle.OData https://github.com/rbeauchamp/Swashbuckle.OData/blob/master/License.txt Copyright 2015 Swashbuckle Swagger SwaggerUi OData Documentation Discovery Help WebApi AspNet AspNetWebApi Docs WebHost IIS - 2.10.0 + 2.10.1 diff --git a/Swashbuckle.OData/Properties/AssemblyInfo.cs b/Swashbuckle.OData/Properties/AssemblyInfo.cs index 9a0ad34..3525a42 100644 --- a/Swashbuckle.OData/Properties/AssemblyInfo.cs +++ b/Swashbuckle.OData/Properties/AssemblyInfo.cs @@ -37,4 +37,4 @@ [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("2.10.0")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("2.10.1")] \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 699883d..a395dcc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.10.0.{build} +version: 2.10.1.{build} before_build: - cmd: nuget restore