diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..05936bb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +; Top-most EditorConfig file +root = true + +; 4-column space indention +[*.cs] +indent_style = space +indent_size = 4 diff --git a/README.md b/README.md index bd6153d..77d4b55 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Swashbuckle.OData ========= [![Build status](https://ci.appveyor.com/api/projects/status/lppv9403dgwrntpa?svg=true)](https://ci.appveyor.com/project/rbeauchamp/swashbuckle-odata/) -[![Coverage Status](https://coveralls.io/repos/rbeauchamp/Swashbuckle.OData/badge.svg?branch=master&service=github)](https://coveralls.io/github/rbeauchamp/Swashbuckle.OData?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/rbeauchamp/Swashbuckle.OData/badge.svg?branch=master)](https://coveralls.io/github/rbeauchamp/Swashbuckle.OData?branch=master) [![Issue Stats](http://www.issuestats.com/github/rbeauchamp/Swashbuckle.OData/badge/pr)](http://www.issuestats.com/github/rbeauchamp/Swashbuckle.OData) [![Issue Stats](http://www.issuestats.com/github/rbeauchamp/Swashbuckle.OData/badge/issue)](http://www.issuestats.com/github/rbeauchamp/Swashbuckle.OData) @@ -61,6 +61,32 @@ config.AddCustomSwaggerRoute(restierRoute, "/Customers({CustomerId})/Orders({Ord .PathParameter("OrderId"); ``` +### Route prefixes that have parameters ### + +The follow snippet demonstrates how to configure route prefixes that have parameters: + +```csharp +// For example, if you have a route prefix with a parameter "tenantId" of type long +var odataRoute = config.MapODataServiceRoute("odata", "odata/{tenantId}", builder.GetEdmModel()); + +// Then add the following route constraint so that Swashbuckle.OData knows the parameter type. +// If you don't add this line then the parameter will be assumed to be of type string. +odataRoute.Constraints.Add("tenantId", new LongRouteConstraint()); +``` +Swashbuckle.OData supports the following route constraints: + +| Parameter Type | Route Constraint | +|----------------|---------------------------| +| `bool` | `BoolRouteConstraint` | +| `DateTime` | `DateTimeRouteConstraint` | +| `decimal` | `DecimalRouteConstraint` | +| `double` | `DoubleRouteConstraint` | +| `float` | `FloatRouteConstraint` | +| `Guid` | `GuidRouteConstraint` | +| `int` | `IntRouteConstraint` | +| `long` | `LongRouteConstraint` | + + ### OWIN ### If your service is hosted using OWIN middleware, configure the custom provider as follows: diff --git a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj index 737392c..1df2c67 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! - Provides explicit, unit tested support for OData actions. + OData query parameters are limited to $expand and $select for APIs that return a single result. 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.12.0 + 2.15.0 diff --git a/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs b/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs index 08a5f8f..c1d4673 100644 --- a/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs @@ -1,6 +1,7 @@ using System.Web.Http; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; namespace SwashbuckleODataSample { @@ -18,6 +19,7 @@ public static void Register(HttpConfiguration config) formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver(), NullValueHandling = NullValueHandling.Ignore, DateFormatHandling = DateFormatHandling.IsoDateFormat, DateTimeZoneHandling = DateTimeZoneHandling.Utc, diff --git a/Swashbuckle.OData.Sample/App_Start/ODataConfig.cs b/Swashbuckle.OData.Sample/App_Start/ODataConfig.cs index 51da9ff..4e08830 100644 --- a/Swashbuckle.OData.Sample/App_Start/ODataConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/ODataConfig.cs @@ -63,36 +63,49 @@ private static void ConfigureWebApiOData(HttpConfiguration config) private static IEdmModel GetDefaultModel() { var builder = new ODataConventionModelBuilder(); + builder.EnableLowerCamelCase(); + builder.EntitySet("Customers"); builder.EntitySet("Orders"); + return builder.GetEdmModel(); } private static IEdmModel GetCustomRouteModel() { var builder = new ODataConventionModelBuilder(); + builder.EnableLowerCamelCase(); + builder.EntitySet("Customers"); builder.EntitySet("Orders"); + return builder.GetEdmModel(); } private static IEdmModel GetVersionedModel() { var builder = new ODataConventionModelBuilder(); + builder.EnableLowerCamelCase(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } private static IEdmModel GetFakeModel() { var builder = new ODataConventionModelBuilder(); + builder.EnableLowerCamelCase(); + builder.EntitySet("FakeCustomers"); + return builder.GetEdmModel(); } private static IEdmModel GetFunctionsEdmModel() { - ODataModelBuilder builder = new ODataConventionModelBuilder(); + var builder = new ODataConventionModelBuilder(); + builder.EnableLowerCamelCase(); builder.EntitySet("Products"); @@ -139,11 +152,17 @@ private static IEdmModel GetFunctionsEdmModel() // An action bound to an entity set // Accepts multiple action parameters - var action = productType.Collection.Action("Create"); - action.ReturnsFromEntitySet("Products"); - action.Parameter("Name").OptionalParameter = false; - action.Parameter("Price").OptionalParameter = false; - action.Parameter("EnumValue").OptionalParameter = false; + var createAction = productType.Collection.Action("Create"); + createAction.ReturnsFromEntitySet("Products"); + createAction.Parameter("Name").OptionalParameter = false; + createAction.Parameter("Price").OptionalParameter = false; + createAction.Parameter("EnumValue").OptionalParameter = false; + + // An action bound to an entity set + // Accepts an array of complex types + var postArrayAction = productType.Collection.Action("PostArray"); + postArrayAction.ReturnsFromEntitySet("Products"); + postArrayAction.CollectionParameter("products").OptionalParameter = false; // An action bound to an entity productType.Action("Rate") diff --git a/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs b/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs index bf45eb5..c59dca3 100644 --- a/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs @@ -8,8 +8,7 @@ public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); - config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } - ); + config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional }); } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/Models/Product.cs b/Swashbuckle.OData.Sample/Models/Product.cs index 7d1d5dd..aed5f1e 100644 --- a/Swashbuckle.OData.Sample/Models/Product.cs +++ b/Swashbuckle.OData.Sample/Models/Product.cs @@ -13,4 +13,13 @@ public class Product public MyEnum EnumValue { get; set; } } + + public class ProductDto + { + public string Name { get; set; } + + public double Price { get; set; } + + public MyEnum EnumValue { get; set; } + } } \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs index 8c12e8c..cd39c57 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs @@ -1,5 +1,4 @@ -using System; -using System.Data.Entity.Infrastructure; +using System.Data.Entity.Infrastructure; using System.Linq; using System.Net; using System.Threading.Tasks; diff --git a/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs b/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs index 2fe895c..9e0c31c 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs @@ -125,7 +125,7 @@ public IQueryable ProductsWithIds([FromODataUri]int[] Ids) } /// - /// Creates a product. This action accepts parameters via an ODataActionParameters object. + /// Creates a product from values passed in an ODataActionParameters object. /// /// The OData action parameters. [HttpPost] @@ -143,6 +143,36 @@ public IHttpActionResult Create(ODataActionParameters parameters) return Created(product); } + /// + /// Creates a set of products from an array of ProductDTOs passed in an ODataActionParameters object. + /// + /// The OData action parameters containing an array of ProductDTOs + [HttpPost] + public List PostArray(ODataActionParameters parameters) + { + var productDtos = parameters["products"] as IEnumerable; + + var newProducts = new List(); + + if (productDtos != null && productDtos.Any()) + { + foreach (var productDto in productDtos) + { + var product = new Product + { + Id = Data.Values.Max(existingProduct => existingProduct.Id) + 1, + Name = productDto.Name, + Price = productDto.Price, + EnumValue = productDto.EnumValue + }; + newProducts.Add(product); + Data.TryAdd(product.Id, product); + } + } + + return newProducts; + } + /// /// Rates a product. This action targets a specific entity by id. /// diff --git a/Swashbuckle.OData.Tests/AppBuilderExtensions.cs b/Swashbuckle.OData.Tests/AppBuilderExtensions.cs index 9c54187..5ce5ffc 100644 --- a/Swashbuckle.OData.Tests/AppBuilderExtensions.cs +++ b/Swashbuckle.OData.Tests/AppBuilderExtensions.cs @@ -9,12 +9,22 @@ namespace Swashbuckle.OData.Tests { public static class AppBuilderExtensions { - public static HttpConfiguration GetStandardHttpConfig(this IAppBuilder appBuilder, Type targetController, Action unitTestConfigs = null) + public static HttpConfiguration GetStandardHttpConfig(this IAppBuilder appBuilder, params Type[] targetControllers) + { + return GetStandardHttpConfig(appBuilder, null, targetControllers); + } + + public static HttpConfiguration GetStandardHttpConfig(this IAppBuilder appBuilder, Action unitTestConfigs, params Type[] targetControllers) { var config = new HttpConfiguration { IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always }; + return ConfigureHttpConfig(appBuilder, config, unitTestConfigs, targetControllers); + } + + public static HttpConfiguration ConfigureHttpConfig(this IAppBuilder appBuilder, HttpConfiguration config, Action unitTestConfigs, params Type[] targetControllers) + { var server = new HttpServer(config); appBuilder.UseWebApi(server); config.EnableSwagger(c => @@ -32,12 +42,11 @@ public static HttpConfiguration GetStandardHttpConfig(this IAppBuilder appBuilde // Apply test-specific configs unitTestConfigs?.Invoke(c); - }).EnableSwaggerUi(); FormatterConfig.Register(config); - config.Services.Replace(typeof (IHttpControllerSelector), new UnitTestControllerSelector(config, targetController)); + config.Services.Replace(typeof (IHttpControllerSelector), new UnitTestControllerSelector(config, targetControllers)); return config; } diff --git a/Swashbuckle.OData.Tests/Fixtures/ActionTests.cs b/Swashbuckle.OData.Tests/Fixtures/ActionTests.cs index 555ab54..6eca8c7 100644 --- a/Swashbuckle.OData.Tests/Fixtures/ActionTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/ActionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; @@ -12,6 +13,7 @@ using FluentAssertions; using Microsoft.OData.Edm; using Microsoft.Owin.Hosting; +using Newtonsoft.Json; using NUnit.Framework; using Owin; using Swashbuckle.Swagger; @@ -22,6 +24,62 @@ namespace Swashbuckle.OData.Tests [TestFixture] public class ActionTests { + [Test] + public async Task It_supports_actions_that_accept_an_array_of_complex_types() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(SuppliersController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route and post data to the test controller is valid + var suppliers = new SupplierDtos + { + Suppliers = new List + { + new SupplierDto + { + Name = "SupplierNameOne", + Code = "CodeOne", + Description = "SupplierOneDescription" + }, + new SupplierDto + { + Name = "SupplierNameTwo", + Code = "CodeTwo", + Description = "SupplierTwoDescription" + } + } + }; + + var result = await httpClient.PostAsJsonAsync("/odata/Suppliers/Default.PostArrayOfSuppliers", suppliers); + result.IsSuccessStatusCode.Should().BeTrue(); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/Suppliers/Default.PostArrayOfSuppliers", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.post.Should().NotBeNull(); + pathItem.post.parameters.Count.Should().Be(1); + pathItem.post.parameters.Single().@in.Should().Be("body"); + pathItem.post.parameters.Single().name.Should().Be("parameters"); + pathItem.post.parameters.Single().schema.Should().NotBeNull(); + pathItem.post.parameters.Single().schema.type.Should().Be("object"); + pathItem.post.parameters.Single().schema.properties.Should().NotBeNull(); + pathItem.post.parameters.Single().schema.properties.Count.Should().Be(1); + pathItem.post.parameters.Single().schema.properties.Should().ContainKey("suppliers"); + pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "suppliers").Value.type.Should().Be("array"); + pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "suppliers").Value.items.Should().NotBeNull(); + pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "suppliers").Value.items.@ref.Should().Be("#/definitions/SupplierDto"); + + swaggerDocument.definitions.Keys.Should().Contain("SupplierDto"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + [Test] public async Task It_supports_actions_with_only_body_paramters() { @@ -170,6 +228,7 @@ private static IEdmModel GetEdmModel() { var builder = new ODataConventionModelBuilder(); builder.EntitySet("Suppliers"); + //builder.ComplexType(); var entityType = builder.EntityType(); var create = entityType.Collection.Action("Create"); @@ -182,6 +241,10 @@ private static IEdmModel GetEdmModel() createWithEnum.ReturnsFromEntitySet("Suppliers"); createWithEnum.Parameter("EnumValue"); + var postArray = entityType.Collection.Action("PostArrayOfSuppliers"); + postArray.ReturnsCollectionFromEntitySet("Suppliers"); + postArray.CollectionParameter("suppliers"); + entityType.Action("Rate") .Parameter("Rating"); @@ -189,6 +252,12 @@ private static IEdmModel GetEdmModel() } } + public class SupplierDtos + { + [JsonProperty("suppliers")] + public List Suppliers { get; set; } + } + public class SupplierDto { public string Code { get; set; } @@ -217,6 +286,17 @@ public class Supplier public class SuppliersController : ODataController { + [HttpPost] + [ResponseType(typeof(List))] + public List PostArrayOfSuppliers(ODataActionParameters parameters) + { + parameters.Should().ContainKey("suppliers"); + parameters.Count.Should().Be(1); + parameters["suppliers"].Should().BeAssignableTo>(); + + return new List(); + } + [HttpPost] [ResponseType(typeof(Supplier))] public IHttpActionResult Create(ODataActionParameters parameters) diff --git a/Swashbuckle.OData.Tests/Fixtures/GetTests.cs b/Swashbuckle.OData.Tests/Fixtures/GetTests.cs index 9914e44..6811d19 100644 --- a/Swashbuckle.OData.Tests/Fixtures/GetTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/GetTests.cs @@ -62,6 +62,46 @@ public async Task It_has_all_optional_odata_query_parameters() } } + [Test] + public async Task It_has_collection_odata_query_parameters() + { + 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.get.parameters.Where(parameter => parameter.name.StartsWith("$")).Should().HaveCount(7); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_has_single_entity_odata_query_parameters() + { + 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({Id})", out pathItem); + pathItem.get.parameters.Where(parameter => parameter.name.StartsWith("$")).Should().HaveCount(2); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + [Test] public async Task It_has_a_parameter_with_a_name_equal_to_the_path_name() { diff --git a/Swashbuckle.OData.Tests/Fixtures/ModelSchemaTests.cs b/Swashbuckle.OData.Tests/Fixtures/ModelSchemaTests.cs new file mode 100644 index 0000000..2cc560d --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/ModelSchemaTests.cs @@ -0,0 +1,92 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +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 ModelSchemaTests + { + [Test] + public async Task The_model_schema_matches_the_edm_model() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(BrandsController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var result = await httpClient.GetAsync("/odata/Brands"); + result.IsSuccessStatusCode.Should().BeTrue(); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + swaggerDocument.definitions.Should().ContainKey("Brand"); + var brandSchema = swaggerDocument.definitions["Brand"]; + + brandSchema.properties.Should().ContainKey("id"); + brandSchema.properties.Should().ContainKey("code"); + brandSchema.properties.Should().NotContainKey("name"); + brandSchema.properties.Should().NotContainKey("Name"); + brandSchema.properties.Should().ContainKey("Something"); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + private static void Configuration(IAppBuilder appBuilder, Type targetController) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + config.MapODataServiceRoute("ODataRoute", "odata", GetEdmModel()); + + config.EnsureInitialized(); + } + + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Brands"); + + builder.EnableLowerCamelCase(NameResolverOptions.ProcessReflectedPropertyNames | NameResolverOptions.ProcessExplicitPropertyNames); + + builder.EntityType().Ignore(brand => brand.Name); + + return builder.GetEdmModel(); + } + } + + public class Brand + { + [Key] + public long Id { get; set; } + public string Code { get; set; } + + public string Name { get; set; } + + [DataMember(Name = "Something")] + public string Description { get; set; } + } + + public class BrandsController : ODataController + { + [EnableQuery] + public IQueryable GetBrands() + { + return Enumerable.Empty().AsQueryable(); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/ModelSharedBetweenODataAndWebApiTests.cs b/Swashbuckle.OData.Tests/Fixtures/ModelSharedBetweenODataAndWebApiTests.cs new file mode 100644 index 0000000..8993cc6 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/ModelSharedBetweenODataAndWebApiTests.cs @@ -0,0 +1,142 @@ +using System; +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.Application; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData.Tests +{ + [TestFixture] + public class ModelSharedBetweenODataAndWebApiTests + { + [Test] + public async Task It_consolidates_tags_in_final_swagger_model() + { + Action config = c => c.DocumentFilter(); + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => SharedModelsSetup.Configuration(appBuilder, config, typeof(SharedModelsSetup.SharedModelsController), typeof(SharedModelsSetup.SharedModelsWebApiController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var results = await httpClient.GetJsonAsync>>("odata/SharedModels"); + results.Should().NotBeNull(); + results.Value.Count.Should().Be(4); + + // Verify that the WebApi route in the test controller is valid + var webApiResults = await httpClient.GetJsonAsync>("SharedModels"); + webApiResults.Should().NotBeNull(); + webApiResults.Count.Should().Be(4); + + // Act and Assert + await ValidationUtils.ValidateSwaggerJson(); + } + } + } + + public class SharedModelsSetup + { + public static void Configuration(IAppBuilder appBuilder, Action unitTestConfigs, params Type[] targetControllers) + { + var config = new HttpConfiguration + { + IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always + }; + + config = ConfigureWebApi(config); + + config = ConfigureOData(appBuilder, targetControllers, config, unitTestConfigs); + + config.EnsureInitialized(); + } + + public static HttpConfiguration ConfigureWebApi(HttpConfiguration config) + { + config.MapHttpAttributeRoutes(); + + return config; + } + + private static HttpConfiguration ConfigureOData(IAppBuilder appBuilder, Type[] targetController, HttpConfiguration config, Action unitTestConfigs) + { + config = appBuilder.ConfigureHttpConfig(config, unitTestConfigs, targetController); + + config.MapODataServiceRoute("odata", "odata", GetEdmModel()); + + return config; + } + + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + + builder.EntitySet("SharedModels"); + + return builder.GetEdmModel(); + } + + public class SharedModel + { + [Key] + public int Id { get; set; } + public string Variation { get; set; } + } + + public class SharedModelsController : ODataController + { + [EnableQuery] + public IQueryable GetSharedModels() + { + IEnumerable sharedModels = new[] + { + new SharedModel { Id=1, Variation = "a"}, + new SharedModel { Id=2, Variation = "b"}, + new SharedModel { Id=3, Variation = "c"}, + new SharedModel { Id=4, Variation = "d"} + }; + return sharedModels.AsQueryable(); + } + } + + public class SharedModelsWebApiController : ApiController + { + [Route("SharedModels")] + public List Get() + { + var sharedModels = new List + { + new SharedModel { Id=1, Variation = "a"}, + new SharedModel { Id=2, Variation = "b"}, + new SharedModel { Id=3, Variation = "c"}, + new SharedModel { Id=4, Variation = "d"} + }; + return sharedModels; + } + } + } + + /// + /// Applies top-level Swagger documentation to the resources. + /// + public class ApplySharedModelsDocumentation : IDocumentFilter + { + public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer) + { + swaggerDoc.tags = new List + { + new Tag { name = "SharedModels", description = "A resource shared between OData and WebApi controllers" } + }; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/ODataSwaggerProviderTests.cs b/Swashbuckle.OData.Tests/Fixtures/ODataSwaggerProviderTests.cs index 9dc6c4a..ad91bc3 100644 --- a/Swashbuckle.OData.Tests/Fixtures/ODataSwaggerProviderTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/ODataSwaggerProviderTests.cs @@ -147,7 +147,7 @@ public async Task It_supports_both_webapi_and_odata_controllers() private static void Configuration(IAppBuilder appBuilder, Type targetController = null, Action unitTestConfigs = null) { - var config = appBuilder.GetStandardHttpConfig(targetController, unitTestConfigs); + var config = appBuilder.GetStandardHttpConfig(unitTestConfigs, targetController); var controllerSelector = new UnitTestODataVersionControllerSelector(config, targetController); config.Services.Replace(typeof(IHttpControllerSelector), controllerSelector); diff --git a/Swashbuckle.OData.Tests/Fixtures/ParameterTests/ByteParameterAndResponseTests.cs b/Swashbuckle.OData.Tests/Fixtures/ParameterTests/ByteParameterAndResponseTests.cs index 833982c..c20f65d 100644 --- a/Swashbuckle.OData.Tests/Fixtures/ParameterTests/ByteParameterAndResponseTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/ParameterTests/ByteParameterAndResponseTests.cs @@ -14,7 +14,6 @@ using Microsoft.Owin.Hosting; using NUnit.Framework; using Owin; -using Swashbuckle.Swagger; namespace Swashbuckle.OData.Tests { diff --git a/Swashbuckle.OData.Tests/Fixtures/ParameterTests/DecimalParameterAndResponseTests.cs b/Swashbuckle.OData.Tests/Fixtures/ParameterTests/DecimalParameterAndResponseTests.cs new file mode 100644 index 0000000..470f2b2 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/ParameterTests/DecimalParameterAndResponseTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Concurrent; +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 DecimalParameterAndResponseTests + { + [Test] + public async Task It_supports_entity_with_a_decimal_key() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(DecimalParametersController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var result = await httpClient.GetJsonAsync("/odata/DecimalParameters(2.3m)"); + result.Should().NotBeNull(); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/DecimalParameters({Id})", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_functions_with_a_decimal_parameter() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(DecimalParametersController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var result = await httpClient.GetJsonAsync>("/odata/DecimalParameters/Default.ResponseTest(param=2.5m)"); + result.Value.Should().Be(2.5m); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/DecimalParameters/Default.ResponseTest(param={param})", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.get.Should().NotBeNull(); + var getResponse = pathItem.get.responses.SingleOrDefault(response => response.Key == "200"); + getResponse.Should().NotBeNull(); + getResponse.Value.schema.@ref.Should().Be("#/definitions/ODataResponse[Decimal]"); + swaggerDocument.definitions.Should().ContainKey("ODataResponse[Decimal]"); + var responseSchema = swaggerDocument.definitions["ODataResponse[Decimal]"]; + 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("number"); + responseSchema.properties["value"].format.Should().Be("decimal"); + responseSchema.properties["value"].items.Should().BeNull(); + + 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("ODataRoute", "odata", GetEdmModel()); + + config.EnsureInitialized(); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet("DecimalParameters"); + + var testType = builder.EntityType(); + + testType.Collection.Function("ResponseTest").Returns().Parameter("param"); + + return builder.GetEdmModel(); + } + } + + public class DecimalParameter + { + [Key] + public decimal Id { get; set; } + } + + public class DecimalParametersController : ODataController + { + private static readonly ConcurrentDictionary Data; + + static DecimalParametersController() + { + Data = new ConcurrentDictionary(); + + var instance = new DecimalParameter + { + Id = 2.3m + }; + + Data.TryAdd(instance.Id, instance); + } + + [EnableQuery] + public SingleResult GetDecimalParameter([FromODataUri] decimal key) + { + return SingleResult.Create(Data.Values.AsQueryable().Where(value => value.Id == key)); + } + + [HttpGet] + [ResponseType(typeof(decimal))] + public IHttpActionResult ResponseTest(decimal param) + { + return Ok(param); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/ParameterizedRoutePrefixTests.cs b/Swashbuckle.OData.Tests/Fixtures/ParameterizedRoutePrefixTests.cs new file mode 100644 index 0000000..0243acc --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/ParameterizedRoutePrefixTests.cs @@ -0,0 +1,433 @@ +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.Routing.Constraints; +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 ParameterizedRoutePrefixTests + { + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_long() + { + Action configAction = config => + { + var longRoute = config.MapODataServiceRoute("longParam", "odata/{longParam}", GetEdmModel()); + longRoute.Constraints.Add("longParam", new LongRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/2147483648/Routes"); + 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/{longParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_bool() + { + Action configAction = config => + { + var boolRoute = config.MapODataServiceRoute("boolParam", "odata/{boolParam}", GetEdmModel()); + boolRoute.Constraints.Add("boolParam", new BoolRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/true/Routes"); + 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/{boolParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_dateTime() + { + Action configAction = config => + { + var dateTimeRoute = config.MapODataServiceRoute("dateTimeParam", "odata/{dateTimeParam}", GetEdmModel()); + dateTimeRoute.Constraints.Add("dateTimeParam", new DateTimeRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/2015-10-10T17:00:00Z/Routes"); + 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/{dateTimeParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_decimal() + { + Action configAction = config => + { + var decimalRoute = config.MapODataServiceRoute("decimalParam", "odata/{decimalParam}", GetEdmModel()); + decimalRoute.Constraints.Add("decimalParam", new DecimalRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/1.12/Routes"); + 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/{decimalParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_double() + { + Action configAction = config => + { + var doubleRoute = config.MapODataServiceRoute("doubleParam", "odata/{doubleParam}", GetEdmModel()); + doubleRoute.Constraints.Add("doubleParam", new DoubleRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/2.34/Routes"); + 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/{doubleParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_float() + { + Action configAction = config => + { + var floatRoute = config.MapODataServiceRoute("floatParam", "odata/{floatParam}", GetEdmModel()); + floatRoute.Constraints.Add("floatParam", new FloatRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/2.34/Routes"); + 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/{floatParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_guid() + { + Action configAction = config => + { + var guidRoute = config.MapODataServiceRoute("guidParam", "odata/{guidParam}", GetEdmModel()); + guidRoute.Constraints.Add("guidParam", new GuidRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/8b3434cb-112e-494d-82d5-17021c928012/Routes"); + 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/{guidParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_int() + { + Action configAction = config => + { + var intRoute = config.MapODataServiceRoute("intParam", "odata/{intParam}", GetEdmModel()); + intRoute.Constraints.Add("intParam", new IntRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/45/Routes"); + 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/{intParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_route_prefixes_with_multiple_parameters() + { + Action configAction = config => + { + var intRoute = config.MapODataServiceRoute("multiParam", "odata/{intParam}/{boolParam}", GetEdmModel()); + intRoute.Constraints.Add("intParam", new IntRouteConstraint()); + intRoute.Constraints.Add("boolParam", new BoolRouteConstraint()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/45/true/Routes"); + 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/{intParam}/{boolParam}/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + [Test] + public async Task It_supports_parameterized_route_prefixes_of_type_string() + { + Action configAction = config => + { + config.MapODataServiceRoute("stringParam", "odata/{stringParam}", GetEdmModel()); + }; + + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(RoutesController), configAction))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var oDataResponse = await httpClient.GetJsonAsync>>("/odata/'foo'/Routes"); + 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/'{stringParam}'/Routes", out pathItem); + pathItem.get.Should().NotBeNull(); + + await ValidationUtils.ValidateSwaggerJson(); + } + } + + private static void Configuration(IAppBuilder appBuilder, Type targetController, Action configAction) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + configAction(config); + + config.EnsureInitialized(); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Routes"); + + return builder.GetEdmModel(); + } + } + + public class Route + { + [Key] + public string Id { get; set; } + } + + public class RoutesController : ODataController + { + private static readonly ConcurrentDictionary Data; + + static RoutesController() + { + Data = new ConcurrentDictionary(); + + Enumerable.Range(0, 20).Select(i => new Route + { + Id = Guid.NewGuid().ToString() + }).ToList().ForEach(p => Data.TryAdd(p.Id, p)); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(long longParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(bool boolParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(DateTime dateTimeParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(decimal decimalParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(double doubleParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(float floatParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(Guid guidParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(int intParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(string stringParam) + { + return Data.Values.AsQueryable(); + } + + [HttpGet] + [EnableQuery] + public IQueryable GetRoutes(int intParam, bool boolParam) + { + return Data.Values.AsQueryable(); + } + } +} \ No newline at end of file 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/Fixtures/RoutePrefixTests.cs b/Swashbuckle.OData.Tests/Fixtures/RoutePrefixTests.cs new file mode 100644 index 0000000..29de577 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/RoutePrefixTests.cs @@ -0,0 +1,71 @@ +using System; +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 FluentAssertions; +using Microsoft.OData.Edm; +using Microsoft.Owin.Hosting; +using NUnit.Framework; +using Owin; + +namespace Swashbuckle.OData.Tests +{ + [TestFixture] + public class RoutePrefixTests + { + [Test] + public async Task It_handles_a_null_route_prefix() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(PinsController)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress); + // Verify that the OData route in the test controller is valid + var result = await httpClient.GetAsync("/Pins"); + result.IsSuccessStatusCode.Should().BeTrue(); + + // Act and Assert + await ValidationUtils.ValidateSwaggerJson(); + } + } + + private static void Configuration(IAppBuilder appBuilder, Type targetController) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + config.MapODataServiceRoute("ODataRoute", null, GetEdmModel()); + + config.EnsureInitialized(); + } + + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Pins"); + + return builder.GetEdmModel(); + } + } + + public class Pin + { + [Key] + public long Id { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + + public class PinsController : ODataController + { + [EnableQuery] + public IQueryable GetPins() + { + return Enumerable.Empty().AsQueryable(); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs b/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs index 8e54bd0..2a8bd68 100644 --- a/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs @@ -69,7 +69,7 @@ public async Task Schema_contains_nested_reference_types_for_web_api_controllers var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); // Assert - swaggerDocument.definitions["Client"].properties.ContainsKey("Projects").Should().BeTrue(); + swaggerDocument.definitions["Client"].properties.ContainsKey("projects").Should().BeTrue(); await ValidationUtils.ValidateSwaggerJson(); } diff --git a/Swashbuckle.OData.Tests/ODataVersionControllerSelector.cs b/Swashbuckle.OData.Tests/ODataVersionControllerSelector.cs index 5034078..577d2ae 100644 --- a/Swashbuckle.OData.Tests/ODataVersionControllerSelector.cs +++ b/Swashbuckle.OData.Tests/ODataVersionControllerSelector.cs @@ -12,8 +12,8 @@ namespace Swashbuckle.OData.Tests /// public class UnitTestODataVersionControllerSelector : UnitTestControllerSelector { - public UnitTestODataVersionControllerSelector(HttpConfiguration configuration, Type targetController) - : base(configuration, targetController) + public UnitTestODataVersionControllerSelector(HttpConfiguration configuration, params Type[] targetControllers) + : base(configuration, targetControllers) { } diff --git a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index 3eae867..bab9eee 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -120,6 +120,7 @@ True + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll True @@ -143,6 +144,12 @@ + + + + + + diff --git a/Swashbuckle.OData.Tests/UnitTestControllerSelector.cs b/Swashbuckle.OData.Tests/UnitTestControllerSelector.cs index 8204433..7ce4869 100644 --- a/Swashbuckle.OData.Tests/UnitTestControllerSelector.cs +++ b/Swashbuckle.OData.Tests/UnitTestControllerSelector.cs @@ -9,24 +9,24 @@ namespace Swashbuckle.OData.Tests { public class UnitTestControllerSelector : DefaultHttpControllerSelector { - private readonly Type _targetController; + private readonly Type[] _targetControllers; /// /// Initializes a new instance of the class. /// /// The configuration. - /// The controller being targeted in the unit test. - public UnitTestControllerSelector(HttpConfiguration configuration, Type targetController) : base(configuration) + /// The controller being targeted in the unit test. + public UnitTestControllerSelector(HttpConfiguration configuration, Type[] targetControllers) : base(configuration) { - _targetController = targetController; + _targetControllers = targetControllers; } public override IDictionary GetControllerMapping() { - if (_targetController != null) + if (_targetControllers != null) { return base.GetControllerMapping() - .Where(pair => pair.Value.ControllerType == _targetController) + .Where(pair => _targetControllers.Contains(pair.Value.ControllerType)) .ToDictionary(pair => pair.Key, pair => pair.Value); } return new Dictionary(); diff --git a/Swashbuckle.OData/DefaultCompositionRoot.cs b/Swashbuckle.OData/DefaultCompositionRoot.cs index a25f12f..65c8fce 100644 --- a/Swashbuckle.OData/DefaultCompositionRoot.cs +++ b/Swashbuckle.OData/DefaultCompositionRoot.cs @@ -73,7 +73,7 @@ public static SwaggerProviderOptions GetSwaggerProviderOptions(SwaggerDocsConfig swaggerDocsConfig.GetFieldValue("_ignoreObsoleteActions"), swaggerDocsConfig.GetFieldValue>("_groupingKeySelector"), swaggerDocsConfig.GetFieldValue>("_groupingKeyComparer"), - swaggerDocsConfig.GetFieldValue>>("_customSchemaMappings"), + GetODataCustomSchemaMappings(swaggerDocsConfig), swaggerDocsConfig.GetFieldValue>>("_schemaFilters", true).Select(factory => factory()), swaggerDocsConfig.GetFieldValue>>("_modelFilters", true).Select(factory => factory()), swaggerDocsConfig.GetFieldValue("_ignoreObsoleteProperties"), @@ -86,6 +86,18 @@ public static SwaggerProviderOptions GetSwaggerProviderOptions(SwaggerDocsConfig ); } + /// + /// Gets custom schema mappings that will only be applied to OData operations. + /// + /// The swagger docs configuration. + /// + private static IDictionary> GetODataCustomSchemaMappings(SwaggerDocsConfig swaggerDocsConfig) + { + var customSchemaMappings = swaggerDocsConfig.GetFieldValue>>("_customSchemaMappings", true); + customSchemaMappings[typeof(decimal)] = () => new Schema { type = "number", format = "decimal" }; + return customSchemaMappings; + } + /// /// Gets operation filters that will only be applied to OData operations. /// diff --git a/Swashbuckle.OData/Descriptions/ApiDescriptionEqualityComparer.cs b/Swashbuckle.OData/Descriptions/ApiDescriptionEqualityComparer.cs deleted file mode 100644 index b46c438..0000000 --- a/Swashbuckle.OData/Descriptions/ApiDescriptionEqualityComparer.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Web.Http.Description; - -namespace Swashbuckle.OData.Descriptions -{ - internal class ApiDescriptionEqualityComparer : IEqualityComparer - { - public bool Equals(ApiDescription x, ApiDescription y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - return x.HttpMethod.Equals(y.HttpMethod) - && string.Equals(NormalizeRelativePath(x.RelativePath), NormalizeRelativePath(y.RelativePath), StringComparison.OrdinalIgnoreCase) - && x.ActionDescriptor.Equals(y.ActionDescriptor); - } - - public int GetHashCode(ApiDescription obj) - { - var hashCode = obj.HttpMethod.GetHashCode(); - hashCode = (hashCode * 397) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(NormalizeRelativePath(obj.RelativePath)); - 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/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/AttributeRouteStrategy.cs b/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs index f7479e0..90acae1 100644 --- a/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs +++ b/Swashbuckle.OData/Descriptions/AttributeRouteStrategy.cs @@ -51,7 +51,7 @@ private static ODataActionDescriptor GetODataActionDescriptorFromAttributeRoute( var odataRouteAttribute = actionDescriptor.GetCustomAttributes()?.FirstOrDefault(); Contract.Assume(odataRouteAttribute != null); - var pathTemplate = HttpUtility.UrlDecode(oDataRoute.RoutePrefix.AppendPathSegment(odataRouteAttribute.PathTemplate)); + var pathTemplate = HttpUtility.UrlDecode(oDataRoute.GetRoutePrefix().AppendPathSegment(odataRouteAttribute.PathTemplate)); Contract.Assume(pathTemplate != null); return new ODataActionDescriptor(actionDescriptor, oDataRoute, pathTemplate, CreateHttpRequestMessage(actionDescriptor, oDataRoute, httpConfig)); } diff --git a/Swashbuckle.OData/Descriptions/EntityDataModelRouteGenerator.cs b/Swashbuckle.OData/Descriptions/EntityDataModelRouteGenerator.cs index 8279e5c..31dd208 100644 --- a/Swashbuckle.OData/Descriptions/EntityDataModelRouteGenerator.cs +++ b/Swashbuckle.OData/Descriptions/EntityDataModelRouteGenerator.cs @@ -45,7 +45,7 @@ private static IEnumerable GenerateEntitySetRoutes(ODataRoute oDat return oDataRoute.GetEdmModel() .EntityContainer .EntitySets()? - .Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForEntitySet(oDataRoute.RoutePrefix, entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForEntitySet(entitySet))); + .Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForEntitySet(entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForEntitySet(entitySet, oDataRoute))); } private static IEnumerable GenerateEntityRoutes(ODataRoute oDataRoute) @@ -56,7 +56,7 @@ private static IEnumerable GenerateEntityRoutes(ODataRoute oDataRo return oDataRoute.GetEdmModel() .EntityContainer .EntitySets()? - .Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForEntity(oDataRoute.RoutePrefix, entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForEntity(entitySet))); + .Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForEntity(entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForEntity(entitySet, oDataRoute))); } private static IEnumerable GenerateOperationImportRoutes(ODataRoute oDataRoute) @@ -67,7 +67,7 @@ private static IEnumerable GenerateOperationImportRoutes(ODataRout return oDataRoute.GetEdmModel() .EntityContainer .OperationImports()? - .Select(operationImport => new SwaggerRoute(ODataSwaggerUtilities.GetPathForOperationImport(oDataRoute.RoutePrefix, operationImport), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForOperationImport(operationImport))); + .Select(operationImport => new SwaggerRoute(ODataSwaggerUtilities.GetPathForOperationImport(operationImport), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForOperationImport(operationImport, oDataRoute))); } /// @@ -107,7 +107,7 @@ private static IEnumerable GenerateOperationRoutes(ODataRoute oDat var entityType = (IEdmEntityType)boundType; var edmEntitySets = oDataRoute.GetEdmModel().EntityContainer.EntitySets(); Contract.Assume(edmEntitySets != null); - routes.AddRange(edmEntitySets.Where(es => es.GetEntityType().Equals(entityType)).Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForOperationOfEntity(oDataRoute.RoutePrefix, operation, entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForOperationOfEntity(operation, entitySet)))); + routes.AddRange(edmEntitySets.Where(es => es.GetEntityType().Equals(entityType)).Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForOperationOfEntity(operation, entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForOperationOfEntity(operation, entitySet)))); } else if (boundType.TypeKind == EdmTypeKind.Collection) { @@ -118,7 +118,7 @@ private static IEnumerable GenerateOperationRoutes(ODataRoute oDat var entityType = (IEdmEntityType)collectionType.ElementType?.GetDefinition(); var edmEntitySets = oDataRoute.GetEdmModel().EntityContainer.EntitySets(); Contract.Assume(edmEntitySets != null); - routes.AddRange(edmEntitySets.Where(es => es.GetEntityType().Equals(entityType)).Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForOperationOfEntitySet(operation, entitySet, oDataRoute.RoutePrefix), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForOperationOfEntitySet(operation, entitySet)))); + routes.AddRange(edmEntitySets.Where(es => es.GetEntityType().Equals(entityType)).Select(entitySet => new SwaggerRoute(ODataSwaggerUtilities.GetPathForOperationOfEntitySet(operation, entitySet), oDataRoute, ODataSwaggerUtilities.CreateSwaggerPathForOperationOfEntitySet(operation, entitySet, oDataRoute)))); } } } 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/MapByParameterName.cs b/Swashbuckle.OData/Descriptions/MapByParameterName.cs index 080a018..97a6aeb 100644 --- a/Swashbuckle.OData/Descriptions/MapByParameterName.cs +++ b/Swashbuckle.OData/Descriptions/MapByParameterName.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.Contracts; using System.Linq; using System.Web.Http.Controllers; using Swashbuckle.Swagger; @@ -9,7 +10,9 @@ internal class MapByParameterName : IParameterMapper { public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterIndex, HttpActionDescriptor actionDescriptor) { - return actionDescriptor.GetParameters()? + var httpParameterDescriptors = actionDescriptor.GetParameters(); + Contract.Assume(httpParameterDescriptors != null); + return httpParameterDescriptors .SingleOrDefault(descriptor => string.Equals(descriptor.ParameterName, swaggerParameter.name, StringComparison.CurrentCultureIgnoreCase)); } } 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/ODataActionDescriptor.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs index 6a57746..c334080 100644 --- a/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptor.cs @@ -8,12 +8,6 @@ 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. /// @@ -29,45 +23,21 @@ public ODataActionDescriptor(HttpActionDescriptor actionDescriptor, ODataRoute r Contract.Requires(relativePathTemplate != null); Contract.Requires(request != null); - _actionDescriptor = actionDescriptor; - _route = route; - _relativePathTemplate = relativePathTemplate; - _request = request; - _operation = operation; + ActionDescriptor = actionDescriptor; + Route = route; + RelativePathTemplate = relativePathTemplate; + Request = request; + Operation = operation; } - public HttpActionDescriptor ActionDescriptor - { - get { return _actionDescriptor; } - } + public HttpActionDescriptor ActionDescriptor { get; } - public ODataRoute Route - { - get { return _route; } - } - - public string RelativePathTemplate - { - get { return _relativePathTemplate; } - } + public ODataRoute Route { get; } - public Operation Operation - { - get { return _operation; } - } + public string RelativePathTemplate { get; } - public HttpRequestMessage Request - { - get { return _request; } - } + public Operation Operation { get; } - [ContractInvariantMethod] - private void ObjectInvariants() - { - Contract.Invariant(ActionDescriptor != null); - Contract.Invariant(Route != null); - Contract.Invariant(RelativePathTemplate != null); - Contract.Invariant(Request != null); - } + public HttpRequestMessage Request { get; } } } \ No newline at end of file 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..d0138d3 100644 --- a/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs +++ b/Swashbuckle.OData/Descriptions/ODataApiExplorer.cs @@ -43,11 +43,11 @@ private Collection GetApiDescriptions() return _actionDescriptorExplorers // Gather ODataActionDescriptors from the API .SelectMany(explorer => explorer.Generate(_httpConfig)) + // Remove Duplicates + .Distinct(new ODataActionDescriptorEqualityComparer()) // Map them to ApiDescriptors .SelectMany(oDataActionDescriptor => _actionDescriptorMappers.Select(mapper => mapper.Map(oDataActionDescriptor)) .FirstOrDefault(apiDescriptions => apiDescriptions.Any()) ?? new List()) - // Remove Duplicates - .Distinct(new ApiDescriptionEqualityComparer()) .ToCollection(); } } 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/ODataRouteExtensions.cs b/Swashbuckle.OData/Descriptions/ODataRouteExtensions.cs index 1bcf3d8..0b1ed15 100644 --- a/Swashbuckle.OData/Descriptions/ODataRouteExtensions.cs +++ b/Swashbuckle.OData/Descriptions/ODataRouteExtensions.cs @@ -17,6 +17,14 @@ public static IEdmModel GetEdmModel(this ODataRoute oDataRoute) return result; } + public static string GetRoutePrefix(this ODataRoute oDataRoute) + { + Contract.Requires(oDataRoute != null); + Contract.Ensures(Contract.Result() != null); + + return oDataRoute.RoutePrefix ?? string.Empty; + } + public static ODataPathRouteConstraint GetODataPathRouteConstraint(this ODataRoute oDataRoute) { Contract.Requires(oDataRoute != null); diff --git a/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs b/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs index 1e995fe..326d9f2 100644 --- a/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs @@ -5,8 +5,10 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; +using System.Web.Http.Routing; +using System.Web.Http.Routing.Constraints; using System.Web.OData.Formatter; -using Flurl; +using System.Web.OData.Routing; using Microsoft.OData.Edm; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -23,7 +25,8 @@ internal static class ODataSwaggerUtilities /// Create the Swagger path for the Edm entity set. /// /// The entity set. - public static PathItem CreateSwaggerPathForEntitySet(IEdmEntitySet entitySet) + /// + public static PathItem CreateSwaggerPathForEntitySet(IEdmEntitySet entitySet, ODataRoute oDataRoute) { Contract.Requires(entitySet != null); Contract.Ensures(Contract.Result() != null); @@ -35,7 +38,8 @@ public static PathItem CreateSwaggerPathForEntitySet(IEdmEntitySet entitySet) .OperationId(entitySet.Name + "_Get") .Description("Returns the EntitySet " + entitySet.Name) .Tags(entitySet.Name) - .Parameters(AddQueryOptionParameters(new List())) + .Parameters(AddQueryOptionParametersForEntitySet(new List())) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.Type).DefaultErrorResponse()), post = new Operation() .Summary("Post a new entity to EntitySet " + entitySet.Name) @@ -44,11 +48,89 @@ public static PathItem CreateSwaggerPathForEntitySet(IEdmEntitySet entitySet) .Tags(entitySet.Name) .Parameters(new List() .Parameter(entitySet.GetEntityType().Name, "body", "The entity to post", entitySet.GetEntityType(), true)) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.GetEntityType()).DefaultErrorResponse()) }; } - public static IList AddQueryOptionParameters(IList parameterList) + private static IList AddRoutePrefixParameters(ODataRoute oDataRoute) + { + Contract.Requires(oDataRoute != null); + var routePrefixParameters = new List(); + var routePrefixTemplate = new UriTemplate(oDataRoute.GetRoutePrefix()); + if (routePrefixTemplate.PathSegmentVariableNames.Any()) + { + routePrefixParameters.AddRange(routePrefixTemplate.PathSegmentVariableNames.Select(pathSegmentVariableName => CreateParameter(pathSegmentVariableName, oDataRoute))); + } + return routePrefixParameters; + } + + private static Parameter CreateParameter(string pathSegmentVariableName, ODataRoute oDataRoute) + { + var parameter = new Parameter + { + name = GetOriginalParameterNameFromRoutePrefix(pathSegmentVariableName, oDataRoute), + @in = "path", + required = true + }; + object routeConstraint; + if (oDataRoute.Constraints.TryGetValue(pathSegmentVariableName, out routeConstraint) && routeConstraint is IHttpRouteConstraint) + { + SetSwaggerType(parameter, (IHttpRouteConstraint)routeConstraint); + } + else + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(string))); + } + return parameter; + } + + private static string GetOriginalParameterNameFromRoutePrefix(string pathSegmentVariableName, ODataRoute oDataRoute) + { + return oDataRoute.GetRoutePrefix().Substring(oDataRoute.GetRoutePrefix().IndexOf(pathSegmentVariableName, StringComparison.CurrentCultureIgnoreCase), pathSegmentVariableName.Length); + } + + private static void SetSwaggerType(Parameter parameter, IHttpRouteConstraint routeConstraint) + { + if (routeConstraint is BoolRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(bool))); + } + else if (routeConstraint is DateTimeRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(DateTime))); + } + else if (routeConstraint is DecimalRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(decimal))); + } + else if (routeConstraint is DoubleRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(double))); + } + else if (routeConstraint is FloatRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(float))); + } + else if (routeConstraint is GuidRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(Guid))); + } + else if (routeConstraint is IntRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(int))); + } + else if (routeConstraint is LongRouteConstraint) + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof (long))); + } + else + { + SetSwaggerType(parameter, EdmLibHelpers.GetEdmPrimitiveTypeOrNull(typeof(string))); + } + } + + public static IList AddQueryOptionParametersForEntitySet(IList parameterList) { return parameterList .Parameter("$expand", "query", "Expands related entities inline.", "string", false) @@ -60,12 +142,20 @@ public static IList AddQueryOptionParameters(IList paramet .Parameter("$count", "query", "Includes a count of the matching results in the response.", "boolean", false); } + public static IList AddQueryOptionParametersForEntity(IList parameterList) + { + return parameterList + .Parameter("$expand", "query", "Expands related entities inline.", "string", false) + .Parameter("$select", "query", "Selects which properties to include in the response.", "string", false); + } + /// /// Create the Swagger path for the Edm entity. /// /// The entity set. + /// /// - public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet) + public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet, ODataRoute oDataRoute) { Contract.Requires(entitySet != null); Contract.Ensures(Contract.Result() != null); @@ -91,6 +181,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet) .Parameters(keyParameters.DeepClone() .Parameter("$expand", "query", "Expands related entities inline.", "string", false) .Parameter("$select", "query", "Selects which properties to include in the response.", "string", false)) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Responses(new Dictionary().Response("200", "EntitySet " + entitySet.Name, entitySet.GetEntityType()).DefaultErrorResponse()), patch = new Operation() @@ -100,6 +191,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet) .Tags(entitySet.Name) .Parameters(keyParameters.DeepClone() .Parameter(entitySet.GetEntityType().Name, "body", "The entity to patch", entitySet.GetEntityType(), true)) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Responses(new Dictionary() .Response("204", "Empty response").DefaultErrorResponse()), @@ -110,6 +202,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet) .Tags(entitySet.Name) .Parameters(keyParameters.DeepClone() .Parameter(entitySet.GetEntityType().Name, "body", "The entity to put", entitySet.GetEntityType(), true)) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()), delete = new Operation().Summary("Delete entity in EntitySet " + entitySet.Name) @@ -118,6 +211,7 @@ public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet) .Tags(entitySet.Name) .Parameters(keyParameters.DeepClone() .Parameter("If-Match", "header", "If-Match header", "string", false)) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Responses(new Dictionary().Response("204", "Empty response").DefaultErrorResponse()) }; } @@ -126,8 +220,9 @@ public static PathItem CreateSwaggerPathForEntity(IEdmEntitySet entitySet) /// Create the Swagger path for the Edm operation import. /// /// The Edm operation import + /// /// The represents the related Edm operation import. - public static PathItem CreateSwaggerPathForOperationImport(IEdmOperationImport operationImport) + public static PathItem CreateSwaggerPathForOperationImport(IEdmOperationImport operationImport, ODataRoute oDataRoute) { Contract.Requires(operationImport != null); @@ -164,6 +259,9 @@ public static PathItem CreateSwaggerPathForOperationImport(IEdmOperationImport o { swaggerOperationImport.Parameters(swaggerParameters); } + + swaggerOperationImport.Parameters(AddRoutePrefixParameters(oDataRoute)); + swaggerOperationImport.Responses(swaggerResponses.DefaultErrorResponse()); return isFunctionImport ? new PathItem @@ -180,7 +278,8 @@ public static PathItem CreateSwaggerPathForOperationImport(IEdmOperationImport o /// /// The Edm operation. /// The entity set. - public static PathItem CreateSwaggerPathForOperationOfEntitySet(IEdmOperation operation, IEdmEntitySet entitySet) + /// + public static PathItem CreateSwaggerPathForOperationOfEntitySet(IEdmOperation operation, IEdmEntitySet entitySet, ODataRoute oDataRoute) { Contract.Requires(operation != null); Contract.Requires(entitySet != null); @@ -210,6 +309,7 @@ public static PathItem CreateSwaggerPathForOperationOfEntitySet(IEdmOperation op .OperationId(operation.Name + (isFunction ? "_FunctionGet" : "_ActionPost")) .Description("Call operation " + operation.Name) .OperationId(operation.Name + (isFunction ? "_FunctionGetById" : "_ActionPostById")) + .Parameters(AddRoutePrefixParameters(oDataRoute)) .Tags(entitySet.Name, isFunction ? "Function" : "Action"); if (swaggerParameters.Count > 0) @@ -344,30 +444,27 @@ public static PathItem CreateSwaggerPathForOperationOfEntity(IEdmOperation opera /// /// Gets the path for entity set. /// - /// The route prefix. /// The entity set. /// - public static Url GetPathForEntitySet(string routePrefix, IEdmEntitySet entitySet) + public static string GetPathForEntitySet(IEdmEntitySet entitySet) { Contract.Requires(entitySet != null); - return routePrefix.AppendPathSegment(entitySet.Name); + return entitySet.Name; } /// /// Get the Uri Swagger path for the Edm entity set. /// - /// The route prefix. /// The entity set. /// /// The path represents the related Edm entity set. /// - public static string GetPathForEntity(string routePrefix, IEdmEntitySet entitySet) + public static string GetPathForEntity(IEdmEntitySet entitySet) { - Contract.Requires(routePrefix != null); Contract.Requires(entitySet != null); - var singleEntityPath = GetPathForEntitySet(routePrefix, entitySet) + "("; + var singleEntityPath = GetPathForEntitySet(entitySet) + "("; singleEntityPath = entitySet.GetEntityType().GetKey().Count() == 1 ? AppendSingleColumnKeyTemplate(entitySet, singleEntityPath) : AppendMultiColumnKeyTemplate(entitySet, singleEntityPath); @@ -404,17 +501,15 @@ private static string AppendMultiColumnKeyTemplate(IEdmEntitySet entitySet, stri /// /// Get the Uri Swagger path for Edm operation import. /// - /// The route prefix. /// The Edm operation import. /// /// The path represents the related Edm operation import. /// - public static string GetPathForOperationImport(string routePrefix, IEdmOperationImport operationImport) + public static string GetPathForOperationImport(IEdmOperationImport operationImport) { - Contract.Requires(routePrefix != null); Contract.Requires(operationImport != null); - var swaggerOperationImportPath = routePrefix.AppendPathSegment(operationImport.Name).ToString(); + var swaggerOperationImportPath = operationImport.Name; if (operationImport.IsFunctionImport()) { swaggerOperationImportPath += "("; @@ -438,17 +533,15 @@ public static string GetPathForOperationImport(string routePrefix, IEdmOperation /// /// The Edm operation. /// The entity set. - /// The route prefix. /// /// The path represents the related Edm operation. /// - public static string GetPathForOperationOfEntitySet(IEdmOperation operation, IEdmEntitySet entitySet, string routePrefix) + public static string GetPathForOperationOfEntitySet(IEdmOperation operation, IEdmEntitySet entitySet) { Contract.Requires(operation != null); Contract.Requires(entitySet != null); - Contract.Requires(routePrefix != null); - var swaggerOperationPath = GetPathForEntitySet(routePrefix, entitySet) + "/" + operation.FullName(); + var swaggerOperationPath = GetPathForEntitySet(entitySet) + "/" + operation.FullName(); if (operation.IsFunction()) { swaggerOperationPath += "("; @@ -488,19 +581,17 @@ private static string GetFunctionParameterAssignmentPath(IEdmOperationParameter /// /// Get the Uri Swagger path for Edm operation bound to entity. /// - /// The route prefix. /// The Edm operation. /// The entity set. /// /// The path represents the related Edm operation. /// - public static string GetPathForOperationOfEntity(string routePrefix, IEdmOperation operation, IEdmEntitySet entitySet) + public static string GetPathForOperationOfEntity(IEdmOperation operation, IEdmEntitySet entitySet) { Contract.Requires(operation != null); Contract.Requires(entitySet != null); - Contract.Requires(routePrefix != null); - var swaggerOperationPath = GetPathForEntity(routePrefix, entitySet) + "/" + operation.FullName(); + var swaggerOperationPath = GetPathForEntity(entitySet) + "/" + operation.FullName(); if (operation.IsFunction()) { swaggerOperationPath += "("; @@ -632,6 +723,9 @@ private static string GetPrimitiveTypeAndFormat(IEdmPrimitiveType primitiveType, case EdmPrimitiveTypeKind.Double: format = "double"; return "number"; + case EdmPrimitiveTypeKind.Decimal: + format = "decimal"; + return "number"; case EdmPrimitiveTypeKind.Single: format = "float"; return "number"; @@ -708,12 +802,14 @@ private static IDictionary Response(this IDictionary parameters) + private static Operation Parameters(this Operation operation, IList parameters) { - Contract.Requires(obj != null); + Contract.Requires(operation != null); + Contract.Requires(parameters != null); - obj.parameters = parameters; - return obj; + operation.parameters = operation.parameters?.Concat(parameters).ToList() ?? parameters; + + return operation; } private static IList Parameter(this IList parameters, string name, string kind, string description, string type, bool required, string format = null) diff --git a/Swashbuckle.OData/Descriptions/OperationBuilder.cs b/Swashbuckle.OData/Descriptions/OperationBuilder.cs index 4d84a47..638f3f5 100644 --- a/Swashbuckle.OData/Descriptions/OperationBuilder.cs +++ b/Swashbuckle.OData/Descriptions/OperationBuilder.cs @@ -55,11 +55,5 @@ private IEdmModel GetEdmModel() { return _swaggerRouteBuilder.SwaggerRoute.ODataRoute.GetEdmModel(); } - - [ContractInvariantMethod] - private void ObjectInvariant() - { - Contract.Invariant(_swaggerRouteBuilder != null); - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/OperationExtensions.cs b/Swashbuckle.OData/Descriptions/OperationExtensions.cs index 298d51e..7bfd583 100644 --- a/Swashbuckle.OData/Descriptions/OperationExtensions.cs +++ b/Swashbuckle.OData/Descriptions/OperationExtensions.cs @@ -17,7 +17,7 @@ public static IDictionary GenerateSamplePathParameterValues(this .ToDictionary(queryParameter => queryParameter.name, queryParameter => queryParameter.GenerateSamplePathParameterValue()); } - public static string GenerateSampleODataAbsoluteUri(this Operation operation, string serviceRoot, string pathTemplate) + public static string GenerateSampleODataUri(this Operation operation, string serviceRoot, string pathTemplate) { Contract.Requires(operation != null); Contract.Requires(serviceRoot != null); diff --git a/Swashbuckle.OData/Descriptions/ParameterExtensions.cs b/Swashbuckle.OData/Descriptions/ParameterExtensions.cs index 422313f..809603b 100644 --- a/Swashbuckle.OData/Descriptions/ParameterExtensions.cs +++ b/Swashbuckle.OData/Descriptions/ParameterExtensions.cs @@ -72,14 +72,14 @@ public static Type GetClrType(this Parameter parameter) 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 "decimal": + return typeof(decimal); case "float": return typeof(float); case "guid": @@ -120,16 +120,16 @@ public static string GenerateSamplePathParameterValue(this Parameter parameter) 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"; + return "2.34"; + case "decimal": + return "1.12"; case "float": - return "2.0f"; + return "2.0"; case "guid": return Guid.NewGuid().ToString(); case "binary": @@ -146,27 +146,7 @@ private static Type GetEntityTypeForBodyParameter(Parameter parameter) Contract.Requires(!string.IsNullOrWhiteSpace(parameter.schema.@ref)); Contract.Requires(parameter.@in == "body"); - var fullTypeName = parameter.schema.@ref.Replace("#/definitions/", string.Empty); - - return FindType(fullTypeName); - } - - /// - /// Looks in all loaded assemblies for the given type. - /// - /// - /// The full name of the type. - /// - /// - /// The found; null if not found. - /// - private static Type FindType(string fullName) - { - return - AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic) - .SelectMany(a => a.GetTypes()) - .First(t => t.FullName.Equals(fullName)); + return parameter.schema.GetReferencedType(); } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/RestierParameterDescriptor.cs b/Swashbuckle.OData/Descriptions/RestierParameterDescriptor.cs index be9ad6b..d8be389 100644 --- a/Swashbuckle.OData/Descriptions/RestierParameterDescriptor.cs +++ b/Swashbuckle.OData/Descriptions/RestierParameterDescriptor.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.ObjectModel; using System.Diagnostics.Contracts; -using System.Web.Http; using System.Web.Http.Controllers; using Swashbuckle.Swagger; 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/Descriptions/SwaggerRoute.cs b/Swashbuckle.OData/Descriptions/SwaggerRoute.cs index d541206..36d2c91 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerRoute.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerRoute.cs @@ -1,5 +1,7 @@ using System.Diagnostics.Contracts; +using System.Web; using System.Web.OData.Routing; +using Flurl; using Swashbuckle.Swagger; namespace Swashbuckle.OData.Descriptions @@ -37,6 +39,15 @@ public string Template } } + public string PrefixedTemplate + { + get + { + Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result())); + return HttpUtility.UrlDecode(ODataRoute.GetRoutePrefix().AppendPathSegment(_template)); + } + } + public ODataRoute ODataRoute { get @@ -54,13 +65,5 @@ public PathItem PathItem return _pathItem; } } - - [ContractInvariantMethod] - private void ObjectInvariants() - { - Contract.Invariant(!string.IsNullOrWhiteSpace(_template)); - Contract.Invariant(_oDataRoute != null); - Contract.Invariant(_pathItem != null); - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/SwaggerRouteBuilder.cs b/Swashbuckle.OData/Descriptions/SwaggerRouteBuilder.cs index 58581a3..a936852 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerRouteBuilder.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerRouteBuilder.cs @@ -81,11 +81,5 @@ public Operation GetOperation(HttpMethod httpMethod) throw new ArgumentOutOfRangeException(nameof(httpMethod)); } } - - [ContractInvariantMethod] - private void ObjectInvariants() - { - Contract.Invariant(SwaggerRoute != null); - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs b/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs index 0de0ba9..44f5080 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerRouteStrategy.cs @@ -8,7 +8,6 @@ using System.Web.OData.Extensions; using System.Web.OData.Formatter; using System.Web.OData.Routing; -using Flurl; using Microsoft.OData.Edm; using Swashbuckle.Swagger; @@ -48,23 +47,23 @@ private static IEnumerable GetActionDescriptors(SwaggerRo var oDataActionDescriptors = new List(); - oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("DELETE"), potentialSwaggerRoute.PathItem.delete, potentialSwaggerRoute.Template, potentialSwaggerRoute.ODataRoute, httpConfig)); - oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("GET"), potentialSwaggerRoute.PathItem.get, potentialSwaggerRoute.Template, potentialSwaggerRoute.ODataRoute, httpConfig)); - oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("POST"), potentialSwaggerRoute.PathItem.post, potentialSwaggerRoute.Template, potentialSwaggerRoute.ODataRoute, httpConfig)); - oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("PUT"), potentialSwaggerRoute.PathItem.put, potentialSwaggerRoute.Template, potentialSwaggerRoute.ODataRoute, httpConfig)); - oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("PATCH"), potentialSwaggerRoute.PathItem.patch, potentialSwaggerRoute.Template, potentialSwaggerRoute.ODataRoute, httpConfig)); + oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("DELETE"), potentialSwaggerRoute.PathItem.delete, potentialSwaggerRoute, httpConfig)); + oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("GET"), potentialSwaggerRoute.PathItem.get, potentialSwaggerRoute, httpConfig)); + oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("POST"), potentialSwaggerRoute.PathItem.post, potentialSwaggerRoute, httpConfig)); + oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("PUT"), potentialSwaggerRoute.PathItem.put, potentialSwaggerRoute, httpConfig)); + oDataActionDescriptors.AddIfNotNull(GetActionDescriptors(new HttpMethod("PATCH"), potentialSwaggerRoute.PathItem.patch, potentialSwaggerRoute, httpConfig)); return oDataActionDescriptors; } - private static ODataActionDescriptor GetActionDescriptors(HttpMethod httpMethod, Operation potentialOperation, string potentialPathTemplate, ODataRoute oDataRoute, HttpConfiguration httpConfig) + private static ODataActionDescriptor GetActionDescriptors(HttpMethod httpMethod, Operation potentialOperation, SwaggerRoute potentialSwaggerRoute, HttpConfiguration httpConfig) { Contract.Requires(potentialOperation == null || httpConfig != null); - Contract.Requires(potentialPathTemplate != null); + Contract.Requires(potentialSwaggerRoute != null); if (potentialOperation != null) { - var request = CreateHttpRequestMessage(httpMethod, potentialOperation, potentialPathTemplate, oDataRoute, httpConfig); + var request = CreateHttpRequestMessage(httpMethod, potentialOperation, potentialSwaggerRoute, httpConfig); var actionDescriptor = request.GetHttpActionDescriptor(httpConfig); @@ -72,26 +71,26 @@ private static ODataActionDescriptor GetActionDescriptors(HttpMethod httpMethod, { actionDescriptor = MapForRestierIfNecessary(request, actionDescriptor); - return new ODataActionDescriptor(actionDescriptor, oDataRoute, potentialPathTemplate, request, potentialOperation); + return new ODataActionDescriptor(actionDescriptor, potentialSwaggerRoute.ODataRoute, potentialSwaggerRoute.PrefixedTemplate, request, potentialOperation); } } return null; } - private static HttpRequestMessage CreateHttpRequestMessage(HttpMethod httpMethod, Operation potentialOperation, string potentialPathTemplate, ODataRoute oDataRoute, HttpConfiguration httpConfig) + private static HttpRequestMessage CreateHttpRequestMessage(HttpMethod httpMethod, Operation potentialOperation, SwaggerRoute potentialSwaggerRoute, HttpConfiguration httpConfig) { Contract.Requires(httpConfig != null); - Contract.Requires(oDataRoute != null); + Contract.Requires(potentialSwaggerRoute != null); Contract.Ensures(Contract.Result() != null); - Contract.Assume(oDataRoute.Constraints != null); + Contract.Assume(potentialSwaggerRoute.ODataRoute.Constraints != null); - var oDataAbsoluteUri = potentialOperation.GenerateSampleODataAbsoluteUri(ServiceRoot, potentialPathTemplate); + var oDataAbsoluteUri = potentialOperation.GenerateSampleODataUri(ServiceRoot, potentialSwaggerRoute.PrefixedTemplate); var httpRequestMessage = new HttpRequestMessage(httpMethod, oDataAbsoluteUri); - var odataPath = GenerateSampleODataPath(oDataRoute, oDataAbsoluteUri); + var odataPath = GenerateSampleODataPath(potentialOperation, potentialSwaggerRoute); var requestContext = new HttpRequestContext { @@ -99,7 +98,7 @@ private static HttpRequestMessage CreateHttpRequestMessage(HttpMethod httpMethod }; httpRequestMessage.SetConfiguration(httpConfig); httpRequestMessage.SetRequestContext(requestContext); - + var oDataRoute = potentialSwaggerRoute.ODataRoute; var httpRequestMessageProperties = httpRequestMessage.ODataProperties(); Contract.Assume(httpRequestMessageProperties != null); httpRequestMessageProperties.Model = oDataRoute.GetEdmModel(); @@ -151,27 +150,24 @@ private static bool ReturnsValue(HttpRequestMessage request) return request.Method == HttpMethod.Get || request.Method == HttpMethod.Post; } - private static ODataPath GenerateSampleODataPath(ODataRoute oDataRoute, string sampleODataAbsoluteUri) + private static ODataPath GenerateSampleODataPath(Operation operation, SwaggerRoute swaggerRoute) { - Contract.Requires(oDataRoute != null); - Contract.Requires(oDataRoute.Constraints != null); + Contract.Requires(operation != null); + Contract.Requires(swaggerRoute != null); + Contract.Requires(swaggerRoute.ODataRoute.Constraints != null); Contract.Ensures(Contract.Result() != null); - var oDataPathRouteConstraint = oDataRoute.GetODataPathRouteConstraint(); + var oDataPathRouteConstraint = swaggerRoute.ODataRoute.GetODataPathRouteConstraint(); - var model = oDataRoute.GetEdmModel(); + var model = swaggerRoute.ODataRoute.GetEdmModel(); Contract.Assume(oDataPathRouteConstraint.PathHandler != null); - var result = oDataPathRouteConstraint.PathHandler.Parse(model, ServiceRoot.AppendPathSegment(oDataRoute.RoutePrefix), sampleODataAbsoluteUri); + var odataPath = operation.GenerateSampleODataUri(ServiceRoot, swaggerRoute.Template).Replace(ServiceRoot, string.Empty); + + var result = oDataPathRouteConstraint.PathHandler.Parse(model, ServiceRoot, odataPath); Contract.Assume(result != null); return result; } - - [ContractInvariantMethod] - private void ObjectInvariant() - { - Contract.Invariant(_swaggerRouteGenerators != null); - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/TypeExtensions.cs b/Swashbuckle.OData/Descriptions/TypeExtensions.cs index f1d08f2..30ec76b 100644 --- a/Swashbuckle.OData/Descriptions/TypeExtensions.cs +++ b/Swashbuckle.OData/Descriptions/TypeExtensions.cs @@ -1,7 +1,6 @@ // 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.ComponentModel; using System.Diagnostics.Contracts; diff --git a/Swashbuckle.OData/Descriptions/TypeHelper.cs b/Swashbuckle.OData/Descriptions/TypeHelper.cs index 740924c..1e55f24 100644 --- a/Swashbuckle.OData/Descriptions/TypeHelper.cs +++ b/Swashbuckle.OData/Descriptions/TypeHelper.cs @@ -220,5 +220,23 @@ private static Type GetInnerGenericType(Type interfaceType) return null; } + + /// + /// Looks in all loaded assemblies for the given type. + /// + /// + /// The full name of the type. + /// + /// + /// The found; null if not found. + /// + public static Type FindType(string fullName) + { + return + AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .SelectMany(a => a.GetTypes()) + .First(t => t.FullName.Equals(fullName)); + } } } \ No newline at end of file diff --git a/Swashbuckle.OData/EnableQueryFilter.cs b/Swashbuckle.OData/EnableQueryFilter.cs index c6cb1dd..1cce723 100644 --- a/Swashbuckle.OData/EnableQueryFilter.cs +++ b/Swashbuckle.OData/EnableQueryFilter.cs @@ -5,6 +5,7 @@ using System.Web.OData; using Swashbuckle.OData.Descriptions; using Swashbuckle.Swagger; +using System; namespace Swashbuckle.OData { @@ -21,7 +22,9 @@ public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescrip if (HasEnableQueryAttribute(apiDescription) && !HasAnyQueryOptionParameters(operation)) { - operation.parameters = ODataSwaggerUtilities.AddQueryOptionParameters(operation.parameters ?? new List()); + operation.parameters = ReturnsCollection(apiDescription) + ? ODataSwaggerUtilities.AddQueryOptionParametersForEntitySet(operation.parameters ?? new List()) + : ODataSwaggerUtilities.AddQueryOptionParametersForEntity(operation.parameters ?? new List()); } } @@ -36,5 +39,21 @@ private static bool HasEnableQueryAttribute(ApiDescription apiDescription) Contract.Assume(httpActionDescriptor != null); return httpActionDescriptor.GetCustomAttributes().Any(); } + + private static bool ReturnsCollection(ApiDescription apiDescription) + { + var httpActionDescriptor = apiDescription.ActionDescriptor; + Contract.Assume(httpActionDescriptor != null); + + Type returnType = httpActionDescriptor.ReturnType; + + var responseTypeAttr = httpActionDescriptor.GetCustomAttributes().FirstOrDefault(); + if (responseTypeAttr != null) + returnType = responseTypeAttr.ResponseType; + + return returnType.IsCollection(); + } + + } } \ No newline at end of file diff --git a/Swashbuckle.OData/HttpConfigurationExtensions.cs b/Swashbuckle.OData/HttpConfigurationExtensions.cs index 0b5ccfc..05ab064 100644 --- a/Swashbuckle.OData/HttpConfigurationExtensions.cs +++ b/Swashbuckle.OData/HttpConfigurationExtensions.cs @@ -5,7 +5,6 @@ using System.Web.Http; using System.Web.Http.Routing; using System.Web.OData.Routing; -using Flurl; using Newtonsoft.Json; using Swashbuckle.OData.Descriptions; @@ -59,10 +58,10 @@ public static SwaggerRouteBuilder AddCustomSwaggerRoute(this HttpConfiguration h Contract.Requires(httpConfig.Properties != null); Contract.Ensures(Contract.Result() != null); - var fullRouteTemplate = HttpUtility.UrlDecode(oDataRoute.RoutePrefix.AppendPathSegment(routeTemplate)); - Contract.Assume(!string.IsNullOrWhiteSpace(fullRouteTemplate)); + var urlDecodedTemplate = HttpUtility.UrlDecode(routeTemplate); + Contract.Assume(!string.IsNullOrWhiteSpace(urlDecodedTemplate)); - var swaggerRoute = new SwaggerRoute(fullRouteTemplate, oDataRoute); + var swaggerRoute = new SwaggerRoute(urlDecodedTemplate, oDataRoute); var swaggerRouteBuilder = new SwaggerRouteBuilder(swaggerRoute); diff --git a/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs b/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs index de7833e..ebb096c 100644 --- a/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs +++ b/Swashbuckle.OData/LimitSchemaGraphToTopLevelEntity.cs @@ -33,7 +33,7 @@ public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IAp private static bool IsEntityType(KeyValuePair definition) { - return !definition.Key.StartsWith("ODataResponse["); + return !definition.Key.Contains("ODataResponse["); } private static void RemoveCollectionTypeProperty(KeyValuePair property, ICollection> properties) diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index e9670fc..81e22df 100644 --- a/Swashbuckle.OData/ODataSwaggerProvider.cs +++ b/Swashbuckle.OData/ODataSwaggerProvider.cs @@ -2,9 +2,10 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; -using System.ServiceModel.Description; using System.Web.Http; using System.Web.Http.Description; +using System.Web.OData.Routing; +using Microsoft.OData.Edm; using Swashbuckle.Application; using Swashbuckle.OData.Descriptions; using Swashbuckle.Swagger; @@ -90,7 +91,7 @@ public SwaggerDocument GetSwagger(string rootUrl, string apiVersion) .Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.IsObsolete())) .OrderBy(_options.GroupingKeySelector, _options.GroupingKeyComparer) .GroupBy(apiDesc => apiDesc.RelativePathSansQueryString()) - .ToDictionary(group => "/" + group.Key, group => CreatePathItem(group, schemaRegistry)); + .ToDictionary(group => "/" + group.Key, group => CreatePathItem(@group, schemaRegistry)); var rootUri = new Uri(rootUrl); var port = !rootUri.IsDefaultPort ? ":" + rootUri.Port : string.Empty; @@ -200,23 +201,25 @@ private Operation CreateOperation(ApiDescription apiDescription, SchemaRegistry Contract.Requires(schemaRegistry != null); Contract.Requires(apiDescription.ParameterDescriptions != null); + + var edmModel = ((ODataRoute)apiDescription.Route).GetEdmModel(); + var parameters = apiDescription.ParameterDescriptions .Select(paramDesc => { var inPath = apiDescription.RelativePathSansQueryString().Contains("{" + paramDesc.Name + "}"); var swaggerApiParameterDescription = paramDesc as SwaggerApiParameterDescription; return swaggerApiParameterDescription != null - ? CreateParameter(swaggerApiParameterDescription, inPath, schemaRegistry) - : CreateParameter(paramDesc, inPath, schemaRegistry); + ? CreateParameter(swaggerApiParameterDescription, inPath, schemaRegistry, edmModel) + : CreateParameter(paramDesc, inPath, schemaRegistry, edmModel); }) .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.GetOrRegisterResponseType(responseType) }); + responses.Add("200", new Response { description = "OK", schema = schemaRegistry.GetOrRegisterResponseType(edmModel, responseType) }); var operation = new Operation { @@ -239,7 +242,7 @@ private Operation CreateOperation(ApiDescription apiDescription, SchemaRegistry return operation; } - private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry) + private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry, IEdmModel edmModel) { Contract.Requires(paramDesc != null); Contract.Requires(schemaRegistry != null); @@ -257,7 +260,7 @@ private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool @default = paramDesc.ParameterDescriptor.DefaultValue }; - var schema = schemaRegistry.GetOrRegisterParameterType(paramDesc.ParameterDescriptor); + var schema = schemaRegistry.GetOrRegisterParameterType(edmModel, paramDesc.ParameterDescriptor); if (parameter.@in == "body") parameter.schema = schema; else @@ -266,7 +269,7 @@ private static Parameter CreateParameter(ApiParameterDescription paramDesc, bool return parameter; } - private static Parameter CreateParameter(SwaggerApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry) + private static Parameter CreateParameter(SwaggerApiParameterDescription paramDesc, bool inPath, SchemaRegistry schemaRegistry, IEdmModel edmModel) { Contract.Requires(paramDesc != null); Contract.Requires(schemaRegistry != null); @@ -288,7 +291,7 @@ private static Parameter CreateParameter(SwaggerApiParameterDescription paramDes var parameterType = paramDesc.ParameterDescriptor.ParameterType; Contract.Assume(parameterType != null); - var schema = schemaRegistry.GetOrRegisterParameterType(paramDesc.ParameterDescriptor); + var schema = schemaRegistry.GetOrRegisterParameterType(edmModel, paramDesc.ParameterDescriptor); if (parameter.@in == "body") parameter.schema = schema; else @@ -329,14 +332,5 @@ private IEnumerable GetApiDescriptionsFor(string apiVersion) Contract.Assume(result != null); return result; } - - [ContractInvariantMethod] - private void ObjectInvariant() - { - Contract.Invariant(_options != null); - Contract.Invariant(_apiVersions != null); - Contract.Invariant(_httpConfig != null); - Contract.Invariant(_odataApiExplorer != null); - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/ODataSwaggerProviderOptions.cs b/Swashbuckle.OData/ODataSwaggerProviderOptions.cs index c582756..5759a08 100644 --- a/Swashbuckle.OData/ODataSwaggerProviderOptions.cs +++ b/Swashbuckle.OData/ODataSwaggerProviderOptions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; -using System.Linq; using System.Web.Http.Description; using Swashbuckle.Swagger; @@ -28,7 +27,7 @@ public ODataSwaggerProviderOptions(SwaggerProviderOptions swaggerProviderOptions DescribeStringEnumsInCamelCase = swaggerProviderOptions.DescribeStringEnumsInCamelCase; OperationFilters = swaggerProviderOptions.OperationFilters ?? new List(); DocumentFilters = swaggerProviderOptions.DocumentFilters ?? new List(); - ConflictingActionsResolver = swaggerProviderOptions.ConflictingActionsResolver ?? DefaultConflictingActionsResolver; + ConflictingActionsResolver = swaggerProviderOptions.ConflictingActionsResolver; } public Func VersionSupportResolver { get; private set; } @@ -67,14 +66,5 @@ private static string DefaultSchemaIdSelector(Type type) { return type.FriendlyId(); } - - private static ApiDescription DefaultConflictingActionsResolver(IEnumerable apiDescriptions) - { - Contract.Requires(apiDescriptions != null); - Contract.Requires(apiDescriptions.Any()); - - var first = apiDescriptions.First(); - throw new NotSupportedException($"Not supported by Swagger 2.0: Multiple operations with path '{first.RelativePathSansQueryString()}' and method '{first.HttpMethod}'. " + "See the config setting - \"ResolveConflictingActions\" for a potential workaround"); - } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Properties/AssemblyInfo.cs b/Swashbuckle.OData/Properties/AssemblyInfo.cs index 3400bc4..25aa7c0 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.12.0")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("2.15.0")] \ No newline at end of file diff --git a/Swashbuckle.OData/SchemaExtensions.cs b/Swashbuckle.OData/SchemaExtensions.cs new file mode 100644 index 0000000..6f61496 --- /dev/null +++ b/Swashbuckle.OData/SchemaExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Diagnostics.Contracts; +using System.Web.OData; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData +{ + internal static class SchemaExtensions + { + public static Type GetReferencedType(this Schema schema) + { + Contract.Requires(schema != null); + + if (schema.@ref != null) + { + var fullTypeName = schema.@ref.Replace("#/definitions/", string.Empty); + + return TypeHelper.FindType(fullTypeName); + } + + return null; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/SchemaRegistryExtensions.cs b/Swashbuckle.OData/SchemaRegistryExtensions.cs index 24791f1..d786598 100644 --- a/Swashbuckle.OData/SchemaRegistryExtensions.cs +++ b/Swashbuckle.OData/SchemaRegistryExtensions.cs @@ -1,10 +1,15 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; using System.ServiceModel.Description; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.OData; +using System.Web.OData.Formatter; +using Microsoft.OData.Edm; using Swashbuckle.OData.Descriptions; using Swashbuckle.Swagger; @@ -12,19 +17,76 @@ namespace Swashbuckle.OData { internal static class SchemaRegistryExtensions { - public static Schema GetOrRegisterParameterType(this SchemaRegistry registry, HttpParameterDescriptor parameterDescriptor) + public static Schema GetOrRegisterParameterType(this SchemaRegistry registry, IEdmModel edmModel, HttpParameterDescriptor parameterDescriptor) { if (IsODataActionParameter(parameterDescriptor)) { - return ((ODataActionParameterDescriptor) parameterDescriptor).Schema; + var schema = ((ODataActionParameterDescriptor) parameterDescriptor).Schema; + RegisterReferencedTypes(registry, edmModel, schema); + return schema; } if (IsAGenericODataTypeThatShouldBeUnwrapped(parameterDescriptor.ParameterType, MessageDirection.Input)) { - var genericArguments = parameterDescriptor.ParameterType.GetGenericArguments(); - Contract.Assume(genericArguments != null); - return registry.GetOrRegister(genericArguments[0]); + return HandleGenericODataTypeThatShouldBeUnwrapped(registry, edmModel, parameterDescriptor.ParameterType); } - return registry.GetOrRegister(parameterDescriptor.ParameterType); + var schema1 = registry.GetOrRegister(parameterDescriptor.ParameterType); + ApplyEdmModelPropertyNamesToSchema(registry, edmModel, parameterDescriptor.ParameterType); + return schema1; + } + + private static void RegisterReferencedTypes(SchemaRegistry registry, IEdmModel edmModel, Schema schema) + { + Contract.Requires(registry != null); + Contract.Requires(schema != null); + + while (true) + { + Contract.Assume(schema != null); + + var referencedType = schema.GetReferencedType(); + + if (referencedType != null) + { + registry.GetOrRegister(referencedType); + FixSchemaReference(registry, schema, referencedType); + ApplyEdmModelPropertyNamesToSchema(registry, edmModel, referencedType); + return; + } + + if (schema.properties != null && schema.properties.Any()) + { + foreach (var property in schema.properties) + { + RegisterReferencedTypes(registry, edmModel, property.Value); + } + return; + } + + if (schema.items != null) + { + schema = schema.items; + continue; + } + break; + } + } + + private static void FixSchemaReference(SchemaRegistry registry, Schema schema, Type referencedType) + { + Contract.Requires(schema.@ref != null); + + var schemaIdSelector = registry.GetInstanceField>("_schemaIdSelector", true); + + schema.@ref = "#/definitions/" + schemaIdSelector(referencedType); + } + + private static Schema HandleGenericODataTypeThatShouldBeUnwrapped(SchemaRegistry registry, IEdmModel edmModel, Type type) + { + var genericArguments = type.GetGenericArguments(); + Contract.Assume(genericArguments != null); + var schema = registry.GetOrRegister(genericArguments[0]); + ApplyEdmModelPropertyNamesToSchema(registry, edmModel, genericArguments[0]); + return schema; } private static bool IsODataActionParameter(HttpParameterDescriptor parameterDescriptor) @@ -32,16 +94,14 @@ private static bool IsODataActionParameter(HttpParameterDescriptor parameterDesc return parameterDescriptor is ODataActionParameterDescriptor; } - public static Schema GetOrRegisterResponseType(this SchemaRegistry registry, Type type) + public static Schema GetOrRegisterResponseType(this SchemaRegistry registry, IEdmModel edmModel, Type type) { Contract.Requires(registry != null); Contract.Requires(type != null); if (IsAGenericODataTypeThatShouldBeUnwrapped(type, MessageDirection.Output)) { - var genericArguments = type.GetGenericArguments(); - Contract.Assume(genericArguments != null); - return registry.GetOrRegister(genericArguments[0]); + return HandleGenericODataTypeThatShouldBeUnwrapped(registry, edmModel, type); } Type elementType; if (IsResponseCollection(type, MessageDirection.Output, out elementType)) @@ -50,15 +110,65 @@ public static Schema GetOrRegisterResponseType(this SchemaRegistry registry, Typ var listType = openListType.MakeGenericType(elementType); var openOdataType = typeof (ODataResponse<>); var odataType = openOdataType.MakeGenericType(listType); - return registry.GetOrRegister(odataType); + var schema = registry.GetOrRegister(odataType); + ApplyEdmModelPropertyNamesToSchema(registry, edmModel, elementType); + return schema; } if (IsResponseWithPrimiveTypeNotSupportedByJson(type, MessageDirection.Output)) { var openOdataType = typeof(ODataResponse<>); var odataType = openOdataType.MakeGenericType(type); - return registry.GetOrRegister(odataType); + var schema = registry.GetOrRegister(odataType); + return schema; } - return registry.GetOrRegister(type); + var schema1 = registry.GetOrRegister(type); + ApplyEdmModelPropertyNamesToSchema(registry, edmModel, type); + return schema1; + } + + private static void ApplyEdmModelPropertyNamesToSchema(SchemaRegistry registry, IEdmModel edmModel, Type type) + { + var entityReference = registry.GetOrRegister(type); + if (entityReference.@ref != null) + { + var definitionKey = entityReference.@ref.Replace("#/definitions/", string.Empty); + var schemaDefinition = registry.Definitions[definitionKey]; + var edmType = edmModel.GetEdmType(type) as IEdmStructuredType; + if (edmType != null) + { + var edmProperties = new Dictionary(); + foreach (var property in schemaDefinition.properties) + { + var currentProperty = type.GetProperty(property.Key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + var edmPropertyName = GetEdmPropertyName(currentProperty, edmType); + if (edmPropertyName != null) + { + edmProperties.Add(edmPropertyName, property.Value); + } + } + schemaDefinition.properties = edmProperties; + } + } + } + + private static string GetEdmPropertyName(MemberInfo currentProperty, IEdmStructuredType edmType) + { + Contract.Requires(currentProperty != null); + Contract.Requires(edmType != null); + + var edmProperty = edmType.Properties().SingleOrDefault(property => property.Name.Equals(currentProperty.Name, StringComparison.CurrentCultureIgnoreCase)); + + return edmProperty != null ? GetPropertyNameForEdmModel(currentProperty, edmProperty) : null; + } + + private static string GetPropertyNameForEdmModel(MemberInfo currentProperty, IEdmNamedElement edmProperty) + { + Contract.Requires(currentProperty != null); + Contract.Requires(edmProperty != null); + + var dataMemberAttribute = currentProperty.GetCustomAttributes()?.SingleOrDefault(); + + return !string.IsNullOrWhiteSpace(dataMemberAttribute?.Name) ? dataMemberAttribute.Name : edmProperty.Name; } private static bool IsResponseWithPrimiveTypeNotSupportedByJson(Type type, MessageDirection messageDirection) @@ -69,6 +179,10 @@ private static bool IsResponseWithPrimiveTypeNotSupportedByJson(Type type, Messa { return true; } + if (type == typeof(decimal)) + { + return true; + } } return false; } diff --git a/Swashbuckle.OData/SwaggerDocsConfigExtensions.cs b/Swashbuckle.OData/SwaggerDocsConfigExtensions.cs index 685b32c..09a9e40 100644 --- a/Swashbuckle.OData/SwaggerDocsConfigExtensions.cs +++ b/Swashbuckle.OData/SwaggerDocsConfigExtensions.cs @@ -20,9 +20,9 @@ public static T GetFieldValue(this SwaggerDocsConfig swaggerDocsConfig, strin public static Dictionary GetSecurityDefinitions(this SwaggerDocsConfig swaggerDocsConfig) { var securitySchemeBuilders = swaggerDocsConfig.GetFieldValue>("_securitySchemeBuilders"); - return securitySchemeBuilders != null && securitySchemeBuilders.Any() - ? securitySchemeBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.InvokeFunction("Build")) - : new Dictionary(); + Contract.Assume(securitySchemeBuilders != null); + + return securitySchemeBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.InvokeFunction("Build")); } } } \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index ea2344b..c4fd3b1 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -154,6 +154,7 @@ True + @@ -173,14 +174,15 @@ + - + @@ -214,6 +216,7 @@ + diff --git a/appveyor.yml b/appveyor.yml index 6f858e6..6bc5a07 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.12.0.{build} +version: 2.15.0.{build} before_build: - cmd: nuget restore