From 684a3ab325a1dff690e4014db59494c4d49ec95c Mon Sep 17 00:00:00 2001 From: desjoerd Date: Fri, 12 Sep 2025 09:31:24 +0200 Subject: [PATCH 01/15] =?UTF-8?q?=F0=9F=94=A7=20start=20versioning=20with?= =?UTF-8?q?=20previews?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- version.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/version.json b/version.json index 7cf6f37..fe3ec17 100644 --- a/version.json +++ b/version.json @@ -1,13 +1,20 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.7", + "version": "0.8-preview", "publicReleaseRefSpec": [ "^refs/heads/main$", - "^refs/heads/v\\d+(?:\\.\\d+)?$" + "^refs/heads/v\\d+(?:\\.\\d+)?$", + "^refs/heads/release/v\\d+(?:\\.\\d+)?$" ], "cloudBuild": { "buildNumber": { "enabled": true } + }, + "release": { + "tagName": "v{version}", + "branchName": "release/v{version}", + "versionIncrement": "minor", + "firstUnstableTag": "preview" } } From 0938a71ce6809278ebb1827740fdb8914e80e082 Mon Sep 17 00:00:00 2001 From: desjoerd Date: Fri, 12 Sep 2025 09:31:44 +0200 Subject: [PATCH 02/15] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20to=20SDK=20?= =?UTF-8?q?.NET=2010=20RC1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 345f67e..e7cce33 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "9.0.306" + "version": "10.0.100-rc.1.25451.107" } } From f9f5b6b82cbaf23a0aa9c78265714552d549a5a6 Mon Sep 17 00:00:00 2001 From: desjoerd Date: Fri, 12 Sep 2025 10:27:32 +0200 Subject: [PATCH 03/15] =?UTF-8?q?=F0=9F=93=8C=20Pin=20Swashbuckle=20to=20.?= =?UTF-8?q?NET=208=20&=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 9 +++++---- .../OptionalValues.Examples.Swashbuckle.csproj | 2 +- .../OptionalValues.Examples.Swashbuckle/openapi.yaml | 2 +- .../OptionalValues.Swashbuckle.csproj | 2 +- .../OptionalValues.Swashbuckle.Tests.csproj | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1e89f23..13eb350 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,11 +1,12 @@ - net8.0;net9.0 - net8.0;net9.0 + net8.0;net9.0;net10.0 + net8.0;net9.0 + net8.0;net9.0;net10.0 - optional partial json undefined jsonpatch jsonmergepatch patch System.Text.Json Api - unspecified + optional partial json undefined jsonpatch jsonmergepatch patch + System.Text.Json Api unspecified diff --git a/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj b/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj index 40b5c57..3987a08 100644 --- a/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj +++ b/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj @@ -11,7 +11,7 @@ - + diff --git a/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml b/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml index db5149c..8650523 100644 --- a/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml +++ b/examples/OptionalValues.Examples.Swashbuckle/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.1 +openapi: 3.0.4 info: title: OptionalValues.Examples.Swashbuckle version: '1.0' diff --git a/src/OptionalValues.Swashbuckle/OptionalValues.Swashbuckle.csproj b/src/OptionalValues.Swashbuckle/OptionalValues.Swashbuckle.csproj index 327ef65..35919e5 100644 --- a/src/OptionalValues.Swashbuckle/OptionalValues.Swashbuckle.csproj +++ b/src/OptionalValues.Swashbuckle/OptionalValues.Swashbuckle.csproj @@ -1,7 +1,7 @@  - $(OptionalValuesLibraryTargetFrameworks) + $(OptionalValuesSwashbuckleTargetFrameworks) diff --git a/test/OptionalValues.Swashbuckle.Tests/OptionalValues.Swashbuckle.Tests.csproj b/test/OptionalValues.Swashbuckle.Tests/OptionalValues.Swashbuckle.Tests.csproj index 78ed1d5..9eec8aa 100644 --- a/test/OptionalValues.Swashbuckle.Tests/OptionalValues.Swashbuckle.Tests.csproj +++ b/test/OptionalValues.Swashbuckle.Tests/OptionalValues.Swashbuckle.Tests.csproj @@ -1,7 +1,7 @@ - $(OptionalValuesTestsTargetFrameworks) + $(OptionalValuesSwashbuckleTargetFrameworks) enable enable From d798cfd2c749e4b8bd899df37c42f2ce19ed9073 Mon Sep 17 00:00:00 2001 From: desjoerd Date: Thu, 23 Oct 2025 21:08:39 +0200 Subject: [PATCH 04/15] Initial setup for .net 10 openapi --- Directory.Build.props | 1 + Directory.Packages.props | 3 +- OptionalValues.slnx | 6 +- .../OptionalValues.Examples.OpenApi.csproj | 33 ++++ .../Program.cs | 116 ++++++++++++++ .../Properties/launchSettings.json | 14 ++ .../appsettings.json | 9 ++ .../openapi.json | 151 ++++++++++++++++++ ...OptionalValues.Examples.Swashbuckle.csproj | 3 +- global.json | 2 +- .../OpenApiOptionsExtensions.cs | 42 +++++ .../OptionalValues.OpenApi.csproj | 17 ++ .../OptionalValuesSchemaTransformer.cs | 151 ++++++++++++++++++ .../PublicAPI.Shipped.txt | 0 .../PublicAPI.Unshipped.txt | 0 .../Endpoints/Annotations.cs | 6 + .../Endpoints/Nullability.cs | 6 + .../Endpoints/Primitives.cs | 8 + .../Endpoints/References.cs | 6 + .../OptionalValues.OpenApi.TestApp.csproj | 20 +++ .../OptionalValues.OpenApi.TestApp/Program.cs | 45 ++++++ .../Properties/launchSettings.json | 23 +++ .../appsettings.json | 9 ++ .../OptionalValues.OpenApi.Tests.csproj | 33 ++++ .../OptionalValues.OpenApi.Tests/UnitTest1.cs | 21 +++ 25 files changed, 720 insertions(+), 5 deletions(-) create mode 100644 examples/OptionalValues.Examples.OpenApi/OptionalValues.Examples.OpenApi.csproj create mode 100644 examples/OptionalValues.Examples.OpenApi/Program.cs create mode 100644 examples/OptionalValues.Examples.OpenApi/Properties/launchSettings.json create mode 100644 examples/OptionalValues.Examples.OpenApi/appsettings.json create mode 100644 examples/OptionalValues.Examples.OpenApi/openapi.json create mode 100644 src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs create mode 100644 src/OptionalValues.OpenApi/OptionalValues.OpenApi.csproj create mode 100644 src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs create mode 100644 src/OptionalValues.OpenApi/PublicAPI.Shipped.txt create mode 100644 src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt create mode 100644 test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs create mode 100644 test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs create mode 100644 test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs create mode 100644 test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs create mode 100644 test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj create mode 100644 test/OptionalValues.OpenApi.TestApp/Program.cs create mode 100644 test/OptionalValues.OpenApi.TestApp/Properties/launchSettings.json create mode 100644 test/OptionalValues.OpenApi.TestApp/appsettings.json create mode 100644 test/OptionalValues.OpenApi.Tests/OptionalValues.OpenApi.Tests.csproj create mode 100644 test/OptionalValues.OpenApi.Tests/UnitTest1.cs diff --git a/Directory.Build.props b/Directory.Build.props index 13eb350..c81e613 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,6 +3,7 @@ net8.0;net9.0;net10.0 net8.0;net9.0 + net10.0 net8.0;net9.0;net10.0 optional partial json undefined jsonpatch jsonmergepatch patch diff --git a/Directory.Packages.props b/Directory.Packages.props index 2bdb338..fb0f0ce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,12 +11,13 @@ + - + diff --git a/OptionalValues.slnx b/OptionalValues.slnx index 4c14d16..56f33be 100644 --- a/OptionalValues.slnx +++ b/OptionalValues.slnx @@ -12,6 +12,7 @@ + @@ -19,6 +20,7 @@ + @@ -27,5 +29,7 @@ + + - + \ No newline at end of file diff --git a/examples/OptionalValues.Examples.OpenApi/OptionalValues.Examples.OpenApi.csproj b/examples/OptionalValues.Examples.OpenApi/OptionalValues.Examples.OpenApi.csproj new file mode 100644 index 0000000..4ade081 --- /dev/null +++ b/examples/OptionalValues.Examples.OpenApi/OptionalValues.Examples.OpenApi.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + + false + true + + true + + + + . + --file-name openapi + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + \ No newline at end of file diff --git a/examples/OptionalValues.Examples.OpenApi/Program.cs b/examples/OptionalValues.Examples.OpenApi/Program.cs new file mode 100644 index 0000000..0d3ac8d --- /dev/null +++ b/examples/OptionalValues.Examples.OpenApi/Program.cs @@ -0,0 +1,116 @@ +using System.Text.Json.Serialization; + +using OptionalValues; +using OptionalValues.DataAnnotations; +using OptionalValues.OpenApi; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(options => +{ + options.AddOptionalValueSupport(); +}); + +// Add OptionalValue support to the JSON serializer +builder.Services.ConfigureHttpJsonOptions(jsonOptions => +{ + jsonOptions.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; + jsonOptions.SerializerOptions.AddOptionalValueSupport(); +}); + +WebApplication app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +// This is an example of how to use OptionalValues with NSwag +// It also shows what the JSON Serializer will do with the OptionalValues +// You can play with the values in the Company object to see how the JSON Serializer will handle them +// Play around by omitting properties or whole objects +app.MapPost("/company", (Company company) => company) + .WithName("Example Post") + .WithDescription("This directly returns the posted object") + .WithTags("Example"); + +app.Run(); + +class NullableProperty +{ + [Specified] + public OptionalValue Name { get; init; } +} + +/// +/// This is the main example company model. +/// +class Company +{ + public Guid Id { get; init; } = Guid.NewGuid(); + + /// + /// Name of the company. + /// + [RequiredValue] + public OptionalValue Name { get; init; } + + [OptionalLength(0, 50)] + public OptionalValue Summary { get; init; } + + /// + /// The contact person for the company. + /// + [Specified] + public OptionalValue Contact { get; init; } +} + +/// +/// This is a person. +/// +class Person +{ + /// + /// The full name of the person. + /// + [Specified] + public OptionalValue Name { get; init; } = "John Doe"; + + [OptionalRange(0, 120)] + public OptionalValue Age { get; init; } + + /// + /// Contact address for the person. + /// + public OptionalValue
Address { get; init; } + + /// + /// Billing address for the person. + /// + public OptionalValue BillingAddress { get; init; } +} + +/// +/// A mailing address. +/// +class Address +{ + [Specified] + public OptionalValue Street { get; init; } + + public OptionalValue City { get; init; } + + [OptionalRegularExpression("^[a-zA-Z ]+$")] + public OptionalValue State { get; init; } + + public OptionalValue Zip { get; init; } +} + +/// +/// Entry point for the OptionalValues.Examples.OpenApi application. +/// +public partial class Program { +} \ No newline at end of file diff --git a/examples/OptionalValues.Examples.OpenApi/Properties/launchSettings.json b/examples/OptionalValues.Examples.OpenApi/Properties/launchSettings.json new file mode 100644 index 0000000..1eaadee --- /dev/null +++ b/examples/OptionalValues.Examples.OpenApi/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7022;http://localhost:5138", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/OptionalValues.Examples.OpenApi/appsettings.json b/examples/OptionalValues.Examples.OpenApi/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/examples/OptionalValues.Examples.OpenApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/OptionalValues.Examples.OpenApi/openapi.json b/examples/OptionalValues.Examples.OpenApi/openapi.json new file mode 100644 index 0000000..240a793 --- /dev/null +++ b/examples/OptionalValues.Examples.OpenApi/openapi.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "OptionalValues.Examples.OpenApi | v1", + "version": "1.0.0" + }, + "paths": { + "/company": { + "post": { + "tags": [ + "Example" + ], + "description": "This directly returns the posted object", + "operationId": "Example Post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Company" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Company" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Address": { + "required": [ + "street" + ], + "type": "object", + "properties": { + "street": { + "type": [ + "null", + "string" + ] + }, + "city": { + "type": "string" + }, + "state": { + "pattern": "^[a-zA-Z ]+$", + "type": "string" + }, + "zip": { + "type": [ + "null", + "string" + ] + } + }, + "description": "A mailing address." + }, + "Company": { + "required": [ + "name", + "contact" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "description": "Name of the company." + }, + "summary": { + "maxLength": 50, + "minLength": 0, + "type": [ + "null", + "string" + ] + }, + "contact": { + "oneOf": [ + { + "type": "null" + }, + { + "description": "The contact person for the company.", + "$ref": "#/components/schemas/Person" + } + ] + } + }, + "description": "This is the main example company model." + }, + "Person": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": [ + "null", + "string" + ], + "description": "The full name of the person." + }, + "age": { + "maximum": 120, + "minimum": 0, + "type": "integer", + "format": "int32" + }, + "address": { + "description": "Contact address for the person.", + "$ref": "#/components/schemas/Address" + }, + "billingAddress": { + "oneOf": [ + { + "type": "null" + }, + { + "description": "Billing address for the person.", + "$ref": "#/components/schemas/Address" + } + ] + } + }, + "description": "This is a person." + } + } + }, + "tags": [ + { + "name": "Example" + } + ] +} \ No newline at end of file diff --git a/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj b/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj index 3987a08..6bcb941 100644 --- a/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj +++ b/examples/OptionalValues.Examples.Swashbuckle/OptionalValues.Examples.Swashbuckle.csproj @@ -10,8 +10,7 @@ - - + diff --git a/global.json b/global.json index e7cce33..83e6a8b 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.100-rc.1.25451.107" + "version": "10.0.100-rc.2.25502.107" } } diff --git a/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs b/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs new file mode 100644 index 0000000..2af633c --- /dev/null +++ b/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization.Metadata; + +using Microsoft.AspNetCore.OpenApi; + +namespace OptionalValues.OpenApi; + +/// +/// Extension methods for to add support for types. +/// +public static class OpenApiOptionsExtensions +{ + /// + /// Adds support for types in OpenAPI generation. + /// This method configures the to correctly handle + /// types by applying a custom schema reference ID + /// creation logic and adding a schema transformer. + /// + /// + public static void AddOptionalValueSupport(this OpenApiOptions options) + { + options.ApplyOptionalValueCreateSchemaReferenceId(); + options.AddSchemaTransformer(); + } + + private static void ApplyOptionalValueCreateSchemaReferenceId(this OpenApiOptions options) + { + Func originalCreateSchemaReferenceId = options.CreateSchemaReferenceId; + + options.CreateSchemaReferenceId = jsonTypeInfo => + { + if (OptionalValue.IsOptionalValueType(jsonTypeInfo.Type)) + { + Type underlyingType = OptionalValue.GetUnderlyingType(jsonTypeInfo.Type); + var underlyingJsonTypeInfo = JsonTypeInfo.CreateJsonTypeInfo(underlyingType, jsonTypeInfo.Options); + + return originalCreateSchemaReferenceId(underlyingJsonTypeInfo); + } + + return originalCreateSchemaReferenceId(jsonTypeInfo); + }; + } +} \ No newline at end of file diff --git a/src/OptionalValues.OpenApi/OptionalValues.OpenApi.csproj b/src/OptionalValues.OpenApi/OptionalValues.OpenApi.csproj new file mode 100644 index 0000000..548dfc9 --- /dev/null +++ b/src/OptionalValues.OpenApi/OptionalValues.OpenApi.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs new file mode 100644 index 0000000..d038b55 --- /dev/null +++ b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs @@ -0,0 +1,151 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Text.Json.Serialization.Metadata; + +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace OptionalValues.OpenApi; + +internal class OptionalValuesSchemaTransformer : IOpenApiSchemaTransformer +{ + public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + TransformObjectSchema(schema, context); + await TransformPropertySchema(schema, context, cancellationToken); + } + + private async Task TransformPropertySchema(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + if (context.JsonPropertyInfo == null) + { + return; + } + + if (!OptionalValue.IsOptionalValueType(context.JsonPropertyInfo.PropertyType)) + { + return; + } + + Type underlyingType = OptionalValue.GetUnderlyingType(context.JsonPropertyInfo.PropertyType); + OpenApiSchema underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); + + schema.Type = underlyingSchema.Type; + + schema.Format = underlyingSchema.Format; + schema.Properties = underlyingSchema.Properties; + schema.Items = underlyingSchema.Items; + schema.AnyOf = underlyingSchema.AnyOf; + schema.AllOf = underlyingSchema.AllOf; + schema.OneOf = underlyingSchema.OneOf; + schema.Not = underlyingSchema.Not; + schema.AdditionalProperties = underlyingSchema.AdditionalProperties; + schema.Enum = underlyingSchema.Enum; + schema.AdditionalPropertiesAllowed = underlyingSchema.AdditionalPropertiesAllowed; + schema.Required = underlyingSchema.Required; + + // Merge annotations + schema.Description ??= underlyingSchema.Description; + schema.Default ??= underlyingSchema.Default; + schema.Example ??= underlyingSchema.Example; + + // Merge the metadata + if (underlyingSchema.Metadata is not null) + { + schema.Metadata ??= new Dictionary(); + foreach (KeyValuePair keyValuePair in underlyingSchema.Metadata) + { + schema.Metadata.TryAdd(keyValuePair.Key, keyValuePair.Value); + } + } + + // Patch nullability + var isUnderlyingIsReferencedSchema = IsSchemaReference(underlyingSchema); + + var customAttributes = context.JsonPropertyInfo.AttributeProvider?.GetCustomAttributes(false) ?? []; + var isNullable = !customAttributes.OfType().Any() + && GetOptionalValueIsNullable(context.JsonPropertyInfo.AttributeProvider as MemberInfo); + if (isNullable) + { + if (!isUnderlyingIsReferencedSchema) + { + schema.Type |= JsonSchemaType.Null; + } + else + { + schema.Metadata ??= new Dictionary(); + schema.Metadata["x-is-nullable-property"] = true; + } + } + } + + private void TransformObjectSchema(OpenApiSchema schema, OpenApiSchemaTransformerContext context) + { + foreach (JsonPropertyInfo prop in context.JsonTypeInfo.Properties) + { + if (OptionalValue.IsOptionalValueType(prop.PropertyType)) + { + var propAttributes = prop.AttributeProvider?.GetCustomAttributes(false) ?? []; + + // If the property has a [Specified] attribute, mark it as required + if (propAttributes.Any(x => x.GetType().FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute")) + { + schema.Required ??= new HashSet(StringComparer.Ordinal); + schema.Required.Add(prop.Name); + } + } + } + } + + private static bool IsSchemaReference(IOpenApiSchema? schema) + => schema switch + { + OpenApiSchema actualSchema => actualSchema.Metadata?.TryGetValue("x-schema-id", out var schemaId) == true + && !string.IsNullOrEmpty(schemaId as string), + OpenApiSchemaReference => true, + _ => false, + }; + + private static bool GetOptionalValueIsNullable(ICustomAttributeProvider? memberInfo) + { + NullabilityInfo? nullabilityInfo = GetOptionalValueNullabilityInfo(memberInfo); + if (nullabilityInfo == null) + { + return false; + } + return nullabilityInfo.ReadState == NullabilityState.Nullable; + } + + private static NullabilityInfo? GetNullabilityInfo(ICustomAttributeProvider memberInfo) + { + var nullabilityInfoContext = new NullabilityInfoContext(); + return memberInfo switch + { + PropertyInfo propertyInfo => nullabilityInfoContext.Create(propertyInfo), + FieldInfo fieldInfo => nullabilityInfoContext.Create(fieldInfo), + ParameterInfo parameterInfo => nullabilityInfoContext.Create(parameterInfo), + _ => null, + }; + } + + private static NullabilityInfo? GetOptionalValueNullabilityInfo(ICustomAttributeProvider? memberInfo) + { + if (memberInfo == null) + { + return null; + } + + NullabilityInfo? nullabilityInfo = GetNullabilityInfo(memberInfo); + if (nullabilityInfo == null) + { + return null; + } + + if (OptionalValue.IsOptionalValueType(nullabilityInfo.Type)) + { + return nullabilityInfo.GenericTypeArguments[0]; + } + + return null; + } +} \ No newline at end of file diff --git a/src/OptionalValues.OpenApi/PublicAPI.Shipped.txt b/src/OptionalValues.OpenApi/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt b/src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs new file mode 100644 index 0000000..ecf2b4a --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs @@ -0,0 +1,6 @@ +namespace OptionalValues.OpenApi.TestApp.Endpoints; + +public static class Annotations +{ + +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs new file mode 100644 index 0000000..6b35e9f --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs @@ -0,0 +1,6 @@ +namespace OptionalValues.OpenApi.TestApp.Endpoints; + +public class Nullability +{ + +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs new file mode 100644 index 0000000..1beacd6 --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs @@ -0,0 +1,8 @@ +namespace OptionalValues.OpenApi.TestApp.Endpoints; + +public static class Primitives +{ + + + +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs new file mode 100644 index 0000000..c37b92f --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs @@ -0,0 +1,6 @@ +namespace OptionalValues.OpenApi.TestApp.Endpoints; + +public static class References +{ + +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj b/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj new file mode 100644 index 0000000..6c5392a --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/test/OptionalValues.OpenApi.TestApp/Program.cs b/test/OptionalValues.OpenApi.TestApp/Program.cs new file mode 100644 index 0000000..3e52598 --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/Program.cs @@ -0,0 +1,45 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast"); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} + +public partial class Program +{ +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Properties/launchSettings.json b/test/OptionalValues.OpenApi.TestApp/Properties/launchSettings.json new file mode 100644 index 0000000..a6c0942 --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7090;http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/OptionalValues.OpenApi.TestApp/appsettings.json b/test/OptionalValues.OpenApi.TestApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/OptionalValues.OpenApi.Tests/OptionalValues.OpenApi.Tests.csproj b/test/OptionalValues.OpenApi.Tests/OptionalValues.OpenApi.Tests.csproj new file mode 100644 index 0000000..bf0d5d3 --- /dev/null +++ b/test/OptionalValues.OpenApi.Tests/OptionalValues.OpenApi.Tests.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + + false + true + + + + $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated + + + + + + + + + + + + + + + + + + + + diff --git a/test/OptionalValues.OpenApi.Tests/UnitTest1.cs b/test/OptionalValues.OpenApi.Tests/UnitTest1.cs new file mode 100644 index 0000000..e7ed011 --- /dev/null +++ b/test/OptionalValues.OpenApi.Tests/UnitTest1.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +using Shouldly; + +namespace OptionalValues.OpenApi.Tests; + +public class UnitTest1 +{ + [Fact] + public async Task Test1() + { + await using var application = new WebApplicationFactory(); + IOpenApiDocumentProvider? documentProvider = application.Services.GetKeyedService("v1"); + + OpenApiDocument document = await documentProvider!.GetOpenApiDocumentAsync(); + + document.ShouldNotBeNull(); + } +} \ No newline at end of file From 0b9196b26967bb360421fd447c3091a993733e93 Mon Sep 17 00:00:00 2001 From: desjoerd Date: Fri, 24 Oct 2025 23:02:38 +0200 Subject: [PATCH 05/15] Fix missing package --- Directory.Packages.props | 1 + src/OptionalValues.OpenApi/PublicAPI.Shipped.txt | 1 + src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index fb0f0ce..34e2ec0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,6 +26,7 @@ + diff --git a/src/OptionalValues.OpenApi/PublicAPI.Shipped.txt b/src/OptionalValues.OpenApi/PublicAPI.Shipped.txt index e69de29..7dc5c58 100644 --- a/src/OptionalValues.OpenApi/PublicAPI.Shipped.txt +++ b/src/OptionalValues.OpenApi/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt b/src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt index e69de29..7f5b13a 100644 --- a/src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt +++ b/src/OptionalValues.OpenApi/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OptionalValues.OpenApi.OpenApiOptionsExtensions +static OptionalValues.OpenApi.OpenApiOptionsExtensions.AddOptionalValueSupport(this Microsoft.AspNetCore.OpenApi.OpenApiOptions! options) -> void \ No newline at end of file From 369b4a2d2322bb9fd052967b08d3799441728fb7 Mon Sep 17 00:00:00 2001 From: desjoerd Date: Thu, 6 Nov 2025 12:32:50 +0100 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=90=9B=20Fix=20circular=20reference?= =?UTF-8?q?=20handling=20and=20"pattern"=20for=20OpenApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptionalValuesSchemaTransformer.cs | 49 +- .../Endpoints/Annotations.cs | 6 - .../Endpoints/Nullability.cs | 6 - .../Endpoints/Primitives.cs | 61 ++- .../Endpoints/References.cs | 60 ++- .../OptionalValues.OpenApi.TestApp.csproj | 14 + .../OptionalValues.OpenApi.TestApp/Program.cs | 50 +- .../openapi.json | 438 ++++++++++++++++++ .../OpenApiDocumentSchemaExtensions.cs | 40 ++ ...nitTest1.cs => OpenApiDocumentTestBase.cs} | 10 +- .../PrimitivesTest.cs | 14 + .../ReferencesTest.cs | 15 + .../SchemaAssertionExtensions.cs | 22 + 13 files changed, 717 insertions(+), 68 deletions(-) delete mode 100644 test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs delete mode 100644 test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs create mode 100644 test/OptionalValues.OpenApi.TestApp/openapi.json create mode 100644 test/OptionalValues.OpenApi.Tests/OpenApiDocumentSchemaExtensions.cs rename test/OptionalValues.OpenApi.Tests/{UnitTest1.cs => OpenApiDocumentTestBase.cs} (78%) create mode 100644 test/OptionalValues.OpenApi.Tests/PrimitivesTest.cs create mode 100644 test/OptionalValues.OpenApi.Tests/ReferencesTest.cs create mode 100644 test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs diff --git a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs index d038b55..e1584db 100644 --- a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs +++ b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs @@ -3,12 +3,16 @@ using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.OpenApi; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.OpenApi; namespace OptionalValues.OpenApi; internal class OptionalValuesSchemaTransformer : IOpenApiSchemaTransformer { + private readonly HashSet _generatedSchemaIds = []; + public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) { TransformObjectSchema(schema, context); @@ -27,8 +31,34 @@ private async Task TransformPropertySchema(OpenApiSchema schema, OpenApiSchemaTr return; } + // Get the OpenApiOptions of the current document + IOptionsMonitor openApiOptionsSnapshot = context.ApplicationServices.GetRequiredService>(); + OpenApiOptions openApiOptions = openApiOptionsSnapshot.Get(context.DocumentName); + Type underlyingType = OptionalValue.GetUnderlyingType(context.JsonPropertyInfo.PropertyType); - OpenApiSchema underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); + var underlyingSchemaId = openApiOptions.CreateSchemaReferenceId( + JsonTypeInfo.CreateJsonTypeInfo(underlyingType, context.JsonTypeInfo.Options)); + var isSchemaReference = !string.IsNullOrEmpty(underlyingSchemaId); + + OpenApiSchema? underlyingSchema = null; + if (isSchemaReference) + { + underlyingSchema = new OpenApiSchema + { + Metadata = schema.Metadata, + }; + underlyingSchema.Metadata ??= new Dictionary(); + underlyingSchema.Metadata["x-schema-id"] = underlyingSchemaId!; + + if (_generatedSchemaIds.Add(underlyingSchemaId!)) + { + await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); + } + } + else + { + underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); + } schema.Type = underlyingSchema.Type; @@ -43,6 +73,7 @@ private async Task TransformPropertySchema(OpenApiSchema schema, OpenApiSchemaTr schema.Enum = underlyingSchema.Enum; schema.AdditionalPropertiesAllowed = underlyingSchema.AdditionalPropertiesAllowed; schema.Required = underlyingSchema.Required; + schema.Pattern = underlyingSchema.Pattern; // Merge annotations schema.Description ??= underlyingSchema.Description; @@ -60,14 +91,12 @@ private async Task TransformPropertySchema(OpenApiSchema schema, OpenApiSchemaTr } // Patch nullability - var isUnderlyingIsReferencedSchema = IsSchemaReference(underlyingSchema); - var customAttributes = context.JsonPropertyInfo.AttributeProvider?.GetCustomAttributes(false) ?? []; var isNullable = !customAttributes.OfType().Any() && GetOptionalValueIsNullable(context.JsonPropertyInfo.AttributeProvider as MemberInfo); if (isNullable) { - if (!isUnderlyingIsReferencedSchema) + if (!isSchemaReference) { schema.Type |= JsonSchemaType.Null; } @@ -88,7 +117,7 @@ private void TransformObjectSchema(OpenApiSchema schema, OpenApiSchemaTransforme var propAttributes = prop.AttributeProvider?.GetCustomAttributes(false) ?? []; // If the property has a [Specified] attribute, mark it as required - if (propAttributes.Any(x => x.GetType().FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute")) + if (propAttributes.Any(x => x.GetType()?.FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute")) { schema.Required ??= new HashSet(StringComparer.Ordinal); schema.Required.Add(prop.Name); @@ -97,15 +126,6 @@ private void TransformObjectSchema(OpenApiSchema schema, OpenApiSchemaTransforme } } - private static bool IsSchemaReference(IOpenApiSchema? schema) - => schema switch - { - OpenApiSchema actualSchema => actualSchema.Metadata?.TryGetValue("x-schema-id", out var schemaId) == true - && !string.IsNullOrEmpty(schemaId as string), - OpenApiSchemaReference => true, - _ => false, - }; - private static bool GetOptionalValueIsNullable(ICustomAttributeProvider? memberInfo) { NullabilityInfo? nullabilityInfo = GetOptionalValueNullabilityInfo(memberInfo); @@ -113,6 +133,7 @@ private static bool GetOptionalValueIsNullable(ICustomAttributeProvider? memberI { return false; } + return nullabilityInfo.ReadState == NullabilityState.Nullable; } diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs deleted file mode 100644 index ecf2b4a..0000000 --- a/test/OptionalValues.OpenApi.TestApp/Endpoints/Annotations.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OptionalValues.OpenApi.TestApp.Endpoints; - -public static class Annotations -{ - -} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs deleted file mode 100644 index 6b35e9f..0000000 --- a/test/OptionalValues.OpenApi.TestApp/Endpoints/Nullability.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OptionalValues.OpenApi.TestApp.Endpoints; - -public class Nullability -{ - -} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs index 1beacd6..fd75abd 100644 --- a/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/Primitives.cs @@ -1,8 +1,65 @@ -namespace OptionalValues.OpenApi.TestApp.Endpoints; +using Microsoft.AspNetCore.Mvc; + +namespace OptionalValues.OpenApi.TestApp.Endpoints; public static class Primitives { + public static void MapPrimitives(this IEndpointRouteBuilder routes) + { + RouteGroupBuilder group = routes.MapGroup("/primitives"); + group.MapPost("body/baseline", (PrimitivesBodyBaseline body) => TypedResults.Ok()); + group.MapPost("body/optional", (PrimitivesBodyOptional body) => TypedResults.Ok()); + } + public class PrimitivesBodyOptional + { + public OptionalValue Int { get; set; } + public OptionalValue NullableInt { get; set; } + public OptionalValue UInt { get; set; } + public OptionalValue NullableUInt { get; set; } + public OptionalValue Double { get; set; } + public OptionalValue NullableDouble { get; set; } + public OptionalValue Bool { get; set; } + public OptionalValue NullableBool { get; set; } + public OptionalValue String { get; set; } + public OptionalValue NullableString { get; set; } + public OptionalValue Guid { get; set; } + public OptionalValue NullableGuid { get; set; } + public OptionalValue DateTime { get; set; } + public OptionalValue NullableDateTime { get; set; } + public OptionalValue DateOnly { get; set; } + public OptionalValue NullableDateOnly { get; set; } + public OptionalValue DateTimeOffset { get; set; } + public OptionalValue NullableDateTimeOffset { get; set; } + public OptionalValue TimeOnly { get; set; } + public OptionalValue NullableTimeOnly { get; set; } + public OptionalValue TimeSpan { get; set; } + public OptionalValue NullableTimeSpan { get; set; } + } - + public class PrimitivesBodyBaseline + { + public int Int { get; set; } + public int? NullableInt { get; set; } + public uint UInt { get; set; } + public uint? NullableUInt { get; set; } + public double Double { get; set; } + public double? NullableDouble { get; set; } + public bool Bool { get; set; } + public bool? NullableBool { get; set; } + public string String { get; set; } = null!; + public string? NullableString { get; set; } + public Guid Guid { get; set; } + public Guid? NullableGuid { get; set; } + public DateTime DateTime { get; set; } + public DateTime? NullableDateTime { get; set; } + public DateOnly DateOnly { get; set; } + public DateOnly? NullableDateOnly { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + public DateTimeOffset? NullableDateTimeOffset { get; set; } + public TimeOnly TimeOnly { get; set; } + public TimeOnly? NullableTimeOnly { get; set; } + public TimeSpan TimeSpan { get; set; } + public TimeSpan? NullableTimeSpan { get; set; } + } } \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs index c37b92f..6fc3119 100644 --- a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs @@ -1,6 +1,62 @@ -namespace OptionalValues.OpenApi.TestApp.Endpoints; +using Microsoft.AspNetCore.Mvc; +using OptionalValues; + +namespace OptionalValues.OpenApi.TestApp.Endpoints; public static class References { - + public static void MapReferences(this IEndpointRouteBuilder routes) + { + RouteGroupBuilder group = routes.MapGroup("/references"); + group.MapPost("body/baseline", (ReferencesBodyBaseline body) => TypedResults.Ok()); + group.MapPost("body/optional", (ReferencesBodyOptional body) => TypedResults.Ok()); + } + + public class ReferencesBodyOptional + { + public OptionalValue A { get; set; } + } + + public class ReferencesBodyBaseline + { + public ReferenceA A { get; set; } = null!; + } + + // Baseline nested models (A -> B -> C -> A circular) + public class ReferenceA + { + public string Name { get; set; } = null!; + public ReferenceB B { get; set; } = null!; + } + + public class ReferenceB + { + public string Description { get; set; } = null!; + public ReferenceC C { get; set; } = null!; + } + + public class ReferenceC + { + public int Value { get; set; } + public ReferenceA? Parent { get; set; } // circular reference back to A + } + + // Optional versions of the nested models + public class ReferenceAOptional + { + public OptionalValue Name { get; set; } + public OptionalValue B { get; set; } + } + + public class ReferenceBOptional + { + public OptionalValue Description { get; set; } + public OptionalValue C { get; set; } + } + + public class ReferenceCOptional + { + public OptionalValue Value { get; set; } + public OptionalValue Parent { get; set; } // circular back to A (nullable) + } } \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj b/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj index 6c5392a..2bfdc9d 100644 --- a/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj +++ b/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj @@ -4,11 +4,25 @@ net10.0 enable enable + + false true + + true + $(NoWarn);CS1591 + + + + . + --file-name openapi + + + + diff --git a/test/OptionalValues.OpenApi.TestApp/Program.cs b/test/OptionalValues.OpenApi.TestApp/Program.cs index 3e52598..ac40451 100644 --- a/test/OptionalValues.OpenApi.TestApp/Program.cs +++ b/test/OptionalValues.OpenApi.TestApp/Program.cs @@ -1,45 +1,33 @@ -var builder = WebApplication.CreateBuilder(args); +using System.Text.Json.Serialization; -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +using OptionalValues; +using OptionalValues.OpenApi; +using OptionalValues.OpenApi.TestApp.Endpoints; -var app = builder.Build(); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.AddOptionalValueSupport(); + options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; +}); +builder.Services.AddOpenApi(options => +{ + options.AddOptionalValueSupport(); +}); + +WebApplication app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } -app.UseHttpsRedirection(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => - { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast"); +app.MapPrimitives(); +app.MapReferences(); app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} - public partial class Program { } \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/openapi.json b/test/OptionalValues.OpenApi.TestApp/openapi.json new file mode 100644 index 0000000..aade8d9 --- /dev/null +++ b/test/OptionalValues.OpenApi.TestApp/openapi.json @@ -0,0 +1,438 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "OptionalValues.OpenApi.TestApp | v1", + "version": "1.0.0" + }, + "paths": { + "/primitives/body/baseline": { + "post": { + "tags": [ + "OptionalValues.OpenApi.TestApp" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrimitivesBodyBaseline" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/primitives/body/optional": { + "post": { + "tags": [ + "OptionalValues.OpenApi.TestApp" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrimitivesBodyOptional" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/references/body/baseline": { + "post": { + "tags": [ + "OptionalValues.OpenApi.TestApp" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReferencesBodyBaseline" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/references/body/optional": { + "post": { + "tags": [ + "OptionalValues.OpenApi.TestApp" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReferencesBodyOptional" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "PrimitivesBodyBaseline": { + "type": "object", + "properties": { + "int": { + "type": "integer", + "format": "int32" + }, + "nullableInt": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "uInt": { + "type": "integer", + "format": "uint32" + }, + "nullableUInt": { + "type": [ + "null", + "integer" + ], + "format": "uint32" + }, + "double": { + "type": "number", + "format": "double" + }, + "nullableDouble": { + "type": [ + "null", + "number" + ], + "format": "double" + }, + "bool": { + "type": "boolean" + }, + "nullableBool": { + "type": [ + "null", + "boolean" + ] + }, + "string": { + "type": "string" + }, + "nullableString": { + "type": [ + "null", + "string" + ] + }, + "guid": { + "type": "string", + "format": "uuid" + }, + "nullableGuid": { + "type": [ + "null", + "string" + ], + "format": "uuid" + }, + "dateTime": { + "type": "string", + "format": "date-time" + }, + "nullableDateTime": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "dateOnly": { + "type": "string", + "format": "date" + }, + "nullableDateOnly": { + "type": [ + "null", + "string" + ], + "format": "date" + }, + "dateTimeOffset": { + "type": "string", + "format": "date-time" + }, + "nullableDateTimeOffset": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "timeOnly": { + "type": "string", + "format": "time" + }, + "nullableTimeOnly": { + "type": [ + "null", + "string" + ], + "format": "time" + }, + "timeSpan": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": "string" + }, + "nullableTimeSpan": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": [ + "null", + "string" + ] + } + } + }, + "PrimitivesBodyOptional": { + "type": "object", + "properties": { + "int": { + "type": "integer", + "format": "int32" + }, + "nullableInt": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "uInt": { + "type": "integer", + "format": "uint32" + }, + "nullableUInt": { + "type": [ + "null", + "integer" + ], + "format": "uint32" + }, + "double": { + "type": "number", + "format": "double" + }, + "nullableDouble": { + "type": [ + "null", + "number" + ], + "format": "double" + }, + "bool": { + "type": "boolean" + }, + "nullableBool": { + "type": [ + "null", + "boolean" + ] + }, + "string": { + "type": "string" + }, + "nullableString": { + "type": [ + "null", + "string" + ] + }, + "guid": { + "type": "string", + "format": "uuid" + }, + "nullableGuid": { + "type": [ + "null", + "string" + ], + "format": "uuid" + }, + "dateTime": { + "type": "string", + "format": "date-time" + }, + "nullableDateTime": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "dateOnly": { + "type": "string", + "format": "date" + }, + "nullableDateOnly": { + "type": [ + "null", + "string" + ], + "format": "date" + }, + "dateTimeOffset": { + "type": "string", + "format": "date-time" + }, + "nullableDateTimeOffset": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "timeOnly": { + "type": "string", + "format": "time" + }, + "nullableTimeOnly": { + "type": [ + "null", + "string" + ], + "format": "time" + }, + "timeSpan": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": "string" + }, + "nullableTimeSpan": { + "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$", + "type": [ + "null", + "string" + ] + } + } + }, + "ReferenceA": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "b": { + "$ref": "#/components/schemas/ReferenceB" + } + } + }, + "ReferenceAOptional": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "b": { + "$ref": "#/components/schemas/ReferenceBOptional" + } + } + }, + "ReferenceB": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "c": { + "$ref": "#/components/schemas/ReferenceC" + } + } + }, + "ReferenceBOptional": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "c": { + "$ref": "#/components/schemas/ReferenceCOptional" + } + } + }, + "ReferenceC": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32" + }, + "parent": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ReferenceA" + } + ] + } + } + }, + "ReferenceCOptional": { + "type": "object", + "properties": { + "value": { + "type": "integer", + "format": "int32" + } + } + }, + "ReferencesBodyBaseline": { + "type": "object", + "properties": { + "a": { + "$ref": "#/components/schemas/ReferenceA" + } + } + }, + "ReferencesBodyOptional": { + "type": "object", + "properties": { + "a": { + "$ref": "#/components/schemas/ReferenceAOptional" + } + } + } + } + }, + "tags": [ + { + "name": "OptionalValues.OpenApi.TestApp" + } + ] +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.Tests/OpenApiDocumentSchemaExtensions.cs b/test/OptionalValues.OpenApi.Tests/OpenApiDocumentSchemaExtensions.cs new file mode 100644 index 0000000..266872e --- /dev/null +++ b/test/OptionalValues.OpenApi.Tests/OpenApiDocumentSchemaExtensions.cs @@ -0,0 +1,40 @@ +using Microsoft.OpenApi; + +namespace OptionalValues.OpenApi.Tests; + +public static class OpenApiDocumentSchemaExtensions +{ + public static (OpenApiSchema baseline, OpenApiSchema optional) GetComparisonOperationRequestBodySchemasByPath(this OpenApiDocument document, string pathBase) + { + IOpenApiSchema? baselineRequestBodySchema = document + .Paths[$"{pathBase}/baseline"] + .Operations![HttpMethod.Post] + .RequestBody! + .Content!["application/json"] + .Schema; + + IOpenApiSchema? optionalRequestBodySchema = document + .Paths[$"{pathBase}/optional"] + .Operations![HttpMethod.Post] + .RequestBody! + .Content!["application/json"] + .Schema; + + return (baselineRequestBodySchema!.Unwrap(), optionalRequestBodySchema!.Unwrap()); + } + + private static OpenApiSchema Unwrap(this IOpenApiSchema schema) + { + if (schema is OpenApiSchema actualSchema) + { + return actualSchema; + } + + if (schema is OpenApiSchemaReference reference) + { + return reference.RecursiveTarget ?? throw new InvalidOperationException("Schema reference target is null."); + } + + throw new InvalidOperationException("Schema is neither a schema nor a reference."); + } +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.Tests/UnitTest1.cs b/test/OptionalValues.OpenApi.Tests/OpenApiDocumentTestBase.cs similarity index 78% rename from test/OptionalValues.OpenApi.Tests/UnitTest1.cs rename to test/OptionalValues.OpenApi.Tests/OpenApiDocumentTestBase.cs index e7ed011..726058f 100644 --- a/test/OptionalValues.OpenApi.Tests/UnitTest1.cs +++ b/test/OptionalValues.OpenApi.Tests/OpenApiDocumentTestBase.cs @@ -2,20 +2,16 @@ using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; -using Shouldly; - namespace OptionalValues.OpenApi.Tests; -public class UnitTest1 +public class OpenApiDocumentTestBase { - [Fact] - public async Task Test1() + protected static async Task GetDocument() { await using var application = new WebApplicationFactory(); IOpenApiDocumentProvider? documentProvider = application.Services.GetKeyedService("v1"); OpenApiDocument document = await documentProvider!.GetOpenApiDocumentAsync(); - - document.ShouldNotBeNull(); + return document; } } \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.Tests/PrimitivesTest.cs b/test/OptionalValues.OpenApi.Tests/PrimitivesTest.cs new file mode 100644 index 0000000..147f250 --- /dev/null +++ b/test/OptionalValues.OpenApi.Tests/PrimitivesTest.cs @@ -0,0 +1,14 @@ +using Microsoft.OpenApi; + +namespace OptionalValues.OpenApi.Tests; + +public class PrimitivesTest : OpenApiDocumentTestBase +{ + [Fact] + public async Task Body() + { + OpenApiDocument document = await GetDocument(); + (OpenApiSchema baselineSchema, OpenApiSchema optionalSchema) = document.GetComparisonOperationRequestBodySchemasByPath("/primitives/body"); + await optionalSchema.ShouldBeEqualToBaselineSchema(baselineSchema); + } +} \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.Tests/ReferencesTest.cs b/test/OptionalValues.OpenApi.Tests/ReferencesTest.cs new file mode 100644 index 0000000..72319af --- /dev/null +++ b/test/OptionalValues.OpenApi.Tests/ReferencesTest.cs @@ -0,0 +1,15 @@ +using Microsoft.OpenApi; + +namespace OptionalValues.OpenApi.Tests; + +public class ReferencesTest : OpenApiDocumentTestBase +{ + [Fact] + public async Task Body() + { + OpenApiDocument document = await GetDocument(); + (OpenApiSchema baselineSchema, OpenApiSchema optionalSchema) = document.GetComparisonOperationRequestBodySchemasByPath("/references/body"); + await optionalSchema.ShouldBeEqualToBaselineSchema(baselineSchema, "Optional", "Baseline"); + } +} + diff --git a/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs b/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs new file mode 100644 index 0000000..fe114d0 --- /dev/null +++ b/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.OpenApi; + +using Shouldly; + +namespace OptionalValues.OpenApi.Tests; + +public static class SchemaAssertionExtensions +{ + public static async Task ShouldBeEqualToBaselineSchema(this OpenApiSchema actualSchema, OpenApiSchema baselineSchema, params string[] ignoreStrings) + { + var actualSchemaJson = await actualSchema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + var baselineSchemaJson = await baselineSchema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + foreach(var ignoreString in ignoreStrings) + { + actualSchemaJson = actualSchemaJson.Replace(ignoreString, ""); + baselineSchemaJson = baselineSchemaJson.Replace(ignoreString, ""); + } + + actualSchemaJson.ShouldBe(baselineSchemaJson); + } +} \ No newline at end of file From b317071867b71d864adef7ef2ce59baf6ce89eb8 Mon Sep 17 00:00:00 2001 From: desjoerd Date: Sat, 29 Nov 2025 13:00:42 +0100 Subject: [PATCH 07/15] Fix references in OpenAPI for now as circular references are a problem --- Directory.Packages.props | 6 ++-- .../openapi.json | 1 - global.json | 2 +- .../OptionalValuesSchemaTransformer.cs | 30 +++---------------- .../Endpoints/References.cs | 8 +++-- .../OptionalValues.OpenApi.TestApp.csproj | 8 ++--- .../openapi.json | 10 ------- 7 files changed, 18 insertions(+), 47 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 34e2ec0..fc81163 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,13 +11,13 @@ - + - + @@ -26,7 +26,7 @@ - + diff --git a/examples/OptionalValues.Examples.OpenApi/openapi.json b/examples/OptionalValues.Examples.OpenApi/openapi.json index 240a793..5da2bdd 100644 --- a/examples/OptionalValues.Examples.OpenApi/openapi.json +++ b/examples/OptionalValues.Examples.OpenApi/openapi.json @@ -55,7 +55,6 @@ "type": "string" }, "state": { - "pattern": "^[a-zA-Z ]+$", "type": "string" }, "zip": { diff --git a/global.json b/global.json index 83e6a8b..376af49 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.100-rc.2.25502.107" + "version": "10.0.100" } } diff --git a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs index e1584db..bc277d4 100644 --- a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs +++ b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs @@ -40,25 +40,7 @@ private async Task TransformPropertySchema(OpenApiSchema schema, OpenApiSchemaTr JsonTypeInfo.CreateJsonTypeInfo(underlyingType, context.JsonTypeInfo.Options)); var isSchemaReference = !string.IsNullOrEmpty(underlyingSchemaId); - OpenApiSchema? underlyingSchema = null; - if (isSchemaReference) - { - underlyingSchema = new OpenApiSchema - { - Metadata = schema.Metadata, - }; - underlyingSchema.Metadata ??= new Dictionary(); - underlyingSchema.Metadata["x-schema-id"] = underlyingSchemaId!; - - if (_generatedSchemaIds.Add(underlyingSchemaId!)) - { - await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); - } - } - else - { - underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); - } + OpenApiSchema underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken);; schema.Type = underlyingSchema.Type; @@ -157,16 +139,12 @@ private static bool GetOptionalValueIsNullable(ICustomAttributeProvider? memberI } NullabilityInfo? nullabilityInfo = GetNullabilityInfo(memberInfo); - if (nullabilityInfo == null) + if (nullabilityInfo == null + || !OptionalValue.IsOptionalValueType(nullabilityInfo.Type)) { return null; } - if (OptionalValue.IsOptionalValueType(nullabilityInfo.Type)) - { - return nullabilityInfo.GenericTypeArguments[0]; - } - - return null; + return nullabilityInfo.GenericTypeArguments[0]; } } \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs index 6fc3119..075550d 100644 --- a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs @@ -38,7 +38,9 @@ public class ReferenceB public class ReferenceC { public int Value { get; set; } - public ReferenceA? Parent { get; set; } // circular reference back to A + + // NOT POSSIBLE TO HAVE CIRCULAR REFERENCES TILL https://github.com/dotnet/aspnetcore/pull/64109 is merged + //public ReferenceA? Parent { get; set; } // circular reference back to A } // Optional versions of the nested models @@ -57,6 +59,8 @@ public class ReferenceBOptional public class ReferenceCOptional { public OptionalValue Value { get; set; } - public OptionalValue Parent { get; set; } // circular back to A (nullable) + + // NOT POSSIBLE TO HAVE CIRCULAR REFERENCES TILL https://github.com/dotnet/aspnetcore/pull/64109 is merged + //public OptionalValue Parent { get; set; } // circular back to A (nullable) } } \ No newline at end of file diff --git a/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj b/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj index 2bfdc9d..465a71d 100644 --- a/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj +++ b/test/OptionalValues.OpenApi.TestApp/OptionalValues.OpenApi.TestApp.csproj @@ -19,10 +19,10 @@ - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/test/OptionalValues.OpenApi.TestApp/openapi.json b/test/OptionalValues.OpenApi.TestApp/openapi.json index aade8d9..1cb1285 100644 --- a/test/OptionalValues.OpenApi.TestApp/openapi.json +++ b/test/OptionalValues.OpenApi.TestApp/openapi.json @@ -390,16 +390,6 @@ "value": { "type": "integer", "format": "int32" - }, - "parent": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/ReferenceA" - } - ] } } }, From 10656414e6953f5d3583a4c445d1a7ec2532347e Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:31:10 +0100 Subject: [PATCH 08/15] Update OptionalValues.slnx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- OptionalValues.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OptionalValues.slnx b/OptionalValues.slnx index 56f33be..6e73cc8 100644 --- a/OptionalValues.slnx +++ b/OptionalValues.slnx @@ -20,7 +20,7 @@ - + From c45e6dbfe28689cef86f46117a80f41af10479a6 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:31:30 +0100 Subject: [PATCH 09/15] Update test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs Fix formatting Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs b/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs index fe114d0..6faa62c 100644 --- a/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs +++ b/test/OptionalValues.OpenApi.Tests/SchemaAssertionExtensions.cs @@ -11,7 +11,7 @@ public static async Task ShouldBeEqualToBaselineSchema(this OpenApiSchema actual var actualSchemaJson = await actualSchema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); var baselineSchemaJson = await baselineSchema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); - foreach(var ignoreString in ignoreStrings) + foreach (var ignoreString in ignoreStrings) { actualSchemaJson = actualSchemaJson.Replace(ignoreString, ""); baselineSchemaJson = baselineSchemaJson.Replace(ignoreString, ""); From da2ea7dc25947b7d7a42f9a31c0fa4ddf2dcfd4e Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:31:46 +0100 Subject: [PATCH 10/15] Update comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs index 075550d..da80e95 100644 --- a/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs +++ b/test/OptionalValues.OpenApi.TestApp/Endpoints/References.cs @@ -22,7 +22,7 @@ public class ReferencesBodyBaseline public ReferenceA A { get; set; } = null!; } - // Baseline nested models (A -> B -> C -> A circular) + // Baseline nested models (A -> B -> C; no circular reference currently, pending https://github.com/dotnet/aspnetcore/pull/64109) public class ReferenceA { public string Name { get; set; } = null!; From d3b710503df9757c842060f8a8560f2415d91f13 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:31:59 +0100 Subject: [PATCH 11/15] Update src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs index bc277d4..255bcd9 100644 --- a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs +++ b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs @@ -11,7 +11,6 @@ namespace OptionalValues.OpenApi; internal class OptionalValuesSchemaTransformer : IOpenApiSchemaTransformer { - private readonly HashSet _generatedSchemaIds = []; public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) { From 7d4b21460cfbd6b9b518435c110d8b48285f3280 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:32:11 +0100 Subject: [PATCH 12/15] Update src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs b/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs index 2af633c..8e735f0 100644 --- a/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs +++ b/src/OptionalValues.OpenApi/OpenApiOptionsExtensions.cs @@ -15,7 +15,7 @@ public static class OpenApiOptionsExtensions /// types by applying a custom schema reference ID /// creation logic and adding a schema transformer. /// - /// + /// The OpenApiOptions instance to configure. public static void AddOptionalValueSupport(this OpenApiOptions options) { options.ApplyOptionalValueCreateSchemaReferenceId(); From 4361be4622ba38d386144c0293b727b6dd0b193a Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:32:18 +0100 Subject: [PATCH 13/15] Update src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs index 255bcd9..ecb4549 100644 --- a/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs +++ b/src/OptionalValues.OpenApi/OptionalValuesSchemaTransformer.cs @@ -39,7 +39,7 @@ private async Task TransformPropertySchema(OpenApiSchema schema, OpenApiSchemaTr JsonTypeInfo.CreateJsonTypeInfo(underlyingType, context.JsonTypeInfo.Options)); var isSchemaReference = !string.IsNullOrEmpty(underlyingSchemaId); - OpenApiSchema underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken);; + OpenApiSchema underlyingSchema = await context.GetOrCreateSchemaAsync(underlyingType, cancellationToken: cancellationToken); schema.Type = underlyingSchema.Type; From 9266915f98196f1008217b5878477c030b389802 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:32:26 +0100 Subject: [PATCH 14/15] Update OptionalValues.slnx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- OptionalValues.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OptionalValues.slnx b/OptionalValues.slnx index 6e73cc8..a7ab6a5 100644 --- a/OptionalValues.slnx +++ b/OptionalValues.slnx @@ -12,7 +12,7 @@ - + From 34a88a9a9e17bc5b4723099758cc2bb389a53190 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Sun, 30 Nov 2025 21:32:33 +0100 Subject: [PATCH 15/15] Update OptionalValues.slnx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- OptionalValues.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OptionalValues.slnx b/OptionalValues.slnx index a7ab6a5..d79f941 100644 --- a/OptionalValues.slnx +++ b/OptionalValues.slnx @@ -30,6 +30,6 @@ - + \ No newline at end of file