diff --git a/.vscode/launch.json b/.vscode/launch.json index 149c5b96..af0ffb2e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/TodoApp/bin/Debug/net9.0/TodoApp.dll", + "program": "${workspaceFolder}/src/TodoApp/bin/Debug/net10.0/TodoApp.dll", "args": [], "cwd": "${workspaceFolder}/src/TodoApp", "stopAtEntry": false, diff --git a/.vsconfig b/.vsconfig index 96ff329c..99efa2ec 100644 --- a/.vsconfig +++ b/.vsconfig @@ -2,7 +2,7 @@ "version": "1.0", "components": [ "Component.Microsoft.VisualStudio.RazorExtension", - "Microsoft.NetCore.Component.Runtime.9.0", + "Microsoft.NetCore.Component.Runtime.10.0", "Microsoft.NetCore.Component.SDK", "Microsoft.VisualStudio.Component.CoreEditor", "Microsoft.VisualStudio.Component.JavaScript.Diagnostics", diff --git a/NuGet.config b/NuGet.config index 38ac8e75..dc1b1613 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,9 +2,13 @@ + + + + diff --git a/global.json b/global.json index 26040ae0..d37f8c39 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.300", + "version": "10.0.100-preview.5.25277.114", "allowPrerelease": false, "rollForward": "latestMajor" } diff --git a/perf/TodoApp.Benchmarks/TodoApp.Benchmarks.csproj b/perf/TodoApp.Benchmarks/TodoApp.Benchmarks.csproj index e309c908..8cfb5486 100644 --- a/perf/TodoApp.Benchmarks/TodoApp.Benchmarks.csproj +++ b/perf/TodoApp.Benchmarks/TodoApp.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe TodoApp - net9.0 + net10.0 diff --git a/src/TodoApp/OpenApi/AspNetCore/AspNetCoreOpenApiEndpoints.cs b/src/TodoApp/OpenApi/AspNetCore/AspNetCoreOpenApiEndpoints.cs index c4f4f921..21a78cbd 100644 --- a/src/TodoApp/OpenApi/AspNetCore/AspNetCoreOpenApiEndpoints.cs +++ b/src/TodoApp/OpenApi/AspNetCore/AspNetCoreOpenApiEndpoints.cs @@ -2,6 +2,8 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; +using Microsoft.OpenApi.Models.References; namespace TodoApp.OpenApi.AspNetCore; @@ -14,6 +16,9 @@ public static IServiceCollection AddAspNetCoreOpenApi(this IServiceCollection se // Add a document transformer to customise the generated OpenAPI document options.AddDocumentTransformer((document, _, _) => { + // TODO Use 3.1 when all three OpenAPI implementations support it + options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; + // Add a title and version for the OpenAPI document document.Info.Title = "Todo API (ASP.NET Core OpenAPI)"; document.Info.Description = "An API for managing Todo items."; @@ -39,18 +44,16 @@ public static IServiceCollection AddAspNetCoreOpenApi(this IServiceCollection se Description = "Bearer authentication using a JWT.", Scheme = "bearer", Type = SecuritySchemeType.Http, - Reference = new() - { - Id = "Bearer", - Type = ReferenceType.SecurityScheme, - }, }; + var referenceId = "Bearer"; + var reference = new OpenApiSecuritySchemeReference(referenceId, document); + document.Components ??= new(); - document.Components.SecuritySchemes ??= new Dictionary(); - document.Components.SecuritySchemes[scheme.Reference.Id] = scheme; - document.SecurityRequirements ??= []; - document.SecurityRequirements.Add(new() { [scheme] = [] }); + document.Components.SecuritySchemes ??= []; + document.Components.SecuritySchemes[referenceId] = scheme; + document.Security ??= []; + document.Security.Add(new() { [reference] = [] }); return Task.CompletedTask; }); diff --git a/src/TodoApp/OpenApi/ExampleFormatter.cs b/src/TodoApp/OpenApi/ExampleFormatter.cs index e56c62e1..915b8bb1 100644 --- a/src/TodoApp/OpenApi/ExampleFormatter.cs +++ b/src/TodoApp/OpenApi/ExampleFormatter.cs @@ -2,8 +2,8 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using Microsoft.OpenApi.Any; namespace TodoApp.OpenApi; @@ -19,9 +19,9 @@ internal static class ExampleFormatter /// The type of the example provider. /// The JSON serializer context to use. /// - /// The to use as the example. + /// The to use as the example. /// - public static IOpenApiAny AsJson(JsonSerializerContext context) + public static JsonNode? AsJson(JsonSerializerContext context) where TProvider : IExampleProvider => AsJson(TProvider.GenerateExample(), context); @@ -32,87 +32,12 @@ public static IOpenApiAny AsJson(JsonSerializerContext conte /// The example value to format as JSON. /// The JSON serializer context to use. /// - /// The to use as the example. + /// The to use as the example. /// - public static IOpenApiAny AsJson(T example, JsonSerializerContext context) + public static JsonNode? AsJson(T example, JsonSerializerContext context) { // Apply any formatting rules configured for the API (e.g. camel casing) var json = JsonSerializer.Serialize(example, typeof(T), context); - using var document = JsonDocument.Parse(json); - - if (document.RootElement.ValueKind == JsonValueKind.String) - { - return new OpenApiString(document.RootElement.ToString()); - } - - var result = new OpenApiObject(); - - // Recursively build up the example from the properties of the object - foreach (var token in document.RootElement.EnumerateObject()) - { - if (TryParse(token.Value, out var any)) - { - result[token.Name] = any; - } - } - - return result; - } - - private static bool TryParse(JsonElement token, out IOpenApiAny? any) - { - any = null; - - switch (token.ValueKind) - { - case JsonValueKind.Array: - var array = new OpenApiArray(); - - foreach (var value in token.EnumerateArray()) - { - if (TryParse(value, out var child)) - { - array.Add(child); - } - } - - any = array; - return true; - - case JsonValueKind.False: - any = new OpenApiBoolean(false); - return true; - - case JsonValueKind.True: - any = new OpenApiBoolean(true); - return true; - - case JsonValueKind.Number: - any = new OpenApiDouble(token.GetDouble()); - return true; - - case JsonValueKind.String: - any = new OpenApiString(token.GetString()); - return true; - - case JsonValueKind.Object: - var obj = new OpenApiObject(); - - foreach (var child in token.EnumerateObject()) - { - if (TryParse(child.Value, out var value)) - { - obj[child.Name] = value; - } - } - - any = obj; - return true; - - case JsonValueKind.Null: - case JsonValueKind.Undefined: - default: - return false; - } + return JsonNode.Parse(json); } } diff --git a/src/TodoApp/OpenApi/ExamplesProcessor.cs b/src/TodoApp/OpenApi/ExamplesProcessor.cs index 5a9f230e..f5ff54bc 100644 --- a/src/TodoApp/OpenApi/ExamplesProcessor.cs +++ b/src/TodoApp/OpenApi/ExamplesProcessor.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; namespace TodoApp.OpenApi; @@ -56,7 +57,7 @@ protected void Process(OpenApiSchema schema, Type type) } private static void TryAddParameterExamples( - IList parameters, + IList parameters, ApiDescription description, IList examples) { @@ -89,11 +90,14 @@ private static void TryAddParameterExamples( } private static void TryAddRequestExamples( - OpenApiRequestBody body, + IOpenApiRequestBody body, ApiDescription description, IList examples) { - if (!body.Content.TryGetValue("application/json", out var mediaType) || mediaType.Example is not null) + if (body is null || + body.Content is null || + !body.Content.TryGetValue("application/json", out var mediaType) || + mediaType.Example is not null) { return; } @@ -130,7 +134,7 @@ private static void TryAddResponseExamples( foreach (var responseFormat in schemaResponse.ApiResponseFormats) { if (responses.TryGetValue(schemaResponse.StatusCode.ToString(CultureInfo.InvariantCulture), out var response) && - response.Content.TryGetValue(responseFormat.MediaType, out var mediaType)) + response.Content?.TryGetValue(responseFormat.MediaType, out var mediaType) is true) { mediaType.Example ??= (metadata ?? examples.SingleOrDefault((p) => p.SchemaType == schemaResponse.Type))?.GenerateExample(Context); } diff --git a/src/TodoApp/OpenApi/IOpenApiExampleMetadata.cs b/src/TodoApp/OpenApi/IOpenApiExampleMetadata.cs index 1af9d26e..b71d6761 100644 --- a/src/TodoApp/OpenApi/IOpenApiExampleMetadata.cs +++ b/src/TodoApp/OpenApi/IOpenApiExampleMetadata.cs @@ -1,8 +1,8 @@ // Copyright (c) Martin Costello, 2024. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using Microsoft.OpenApi.Any; namespace TodoApp.OpenApi; @@ -29,7 +29,7 @@ public interface IOpenApiExampleMetadata /// /// The JSON serializer context to use to generate the example. /// - /// The OpenAPI example to use. + /// The OpenAPI example to use, if any. /// - IOpenApiAny GenerateExample(JsonSerializerContext context); + JsonNode? GenerateExample(JsonSerializerContext context); } diff --git a/src/TodoApp/OpenApi/OpenApiExampleAttribute`2.cs b/src/TodoApp/OpenApi/OpenApiExampleAttribute`2.cs index 3b1a1241..54d4c74d 100644 --- a/src/TodoApp/OpenApi/OpenApiExampleAttribute`2.cs +++ b/src/TodoApp/OpenApi/OpenApiExampleAttribute`2.cs @@ -1,8 +1,8 @@ // Copyright (c) Martin Costello, 2024. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using Microsoft.OpenApi.Any; namespace TodoApp.OpenApi; @@ -30,6 +30,6 @@ public class OpenApiExampleAttribute : Attribute, IOpenApiEx object? IOpenApiExampleMetadata.GenerateExample() => GenerateExample(); /// - IOpenApiAny IOpenApiExampleMetadata.GenerateExample(JsonSerializerContext context) + JsonNode? IOpenApiExampleMetadata.GenerateExample(JsonSerializerContext context) => ExampleFormatter.AsJson(GenerateExample(), context); } diff --git a/src/TodoApp/OpenApi/Swashbuckle/ExampleFilter.cs b/src/TodoApp/OpenApi/Swashbuckle/ExampleFilter.cs index 53dd0cda..59b007b3 100644 --- a/src/TodoApp/OpenApi/Swashbuckle/ExampleFilter.cs +++ b/src/TodoApp/OpenApi/Swashbuckle/ExampleFilter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.Interfaces; using Swashbuckle.AspNetCore.SwaggerGen; namespace TodoApp.OpenApi.Swashbuckle; @@ -16,6 +17,11 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) => Process(operation, context.ApiDescription); /// - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - => Process(schema, context.Type); + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) + { + if (schema is OpenApiSchema concrete) + { + Process(concrete, context.Type); + } + } } diff --git a/src/TodoApp/OpenApi/Swashbuckle/SwashbuckleOpenApiEndpoints.cs b/src/TodoApp/OpenApi/Swashbuckle/SwashbuckleOpenApiEndpoints.cs index f81822ea..2bdc674a 100644 --- a/src/TodoApp/OpenApi/Swashbuckle/SwashbuckleOpenApiEndpoints.cs +++ b/src/TodoApp/OpenApi/Swashbuckle/SwashbuckleOpenApiEndpoints.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models.References; namespace TodoApp.OpenApi.Swashbuckle; @@ -39,15 +40,10 @@ public static IServiceCollection AddSwashbuckleOpenApi(this IServiceCollection s Description = "Bearer authentication using a JWT.", Scheme = "bearer", Type = SecuritySchemeType.Http, - Reference = new() - { - Id = "Bearer", - Type = ReferenceType.SecurityScheme, - }, - UnresolvedReference = false, }; - options.AddSecurityDefinition(scheme.Reference.Id, scheme); - options.AddSecurityRequirement(new() { [scheme] = [] }); + + options.AddSecurityDefinition("Bearer", scheme); + options.AddSecurityRequirement((document) => new() { [new("Bearer", document)] = [] }); // Enable reading OpenAPI metadata from attributes options.EnableAnnotations(); diff --git a/src/TodoApp/TodoApp.csproj b/src/TodoApp/TodoApp.csproj index 34116a64..0017be8d 100644 --- a/src/TodoApp/TodoApp.csproj +++ b/src/TodoApp/TodoApp.csproj @@ -9,19 +9,19 @@ true --openapi-version OpenApi3_0 TodoApp - net9.0 + net10.0 true latest - - - - + + + + - - + + diff --git a/startvscode.cmd b/startvscode.cmd index d321aa83..91142b50 100644 --- a/startvscode.cmd +++ b/startvscode.cmd @@ -11,7 +11,7 @@ SET DOTNET_ROOT(x86)=%~dp0.dotnet\x86 SET PATH=%DOTNET_ROOT%;%PATH% :: Sets the Target Framework for Visual Studio Code. -SET TARGET=net9.0 +SET TARGET=net10.0 SET FOLDER=%~1 diff --git a/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=openapi.verified.txt b/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=openapi.verified.txt index a51ff5f9..8580d9cd 100644 --- a/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=openapi.verified.txt +++ b/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=openapi.verified.txt @@ -299,8 +299,8 @@ }, status: { type: integer, - format: int32, - nullable: true + nullable: true, + format: int32 }, detail: { type: string, diff --git a/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=swagger.verified.txt b/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=swagger.verified.txt index 53436825..4ce97312 100644 --- a/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=swagger.verified.txt +++ b/tests/TodoApp.Tests/OpenApiTests.Schema_Is_Correct_schemaUrl=swagger.verified.txt @@ -260,8 +260,8 @@ properties: { text: { type: string, - description: Gets or sets the text of the Todo item., - nullable: true + nullable: true, + description: Gets or sets the text of the Todo item. } }, additionalProperties: false, @@ -275,8 +275,8 @@ properties: { id: { type: string, - description: Gets or sets the ID of the created Todo item., - nullable: true + nullable: true, + description: Gets or sets the ID of the created Todo item. } }, additionalProperties: false, @@ -298,8 +298,8 @@ }, status: { type: integer, - format: int32, - nullable: true + nullable: true, + format: int32 }, detail: { type: string, @@ -322,13 +322,13 @@ properties: { id: { type: string, - description: Gets or sets the ID of the Todo item., - nullable: true + nullable: true, + description: Gets or sets the ID of the Todo item. }, text: { type: string, - description: Gets or sets the text of the Todo item., - nullable: true + nullable: true, + description: Gets or sets the text of the Todo item. }, isCompleted: { type: boolean, @@ -336,8 +336,8 @@ }, lastUpdated: { type: string, - description: Gets or sets the date and time the Todo item was last updated., - nullable: true + nullable: true, + description: Gets or sets the date and time the Todo item was last updated. } }, additionalProperties: false, @@ -353,11 +353,11 @@ properties: { items: { type: array, + nullable: true, items: { $ref: #/components/schemas/TodoItemModel }, - description: Gets or sets the Todo item(s)., - nullable: true + description: Gets or sets the Todo item(s). } }, additionalProperties: false, diff --git a/tests/TodoApp.Tests/OpenApiTests.cs b/tests/TodoApp.Tests/OpenApiTests.cs index 0d51c9a1..06a2f654 100644 --- a/tests/TodoApp.Tests/OpenApiTests.cs +++ b/tests/TodoApp.Tests/OpenApiTests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using Microsoft.OpenApi.Extensions; -using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Validations; namespace TodoApp; @@ -63,12 +63,14 @@ public async Task Schema_Has_No_Validation_Warnings(string schemaUrl) using var schema = await client.GetStreamAsync(schemaUrl, TestContext.Current.CancellationToken); // Assert - var reader = new OpenApiStreamReader(); - var actual = await reader.ReadAsync(schema, TestContext.Current.CancellationToken); + var actual = await OpenApiDocument.LoadAsync(schema, "json", cancellationToken: TestContext.Current.CancellationToken); - Assert.Empty(actual.OpenApiDiagnostic.Errors); + Assert.NotNull(actual); + Assert.NotNull(actual.Diagnostic); + Assert.Empty(actual.Diagnostic.Errors); + Assert.NotNull(actual.Document); - var errors = actual.OpenApiDocument.Validate(ruleSet); + var errors = actual.Document.Validate(ruleSet); Assert.Empty(errors); } diff --git a/tests/TodoApp.Tests/TodoApp.Tests.csproj b/tests/TodoApp.Tests/TodoApp.Tests.csproj index 2dafa13c..d16afcf3 100644 --- a/tests/TodoApp.Tests/TodoApp.Tests.csproj +++ b/tests/TodoApp.Tests/TodoApp.Tests.csproj @@ -4,15 +4,14 @@ $(NoWarn);CA1861 Exe TodoApp - net9.0 + net10.0 - + -