Skip to content

Commit

Permalink
Upgrade to OpenAPI.NET v2.0.0-preview7 (#60269)
Browse files Browse the repository at this point in the history
  • Loading branch information
captainsafia authored Feb 14, 2025
1 parent e64a99d commit 58ac2f7
Show file tree
Hide file tree
Showing 29 changed files with 337 additions and 310 deletions.
4 changes: 2 additions & 2 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@
<XunitExtensibilityExecutionVersion>$(XunitVersion)</XunitExtensibilityExecutionVersion>
<XUnitRunnerVisualStudioVersion>2.8.2</XUnitRunnerVisualStudioVersion>
<MicrosoftDataSqlClientVersion>5.2.2</MicrosoftDataSqlClientVersion>
<MicrosoftOpenApiVersion>2.0.0-preview5</MicrosoftOpenApiVersion>
<MicrosoftOpenApiReadersVersion>2.0.0-preview5</MicrosoftOpenApiReadersVersion>
<MicrosoftOpenApiVersion>2.0.0-preview7</MicrosoftOpenApiVersion>
<MicrosoftOpenApiReadersVersion>2.0.0-preview7</MicrosoftOpenApiReadersVersion>
<!-- dotnet tool versions (see also auto-updated DotnetEfVersion property). -->
<DotnetDumpVersion>6.0.322601</DotnetDumpVersion>
<DotnetServeVersion>1.10.93</DotnetServeVersion>
Expand Down
10 changes: 7 additions & 3 deletions src/OpenApi/gen/XmlCommentGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore.OpenApi.Generated
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.References;
using Microsoft.OpenApi.Any;
{{GeneratedCodeAttribute}}
Expand Down Expand Up @@ -256,12 +257,15 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform
var operationParameter = operation.Parameters?.SingleOrDefault(parameter => parameter.Name == parameterComment.Name);
if (operationParameter is not null)
{
operationParameter.Description = parameterComment.Description;
var targetOperationParameter = operationParameter is OpenApiParameterReference reference
? reference.Target
: (OpenApiParameter)operationParameter;
targetOperationParameter.Description = parameterComment.Description;
if (parameterComment.Example is { } jsonString)
{
operationParameter.Example = jsonString.Parse();
targetOperationParameter.Example = jsonString.Parse();
}
operationParameter.Deprecated = parameterComment.Deprecated;
targetOperationParameter.Deprecated = parameterComment.Deprecated;
}
else
{
Expand Down
8 changes: 8 additions & 0 deletions src/OpenApi/sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Serialization;
using Microsoft.OpenApi.Models;
using Sample.Transformers;

Expand All @@ -11,6 +12,13 @@
#pragma warning restore IL2026
builder.Services.AddAuthentication().AddJwtBearer();

// Supports representing integer formats as strictly numerically values
// inside the schema.
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
});

builder.Services.AddOpenApi("v1", options =>
{
options.AddHeader("X-Version", "1.0");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;

namespace Sample.Transformers;

Expand All @@ -14,7 +15,7 @@ public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransf
var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
{
var requirements = new Dictionary<string, OpenApiSecurityScheme>
var requirements = new Dictionary<string, IOpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Expand Down
86 changes: 72 additions & 14 deletions src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,15 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable
}
else if (attribute is MinLengthAttribute minLengthAttribute)
{
var targetKey = schema[OpenApiSchemaKeywords.TypeKeyword]?.GetValue<string>() == "array" ? OpenApiSchemaKeywords.MinItemsKeyword : OpenApiSchemaKeywords.MinLengthKeyword;
schema[targetKey] = minLengthAttribute.Length;
if (MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes &&
schemaTypes.HasFlag(JsonSchemaType.Array))
{
schema[OpenApiSchemaKeywords.MinItemsKeyword] = minLengthAttribute.Length;
}
else
{
schema[OpenApiSchemaKeywords.MinLengthKeyword] = minLengthAttribute.Length;
}
}
else if (attribute is LengthAttribute lengthAttribute)
{
Expand Down Expand Up @@ -191,14 +198,13 @@ internal static void ApplyPrimitiveTypesAndFormats(this JsonNode schema, JsonSch
var underlyingType = Nullable.GetUnderlyingType(type);
if (_simpleTypeToOpenApiSchema.TryGetValue(underlyingType ?? type, out var openApiSchema))
{
schema[OpenApiSchemaKeywords.NullableKeyword] = openApiSchema.Nullable || (schema[OpenApiSchemaKeywords.TypeKeyword] is JsonArray schemaType && schemaType.GetValues<string>().Contains("null"));
schema[OpenApiSchemaKeywords.TypeKeyword] = openApiSchema.Type.ToString();
if (underlyingType != null && MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes &&
!schemaTypes.HasFlag(JsonSchemaType.Null))
{
schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes | JsonSchemaType.Null).ToString();
}
schema[OpenApiSchemaKeywords.FormatKeyword] = openApiSchema.Format;
schema[OpenApiConstants.SchemaId] = createSchemaReferenceId(context.TypeInfo);
schema[OpenApiSchemaKeywords.NullableKeyword] = underlyingType != null;
// Clear out patterns that the underlying JSON schema generator uses to represent
// validations for DateTime, DateTimeOffset, and integers.
schema[OpenApiSchemaKeywords.PatternKeyword] = null;
}
}

Expand Down Expand Up @@ -334,14 +340,17 @@ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescri
schema.ApplyRouteConstraints(constraints);
}

if (parameterDescription.Source is { } bindingSource && SupportsNullableProperty(bindingSource))
if (parameterDescription.Source is { } bindingSource
&& SupportsNullableProperty(bindingSource)
&& MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes &&
schemaTypes.HasFlag(JsonSchemaType.Null))
{
schema[OpenApiSchemaKeywords.NullableKeyword] = false;
schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes & ~JsonSchemaType.Null).ToString();
}

