Nguon: Microsoft Learn · .NET 8.0

Tùy chỉnh tài liệu OpenAPI trong ASP.NET Core

Nguồn: Customize OpenAPI documents

OpenAPI document transformers (bộ biến đổi tài liệu)

Transformer (bộ biến đổi) cung cấp API để sửa đổi tài liệu OpenAPI với các tùy chỉnh do người dùng định nghĩa. Transformer hữu ích trong các tình huống như:

Transformer thuộc ba loại:

Transformer có thể được đăng ký vào tài liệu bằng cách gọi phương thức AddDocumentTransformer trên đối tượng OpenApiOptions. Đoạn code sau cho thấy các cách khác nhau để đăng ký transformer:

csharp
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken)
                             => Task.CompletedTask);
    options.AddDocumentTransformer(new MyDocumentTransformer());
    options.AddDocumentTransformer<MyDocumentTransformer>();
    options.AddOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddOperationTransformer(new MyOperationTransformer());
    options.AddOperationTransformer<MyOperationTransformer>();
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddSchemaTransformer(new MySchemaTransformer());
    options.AddSchemaTransformer<MySchemaTransformer>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

Thứ tự thực thi của transformer

Transformer thực thi theo thứ tự sau:

Ví dụ, trong đoạn code sau:

csharp
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<DocumentTransformer1>();
    options.AddSchemaTransformer<SchemaTransformer1>();
    options.AddDocumentTransformer<DocumentTransformer2>();
    options.AddOperationTransformer<OperationTransformer1>();
    options.AddSchemaTransformer<SchemaTransformer2>();
    options.AddOperationTransformer<OperationTransformer2>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

Sử dụng document transformer

Document transformer có quyền truy cập vào context object bao gồm:

Document transformer cũng có thể thay đổi tài liệu OpenAPI được tạo ra. Ví dụ sau mô tả một document transformer thêm thông tin về API vào tài liệu OpenAPI:

csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "Checkout API",
            Version = "v1",
            Description = "API for processing checkouts from cart."
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

Document transformer được kích hoạt bởi service có thể sử dụng các instance từ DI (Dependency Injection - tiêm phụ thuộc) để sửa đổi ứng dụng. Ví dụ sau mô tả một document transformer sử dụng service IAuthenticationSchemeProvider từ tầng authentication (xác thực). Nó kiểm tra xem có JWT bearer-related scheme nào được đăng ký trong ứng dụng không và thêm chúng vào cấp độ cao nhất của tài liệu OpenAPI:

csharp
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var securitySchemes = new Dictionary<string, IOpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = securitySchemes;
        }
    }
}

Document transformer là duy nhất cho document instance mà chúng được liên kết. Trong ví dụ sau, một transformer:

csharp
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi("internal", options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("public");

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/world", () => "Hello world!")
    .WithGroupName("internal");
app.MapGet("/", () => "Hello universe!")
    .WithGroupName("public");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var securitySchemes = new Dictionary<string, IOpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer",
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = securitySchemes;

            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security ??= [];
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                });
            }
        }
    }
}

Sử dụng operation transformer

Operation là các kết hợp duy nhất của HTTP path và method trong tài liệu OpenAPI. Operation transformer hữu ích khi một sửa đổi:

Operation transformer có quyền truy cập vào context object chứa:

Ví dụ, operation transformer sau thêm 500 làm status code phản hồi được hỗ trợ bởi tất cả operations trong tài liệu:

csharp
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses ??= new OpenApiResponses();
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

Operation transformer cũng có thể được thêm vào một endpoint cụ thể bằng API AddOpenApiOperationTransformer, thay vì tất cả endpoint trong tài liệu. Ví dụ sau mô tả việc thêm operation transformer vào một endpoint deprecated (lỗi thời):

csharp
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/old", () => "This endpoint is old and should not be used anymore")
.AddOpenApiOperationTransformer((operation, context, cancellationToken) =>
{
    operation.Deprecated = true;
    return Task.CompletedTask;
});

