diff --git a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs index c09bd50dc67b..78e0f50e489f 100644 --- a/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs @@ -26,16 +26,19 @@ public static IOpenApiSchema AddOpenApiSchemaByReference(this OpenApiDocument do object? description = null; object? example = null; + object? defaultValue = null; if (schema is OpenApiSchema actualSchema) { actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefDescriptionAnnotation, out description); actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefExampleAnnotation, out example); + actualSchema.Metadata?.TryGetValue(OpenApiConstants.RefDefaultAnnotation, out defaultValue); } return new OpenApiSchemaReference(schemaId, document) { Description = description as string, Examples = example is JsonNode exampleJson ? [exampleJson] : null, + Default = defaultValue is JsonNode defaultValueJson ? defaultValueJson : null, }; } } diff --git a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs index 877ac70010db..2050578f9f21 100644 --- a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs +++ b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs @@ -350,11 +350,6 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, schema.Metadata ??= new Dictionary(); schema.Metadata[OpenApiConstants.RefId] = reader.GetString() ?? string.Empty; break; - case OpenApiConstants.RefDescriptionAnnotation: - reader.Read(); - schema.Metadata ??= new Dictionary(); - schema.Metadata[OpenApiConstants.RefDescriptionAnnotation] = reader.GetString() ?? string.Empty; - break; default: reader.Skip(); diff --git a/src/OpenApi/src/Services/OpenApiConstants.cs b/src/OpenApi/src/Services/OpenApiConstants.cs index df4228633556..433e71573eb9 100644 --- a/src/OpenApi/src/Services/OpenApiConstants.cs +++ b/src/OpenApi/src/Services/OpenApiConstants.cs @@ -13,6 +13,7 @@ internal static class OpenApiConstants internal const string DescriptionId = "x-aspnetcore-id"; internal const string SchemaId = "x-schema-id"; internal const string RefId = "x-ref-id"; + internal const string RefDefaultAnnotation = "x-ref-default"; internal const string RefDescriptionAnnotation = "x-ref-description"; internal const string RefExampleAnnotation = "x-ref-example"; internal const string RefKeyword = "$ref"; diff --git a/src/OpenApi/src/Services/OpenApiOptions.cs b/src/OpenApi/src/Services/OpenApiOptions.cs index 6c0936515307..7de3693d2964 100644 --- a/src/OpenApi/src/Services/OpenApiOptions.cs +++ b/src/OpenApi/src/Services/OpenApiOptions.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.OpenApi.Services.Schemas.Transformers; namespace Microsoft.AspNetCore.OpenApi; @@ -14,7 +15,7 @@ public sealed class OpenApiOptions { internal readonly List DocumentTransformers = []; internal readonly List OperationTransformers = []; - internal readonly List SchemaTransformers = []; + internal readonly List SchemaTransformers = [new AttributeAnnotationsSchemaTransformer()]; /// /// A default implementation for creating a schema reference ID for a given . diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 12b0c1ed996c..141011e40d5a 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.IO.Pipelines; @@ -101,10 +100,6 @@ internal sealed class OpenApiSchemaService( { schema.ApplyNullabilityContextInfo(jsonPropertyInfo); } - if (context.TypeInfo.Type.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is { } typeDescriptionAttribute) - { - schema[OpenApiSchemaKeywords.DescriptionKeyword] = typeDescriptionAttribute.Description; - } if (context.PropertyInfo is { AttributeProvider: { } attributeProvider }) { var propertyAttributes = attributeProvider.GetCustomAttributes(inherit: false); @@ -112,25 +107,6 @@ internal sealed class OpenApiSchemaService( { schema.ApplyValidationAttributes(validationAttributes); } - if (propertyAttributes.OfType().LastOrDefault() is { } defaultValueAttribute) - { - schema.ApplyDefaultValue(defaultValueAttribute.Value, context.TypeInfo); - } - var isInlinedSchema = schema[OpenApiConstants.SchemaId] is null; - if (isInlinedSchema) - { - if (propertyAttributes.OfType().LastOrDefault() is { } descriptionAttribute) - { - schema[OpenApiSchemaKeywords.DescriptionKeyword] = descriptionAttribute.Description; - } - } - else - { - if (propertyAttributes.OfType().LastOrDefault() is { } descriptionAttribute) - { - schema[OpenApiConstants.RefDescriptionAnnotation] = descriptionAttribute.Description; - } - } } schema.PruneNullTypeForComponentizedTypes(); return schema; diff --git a/src/OpenApi/src/Services/Schemas/Transformers/AttributeAnnotationsSchemaTransformer.cs b/src/OpenApi/src/Services/Schemas/Transformers/AttributeAnnotationsSchemaTransformer.cs new file mode 100644 index 000000000000..23a808d49a69 --- /dev/null +++ b/src/OpenApi/src/Services/Schemas/Transformers/AttributeAnnotationsSchemaTransformer.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.AspNetCore.OpenApi.Services.Schemas.Transformers; + +internal class AttributeAnnotationsSchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + schema.Metadata ??= new Dictionary(); + var isInlinedSchema = !schema.Metadata.ContainsKey(OpenApiConstants.SchemaId) || string.IsNullOrEmpty(schema.Metadata[OpenApiConstants.SchemaId] as string); + if (context.JsonTypeInfo.Type.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is { } typeDescriptionAttribute) + { + schema.Description = typeDescriptionAttribute.Description; + } + + if (context.JsonPropertyInfo?.AttributeProvider?.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is { } propertyDescriptionAttribute) + { + if (isInlinedSchema) + { + schema.Description = propertyDescriptionAttribute.Description; + } + else + { + schema.Metadata![OpenApiConstants.RefDescriptionAnnotation] = propertyDescriptionAttribute.Description; + } + } + + if (context.JsonTypeInfo.Type.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is { } typeDefaultValueAttribute) + { + schema.Default = GetDefaultValueAsJsonNode(typeDefaultValueAttribute, context.JsonTypeInfo); + } + + if (context.JsonPropertyInfo?.AttributeProvider?.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is { } propertyDefaultValueAttribute) + { + var defaultValueJson = GetDefaultValueAsJsonNode(propertyDefaultValueAttribute, context.JsonTypeInfo); + if (isInlinedSchema) + { + schema.Default = defaultValueJson; + } + else + { + schema.Metadata![OpenApiConstants.RefDefaultAnnotation] = defaultValueJson!; + } + } + + return Task.CompletedTask; + } + + private static JsonNode? GetDefaultValueAsJsonNode(DefaultValueAttribute defaultValueAttribute, JsonTypeInfo jsonTypeInfo) + { + if(defaultValueAttribute.Value is null) + { + return null; + } + + return JsonSerializer.SerializeToNode(defaultValueAttribute.Value, jsonTypeInfo); + } +} diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OpenApiOptionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OpenApiOptionsTests.cs index 5a55dbfd35d5..eb29f8f5304a 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OpenApiOptionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/OpenApiOptionsTests.cs @@ -10,6 +10,8 @@ public void AddDocumentTransformer_WithDocumentTransformerDelegate() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); + var transformer = new Func((document, context, cancellationToken) => { document.Info.Title = "New Title"; @@ -32,6 +34,8 @@ public void AddDocumentTransformer_WithDocumentTransformerInstance() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); + var transformer = new TestOpenApiDocumentTransformer(); // Act @@ -50,6 +54,7 @@ public void AddDocumentTransformer_WithDocumentTransformerType() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); // Act var result = options.AddDocumentTransformer(); @@ -67,6 +72,8 @@ public void AddOperationTransformer_WithOperationTransformerDelegate() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); + var transformer = new Func((operation, context, cancellationToken) => { operation.Description = "New Description"; @@ -89,6 +96,8 @@ public void AddOperationTransformer_WithOperationTransformerInstance() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); + var transformer = new TestOpenApiOperationTransformer(); // Act @@ -107,6 +116,7 @@ public void AddOperationTransformer_WithOperationTransformerType() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); // Act var result = options.AddOperationTransformer(); @@ -124,6 +134,8 @@ public void AddSchemaTransformer_WithSchemaTransformerDelegate() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); + var transformer = new Func((schema, context, cancellationToken) => { schema.Description = "New Description"; @@ -146,6 +158,8 @@ public void AddSchemaTransformer_WithSchemaTransformerInstance() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); + var transformer = new TestOpenApiSchemaTransformer(); // Act @@ -164,6 +178,7 @@ public void AddSchemaTransformer_WithSchemaTransformerType() { // Arrange var options = new OpenApiOptions(); + RemoveBuiltInTransformers(options); // Act var result = options.AddSchemaTransformer(); @@ -176,6 +191,13 @@ public void AddSchemaTransformer_WithSchemaTransformerType() Assert.Empty(options.OperationTransformers); } + private static void RemoveBuiltInTransformers(OpenApiOptions options) + { + options.DocumentTransformers.Clear(); + options.OperationTransformers.Clear(); + options.SchemaTransformers.Clear(); + } + private class TestOpenApiDocumentTransformer : IOpenApiDocumentTransformer { public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)