// Parameters sourced from the header, query, route, and/or form cannot be nullable based on our binding
// rules but can be optional.
static bool SupportsNullableProperty(BindingSource bindingSource) =>bindingSource == BindingSource.Header
static bool SupportsNullableProperty(BindingSource bindingSource) => bindingSource == BindingSource.Header
|| bindingSource == BindingSource.Query
|| bindingSource == BindingSource.Path
|| bindingSource == BindingSource.Form
Expand Down Expand Up @@ -435,9 +444,11 @@ internal static void ApplyNullabilityContextInfo(this JsonNode schema, Parameter

var nullabilityInfoContext = new NullabilityInfoContext();
var nullabilityInfo = nullabilityInfoContext.Create(parameterInfo);
if (nullabilityInfo.WriteState == NullabilityState.Nullable)
if (nullabilityInfo.WriteState == NullabilityState.Nullable
&& MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes
&& !schemaTypes.HasFlag(JsonSchemaType.Null))
{
schema[OpenApiSchemaKeywords.NullableKeyword] = true;
schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes | JsonSchemaType.Null).ToString();
}
}

Expand All @@ -452,7 +463,54 @@ internal static void ApplyNullabilityContextInfo(this JsonNode schema, JsonPrope
// all schema (no type, no format, no constraints).
if (propertyInfo.PropertyType != typeof(object) && (propertyInfo.IsGetNullable || propertyInfo.IsSetNullable))
{
schema[OpenApiSchemaKeywords.NullableKeyword] = true;
if (MapJsonNodeToSchemaType(schema[OpenApiSchemaKeywords.TypeKeyword]) is { } schemaTypes &&
!schemaTypes.HasFlag(JsonSchemaType.Null))
{
schema[OpenApiSchemaKeywords.TypeKeyword] = (schemaTypes | JsonSchemaType.Null).ToString();
}
}
}

private static JsonSchemaType? MapJsonNodeToSchemaType(JsonNode? jsonNode)
{
if (jsonNode is not JsonArray jsonArray)
{
if (Enum.TryParse<JsonSchemaType>(jsonNode?.GetValue<string>(), true, out var openApiSchemaType))
{
return openApiSchemaType;
}

return jsonNode is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var identifier)
? ToSchemaType(identifier)
: null;
}

JsonSchemaType? schemaType = null;

foreach (var node in jsonArray)
{
if (node is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var identifier))
{
var type = ToSchemaType(identifier);
schemaType = schemaType.HasValue ? (schemaType | type) : type;
}
}

return schemaType;

static JsonSchemaType ToSchemaType(string identifier)
{
return identifier.ToLowerInvariant() switch
{
"null" => JsonSchemaType.Null,
"boolean" => JsonSchemaType.Boolean,
"integer" => JsonSchemaType.Integer,
"number" => JsonSchemaType.Number,
"string" => JsonSchemaType.String,
"array" => JsonSchemaType.Array,
"object" => JsonSchemaType.Object,
_ => throw new InvalidOperationException($"Unknown schema type: {identifier}"),
};
}
}
}
11 changes: 6 additions & 5 deletions src/OpenApi/src/Extensions/OpenApiDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi.Models.References;

namespace Microsoft.AspNetCore.OpenApi;

