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ư:
- Thêm parameters vào tất cả operations trong tài liệu.
- Sửa đổi descriptions cho parameters hoặc operations.
- Thêm thông tin cấp cao nhất vào tài liệu OpenAPI.
Transformer thuộc ba loại:
- Document transformer có quyền truy cập vào toàn bộ tài liệu OpenAPI. Dùng để thực hiện sửa đổi toàn cục cho tài liệu.
- Operation transformer áp dụng cho từng operation riêng lẻ. Mỗi operation riêng lẻ là sự kết hợp của path và HTTP method. Dùng để sửa đổi parameters hoặc responses trên các endpoint.
- Schema transformer áp dụng cho từng schema trong tài liệu. Dùng để sửa đổi schema của request hoặc response body, hoặc bất kỳ schema lồng nhau nào.
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:
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:
- Schema transformer thực thi khi schema được đăng ký vào tài liệu. Chúng thực thi theo thứ tự được thêm vào. Tất cả schema được thêm vào tài liệu trước khi bất kỳ xử lý operation nào xảy ra, do đó schema transformer thực thi trước operation transformer.
- Operation transformer thực thi khi operation được thêm vào tài liệu. Chúng thực thi theo thứ tự được thêm vào. Tất cả operation được thêm vào tài liệu trước khi bất kỳ document transformer nào thực thi.
- Document transformer thực thi khi tài liệu được tạo. Đây là lần duyệt cuối cùng qua tài liệu, và tất cả operation và schema đã được thêm vào lúc này.
- Khi ứng dụng được cấu hình để tạo nhiều tài liệu OpenAPI, transformer thực thi cho từng tài liệu một cách độc lập.
Ví dụ, trong đoạn code sau:
SchemaTransformer2thực thi và có quyền truy cập vào các sửa đổi củaSchemaTransformer1.- Cả
OperationTransformer1vàOperationTransformer2đều có quyền truy cập vào các sửa đổi từ cả hai schema transformer. OperationTransformer2thực thi sauOperationTransformer1, nên có quyền truy cập vào các sửa đổi củaOperationTransformer1.- Cả
DocumentTransformer1vàDocumentTransformer2thực thi sau khi tất cả operation và schema đã được thêm vào tài liệu. DocumentTransformer2thực thi sauDocumentTransformer1.
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:
- Tên của tài liệu đang được sửa đổi.
ApiDescriptionGroupsliên kết với tài liệu đó.IServiceProviderđược dùng trong việc tạo tài liệu.
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:
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:
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:
- Đăng ký các yêu cầu authentication-related cho tài liệu
internal. - Để tài liệu
publickhông bị thay đổi.
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:
- Cần được thực hiện cho từng endpoint trong ứng dụng, hoặc
- Áp dụng có điều kiện cho một số route nhất định.
Operation transformer có quyền truy cập vào context object chứa:
- Tên của tài liệu mà operation thuộc về.
ApiDescriptionliên kết với operation.IServiceProviderđược dùng trong việc tạo tài liệu.
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:
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):
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:
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:
- Cần được thực hiện cho từng schema trong tài liệu, hoặc
- Áp dụng có điều kiện cho một số schema nhất định.
Schema transformer có quyền truy cập vào context object chứa:
- Tên của tài liệu mà schema thuộc về.
- Thông tin kiểu JSON liên kết với schema mục tiêu.
IServiceProviderđược dùng trong việc tạo tài liệu.
Ví dụ, schema transformer sau đặt format của kiểu decimal thành decimal thay vì double:
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ụ
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:
- Schema cho kiểu class/record/struct được thay thế bằng
$reftới schema trongcomponents.schemasnếu chúng xuất hiện nhiều hơn một lần trong tài liệu. - Schema cho kiểu primitive và collection chuẩn được để inline (nội tuyến).
- Schema cho kiểu enum luôn được thay thế bằng
$reftới schema trongcomponents.schemas.
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:
builder.Services.AddOpenApi(options =>
{
// Luôn inline enum schemas
options.CreateSchemaReferenceId = (type) =>
type.Type.IsEnum ? null : OpenApiOptions.CreateDefaultSchemaReferenceId(type);
});