-
Notifications
You must be signed in to change notification settings - Fork 87
Add aspnet-openapi skill (#dotnet-aspnet) #486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
raihanmehran
wants to merge
9
commits into
dotnet:main
Choose a base branch
from
raihanmehran:feat/aspnet-openapi-skill
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
03e5f98
Add aspnet-openapi skill to dotnet-aspnet plugin
nickmehran a802f80
Address Copilot PR review comments
nickmehran e7b6449
Fix missing using directives in code snippets
nickmehran b4520f2
Add missing using directives and context comment in transformer snippets
nickmehran 3d9c7f4
Fix Swashbuckle.AspNetCore.SwaggerUI package name casing
nickmehran 45dc0cf
Fix inaccurate .NET version claims, pitfall table contradiction, and …
nickmehran 083c7b6
Merge branch 'main' into feat/aspnet-openapi-skill
raihanmehran 11b85be
Merge branch 'main' into feat/aspnet-openapi-skill
raihanmehran efa719c
Merge branch 'main' into feat/aspnet-openapi-skill
raihanmehran File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,376 @@ | ||
| --- | ||
| name: aspnet-openapi | ||
| description: > | ||
| Add OpenAPI documentation to ASP.NET Core APIs (.NET 8+). Covers technology selection | ||
| (Microsoft.AspNetCore.OpenApi vs Swashbuckle vs NSwag), Scalar and Swagger UI setup, | ||
| JWT Bearer security scheme configuration, endpoint metadata for minimal APIs and MVC | ||
| controllers, and document/operation transformers. | ||
| USE FOR: adding an OpenAPI spec endpoint to a new or existing ASP.NET Core project, | ||
| setting up an interactive API explorer (Scalar, Swagger UI), configuring JWT Bearer | ||
| security in the OpenAPI document, annotating minimal API endpoints with response types | ||
| and tags, customizing the generated spec with transformers, or migrating from | ||
| Swashbuckle.AspNetCore to the built-in package on .NET 9+. | ||
| DO NOT USE FOR: generating typed API client code from an OpenAPI spec (use NSwag CLI | ||
| or the dotnet-openapi tooling instead), adding OpenAPI to non-ASP.NET projects, or | ||
| validating incoming requests against a schema at runtime. | ||
| --- | ||
|
|
||
| # ASP.NET Core OpenAPI | ||
|
|
||
| .NET 9 changed the landscape. `Microsoft.AspNetCore.OpenApi` is now the first-party | ||
| solution, backed by the ASP.NET Core team, and ships in the default web project templates. | ||
| Swashbuckle — the de-facto standard for the previous decade — still works but is no longer | ||
| the default, and its major versions have lagged .NET releases by months. NSwag remains the | ||
| right choice when client code generation is the primary goal. | ||
|
|
||
| ## Stop Signals | ||
|
|
||
| - **Need to generate C# or TypeScript API clients?** — Use NSwag or `dotnet-openapi`. This skill covers spec *generation*, not *consumption*. | ||
| - **Targeting .NET 8 or earlier?** — `Microsoft.AspNetCore.OpenApi` is .NET 9+ only. Use Swashbuckle 6.x on .NET 8. | ||
| - **Already on Swashbuckle and no .NET 9 migration planned?** — Don't migrate just for its own sake. See [references/swashbuckle-migration.md](references/swashbuckle-migration.md) if migration is wanted later. | ||
| - **Review request on existing OpenAPI config?** — Jump directly to the Validation checklist. | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Required | Description | | ||
| |-------|----------|-------------| | ||
| | Target framework | Yes | Determines which packages are available | | ||
| | API style | Yes | Minimal APIs or MVC controllers — affects annotation syntax | | ||
| | Auth scheme | Recommended | JWT Bearer, OAuth2, or API key — drives security scheme config | | ||
| | Existing packages | Recommended | Check whether Swashbuckle or NSwag is already referenced | | ||
|
|
||
| ## Workflow | ||
|
|
||
| ### Step 1: Choose the Package | ||
|
|
||
| | Situation | Package | Reason | | ||
| |-----------|---------|--------| | ||
| | New project on .NET 9+ | `Microsoft.AspNetCore.OpenApi` | First-party, default in templates, actively maintained by the ASP.NET Core team | | ||
| | New project on .NET 8 | `Swashbuckle.AspNetCore` 6.x | Built-in package requires .NET 9+ | | ||
| | Need typed client code generation | `NSwag.AspNetCore` | Only mature option with C#/TypeScript codegen; pair with the spec output | | ||
| | Complex polymorphism or discriminators | `NSwag.AspNetCore` | More schema control than either alternative | | ||
| | Existing project already on Swashbuckle | Keep Swashbuckle | Migration is optional — see [references/swashbuckle-migration.md](references/swashbuckle-migration.md) | | ||
|
|
||
| **Hard rule:** Do not mix `Microsoft.AspNetCore.OpenApi` and Swashbuckle in the same project. | ||
| Both enumerate endpoints at startup and produce conflicting output. Pick one. | ||
|
|
||
| --- | ||
|
|
||
| ### Step 2: Set Up Microsoft.AspNetCore.OpenApi (.NET 9+) | ||
|
|
||
| ```bash | ||
| dotnet add package Microsoft.AspNetCore.OpenApi | ||
| ``` | ||
|
|
||
| ```csharp | ||
| // Program.cs | ||
| var builder = WebApplication.CreateBuilder(args); | ||
|
|
||
| builder.Services.AddOpenApi(); // Register document generation | ||
|
|
||
| var app = builder.Build(); | ||
|
|
||
| if (app.Environment.IsDevelopment()) | ||
| { | ||
| app.MapOpenApi(); // Serves spec JSON at /openapi/v1.json | ||
| } | ||
|
|
||
| app.Run(); | ||
| ``` | ||
|
|
||
| > **CRITICAL:** `MapOpenApi()` serves raw JSON — it is **not** a UI. Add a UI package in Step 4. | ||
|
|
||
| > **CRITICAL:** The default spec path is `/openapi/v1.json`. This is **not** the Swashbuckle | ||
| > default of `/swagger/v1/swagger.json`. Any tooling, CI pipeline, or Postman collection | ||
| > importing from the old path will silently get a 404. | ||
|
|
||
| **Multiple document versions:** | ||
|
|
||
| ```csharp | ||
| builder.Services.AddOpenApi("v1"); | ||
| builder.Services.AddOpenApi("v2"); | ||
|
|
||
| // Both served at their named paths: /openapi/v1.json and /openapi/v2.json | ||
| app.MapOpenApi("/openapi/{documentName}.json"); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Step 3: Set Up Swashbuckle (.NET 8 / existing projects) | ||
|
|
||
| ```bash | ||
| # 6.x for .NET 8 — 7.x for .NET 9 | ||
| dotnet add package Swashbuckle.AspNetCore | ||
| ``` | ||
|
|
||
| ```csharp | ||
| // Program.cs | ||
| var builder = WebApplication.CreateBuilder(args); | ||
|
|
||
| // CRITICAL: Required for minimal API endpoints to appear in the spec. | ||
| // Swashbuckle uses the ApiExplorer infrastructure, which minimal APIs do NOT | ||
| // register automatically. Without this line every MapGet/MapPost endpoint is | ||
| // invisible in the generated spec. MVC controllers are unaffected. | ||
| builder.Services.AddEndpointsApiExplorer(); | ||
| builder.Services.AddSwaggerGen(); | ||
|
|
||
| var app = builder.Build(); | ||
|
|
||
| if (app.Environment.IsDevelopment()) | ||
| { | ||
| app.UseSwagger(); // Serves spec at /swagger/v1/swagger.json | ||
| app.UseSwaggerUI(); // Serves Swagger UI at /swagger | ||
| } | ||
|
|
||
| app.Run(); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Step 4: Add an API Explorer UI | ||
|
|
||
| #### Scalar (recommended with Microsoft.AspNetCore.OpenApi) | ||
|
|
||
| ```bash | ||
| dotnet add package Scalar.AspNetCore | ||
| ``` | ||
|
|
||
| ```csharp | ||
| using Scalar.AspNetCore; | ||
|
|
||
| if (app.Environment.IsDevelopment()) | ||
| { | ||
| app.MapOpenApi(); | ||
| app.MapScalarApiReference(); // Default at /scalar/v1 | ||
| } | ||
| ``` | ||
|
|
||
| Scalar reads from `MapOpenApi()` automatically. For customization: | ||
|
|
||
| ```csharp | ||
| app.MapScalarApiReference(options => | ||
| { | ||
| options.WithTitle("Contoso API"); | ||
| options.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); | ||
| options.WithPreferredScheme("Bearer"); | ||
| }); | ||
| ``` | ||
|
|
||
| #### Swagger UI alongside Microsoft.AspNetCore.OpenApi | ||
|
|
||
| If Swagger UI is a hard requirement, use only the UI package — not the full Swashbuckle stack: | ||
|
|
||
| ```bash | ||
| dotnet add package Swashbuckle.AspNetCore.SwaggerUi | ||
| ``` | ||
|
|
||
| ```csharp | ||
| if (app.Environment.IsDevelopment()) | ||
| { | ||
| app.MapOpenApi(); | ||
| app.UseSwaggerUI(options => | ||
| { | ||
| // CRITICAL: Point at the built-in package's path, not the Swashbuckle default. | ||
| options.SwaggerEndpoint("/openapi/v1.json", "v1"); | ||
| options.RoutePrefix = "swagger"; | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Step 5: Annotate Endpoints | ||
|
|
||
| The spec is only as useful as the metadata you put in. Sparse annotations produce a sparse, | ||
| useless spec — and waste the entire investment in Step 2–4. | ||
|
|
||
| **Minimal APIs:** | ||
|
|
||
| ```csharp | ||
| app.MapGet("/products/{id:int}", async (int id, IProductRepository repo) => | ||
| { | ||
| var product = await repo.GetByIdAsync(id); | ||
| return product is null ? Results.NotFound() : Results.Ok(product); | ||
| }) | ||
| .WithName("GetProductById") | ||
| .WithSummary("Get a product by ID") | ||
| .WithDescription("Returns a single product including pricing and stock level.") | ||
| .WithTags("Products") | ||
| .Produces<Product>(StatusCodes.Status200OK) | ||
| .ProducesProblem(StatusCodes.Status404NotFound); | ||
|
|
||
| app.MapPost("/products", async ([FromBody] CreateProductRequest req, IProductRepository repo) => | ||
| { | ||
| var id = await repo.CreateAsync(req); | ||
| return Results.CreatedAtRoute("GetProductById", new { id }); | ||
| }) | ||
| .WithName("CreateProduct") | ||
| .WithTags("Products") | ||
| .Accepts<CreateProductRequest>("application/json") | ||
| .Produces<Product>(StatusCodes.Status201Created) | ||
| .ProducesValidationProblem() | ||
| .ProducesProblem(StatusCodes.Status409Conflict); | ||
|
|
||
| // Exclude internal/infra endpoints entirely | ||
| app.MapGet("/internal/health", () => "ok") | ||
| .ExcludeFromDescription(); | ||
| ``` | ||
|
|
||
| **MVC Controllers:** Use `[ProducesResponseType]`, `[Produces]`, `[Consumes]`, and XML | ||
| `<summary>` / `<remarks>` doc comments. Both packages consume these through the ApiExplorer | ||
| infrastructure. Enable XML doc generation: | ||
|
|
||
| ```xml | ||
| <!-- In your .csproj --> | ||
| <PropertyGroup> | ||
| <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
| </PropertyGroup> | ||
| ``` | ||
|
|
||
| For Swashbuckle, wire up the XML file in `AddSwaggerGen`: | ||
|
|
||
| ```csharp | ||
| builder.Services.AddSwaggerGen(options => | ||
| { | ||
| var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; | ||
raihanmehran marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile)); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Step 6: Configure JWT Bearer Security Scheme | ||
|
|
||
| The built-in package does not auto-detect `AddAuthentication()`. You must declare the | ||
| security scheme via a document transformer. | ||
|
|
||
| ```csharp | ||
| // Program.cs | ||
| builder.Services.AddOpenApi(options => | ||
| { | ||
| options.AddDocumentTransformer<BearerSecuritySchemeTransformer>(); | ||
| }); | ||
| ``` | ||
|
|
||
| ```csharp | ||
| // BearerSecuritySchemeTransformer.cs | ||
| using Microsoft.AspNetCore.OpenApi; | ||
| using Microsoft.OpenApi.Models; | ||
|
|
||
| internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer | ||
| { | ||
| public Task TransformAsync( | ||
| OpenApiDocument document, | ||
| OpenApiDocumentTransformerContext context, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| document.Components ??= new OpenApiComponents(); | ||
| document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme | ||
| { | ||
| Type = SecuritySchemeType.Http, | ||
| Scheme = "bearer", | ||
| In = ParameterLocation.Header, | ||
| BearerFormat = "JWT", | ||
| Description = "Enter a valid JWT. Example: eyJhbGci..." | ||
| }; | ||
|
|
||
| // Apply to every operation. For per-endpoint selective auth, | ||
| // use an operation transformer — see references/transformers.md. | ||
| var securityRequirement = new OpenApiSecurityRequirement | ||
| { | ||
| [new OpenApiSecurityScheme | ||
| { | ||
| Reference = new OpenApiReference | ||
| { | ||
| Type = ReferenceType.SecurityScheme, | ||
| Id = "Bearer" | ||
| } | ||
| }] = [] | ||
| }; | ||
|
|
||
| foreach (var pathItem in document.Paths.Values) | ||
| { | ||
| foreach (var operation in pathItem.Operations.Values) | ||
| { | ||
| operation.Security ??= []; | ||
| operation.Security.Add(securityRequirement); | ||
| } | ||
| } | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| > For **per-endpoint** security (some routes public, some require auth), or for **OAuth2** | ||
| > and **API key** schemes, see [references/transformers.md](references/transformers.md). | ||
|
|
||
| For **Swashbuckle**, the equivalent uses `AddSecurityDefinition` and `AddSecurityRequirement` | ||
| inside `AddSwaggerGen` — full examples in [references/transformers.md](references/transformers.md). | ||
|
|
||
| --- | ||
|
|
||
| ### Step 7: Secure the Spec Endpoint | ||
|
|
||
| The OpenAPI document lists every endpoint, parameter, and auth scheme. Do not expose it | ||
| publicly in production. | ||
|
|
||
| **Option A — Development only** (most common for internal or backend APIs): | ||
|
|
||
| ```csharp | ||
| // Already shown — wrap MapOpenApi() and the UI in IsDevelopment(). | ||
| if (app.Environment.IsDevelopment()) | ||
| { | ||
| app.MapOpenApi(); | ||
| app.MapScalarApiReference(); | ||
| } | ||
| ``` | ||
|
|
||
| **Option B — Authenticated users only** (staging, internal portals): | ||
|
|
||
| ```csharp | ||
| app.MapOpenApi() | ||
| .RequireAuthorization(); | ||
| ``` | ||
|
|
||
| **Option C — Specific policy or role**: | ||
|
|
||
| ```csharp | ||
| app.MapOpenApi() | ||
| .RequireAuthorization("InternalToolsPolicy"); | ||
| ``` | ||
|
|
||
| > **CRITICAL:** Do not combine Option B/C with the JWT transformer from Step 6 without | ||
| > verifying the flow end-to-end. If the spec endpoint requires a token, and the UI reads the | ||
| > spec before the user authenticates, the UI will load blank. Either serve the UI and spec | ||
| > without auth (Options A), or ensure both are behind the same authenticated session. | ||
|
|
||
| --- | ||
|
|
||
| ## Validation | ||
|
|
||
| - [ ] `dotnet build -warnaserror` completes cleanly | ||
| - [ ] Navigate to the spec path in a running instance — verify well-formed JSON is returned | ||
| - [ ] Every endpoint with a non-trivial return type has at least one `Produces<T>()` or `[ProducesResponseType]` annotation | ||
| - [ ] Error paths return `ProblemDetails` — not anonymous `{ error: "..." }` objects | ||
| - [ ] If auth is configured, the `securitySchemes` section appears in the spec JSON and the scheme name matches the `$ref` value exactly (case-sensitive) | ||
| - [ ] The spec endpoint is either restricted to Development or requires authorization — never `AllowAnonymous` in production | ||
|
|
||
| ## Common Pitfalls | ||
|
|
||
| | Pitfall | Solution | | ||
| |---------|----------| | ||
| | Forgot `AddEndpointsApiExplorer()` with Swashbuckle | Minimal API endpoints won't appear — MVC controllers are unaffected. This is the single most common Swashbuckle setup bug | | ||
| | Spec path mismatch after switching packages | Built-in defaults to `/openapi/v1.json`; Swashbuckle defaults to `/swagger/v1/swagger.json`. Update all downstream tooling explicitly | | ||
| | Mixed `Microsoft.AspNetCore.OpenApi` + Swashbuckle | Both packages enumerate endpoints at startup. Behavior is undefined. Remove one | | ||
| | Security scheme `$ref` name mismatch | The `Id` in `OpenApiReference` must exactly match the key in `SecuritySchemes` — `"Bearer"` ≠ `"bearer"` | | ||
| | Spec endpoint returns 401 unexpectedly | Either wrapped in `IsDevelopment()` (run in Development env) or secured with `RequireAuthorization()` (authenticate first, or remove the auth requirement from the spec endpoint) | | ||
| | Swashbuckle 6.x on .NET 9 | Swashbuckle 6.x does not support .NET 9+ reflection APIs. Upgrade to 7.x or migrate to the built-in package | | ||
| | `MapOpenApi()` produces empty `paths` | All endpoints were registered after `app.Build()` was called, or the project targets .NET 8 where the built-in package is unavailable | | ||
raihanmehran marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| | Scalar UI loads but shows no auth button | `WithPreferredScheme("Bearer")` not set, or the security scheme was not added to `document.Components.SecuritySchemes` | | ||
|
|
||
| ## Reference Files | ||
|
|
||
| - **[references/transformers.md](references/transformers.md)** — Document, operation, and schema transformers for `Microsoft.AspNetCore.OpenApi`; equivalent `IDocumentFilter`, `IOperationFilter`, and `ISchemaFilter` patterns for Swashbuckle; OAuth2 and API key scheme setup. **Load when** customizing the generated spec, configuring non-JWT security schemes, adding XML documentation to the spec, or applying per-endpoint auth annotations. | ||
|
|
||
| - **[references/swashbuckle-migration.md](references/swashbuckle-migration.md)** — Step-by-step migration from `Swashbuckle.AspNetCore` to `Microsoft.AspNetCore.OpenApi` on .NET 9+, including package changes, middleware rewrites, filter-to-transformer mapping, and common post-migration failures. **Load when** the project currently uses Swashbuckle and the developer wants to move to the built-in package. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.