internal static class OpenApiDocumentExtensions
{
/// <summary>
/// Registers a <see cref="OpenApiSchema" /> into the top-level components store on the
/// Registers a <see cref="IOpenApiSchema" /> into the top-level components store on the
/// <see cref="OpenApiDocument" /> and returns a resolvable reference to it.
/// </summary>
/// <param name="document">The <see cref="OpenApiDocument"/> to register the schema onto.</param>
/// <param name="schemaId">The ID that serves as the key for the schema in the schema store.</param>
/// <param name="schema">The <see cref="OpenApiSchema" /> to register into the document.</param>
/// <returns>An <see cref="OpenApiSchema"/> with a reference to the stored schema.</returns>
public static OpenApiSchema AddOpenApiSchemaByReference(this OpenApiDocument document, string schemaId, OpenApiSchema schema)
/// <param name="schema">The <see cref="IOpenApiSchema" /> to register into the document.</param>
/// <returns>An <see cref="IOpenApiSchema"/> with a reference to the stored schema.</returns>
public static IOpenApiSchema AddOpenApiSchemaByReference(this OpenApiDocument document, string schemaId, IOpenApiSchema schema)
{
document.Components ??= new();
document.Components.Schemas ??= new Dictionary<string, OpenApiSchema>();
document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>();
document.Components.Schemas[schemaId] = schema;
document.Workspace ??= new();
var location = document.BaseUri + "/components/schemas/" + schemaId;
Expand Down
12 changes: 5 additions & 7 deletions src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using OpenApiConstants = Microsoft.AspNetCore.OpenApi.OpenApiConstants;

internal sealed partial class OpenApiJsonSchema
Expand Down Expand Up @@ -220,10 +221,6 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
var valueConverter = (JsonConverter<OpenApiJsonSchema>)options.GetTypeInfo(typeof(OpenApiJsonSchema)).Converter;
schema.Items = valueConverter.Read(ref reader, typeof(OpenApiJsonSchema), options)?.Schema;
break;
case OpenApiSchemaKeywords.NullableKeyword:
reader.Read();
schema.Nullable = reader.GetBoolean();
break;
case OpenApiSchemaKeywords.DescriptionKeyword:
reader.Read();
schema.Description = reader.GetString();
Expand Down Expand Up @@ -274,7 +271,7 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
case OpenApiSchemaKeywords.PropertiesKeyword:
reader.Read();
var props = ReadDictionary<OpenApiJsonSchema>(ref reader);
schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema);
schema.Properties = props?.ToDictionary(p => p.Key, p => p.Value.Schema as IOpenApiSchema);
break;
case OpenApiSchemaKeywords.AdditionalPropertiesKeyword:
reader.Read();
Expand All @@ -290,7 +287,7 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
reader.Read();
schema.Type = JsonSchemaType.Object;
var schemas = ReadList<OpenApiJsonSchema>(ref reader);
schema.AnyOf = schemas?.Select(s => s.Schema).ToList();
schema.AnyOf = schemas?.Select(s => s.Schema as IOpenApiSchema).ToList();
break;
case OpenApiSchemaKeywords.DiscriminatorKeyword:
reader.Read();
Expand Down Expand Up @@ -322,7 +319,8 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
break;
case OpenApiSchemaKeywords.RefKeyword:
reader.Read();
schema.Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = reader.GetString() };
schema.Annotations ??= new Dictionary<string, object>();
schema.Annotations[OpenApiConstants.RefId] = reader.GetString();
break;
default:
reader.Skip();
Expand Down
1 change: 0 additions & 1 deletion src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ internal class OpenApiSchemaKeywords
public const string AnyOfKeyword = "anyOf";
public const string EnumKeyword = "enum";
public const string DefaultKeyword = "default";
public const string NullableKeyword = "nullable";
public const string DescriptionKeyword = "description";
public const string DiscriminatorKeyword = "discriminatorName";
public const string DiscriminatorMappingKeyword = "discriminatorMapping";
Expand Down
1 change: 1 addition & 0 deletions src/OpenApi/src/Services/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal static class OpenApiConstants
internal const string DefaultOpenApiRoute = "/openapi/{documentName}.json";
internal const string DescriptionId = "x-aspnetcore-id";
internal const string SchemaId = "x-schema-id";
internal const string RefId = "x-ref-id";
internal const string DefaultOpenApiResponseKey = "default";
// Since there's a finite set of operation types that can be included in a given
// OpenApiPaths, we can pre-allocate an array of these types and use a direct
Expand Down
Loading

0 comments on commit 58ac2f7

Please sign in to comment.