Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion .vsconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
<configuration>
<packageSources>
<clear />
<add key="domaindrivendev" value="https://www.myget.org/F/domaindrivendev/api/v3/index.json" />
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="domaindrivendev">
<package pattern="Swashbuckle.AspNetCore*" />
</packageSource>
<packageSource key="NuGet">
<package pattern="*" />
</packageSource>
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.306",
"version": "10.0.100-rc.2.25502.107",
"allowPrerelease": false,
"rollForward": "latestMajor"
}
Expand Down
2 changes: 1 addition & 1 deletion perf/TodoApp.Benchmarks/TodoApp.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>TodoApp</RootNamespace>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\TodoApp\TodoApp.csproj" />
Expand Down
2 changes: 1 addition & 1 deletion src/TodoApp/OpenApi/AspNetCore/AddExamplesTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

namespace TodoApp.OpenApi.AspNetCore;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using System.Xml;
using System.Xml.XPath;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

namespace TodoApp.OpenApi.AspNetCore;

Expand Down
21 changes: 11 additions & 10 deletions src/TodoApp/OpenApi/AspNetCore/AspNetCoreOpenApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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 Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

namespace TodoApp.OpenApi.AspNetCore;

Expand All @@ -14,6 +14,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.";
Expand All @@ -39,18 +42,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<string, OpenApiSecurityScheme>();
document.Components.SecuritySchemes[scheme.Reference.Id] = scheme;
document.SecurityRequirements ??= [];
document.SecurityRequirements.Add(new() { [scheme] = [] });
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes[referenceId] = scheme;
document.Security ??= [];
document.Security.Add(new() { [reference] = [] });

return Task.CompletedTask;
});
Expand Down
87 changes: 6 additions & 81 deletions src/TodoApp/OpenApi/ExampleFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,9 +19,9 @@ internal static class ExampleFormatter
/// <typeparam name="TProvider">The type of the example provider.</typeparam>
/// <param name="context">The JSON serializer context to use.</param>
/// <returns>
/// The <see cref="IOpenApiAny"/> to use as the example.
/// The <see cref="JsonNode"/> to use as the example.
/// </returns>
public static IOpenApiAny AsJson<TSchema, TProvider>(JsonSerializerContext context)
public static JsonNode? AsJson<TSchema, TProvider>(JsonSerializerContext context)
where TProvider : IExampleProvider<TSchema>
=> AsJson(TProvider.GenerateExample(), context);

Expand All @@ -32,87 +32,12 @@ public static IOpenApiAny AsJson<TSchema, TProvider>(JsonSerializerContext conte
/// <param name="example">The example value to format as JSON.</param>
/// <param name="context">The JSON serializer context to use.</param>
/// <returns>
/// The <see cref="IOpenApiAny"/> to use as the example.
/// The <see cref="JsonNode"/> to use as the example.
/// </returns>
public static IOpenApiAny AsJson<T>(T example, JsonSerializerContext context)
public static JsonNode? AsJson<T>(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);
}
}
13 changes: 8 additions & 5 deletions src/TodoApp/OpenApi/ExamplesProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

namespace TodoApp.OpenApi;

Expand Down Expand Up @@ -56,7 +56,7 @@ protected void Process(OpenApiSchema schema, Type type)
}

private static void TryAddParameterExamples(
IList<OpenApiParameter> parameters,
IList<IOpenApiParameter> parameters,
ApiDescription description,
IList<IOpenApiExampleMetadata> examples)
{
Expand Down Expand Up @@ -89,11 +89,14 @@ private static void TryAddParameterExamples(
}

private static void TryAddRequestExamples(
OpenApiRequestBody body,
IOpenApiRequestBody body,
ApiDescription description,
IList<IOpenApiExampleMetadata> 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;
}
Expand Down Expand Up @@ -130,7 +133,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);
}
Expand Down
6 changes: 3 additions & 3 deletions src/TodoApp/OpenApi/IOpenApiExampleMetadata.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -29,7 +29,7 @@ public interface IOpenApiExampleMetadata
/// </summary>
/// <param name="context">The JSON serializer context to use to generate the example.</param>
/// <returns>
/// The OpenAPI example to use.
/// The OpenAPI example to use, if any.
/// </returns>
IOpenApiAny GenerateExample(JsonSerializerContext context);
JsonNode? GenerateExample(JsonSerializerContext context);
}
4 changes: 2 additions & 2 deletions src/TodoApp/OpenApi/OpenApiExampleAttribute`2.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -30,6 +30,6 @@ public class OpenApiExampleAttribute<TSchema, TProvider> : Attribute, IOpenApiEx
object? IOpenApiExampleMetadata.GenerateExample() => GenerateExample();

/// <inheritdoc/>
IOpenApiAny IOpenApiExampleMetadata.GenerateExample(JsonSerializerContext context)
JsonNode? IOpenApiExampleMetadata.GenerateExample(JsonSerializerContext context)
=> ExampleFormatter.AsJson(GenerateExample(), context);
}
4 changes: 2 additions & 2 deletions src/TodoApp/OpenApi/Swashbuckle/AddDocumentTagsFilter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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 Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace TodoApp.OpenApi.Swashbuckle;
Expand All @@ -13,7 +13,7 @@ public class AddDocumentTagsFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Tags ??= [];
swaggerDoc.Tags ??= new HashSet<OpenApiTag>();
swaggerDoc.Tags.Add(new() { Name = "TodoApp" });
}
}
2 changes: 1 addition & 1 deletion src/TodoApp/OpenApi/Swashbuckle/AddServersFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace TodoApp.OpenApi.Swashbuckle;
Expand Down
11 changes: 8 additions & 3 deletions src/TodoApp/OpenApi/Swashbuckle/ExampleFilter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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 Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace TodoApp.OpenApi.Swashbuckle;
Expand All @@ -16,6 +16,11 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
=> Process(operation, context.ApiDescription);

/// <inheritdoc />
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);
}
}
}
13 changes: 4 additions & 9 deletions src/TodoApp/OpenApi/Swashbuckle/SwashbuckleOpenApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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 Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

namespace TodoApp.OpenApi.Swashbuckle;

Expand Down Expand Up @@ -39,15 +39,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();
Expand Down
Loading