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 Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


<ItemGroup>
<PackageReference Include="Polyfill" PrivateAssets="All" />
<PackageReference Include="Polyfill" PrivateAssets="All" Condition="'$(TargetFramework)' != 'net10.0'" />
</ItemGroup>

<ItemGroup Label="Settings Files">
Expand Down
5 changes: 4 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,16 @@
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.OData" Version="10.0.0-preview.2" />
<PackageVersion Include="Microsoft.AspNetCore.OData.NewtonsoftJson" Version="8.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.7" />
<PackageVersion Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.3.1" />
<PackageVersion Include="SimpleInjector.Integration.AspNetCore.Mvc.Core" Version="5.5.0" />
<PackageVersion Include="Microsoft.Identity.Web" Version="4.8.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.Filters" Version="10.0.1" />

Expand Down Expand Up @@ -157,4 +160,4 @@

</ItemGroup>

</Project>
</Project>
79 changes: 79 additions & 0 deletions docs/openapi/migration-from-swashbuckle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Migration from Swashbuckle to Microsoft OpenAPI

Ark.Tools now uses `Microsoft.AspNetCore.OpenApi` as the default generator in `ArkStartupWebApiCommon`.
Existing applications can keep the legacy Swashbuckle generator by overriding:

```csharp
public override bool UseSwashbuckleOpenApi => true;
```

Swagger UI and Redoc are still hosted by the API and read the same `/swagger/docs/{documentName}` specification route.
The route serves build-generated JSON. In production builds, runtime document generation is not mapped and the route returns `404` if the generated file is missing.

## Build-time generation

Application projects that use the Microsoft OpenAPI path should reference `Microsoft.Extensions.ApiDescription.Server`.
This enables OpenAPI JSON generation during normal `dotnet build`.

Recommended project settings:

```xml
<PropertyGroup>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/bin/$(Configuration)/$(TargetFramework)/</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
```

Build-time generation starts the application entry point with a mock server.
Guard startup code that performs external side effects, migrations, outbound calls, or hosted background work during document generation.

## Step-by-step migration

1. Remove any `UseSwashbuckleOpenApi` override, or leave it set to `false`, so `ArkStartupWebApiCommon` registers Microsoft OpenAPI.
2. Add `Microsoft.Extensions.ApiDescription.Server` to the application project.
3. Set `OpenApiDocumentsDirectory` to the application output directory and keep `OpenApiGenerateDocumentsOptions` on `OpenApi3_1` so production serves static, build-generated specs from `/swagger/docs/{documentName}`.
4. Keep the build-time `AddOpenApi(...)` calls in the application project. The Microsoft source generator needs these calls in the application assembly, while Ark.Tools keeps the runtime serving path guarded by the generator selection in the base startup class.
5. Move Swashbuckle `ISchemaFilter` and `IOperationFilter` customizations to `ConfigureMicrosoftOpenApi` with `IOpenApiSchemaTransformer` and `IOpenApiOperationTransformer`.
6. Keep Swagger UI options and OAuth UI configuration. Add Microsoft OpenAPI document transformers for security schemes previously added with `ConfigureSwaggerGen`.
7. Build the application and verify the expected `{ProjectName}_v*.json` files are generated in the application output directory.

The `samples/Ark.ReferenceProject/Core/Ark.Reference.Core.WebInterface` project demonstrates these steps by:

- generating OpenAPI files at build time,
- configuring schema and operation transformers in `ConfigureMicrosoftOpenApi`,
- preserving Swagger UI OAuth setup, and
- adding OAuth security schemes with Microsoft OpenAPI document transformers, and
- skipping hosted background workers while `dotnet-getdocument` generates the specification.

## Swashbuckle attributes

Swashbuckle-specific attributes are not interpreted automatically by Microsoft OpenAPI.
Prefer ASP.NET Core metadata, XML comments, and Ark.Tools/Microsoft OpenAPI transformers for new code.