app.MapGet("/new", () => "This endpoint replaces /old");

app.Run();

Áp dụng security requirement có điều kiện

Trong một số tình huống, developer có thể muốn áp dụng security requirement cho tất cả endpoint ngoại trừ những endpoint được đánh dấu rõ ràng với attribute AllowAnonymous.

Ví dụ sau mô tả cách bỏ qua việc thêm security requirement cho các endpoint có AllowAnonymousAttribute:

csharp
internal sealed class AuthOperationTransformer : IOpenApiOperationTransformer
{
    public Task TransformAsync(
        OpenApiOperation operation,
        OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        var hasAllowAnonymous = context.Description.ActionDescriptor.EndpointMetadata
            .OfType<AllowAnonymousAttribute>()
            .Any();

        if (hasAllowAnonymous)
        {
            return Task.CompletedTask;
        }

        operation.Security ??= new List<OpenApiSecurityRequirement>();

        operation.Security.Add(new OpenApiSecurityRequirement
        {
            [new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Id = "Bearer",
                    Type = ReferenceType.SecurityScheme
                }
            }] = Array.Empty<string>()
        });

        return Task.CompletedTask;
    }
}

Sử dụng schema transformer

Schema là các data model được sử dụng trong request và response body trong tài liệu OpenAPI. Schema transformer hữu ích khi một sửa đổi:

Schema transformer có quyền truy cập vào context object chứa:

Ví dụ, schema transformer sau đặt format của kiểu decimal thành decimal thay vì double:

csharp
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => new Body { Amount = 1.1m });

app.Run();

public class Body {
    public decimal Amount { get; set; }
}

Hỗ trợ tạo OpenApiSchema trong transformer

Developer có thể tạo schema cho kiểu C# bằng cùng logic như việc tạo tài liệu OpenAPI trong ASP.NET Core và thêm nó vào tài liệu OpenAPI. Tính năng này khả dụng từ .NET 10.

Context được truyền cho document, operation, và schema transformer bao gồm phương thức GetOrCreateSchemaAsync mới có thể được dùng để tạo schema cho một kiểu. Để thêm schema vào tài liệu OpenAPI, thuộc tính Document đã được thêm vào các context của Operation và Schema transformer. Điều này cho phép bất kỳ transformer nào thêm schema vào tài liệu OpenAPI bằng phương thức AddComponent của document.

Ví dụ

csharp
builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer(async (operation, context, cancellationToken) =>
    {
        // Tạo schema cho error responses
        var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), null, cancellationToken);
        context.Document?.AddComponent("Error", errorSchema);

        operation.Responses ??= new OpenApiResponses();
        operation.Responses["4XX"] = new OpenApiResponse
        {
            Description = "Bad Request",
            Content = new Dictionary<string, OpenApiMediaType>
            {
                ["application/problem+json"] = new OpenApiMediaType
                {
                    Schema = new OpenApiSchemaReference("Error", context.Document)
                }
            }
        };
    });
});

Tùy chỉnh tái sử dụng schema

Sau khi tất cả transformer đã được áp dụng, framework (khung làm việc) thực hiện một lần duyệt qua tài liệu để chuyển một số schema sang phần components.schemas, thay thế chúng bằng các tham chiếu $ref. Điều này làm giảm kích thước tài liệu và giúp dễ đọc hơn.

Nhìn chung:

ASP.NET Core cho phép tùy chỉnh schema nào được thay thế bằng $ref bằng cách sử dụng thuộc tính CreateSchemaReferenceId của OpenApiOptions. Ví dụ tùy chỉnh đơn giản để luôn inline schema enum:

csharp
builder.Services.AddOpenApi(options =>
{
    // Luôn inline enum schemas
    options.CreateSchemaReferenceId = (type) =>
        type.Type.IsEnum ? null : OpenApiOptions.CreateDefaultSchemaReferenceId(type);
});