From cbfdbbbbaf58bfcffd7911314c232f9946f8ad0c Mon Sep 17 00:00:00 2001 From: ArcKos00 <105163313+ArcKos00@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:25:27 +0300 Subject: [PATCH 1/4] Added support for multiple FromJson properties Attribute (#21) --- src/Demo/Controllers/ProductController.cs | 20 +- src/Demo/Models/Products/ProductData.cs | 10 + .../Wrapper/ComplexProductWithDataWrapper.cs | 24 ++ .../MultiPartJsonOperationFilter.cs | 279 ++++++++++-------- 4 files changed, 201 insertions(+), 132 deletions(-) create mode 100644 src/Demo/Models/Products/ProductData.cs create mode 100644 src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs diff --git a/src/Demo/Controllers/ProductController.cs b/src/Demo/Controllers/ProductController.cs index 7f37f3d..7791a24 100644 --- a/src/Demo/Controllers/ProductController.cs +++ b/src/Demo/Controllers/ProductController.cs @@ -60,5 +60,23 @@ public IActionResult PostWrapper([FromForm] ComplexProductWrapper wrapper) { images = images?.Select(a => a.FileName) }); } - } + + [HttpPost("wrapper/complex-data")] + public IActionResult PostDataWrapper([FromForm] ComplexProductWithDataWrapper wrapper) + { + var productName = wrapper.ProductName; + var productId = wrapper.ProductId ?? throw new NullReferenceException(nameof(wrapper.ProductId)); + var product = wrapper.Product; + var images = wrapper.Files; + var data = wrapper.ProductData; + return Ok(new + { + productName, + productId, + product = JsonConvert.SerializeObject(product), + images = images?.Select(a => a.FileName), + data = JsonConvert.SerializeObject(data) + }); + } + } } \ No newline at end of file diff --git a/src/Demo/Models/Products/ProductData.cs b/src/Demo/Models/Products/ProductData.cs new file mode 100644 index 0000000..b6b9cc9 --- /dev/null +++ b/src/Demo/Models/Products/ProductData.cs @@ -0,0 +1,10 @@ +using System; + +namespace Demo.Models.Products +{ + public class ProductData + { + public double Price { get; set; } + public DateTime StartDate { get; set; } + } +} diff --git a/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs b/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs new file mode 100644 index 0000000..4eb8ccc --- /dev/null +++ b/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs @@ -0,0 +1,24 @@ +using Demo.Models.Products; +using Microsoft.AspNetCore.Http; +using Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.Attributes; +using System.ComponentModel.DataAnnotations; + +namespace Demo.Models.Wrapper +{ + public class ComplexProductWithDataWrapper + { + [FromJson] + [Required] + public Product Product { get; set; } + + [FromJson] + public ProductData? ProductData { get; set; } + + // [FromJson] <-- not required + [Required] + public int? ProductId { get; set; } + + public string ProductName { get; set; } + public IFormFileCollection Files { get; set; } + } +} diff --git a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs index bf2f891..24d25b8 100644 --- a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs @@ -14,135 +14,152 @@ using Swashbuckle.AspNetCore.SwaggerGen; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.Integrations { - /// - /// Aggregates form fields in Swagger to one JSON field and add example. - /// - public class MultiPartJsonOperationFilter : IOperationFilter { - private readonly IServiceProvider _serviceProvider; - private readonly IOptions _jsonOptions; - private readonly IOptions _newtonsoftJsonOption; - private readonly IOptions _generatorOptions; - - /// - /// Creates - /// - public MultiPartJsonOperationFilter(IServiceProvider serviceProvider, IOptions jsonOptions, - IOptions newtonsoftJsonOption, - IOptions generatorOptions) { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _jsonOptions = jsonOptions; - _newtonsoftJsonOption = newtonsoftJsonOption; - _generatorOptions = generatorOptions; - } - - /// - public void Apply(OpenApiOperation operation, OperationFilterContext context) { - var descriptors = context.ApiDescription.ActionDescriptor.Parameters.ToList(); - foreach (var descriptor in descriptors) { - descriptor.Name = GetParameterName(descriptor.Name); - - // Get property with [FromJson] - var propertyInfo = GetPropertyInfo(descriptor); - - if (propertyInfo != null) { - var mediaType = operation.RequestBody.Content.First().Value; - - // Group all exploded properties. - var groupedProperties = mediaType.Schema.Properties - .GroupBy(pair => pair.Key.Split('.')[0]); - - var schemaProperties = new Dictionary(); - - var propertyInfoName = GetParameterName(propertyInfo.Name); - - foreach (var property in groupedProperties) { - if (property.Key == propertyInfoName) { - AddEncoding(mediaType, propertyInfo); - - var openApiSchema = GetSchema(context, propertyInfo); - if (openApiSchema is null) continue; - schemaProperties.Add(property.Key, openApiSchema); - } - else { - schemaProperties.Add(property.Key, property.First().Value); - } - } - - // Override schema properties - mediaType.Schema.Properties = schemaProperties; - } - } - } - - private string GetParameterName(string name) - { - // Support for DescribeAllParametersInCamelCase - return _generatorOptions.Value.DescribeAllParametersInCamelCase - ? name.ToCamelCase() - : name; - } - - /// - /// Generate schema for propertyInfo - /// - /// - private OpenApiSchema? GetSchema(OperationFilterContext context, PropertyInfo propertyInfo) { - bool present = - context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out OpenApiSchema openApiSchema); - if (!present) { - _ = context.SchemaGenerator.GenerateSchema(propertyInfo.PropertyType, context.SchemaRepository); - if (!context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out openApiSchema)) return null; - var schema = context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; - AddDescription(schema, openApiSchema.Title); - AddExample(propertyInfo, schema); - } - - return context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; - } - - private static void AddDescription(OpenApiSchema openApiSchema, string schemaDisplayName) { - openApiSchema.Description += $"\n See {schemaDisplayName} model."; - } - - private static void AddEncoding(OpenApiMediaType mediaType, PropertyInfo propertyInfo) { - mediaType.Encoding = mediaType.Encoding - .Where(pair => !pair.Key.ToLower().Contains(propertyInfo.Name.ToLower())) - .ToDictionary(pair => pair.Key, pair => pair.Value); - mediaType.Encoding.Add(propertyInfo.Name, new OpenApiEncoding() { - ContentType = "application/json", - Explode = false - }); - } - - private void AddExample(PropertyInfo propertyInfo, OpenApiSchema openApiSchema) { - var example = GetExampleFor(propertyInfo.PropertyType); - // Example do not exist. Use default. - if (example == null) return; - string json; - - if (JsonMultipartFormDataOptions.JsonSerializerChoice == JsonSerializerChoice.SystemText) - json = JsonSerializer.Serialize(example, _jsonOptions.Value.JsonSerializerOptions); - else if (JsonMultipartFormDataOptions.JsonSerializerChoice == JsonSerializerChoice.Newtonsoft) - json = JsonConvert.SerializeObject(example, _newtonsoftJsonOption.Value.SerializerSettings); - else - json = JsonSerializer.Serialize(example); - openApiSchema.Example = new OpenApiString(json); - } - - private object GetExampleFor(Type parameterType) { - var makeGenericType = typeof(IExamplesProvider<>).MakeGenericType(parameterType); - var method = makeGenericType.GetMethod("GetExamples"); - var exampleProvider = _serviceProvider.GetService(makeGenericType); - // Example do not exist. Use default. - if (exampleProvider == null) - return null; - var example = method?.Invoke(exampleProvider, null); - return example; - } - - private static PropertyInfo GetPropertyInfo(ParameterDescriptor descriptor) => - descriptor.ParameterType.GetProperties() - .SingleOrDefault(f => f.GetCustomAttribute() != null); - } +namespace Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.Integrations +{ + /// + /// Aggregates form fields in Swagger to one JSON field and add example. + /// + public class MultiPartJsonOperationFilter : IOperationFilter + { + private readonly IServiceProvider _serviceProvider; + private readonly IOptions _jsonOptions; + private readonly IOptions _newtonsoftJsonOption; + private readonly IOptions _generatorOptions; + + /// + /// Creates + /// + public MultiPartJsonOperationFilter(IServiceProvider serviceProvider, IOptions jsonOptions, + IOptions newtonsoftJsonOption, + IOptions generatorOptions) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _jsonOptions = jsonOptions; + _newtonsoftJsonOption = newtonsoftJsonOption; + _generatorOptions = generatorOptions; + } + + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var descriptors = context.ApiDescription.ActionDescriptor.Parameters.ToList(); + foreach (var descriptor in descriptors) + { + descriptor.Name = GetParameterName(descriptor.Name); + + // Get property with [FromJson] + foreach (var propertyInfo in GetPropertyInfo(descriptor)) + { + if (propertyInfo != null) + { + var mediaType = operation.RequestBody.Content.First().Value; + + // Group all exploded properties. + var groupedProperties = mediaType.Schema.Properties + .GroupBy(pair => pair.Key.Split('.')[0]); + + var schemaProperties = new Dictionary(); + + var propertyInfoName = GetParameterName(propertyInfo.Name); + + foreach (var property in groupedProperties) + { + if (property.Key == propertyInfoName) + { + AddEncoding(mediaType, propertyInfo); + + var openApiSchema = GetSchema(context, propertyInfo); + if (openApiSchema is null) continue; + schemaProperties.Add(property.Key, openApiSchema); + } + else + { + schemaProperties.Add(property.Key, property.First().Value); + } + } + + // Override schema properties + mediaType.Schema.Properties = schemaProperties; + } + } + } + } + + private string GetParameterName(string name) + { + // Support for DescribeAllParametersInCamelCase + return _generatorOptions.Value.DescribeAllParametersInCamelCase + ? name.ToCamelCase() + : name; + } + + /// + /// Generate schema for propertyInfo + /// + /// + private OpenApiSchema? GetSchema(OperationFilterContext context, PropertyInfo propertyInfo) + { + bool present = + context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out OpenApiSchema openApiSchema); + if (!present) + { + _ = context.SchemaGenerator.GenerateSchema(propertyInfo.PropertyType, context.SchemaRepository); + if (!context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out openApiSchema)) return null; + var schema = context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; + AddDescription(schema, openApiSchema.Title); + AddExample(propertyInfo, schema); + } + + return context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; + } + + private static void AddDescription(OpenApiSchema openApiSchema, string schemaDisplayName) + { + openApiSchema.Description += $"\n See {schemaDisplayName} model."; + } + + private static void AddEncoding(OpenApiMediaType mediaType, PropertyInfo propertyInfo) + { + mediaType.Encoding = mediaType.Encoding + .Where(pair => !pair.Key.ToLower().Contains(propertyInfo.Name.ToLower())) + .ToDictionary(pair => pair.Key, pair => pair.Value); + mediaType.Encoding.Add(propertyInfo.Name, new OpenApiEncoding() + { + ContentType = "application/json", + Explode = false + }); + } + + private void AddExample(PropertyInfo propertyInfo, OpenApiSchema openApiSchema) + { + var example = GetExampleFor(propertyInfo.PropertyType); + // Example do not exist. Use default. + if (example == null) return; + string json; + + if (JsonMultipartFormDataOptions.JsonSerializerChoice == JsonSerializerChoice.SystemText) + json = JsonSerializer.Serialize(example, _jsonOptions.Value.JsonSerializerOptions); + else if (JsonMultipartFormDataOptions.JsonSerializerChoice == JsonSerializerChoice.Newtonsoft) + json = JsonConvert.SerializeObject(example, _newtonsoftJsonOption.Value.SerializerSettings); + else + json = JsonSerializer.Serialize(example); + openApiSchema.Example = new OpenApiString(json); + } + + private object GetExampleFor(Type parameterType) + { + var makeGenericType = typeof(IExamplesProvider<>).MakeGenericType(parameterType); + var method = makeGenericType.GetMethod("GetExamples"); + var exampleProvider = _serviceProvider.GetService(makeGenericType); + // Example do not exist. Use default. + if (exampleProvider == null) + return null; + var example = method?.Invoke(exampleProvider, null); + return example; + } + + private static List GetPropertyInfo(ParameterDescriptor descriptor) => + descriptor.ParameterType.GetProperties() + .Where(f => f.GetCustomAttribute() != null).ToList(); + } } \ No newline at end of file From 3ad0df769c008fe81a01a949de6ee23217b1d8db Mon Sep 17 00:00:00 2001 From: Morasiu Date: Tue, 22 Oct 2024 10:34:51 +0200 Subject: [PATCH 2/4] Null reference fix --- src/Demo/Controllers/ProductController.cs | 10 +++++----- src/Demo/Demo.csproj | 1 + src/Demo/Models/Products/Product.cs | 2 +- .../Wrapper/ComplexProductWithDataWrapper.cs | 6 +++--- src/Demo/Models/Wrapper/ComplexProductWrapper.cs | 8 ++++---- src/Demo/Models/Wrapper/ProductWrapper.cs | 4 ++-- src/Demo/Models/Wrapper/RequiredProductWrapper.cs | 4 ++-- src/Demo/Models/Wrapper/SimpleProductWrapper.cs | 4 ++-- .../Integrations/MultiPartJsonOperationFilter.cs | 2 +- .../UnitTests/FormDataJsonBinderProviderTests.cs | 2 +- src/tests/UnitTests/JsonModelBinderTests.cs | 14 ++++++-------- 11 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Demo/Controllers/ProductController.cs b/src/Demo/Controllers/ProductController.cs index 7791a24..83c5580 100644 --- a/src/Demo/Controllers/ProductController.cs +++ b/src/Demo/Controllers/ProductController.cs @@ -29,14 +29,14 @@ public IActionResult Post([FromForm] MultipartRequiredFormData data) { public IActionResult Post([FromForm] RequiredProductWrapper wrapper) { var wrapperProduct = wrapper.Product ?? throw new NullReferenceException(nameof(wrapper.Product)); var images = wrapper.Files; - return Ok(new { wrapperProduct, images = images?.Select(a => a.FileName) }); + return Ok(new { wrapperProduct, images = images.Select(a => a.FileName) }); } [HttpPost("wrapper")] public IActionResult PostWrapper([FromForm] ProductWrapper wrapper) { var wrapperProduct = wrapper.Product ?? throw new NullReferenceException(nameof(wrapper.Product)); var images = wrapper.Files; - return Ok(new { wrapperProduct, images = images?.Select(a => a.FileName) }); + return Ok(new { wrapperProduct, images = images.Select(a => a.FileName) }); } [HttpPost("wrapper/simple")] @@ -44,7 +44,7 @@ public IActionResult PostWrapper([FromForm] SimpleProductWrapper wrapper) { var productName = wrapper.ProductName; var productId = wrapper.ProductId ?? throw new NullReferenceException(nameof(wrapper.ProductId)); var images = wrapper.Files; - return Ok(new { productName, productId, images = images?.Select(a => a.FileName) }); + return Ok(new { productName, productId, images = images.Select(a => a.FileName) }); } [HttpPost("wrapper/complex")] @@ -57,7 +57,7 @@ public IActionResult PostWrapper([FromForm] ComplexProductWrapper wrapper) { productName, productId, product = JsonConvert.SerializeObject(product), - images = images?.Select(a => a.FileName) + images = images.Select(a => a.FileName) }); } @@ -74,7 +74,7 @@ public IActionResult PostDataWrapper([FromForm] ComplexProductWithDataWrapper wr productName, productId, product = JsonConvert.SerializeObject(product), - images = images?.Select(a => a.FileName), + images = images.Select(a => a.FileName), data = JsonConvert.SerializeObject(data) }); } diff --git a/src/Demo/Demo.csproj b/src/Demo/Demo.csproj index e87f800..101644e 100644 --- a/src/Demo/Demo.csproj +++ b/src/Demo/Demo.csproj @@ -2,6 +2,7 @@ net6.0 + enable diff --git a/src/Demo/Models/Products/Product.cs b/src/Demo/Models/Products/Product.cs index b63e0a7..6bdfdb4 100644 --- a/src/Demo/Models/Products/Product.cs +++ b/src/Demo/Models/Products/Product.cs @@ -3,7 +3,7 @@ namespace Demo.Models.Products { public class Product { public Guid Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public ProductType Type { get; set; } } diff --git a/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs b/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs index 4eb8ccc..1b44a94 100644 --- a/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs +++ b/src/Demo/Models/Wrapper/ComplexProductWithDataWrapper.cs @@ -9,7 +9,7 @@ public class ComplexProductWithDataWrapper { [FromJson] [Required] - public Product Product { get; set; } + public Product Product { get; set; } = null!; [FromJson] public ProductData? ProductData { get; set; } @@ -18,7 +18,7 @@ public class ComplexProductWithDataWrapper [Required] public int? ProductId { get; set; } - public string ProductName { get; set; } - public IFormFileCollection Files { get; set; } + public string? ProductName { get; set; } + public IFormFileCollection Files { get; set; } = new FormFileCollection(); } } diff --git a/src/Demo/Models/Wrapper/ComplexProductWrapper.cs b/src/Demo/Models/Wrapper/ComplexProductWrapper.cs index 2b83452..063cb83 100644 --- a/src/Demo/Models/Wrapper/ComplexProductWrapper.cs +++ b/src/Demo/Models/Wrapper/ComplexProductWrapper.cs @@ -8,12 +8,12 @@ namespace Demo.Models.Wrapper; public class ComplexProductWrapper { [FromJson] [Required] - public Product Product { get; set; } - + public Product Product { get; set; } = null!; + // [FromJson] <-- not required [Required] public int? ProductId { get; set; } - public string ProductName { get; set; } - public IFormFileCollection Files { get; set; } + public string? ProductName { get; set; } + public IFormFileCollection Files { get; set; } = new FormFileCollection(); } \ No newline at end of file diff --git a/src/Demo/Models/Wrapper/ProductWrapper.cs b/src/Demo/Models/Wrapper/ProductWrapper.cs index a9f32ee..a1f8c65 100644 --- a/src/Demo/Models/Wrapper/ProductWrapper.cs +++ b/src/Demo/Models/Wrapper/ProductWrapper.cs @@ -5,8 +5,8 @@ namespace Demo.Models.Wrapper { public class ProductWrapper { [FromJson] // <-- This attribute is required for binding. - public Product Product { get; set; } + public Product? Product { get; set; } - public IFormFileCollection Files { get; set; } + public IFormFileCollection Files { get; set; } = new FormFileCollection(); } } \ No newline at end of file diff --git a/src/Demo/Models/Wrapper/RequiredProductWrapper.cs b/src/Demo/Models/Wrapper/RequiredProductWrapper.cs index debf6a7..03256c8 100644 --- a/src/Demo/Models/Wrapper/RequiredProductWrapper.cs +++ b/src/Demo/Models/Wrapper/RequiredProductWrapper.cs @@ -7,9 +7,9 @@ namespace Demo.Models.Wrapper { public class RequiredProductWrapper { [Required] [FromJson] // <-- This attribute is required for binding. - public Product Product { get; set; } + public Product Product { get; set; } = null!; [Required] - public IFormFileCollection Files { get; set; } + public IFormFileCollection Files { get; set; } = new FormFileCollection(); } } \ No newline at end of file diff --git a/src/Demo/Models/Wrapper/SimpleProductWrapper.cs b/src/Demo/Models/Wrapper/SimpleProductWrapper.cs index 5035eba..07e59f4 100644 --- a/src/Demo/Models/Wrapper/SimpleProductWrapper.cs +++ b/src/Demo/Models/Wrapper/SimpleProductWrapper.cs @@ -7,7 +7,7 @@ public class SimpleProductWrapper { [Required] public int? ProductId { get; set; } - public string ProductName { get; set; } - public IFormFileCollection Files { get; set; } + public string? ProductName { get; set; } + public IFormFileCollection Files { get; set; } = new FormFileCollection(); } } \ No newline at end of file diff --git a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs index 24d25b8..1635715 100644 --- a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs @@ -97,7 +97,7 @@ private string GetParameterName(string name) /// Generate schema for propertyInfo /// /// - private OpenApiSchema? GetSchema(OperationFilterContext context, PropertyInfo propertyInfo) + private OpenApiSchema GetSchema(OperationFilterContext context, PropertyInfo propertyInfo) { bool present = context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out OpenApiSchema openApiSchema); diff --git a/src/tests/UnitTests/FormDataJsonBinderProviderTests.cs b/src/tests/UnitTests/FormDataJsonBinderProviderTests.cs index a043222..737f79f 100644 --- a/src/tests/UnitTests/FormDataJsonBinderProviderTests.cs +++ b/src/tests/UnitTests/FormDataJsonBinderProviderTests.cs @@ -14,7 +14,7 @@ public void GetBinder_ContextIsNull_ShouldThrowException() { var options = Substitute.For>(); var sut = new FormDataJsonBinderProvider(options); // Act - var action = () => sut.GetBinder(null); + var action = () => sut.GetBinder(null!); // Assert action.Should().Throw(); } diff --git a/src/tests/UnitTests/JsonModelBinderTests.cs b/src/tests/UnitTests/JsonModelBinderTests.cs index 1b64d06..8e42869 100644 --- a/src/tests/UnitTests/JsonModelBinderTests.cs +++ b/src/tests/UnitTests/JsonModelBinderTests.cs @@ -1,9 +1,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Primitives; -using NUnit.Framework.Internal; using Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.Integrations; -using UnitTests.TestData; using UnitTests.TestData.Types; namespace UnitTests; @@ -14,7 +12,7 @@ public void BindModelAsync_NullContext_ShouldReturnNull() { // Arrange var sut = new JsonModelBinder(); // Act - var action = async () => await sut.BindModelAsync(null); + var action = async () => await sut.BindModelAsync(null!); // Assert action.Should().ThrowExactlyAsync(); } @@ -23,16 +21,16 @@ public void BindModelAsync_NullContext_ShouldReturnNull() { public async Task BindModelAsync_ShouldBindData() { // Arrange var sut = new JsonModelBinder(); - var testType = new TestType() { + var testType = new TestType { Id = 1, Text = Guid.NewGuid().ToString() }; var context = Substitute.For(); context.ValueProvider.GetValue(nameof(TestTypeContainer.Test)) - .Returns(x => new ValueProviderResult(new StringValues(new []{ JsonSerializer.Serialize(testType)}))); - context.ModelName.Returns(x => nameof(TestTypeContainer.Test)); - context.ModelType.Returns(x => typeof(TestType)); - context.ModelState.Returns(x => new ModelStateDictionary()); + .Returns(_ => new ValueProviderResult(new StringValues(new []{ JsonSerializer.Serialize(testType)}))); + context.ModelName.Returns(_ => nameof(TestTypeContainer.Test)); + context.ModelType.Returns(_ => typeof(TestType)); + context.ModelState.Returns(_ => new ModelStateDictionary()); // Act await sut.BindModelAsync(context); // Assert From 298eca417a31cce68b5edbc37405438cc5df8dd3 Mon Sep 17 00:00:00 2001 From: Morasiu Date: Tue, 22 Oct 2024 10:53:43 +0200 Subject: [PATCH 3/4] Cleanup --- .../MultiPartJsonOperationFilter.cs | 102 ++++++++++-------- src/tests/UnitTests/JsonModelBinderTests.cs | 8 +- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs index 1635715..b9a30e6 100644 --- a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Integrations/MultiPartJsonOperationFilter.cs @@ -43,46 +43,55 @@ public MultiPartJsonOperationFilter(IServiceProvider serviceProvider, IOptions GetSchemaProperties(OperationFilterContext context, OpenApiMediaType mediaType, + PropertyInfo propertyInfo) { + // Group all exploded properties. + var allProperties = mediaType.Schema.Properties + .GroupBy(pair => pair.Key.Split('.')[0]); + + var schemaProperties = new Dictionary(); + + var propertyInfoName = GetParameterName(propertyInfo.Name); + + foreach (var property in allProperties) + { + if (property.Key == propertyInfoName) + { + AddEncoding(mediaType, propertyInfo); + + var openApiSchema = GetSchema(context, propertyInfo); + if (openApiSchema is null) continue; + schemaProperties.Add(property.Key, openApiSchema); + } + else { - if (propertyInfo != null) - { - var mediaType = operation.RequestBody.Content.First().Value; - - // Group all exploded properties. - var groupedProperties = mediaType.Schema.Properties - .GroupBy(pair => pair.Key.Split('.')[0]); - - var schemaProperties = new Dictionary(); - - var propertyInfoName = GetParameterName(propertyInfo.Name); - - foreach (var property in groupedProperties) - { - if (property.Key == propertyInfoName) - { - AddEncoding(mediaType, propertyInfo); - - var openApiSchema = GetSchema(context, propertyInfo); - if (openApiSchema is null) continue; - schemaProperties.Add(property.Key, openApiSchema); - } - else - { - schemaProperties.Add(property.Key, property.First().Value); - } - } - - // Override schema properties - mediaType.Schema.Properties = schemaProperties; - } + schemaProperties.Add(property.Key, property.First().Value); } } + + return schemaProperties; } private string GetParameterName(string name) @@ -99,16 +108,16 @@ private string GetParameterName(string name) /// private OpenApiSchema GetSchema(OperationFilterContext context, PropertyInfo propertyInfo) { - bool present = + var present = context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out OpenApiSchema openApiSchema); - if (!present) - { - _ = context.SchemaGenerator.GenerateSchema(propertyInfo.PropertyType, context.SchemaRepository); - if (!context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out openApiSchema)) return null; - var schema = context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; - AddDescription(schema, openApiSchema.Title); - AddExample(propertyInfo, schema); - } + + if (present) return context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; + + _ = context.SchemaGenerator.GenerateSchema(propertyInfo.PropertyType, context.SchemaRepository); + if (!context.SchemaRepository.TryLookupByType(propertyInfo.PropertyType, out openApiSchema)) return null; + var schema = context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; + AddDescription(schema, openApiSchema.Title); + AddExample(propertyInfo, schema); return context.SchemaRepository.Schemas[openApiSchema.Reference.Id]; } @@ -123,8 +132,7 @@ private static void AddEncoding(OpenApiMediaType mediaType, PropertyInfo propert mediaType.Encoding = mediaType.Encoding .Where(pair => !pair.Key.ToLower().Contains(propertyInfo.Name.ToLower())) .ToDictionary(pair => pair.Key, pair => pair.Value); - mediaType.Encoding.Add(propertyInfo.Name, new OpenApiEncoding() - { + mediaType.Encoding.Add(propertyInfo.Name, new OpenApiEncoding { ContentType = "application/json", Explode = false }); @@ -158,7 +166,7 @@ private object GetExampleFor(Type parameterType) return example; } - private static List GetPropertyInfo(ParameterDescriptor descriptor) => + private static List GetPropertyWithFromJson(ParameterDescriptor descriptor) => descriptor.ParameterType.GetProperties() .Where(f => f.GetCustomAttribute() != null).ToList(); } diff --git a/src/tests/UnitTests/JsonModelBinderTests.cs b/src/tests/UnitTests/JsonModelBinderTests.cs index 8e42869..ba94a3d 100644 --- a/src/tests/UnitTests/JsonModelBinderTests.cs +++ b/src/tests/UnitTests/JsonModelBinderTests.cs @@ -4,7 +4,7 @@ using Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.Integrations; using UnitTests.TestData.Types; -namespace UnitTests; +namespace UnitTests; public class JsonModelBinderTests { [Test] @@ -16,7 +16,7 @@ public void BindModelAsync_NullContext_ShouldReturnNull() { // Assert action.Should().ThrowExactlyAsync(); } - + [Test] public async Task BindModelAsync_ShouldBindData() { // Arrange @@ -27,7 +27,7 @@ public async Task BindModelAsync_ShouldBindData() { }; var context = Substitute.For(); context.ValueProvider.GetValue(nameof(TestTypeContainer.Test)) - .Returns(_ => new ValueProviderResult(new StringValues(new []{ JsonSerializer.Serialize(testType)}))); + .Returns(_ => new ValueProviderResult(new StringValues(new[] { JsonSerializer.Serialize(testType) }))); context.ModelName.Returns(_ => nameof(TestTypeContainer.Test)); context.ModelType.Returns(_ => typeof(TestType)); context.ModelState.Returns(_ => new ModelStateDictionary()); @@ -36,7 +36,7 @@ public async Task BindModelAsync_ShouldBindData() { // Assert context.Result.IsModelSet.Should().BeTrue(); context.Result.Model.Should().NotBeNull().And.BeAssignableTo(); - var result = (TestType) context.Result.Model!; + var result = (TestType)context.Result.Model!; result.Id.Should().Be(testType.Id); result.Text.Should().Be(testType.Text); } From 1d51eec0cc8462b207cf1075fffe49c8931c6628 Mon Sep 17 00:00:00 2001 From: Morasiu Date: Tue, 22 Oct 2024 11:14:08 +0200 Subject: [PATCH 4/4] Version update --- CHANGELOG.md | 4 ++++ ...Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.csproj | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199900a..c38b9ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.10.0] - 2024-10-22 +### Added +- Support for multiple FromJson attributes in one wrapper class. Thanks @ArcKos00! + ## [1.9.0] - 2023-12-17 ### Added - Support for DescribeAllParametersInCamelCase. Thanks @machekku! diff --git a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.csproj b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.csproj index f4d6e1e..d3822fc 100644 --- a/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.csproj +++ b/src/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport/Swashbuckle.AspNetCore.JsonMultipartFormDataSupport.csproj @@ -11,7 +11,7 @@ 0.0.0.0 0.0.0.0 git - 1.9.0 + 1.10.0 net6.0 true