| Swashbuckle attribute | Recommended replacement | Notes |
| --- | --- | --- |
| `SwaggerOperationAttribute` | Drop-in compatibility is registered by default. | Ark.Tools maps `Summary`, `Description`, `OperationId`, and `Tags` through a Microsoft OpenAPI operation transformer. Prefer XML comments or native endpoint metadata for new code. |
| `SwaggerResponseAttribute` | Drop-in compatibility is registered by default; `ProducesResponseTypeAttribute` is preferred for new code. | Ark.Tools maps status code, description, response type/schema, and explicit content types through a Microsoft OpenAPI operation transformer. Use `ProducesResponseTypeAttribute` only when you do not need Swashbuckle's description/content-type metadata. |
| `SwaggerParameterAttribute` | XML comments, `FromQuery`/`FromRoute` metadata, or an operation transformer | Use native binding metadata for required and source information. |
| `SwaggerRequestBodyAttribute` | ASP.NET Core request metadata plus XML comments or an operation transformer | Prefer deterministic transformers for examples and descriptions. |
| `SwaggerSchemaAttribute` | System.Text.Json metadata, XML comments, or `IOpenApiSchemaTransformer` | Swashbuckle schema filters remain legacy-only. |
| `SwaggerTagAttribute` | Controller/action grouping metadata or document/operation transformers | Use generator-neutral tag metadata where possible. |
| `SwaggerSchemaFilterAttribute` | `IOpenApiSchemaTransformer` | Treat this as Swashbuckle-only during the transition. |

## Swashbuckle filters

| Swashbuckle extension point | Microsoft OpenAPI replacement |
| --- | --- |
| `IDocumentFilter` | `IOpenApiDocumentTransformer` |
| `IOperationFilter` | `IOpenApiOperationTransformer` |
| `ISchemaFilter` | `IOpenApiSchemaTransformer` |
| `IExamplesProvider<T>` from `Swashbuckle.AspNetCore.Filters` | Prefer a generator-neutral example model or an operation/schema transformer that writes examples deterministically. |

Current Ark.Tools defaults provide Microsoft OpenAPI equivalents for default problem responses, NodaTime schema formats, flags enum query parameters, OData media type cleanup, versioned document selection, and stable operation IDs.

## Compatibility guidance

- Use the Microsoft OpenAPI default for new applications.
- Override `UseSwashbuckleOpenApi` only when existing Swashbuckle filters or attributes cannot be migrated immediately.
- Keep Swagger UI and Redoc pointed at `/swagger/docs/{documentName}` so UI routes do not change while the generator changes.
- Move custom Swashbuckle filters to generator-neutral Ark.Tools options or Microsoft OpenAPI transformers before the .NET 12 removal window.
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,14 @@
"Microsoft.Spatial": "[9.0.0-preview.4, 10.0.0)"
}
},
"Microsoft.AspNetCore.OpenApi": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "vKiAcGXG0BwNVw3bOjjWRLnp9tR18dR7MiwpvC94h0yFS+zfnzGHzS/JmmgwUdRixrGxrlIMRAWrVc+2DfAGlg==",
"dependencies": {
"Microsoft.OpenApi": "2.0.0"
}
},
"Microsoft.AspNetCore.WebUtilities": {
"type": "Transitive",
"resolved": "2.1.1",
Expand Down Expand Up @@ -1944,6 +1952,11 @@
"Swashbuckle.AspNetCore.SwaggerGen": "10.0.0"
}
},
"Swashbuckle.AspNetCore.ReDoc": {
"type": "Transitive",
"resolved": "10.1.7",
"contentHash": "aoVMCDlS4v6X6EgaowPxSSDwdvIP0NQNJR42lq+iQCVFaGRCN67wALoAZYddyxGQeBoiKSdATvP+psj7OVCuPw=="
},
"Swashbuckle.AspNetCore.Swagger": {
"type": "Transitive",
"resolved": "10.1.7",
Expand Down Expand Up @@ -2187,6 +2200,7 @@
"Ark.Reference.Core.Application": "[0.9.1, )",
"Ark.Tools.AspNetCore": "[1.0.0, )",
"Azure.Extensions.AspNetCore.Configuration.Secrets": "[1.5.0, )",
"Microsoft.Extensions.ApiDescription.Server": "[10.0.7, )",
"Microsoft.Identity.Web": "[4.8.0, )"
}
},
Expand Down Expand Up @@ -2224,6 +2238,7 @@
"FluentValidation": "[12.1.1, )",
"Hellang.Middleware.ProblemDetails": "[6.5.1, )",
"Microsoft.AspNetCore.OData": "[10.0.0-preview.2, )",
"Microsoft.AspNetCore.OpenApi": "[10.0.7, )",
"NetEscapades.AspNetCore.SecurityHeaders": "[1.3.1, )",
"SimpleInjector.Integration.AspNetCore.Mvc.Core": "[5.5.0, )"
}
Expand Down Expand Up @@ -2288,6 +2303,7 @@
"Microsoft.AspNetCore.OData": "[10.0.0-preview.2, )",
"Swashbuckle.AspNetCore.Annotations": "[10.1.7, )",
"Swashbuckle.AspNetCore.Filters": "[10.0.1, )",
"Swashbuckle.AspNetCore.ReDoc": "[10.1.7, )",
"Swashbuckle.AspNetCore.Swagger": "[10.1.7, )",
"Swashbuckle.AspNetCore.SwaggerGen": "[10.1.7, )",
"Swashbuckle.AspNetCore.SwaggerUI": "[10.1.7, )"
Expand Down Expand Up @@ -2545,6 +2561,12 @@
"System.Security.Cryptography.Xml": "10.0.7"
}
},
"Microsoft.Extensions.ApiDescription.Server": {
"type": "CentralTransitive",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "VBNJzdInUkjhL/Habj16j3kMHIccjjuWg2zILcapH2QDq5GJF4bfQzJvt2gynuT98PkTpLUvPAsPeefKBDT/mg=="
},
"Microsoft.Identity.Web": {
"type": "CentralTransitive",
"requested": "[4.8.0, )",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
<PropertyGroup>
<AspNetCoreModuleName>AspNetCoreModuleV2</AspNetCoreModuleName>
<AspNetCoreHostingModel>inprocess</AspNetCoreHostingModel>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/bin/$(Configuration)/$(TargetFramework)/</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" />
<PackageReference Include="Microsoft.Identity.Web" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ public static IHostBuilder Config(this IHostBuilder builder, string[] args)
})
.ConfigureServices((ctx, services) =>
{
services.AddSingleton<IHostedService, RebusProcessorService>();
if (!IsRunningUnderOpenApiGenerator())
{
services.AddSingleton<IHostedService, RebusProcessorService>();
}

// remember to add LOCAL_DEBUG to launchSetting.json when developing in local
if (Environment.GetEnvironmentVariable("LOCAL_DEBUG") == "True")
Expand All @@ -63,6 +66,15 @@ public static IHostBuilder Config(this IHostBuilder builder, string[] args)
});
}

