Skip to content

Commit 8bd5f9d

Browse files
committed
[Swashbuckle] Non-null reference types and types with IDisallowDefaultValue are flagged as required.
1 parent 569c7d0 commit 8bd5f9d

16 files changed

+583
-16
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>8.8.0</VersionPrefix>
5+
<VersionPrefix>8.8.1</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Unlike regular C# enums which are limited to numeric values and lack extensibili
7272
* Include additional fields, properties and behavior
7373
* Use polymorphism to define custom behavior for each value
7474
* Prevent creation of invalid values
75-
* Integrate seamlessly with JSON serializers, MessagePack, Entity Framework Core and ASP.NET Core
75+
* Integrate seamlessly with JSON serializers, MessagePack, Entity Framework Core, ASP.NET Core and Swashbuckle (OpenAPI)
7676

7777
Install: `Install-Package Thinktecture.Runtime.Extensions`
7878

@@ -345,6 +345,7 @@ Key Features:
345345
* JSON serialization (System.Text.Json and Newtonsoft.Json)
346346
* Entity Framework Core support
347347
* ASP.NET Core Model Binding
348+
* Swashbuckle (OpenAPI)
348349
* MessagePack serialization
349350
* Rich feature set:
350351
* Type conversion and comparison operators

docs

Submodule docs updated from 2f49925 to 6b43f4a
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Reflection;
12
using Microsoft.OpenApi.Models;
23
using Swashbuckle.AspNetCore.SwaggerGen;
34
using Thinktecture.Internal;
@@ -12,6 +13,19 @@ namespace Thinktecture.Swashbuckle.Internal.ComplexValueObjects;
1213
/// </summary>
1314
public class ComplexValueObjectSchemaFilter : IInternalComplexValueObjectSchemaFilter
1415
{
16+
private readonly IRequiredMemberEvaluator _requiredMemberEvaluator;
17+
18+
/// <summary>
19+
/// This is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to
20+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
21+
/// any release. You should only use it directly in your code with extreme caution and knowing that
22+
/// doing so can result in application failures when updating to a new Thinktecture.Runtime.Extensions release.
23+
/// </summary>
24+
public ComplexValueObjectSchemaFilter(IRequiredMemberEvaluator requiredMemberEvaluator)
25+
{
26+
_requiredMemberEvaluator = requiredMemberEvaluator;
27+
}
28+
1529
/// <inheritdoc />
1630
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
1731
{
@@ -24,6 +38,12 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context)
2438
/// <inheritdoc />
2539
public void Apply(OpenApiSchema schema, SchemaFilterContext context, Metadata.ComplexValueObject metadata)
2640
{
41+
foreach (var memberInfo in metadata.AssignableMembers)
42+
{
43+
if (_requiredMemberEvaluator.IsRequired(schema, context, memberInfo))
44+
schema.Required.Add(memberInfo.Name);
45+
}
46+
2747
// Otherwise the type gets: "additionalProperties": false
2848
schema.AdditionalPropertiesAllowed = true;
2949
schema.AdditionalProperties = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Reflection;
2+
using Microsoft.OpenApi.Models;
3+
using Swashbuckle.AspNetCore.SwaggerGen;
4+
5+
namespace Thinktecture.Swashbuckle.Internal.ComplexValueObjects;
6+
7+
/// <summary>
8+
/// This is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to
9+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
10+
/// any release. You should only use it directly in your code with extreme caution and knowing that
11+
/// doing so can result in application failures when updating to a new Thinktecture.Runtime.Extensions release.
12+
/// </summary>
13+
public class DefaultRequiredMemberEvaluator : IRequiredMemberEvaluator
14+
{
15+
private readonly NullabilityInfoContext _nullabilityInfoContext = new();
16+
17+
/// <inheritdoc />
18+
public bool IsRequired(OpenApiSchema schema, SchemaFilterContext context, MemberInfo member)
19+
{
20+
var (type, nullabilityInfo) = member switch
21+
{
22+
PropertyInfo propertyInfo => (propertyInfo.PropertyType, _nullabilityInfoContext.Create(propertyInfo)),
23+
FieldInfo fieldInfo => (fieldInfo.FieldType, _nullabilityInfoContext.Create(fieldInfo)),
24+
_ => throw new ArgumentException($"Assignable member of a complex value object must be a field or a property but found '{member.GetType().FullName}'.", nameof(member))
25+
};
26+
27+
if (typeof(IDisallowDefaultValue).IsAssignableFrom(type))
28+
return true;
29+
30+
// Use "ReadState" instead of "WriteState" because the members are read-only
31+
if (type.IsClass && nullabilityInfo.ReadState == NullabilityState.NotNull)
32+
return true;
33+
34+
// Is struct, nullable struct or nullable reference type
35+
return false;
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Reflection;
2+
using Microsoft.OpenApi.Models;
3+
using Swashbuckle.AspNetCore.SwaggerGen;
4+
5+
namespace Thinktecture.Swashbuckle.Internal.ComplexValueObjects;
6+
7+
/// <summary>
8+
/// This is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to
9+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
10+
/// any release. You should only use it directly in your code with extreme caution and knowing that
11+
/// doing so can result in application failures when updating to a new Thinktecture.Runtime.Extensions release.
12+
/// </summary>
13+
public interface IRequiredMemberEvaluator
14+
{
15+
/// <summary>
16+
/// This is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to
17+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
18+
/// any release. You should only use it directly in your code with extreme caution and knowing that
19+
/// doing so can result in application failures when updating to a new Thinktecture.Runtime.Extensions release.
20+
/// </summary>
21+
bool IsRequired(OpenApiSchema schema, SchemaFilterContext context, MemberInfo member);
22+
}

src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureParameterFilter.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,23 @@ public void Apply(
2323

2424
metadata?.Switch(
2525
(Filter: this, parameter, context),
26-
keyedSmartEnum: static (state, smartEnumMetadata) => state.Filter.Apply(state.parameter, state.context, smartEnumMetadata),
27-
keyedValueObject: static (state, keyedValueObjectMetadata) => state.Filter.Apply(state.parameter, state.context, keyedValueObjectMetadata),
28-
complexValueObject: static (state, complexValueObjectMetadata) => state.Filter.Apply(state.parameter, state.context, complexValueObjectMetadata),
29-
adHocUnion: static (state, adHocUnionMetadata) => state.Filter.Apply(state.parameter, state.context, adHocUnionMetadata));
26+
keyedSmartEnum: static (state, smartEnumMetadata) => Apply(state.parameter, state.context, smartEnumMetadata),
27+
keyedValueObject: static (state, keyedValueObjectMetadata) => Apply(state.parameter, state.context, keyedValueObjectMetadata),
28+
complexValueObject: static (state, complexValueObjectMetadata) => Apply(state.parameter, state.context, complexValueObjectMetadata),
29+
adHocUnion: static (state, adHocUnionMetadata) => Apply(state.parameter, state.context, adHocUnionMetadata));
3030
}
3131

32-
private void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.Keyed.SmartEnum metadata)
32+
private static void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.Keyed.SmartEnum metadata)
3333
{
3434
parameter.Schema = context.SchemaGenerator.GenerateSchema(metadata.Type, context.SchemaRepository);
3535
}
3636

37-
private void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.Keyed.ValueObject metadata)
37+
private static void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.Keyed.ValueObject metadata)
3838
{
3939
parameter.Schema = context.SchemaGenerator.GenerateSchema(metadata.Type, context.SchemaRepository);
4040
}
4141

