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