private static bool IsRunningUnderOpenApiGenerator()
{
var entryAssemblyName = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name;
return string.Equals(entryAssemblyName, "GetDocument.Insider", StringComparison.Ordinal)
|| string.Equals(entryAssemblyName, "dotnet-getdocument", StringComparison.OrdinalIgnoreCase)
|| Environment.GetCommandLineArgs().Any(arg => arg.Contains("dotnet-getdocument", StringComparison.OrdinalIgnoreCase))
|| AppDomain.CurrentDomain.GetAssemblies().Any(assembly => string.Equals(assembly.GetName().Name, "Microsoft.Extensions.ApiDescription.Tool", StringComparison.Ordinal));
}


public static async Task Main(string[] args)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Tokens;
Expand All @@ -38,6 +39,24 @@ public override OpenApiInfo MakeInfo(ApiVersion version)
Version = version.ToString("VVVV", CultureInfo.InvariantCulture),
};

/// <inheritdoc />
protected override void ConfigureMicrosoftOpenApi(string documentName, OpenApiOptions options)
{
base.ConfigureMicrosoftOpenApi(documentName, options);

options.AddSchemaTransformer(async (schema, context, cancellationToken) =>
{
if (context.JsonTypeInfo.Type != typeof(double?[,]))
{
return;
}

schema.Type = JsonSchemaType.Array;
schema.Items = await context.GetOrCreateSchemaAsync(typeof(double?[]), null, cancellationToken).ConfigureAwait(false);
});
options.AddOperationTransformer<MultiPartJsonOperationTransformer>();
}

public Startup(IConfiguration config, IWebHostEnvironment webHostEnvironment)
: base(config, webHostEnvironment)
{
Expand All @@ -47,6 +66,12 @@ public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);

// The Microsoft OpenAPI source generator requires application-level AddOpenApi calls to generate build-time documents.
foreach (var version in Versions)
{
services.AddOpenApi($"v{version.ToString("VVVV", CultureInfo.InvariantCulture)}");
}

// Configure System.Text.Json source generation with Ark defaults
// Using JsonTypeInfoResolver.Combine to merge application and ProblemDetails contexts
// See: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation
Expand Down Expand Up @@ -168,4 +193,4 @@ protected override void RegisterContainer(IServiceProvider services)
services.GetService<InMemorySubscriberStore>())
;
}
}
}
Loading
Loading