diff --git a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj index 737392c..a465eae 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. + Provides explicit, unit tested support for OData actions. Fixes #59. 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.12.1 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..06ab32e 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"); diff --git a/Swashbuckle.OData.Tests/Fixtures/ModelSchemaTests.cs b/Swashbuckle.OData.Tests/Fixtures/ModelSchemaTests.cs new file mode 100644 index 0000000..e180774 --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/ModelSchemaTests.cs @@ -0,0 +1,89 @@ +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().ContainKey("name"); + brandSchema.properties.Should().ContainKey("Something"); + + 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() + { + var builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Brands"); + + builder.EnableLowerCamelCase(NameResolverOptions.ProcessReflectedPropertyNames | NameResolverOptions.ProcessExplicitPropertyNames); + + 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/SchemaTests.cs b/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs index 669cd5d..808206a 100644 --- a/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/SchemaTests.cs @@ -49,7 +49,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/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index 3eae867..8a24c5b 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,7 @@ + diff --git a/Swashbuckle.OData/ODataSwaggerProvider.cs b/Swashbuckle.OData/ODataSwaggerProvider.cs index e9670fc..edf0db0 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 diff --git a/Swashbuckle.OData/Properties/AssemblyInfo.cs b/Swashbuckle.OData/Properties/AssemblyInfo.cs index 3400bc4..f6ef64c 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.12.1")] \ No newline at end of file diff --git a/Swashbuckle.OData/SchemaRegistryExtensions.cs b/Swashbuckle.OData/SchemaRegistryExtensions.cs index 24791f1..39f52e2 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,7 +17,7 @@ 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)) { @@ -20,11 +25,20 @@ public static Schema GetOrRegisterParameterType(this SchemaRegistry registry, Ht } 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 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 +46,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 +62,55 @@ 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; + } + 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) + { + schemaDefinition.properties = schemaDefinition.properties.ToDictionary(property => + { + var currentProperty = type.GetProperty(property.Key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + return GetEdmPropertyName(currentProperty, edmType); + }, property => property.Value); + } } - return registry.GetOrRegister(type); + } + + private static string GetEdmPropertyName(MemberInfo currentProperty, IEdmStructuredType edmType) + { + var currentPropertyName = GetPropertyNameForEdmModel(currentProperty); + + var edmProperty = edmType.Properties().SingleOrDefault(property => property.Name.Equals(currentPropertyName, StringComparison.CurrentCultureIgnoreCase)); + + return edmProperty != null ? edmProperty.Name : currentPropertyName; + } + + private static string GetPropertyNameForEdmModel(MemberInfo currentProperty) + { + var dataMemberAttribute = currentProperty.GetCustomAttributes()?.SingleOrDefault(); + + return !string.IsNullOrWhiteSpace(dataMemberAttribute?.Name) ? dataMemberAttribute.Name : currentProperty.Name; } private static bool IsResponseWithPrimiveTypeNotSupportedByJson(Type type, MessageDirection messageDirection) diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index ea2344b..50e5f7c 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -154,6 +154,7 @@ True + diff --git a/appveyor.yml b/appveyor.yml index 6f858e6..aefb039 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.12.0.{build} +version: 2.12.1.{build} before_build: - cmd: nuget restore