42-
private void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.ComplexValueObject metadata)
42+
private static void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.ComplexValueObject metadata)
4343
{
4444
// IParsable
4545
if (typeof(IObjectFactory<string>).IsAssignableFrom(metadata.Type))
@@ -48,7 +48,7 @@ private void Apply(OpenApiParameter parameter, ParameterFilterContext context, M
4848
}
4949
}
5050

51-
private void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.AdHocUnion metadata)
51+
private static void Apply(OpenApiParameter parameter, ParameterFilterContext context, Metadata.AdHocUnion metadata)
5252
{
5353
// IParsable
5454
if (typeof(IObjectFactory<string>).IsAssignableFrom(metadata.Type))

src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/ServiceCollectionExtensions.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ public static IServiceCollection AddThinktectureOpenApiFilters(
2525
Action<ThinktectureSchemaFilterOptions>? configureOptions = null)
2626
{
2727
services.TryAddSingleton<IOpenApiValueFactoryProvider, JsonSerializerOpenApiValueFactoryProvider>();
28+
services.TryAddSingleton<IKeyedValueObjectSchemaFilter, KeyedValueObjectSchemaFilter>();
29+
services.TryAddSingleton<IComplexValueObjectSchemaFilter, ComplexValueObjectSchemaFilter>();
30+
services.TryAddSingleton<IRequiredMemberEvaluator, DefaultRequiredMemberEvaluator>();
31+
services.TryAddSingleton<IAdHocUnionSchemaFilter, AdHocUnionSchemaFilter>();
32+
services.TryAddSingleton<ISmartEnumSchemaFilter, DefaultSmartEnumSchemaFilter>();
33+
services.TryAddSingleton<ISmartEnumSchemaExtension, NoSmartEnumSchemaExtension>();
2834

2935
services
3036
.AddSingleton<JsonSerializerOptionsResolver>()
31-
.AddSingleton<ISmartEnumSchemaFilter, DefaultSmartEnumSchemaFilter>()
32-
.AddSingleton<IKeyedValueObjectSchemaFilter, KeyedValueObjectSchemaFilter>()
33-
.AddSingleton<IComplexValueObjectSchemaFilter, ComplexValueObjectSchemaFilter>()
34-
.AddSingleton<IAdHocUnionSchemaFilter, AdHocUnionSchemaFilter>()
35-
.AddSingleton<ISmartEnumSchemaExtension, NoSmartEnumSchemaExtension>()
3637
.Configure<ThinktectureSchemaFilterOptions>(options => configureOptions?.Invoke(options))
3738
.AddOptions<SwaggerGenOptions>()
3839
.Configure((SwaggerGenOptions options,

test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests/Swashbuckle/Helpers/TestController.ComplexValueObjects.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ public class Nullable : ControllerBase
6969
}
7070
}
7171

72+
[Route("/")]
73+
public class BodyWithRequiredProperties : ControllerBase
74+
{
75+
[HttpPost("/test")]
76+
public ValueObjectWithRequiredProperties Get([FromBody] ValueObjectWithRequiredProperties value)
77+
{
78+
return value;
79+
}
80+
81+
[Route("/")]
82+
public class Nullable : ControllerBase
83+
{
84+
[HttpPost("/test")]
85+
public ValueObjectWithRequiredProperties? Get([FromBody] ValueObjectWithRequiredProperties? value = null)
86+
{
87+
return value;
88+
}
89+
}
90+
}
91+
7292
[Route("/")]
7393
public class Form : ControllerBase
7494
{

0 commit comments

Comments
 (0)