Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading