Minimal API Derinlemesine 2: Route Group'lar, Endpoint Filter'lar, TypedResults Kataloğu ve OpenAPI Metadata'sı
Route group'lar ortak ayarları bir kez yazmanı, endpoint filter'lar handler'ın etrafına sarılıp kısa devre yapmayı, TypedResults da sonucun OpenAPI belgesine yansımasını sağlar. Bookstore'un /api/orders alanını MapGroup, filter factory ile idempotency, kendi IResult'ımız, XML comment ve transformer'larla kuruyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.404), Microsoft.AspNetCore.OpenApi 10.0.15 ve Scalar.AspNetCore 2.21.0 ile derlenip denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) sipariş endpoint'lerinden ikisi aynı işi yapıyor, yalnızca yazılışları farklı:
orders.MapGet("/{id:int}/summary", (int id, OrderStore store) =>
store.Find(id) is { } order ? Results.Ok(order) : Results.NotFound());
orders.MapGet("/{id:int}", Get);
public static Results<Ok<OrderDto>, NotFound> Get(int id, ClaimsPrincipal user, OrderStore store) =>
store.Find(id) is { } order && order.Customer == user.Identity!.Name
? TypedResults.Ok(order)
: TypedResults.NotFound();
İkisi de çalışma anında aynı 200 ve 404'ü üretiyor. OpenAPI belgesinde, yani Scalar'da gördüğün sayfada, /summary için kaç response ve hangi şema görünür?
Bir önceki yazıda handler'a giden parametreleri konuştuk. Bu yazıda yolun geri kalanına bakıyoruz: endpoint'leri route group'larla düzenlemek, araya endpoint filter'lar koymak, TypedResults ile dönüş değerleri ve bunların hepsinin OpenAPI belgesine nasıl yansıdığı. .NET 7 yazısında MapGroup, TypedResults ve endpoint filter'ları tanıtmıştık; burada bunları Bookstore'un /api/orders alanında birlikte, gerçek bir düzende kullanıyoruz.
MapGroup: Ortak Ayarları Bir Kez Yazmak
Sipariş endpoint'lerini Program.cs'ten çıkarıp kendi dosyasına, bir extension method'a taşıdık:
public static class OrderEndpoints
{
public static RouteGroupBuilder MapOrderEndpoints(this IEndpointRouteBuilder routes)
{
RouteGroupBuilder orders = routes.MapGroup("/api/orders")
.WithTags("Orders")
.RequireAuthorization()
.AddEndpointFilter<OrderLoggingFilter>()
.AddEndpointFilterFactory(IdempotencyFilter.Create);
orders.MapGet("/", List).WithName("ListOrders");
orders.MapGet("/{id:int}", Get).WithName("GetOrder");
orders.MapPost("/", Create).WithName("CreateOrder");
orders.MapPost("/{id:int}/cancel", Cancel)
.WithName("CancelOrder")
.WithSummary("Cancels an order that has not shipped yet.")
.ProducesProblem(StatusCodes.Status409Conflict);
orders.MapGet("/{id:int}/receipt", Receipt).WithName("GetOrderReceipt");
return orders;
}
}
// Program.cs
app.MapOrderEndpoints();
Grubun üzerine yazdığın her şey (tag, authorization, filter, metadata) gruptaki bütün endpoint'lere uygulanıyor. Grup bir IEndpointConventionBuilder olduğu için endpoint'e yazabildiğin hemen her şeyi gruba da yazabiliyorsun; gruplar iç içe de kurulabiliyor, örneğin bir /api grubunun altında /orders ve /books. Handler'ları lambda yerine adlandırılmış static method olarak yazmanın iki faydası var: test'te doğrudan çağırabiliyorsun ve birazdan göreceğimiz XML comment'ler OpenAPI belgesine geçiyor. Endpoint'leri feature'a göre dosyalara bölmenin mimari tarafı Vertical Slice Architecture serisindeki "MediatR Olmadan İlk Slice" yazısında anlatılıyor; burada yalnızca mekaniğine bakıyoruz.
RequireAuthorization() için JWT bearer authentication ekledik. Yerel geliştirmede token üretmek için ayrıca bir login endpoint'i yazmadık; .NET 7'den beri SDK'da olan dotnet user-jwts aracı bunu yapıyor:
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
// ...
app.UseAuthentication();
app.UseAuthorization();
$ dotnet user-jwts create --name ada@example.com --role Customer
New JWT saved with ID '7d0952e7'.
Name: ada@example.com
Roles: [Customer]
Token: <token>
Araç imzalama anahtarını user secrets'a, issuer ve audience ayarlarını appsettings.Development.json'daki Authentication:Schemes:Bearer bölümüne yazıyor; AddJwtBearer() bu bölümü kendisi okuyor. Token'sız istek grubun kuralına takıldı:
$ curl -i localhost:5238/api/orders
HTTP/1.1 401 Unauthorized
Content-Length: 0
WWW-Authenticate: Bearer
JWT'nin kendisini ve üretimdeki token akışını JWT ile authentication yazısında görmüştük; user-jwts yalnızca geliştirme içindir.
Endpoint Filter'lar: Sıra ve Kısa Devre
Gruba eklediğimiz ilk filter her sipariş isteğini sarıyor:
public class OrderLoggingFilter(ILogger<OrderLoggingFilter> logger) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
long start = Stopwatch.GetTimestamp();
string endpoint = context.HttpContext.GetEndpoint()?.DisplayName ?? "unknown";
logger.LogInformation("[group] before {Endpoint}", endpoint);
object? result = await next(context);
// Sonuç henüz yazılmadı; elimizde yalnız IResult object'i var
logger.LogInformation("[group] after {Endpoint} -> {Result} in {Elapsed:F1} ms",
endpoint, result?.GetType().Name, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
return result;
}
}
AddEndpointFilter<T>() filter'ı DI'dan oluşturuyor, constructor injection çalışıyor. Filter'ın middleware'den farkı, endpoint seçildikten ve parametreler bağlandıktan sonra çalışması: context.Arguments ile handler'a gidecek argümanları görebilir, değiştirebilir, next'i çağırmadan kendi sonucunu dönebilirsin. Dönen şey henüz response değil, bir IResult object'i; yanıt ancak bütün zincir bittikten sonra yazılıyor.
Sırayı görmek için Create endpoint'ine ayrıca endpoint seviyesinde bir filter ekleyip aynı Idempotency-Key ile iki sipariş isteği gönderdim:
info: Bookstore.Api.Ordering.OrderLoggingFilter[0]
[group] before HTTP: POST /api/orders/ => Create
info: Program[0]
[endpoint] before Create
info: Program[0]
[endpoint] after Create
info: Bookstore.Api.Ordering.OrderLoggingFilter[0]
[group] after HTTP: POST /api/orders/ => Create -> Results`2 in 1.7 ms
info: Bookstore.Api.Ordering.OrderLoggingFilter[0]
[group] before HTTP: POST /api/orders/ => Create
info: Bookstore.Api.Ordering.OrderLoggingFilter[0]
[group] after HTTP: POST /api/orders/ => Create -> Ok`1 in 0.0 ms
Grup filter'ları dışta, endpoint filter'ı içte; aynı seviyedekiler eklendikleri sırayla. İkinci istekte idempotency filter'ı kısa devre yaptı: endpoint filter'ı ve handler hiç çalışmadı, sonuç Ok oldu. Bir gözlem daha: AddValidation()'ın doğrulaması bunların hepsinden önce çalıştı. Boş items listesiyle gönderdiğim istek 400 döndü ve log'da [group] satırı hiç yoktu.

Filter Factory: Kararı Açılışta Vermek
Sipariş oluşturmayı idempotent yapmak istiyoruz: ağ koptu diye Angular istemcisi isteği tekrar gönderdiğinde ikinci bir sipariş oluşmasın. İstemci her yeni sipariş için bir Idempotency-Key üretir; aynı key ikinci kez gelirse ilk sonucu döneriz. Bu filter yalnızca sipariş oluşturan endpoint'te anlamlı, ama gruba ekledik. Seçimi bir filter factory yapıyor:
[AttributeUsage(AttributeTargets.Method)]
public sealed class IdempotentAttribute : Attribute;
public static class IdempotencyFilter
{
public const string HeaderName = "Idempotency-Key";
private static readonly ConcurrentDictionary<string, OrderDto> Seen = new();
// Factory uygulama açılırken her endpoint için bir kez çalışır
public static EndpointFilterDelegate Create(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
// [Idempotent] taşımayan handler'lar için filter hiç devreye girmez
if (factoryContext.MethodInfo.GetCustomAttribute<IdempotentAttribute>() is null)
{
return next;
}
return async context =>
{
string? key = context.HttpContext.Request.Headers[HeaderName];
if (string.IsNullOrWhiteSpace(key))
{
return TypedResults.Problem(
title: $"The {HeaderName} header is required.",
statusCode: StatusCodes.Status400BadRequest);
}
if (Seen.TryGetValue(key, out OrderDto? existing))
{
// Aynı istek tekrar geldi: handler'a gitmeden önceki sonucu dön
context.HttpContext.Response.Headers["Idempotent-Replayed"] = "true";
return TypedResults.Ok(existing);
}
object? result = await next(context);
if (result is Results<Created<OrderDto>, ValidationProblem> { Result: Created<OrderDto> { Value: { } order } })
{
Seen[key] = order;
}
return result;
};
}
}
Factory, EndpointFilterFactoryContext üzerinden handler'ın MethodInfo'suna bakıyor. [Idempotent] yoksa next'i olduğu gibi dönüyor ve o endpoint'in pipeline'ına hiçbir şey eklenmiyor; reflection maliyeti uygulama açılırken bir kez ödeniyor, her istekte değil. Sonuç:
$ curl -i -X POST localhost:5238/api/orders -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"items":[{"isbn":"978-0441013593","quantity":2}]}'
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"The Idempotency-Key header is required.","status":400,"traceId":"00-6c4104e0aa1c66a45080fbc5099ce31d-cdd224def0437f91-00"}
$ curl -i -X POST localhost:5238/api/orders -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: 4f1c2a" -H "Content-Type: application/json" -d '{"items":[{"isbn":"978-0441013593","quantity":2},{"isbn":"9780141439587","quantity":1}]}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /api/orders/1001
{"id":1001,"customer":"ada@example.com","lines":[{"isbn":"978-0441013593","title":"Dune","quantity":2,"unitPrice":450},{"isbn":"978-0141439587","title":"Emma","quantity":1,"unitPrice":280}],"total":1180,"status":0}
$ curl -i -X POST localhost:5238/api/orders ... -H "Idempotency-Key: 4f1c2a" ...
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Idempotent-Replayed: true
{"id":1001,"customer":"ada@example.com",...,"total":1180,"status":0}
Bu filter'ın bellekteki dictionary'si yalnızca örnek için yeterli. Gerçek bir uygulamada key'ler TTL'li (yaşam süreli) bir distributed cache'te tutulmalı, birden fazla instance aynı kaydı görmeli ve aynı key'le aynı anda gelen iki isteğin ikisi de handler'a girmemeli. Idempotency'nin tasarım tarafı ilerideki REST API Tasarımı serisinde ayrıca ele alınıyor.
TypedResults Kataloğu ve Results<T1, …>
TypedResults static class'ı, Results'taki her sonucun tipli halini veriyor. Bir API'de en sık kullandıkların: Ok<T>, Created<T> ve CreatedAtRoute<T>, Accepted<T>, NoContent, NotFound ve NotFound<T>, BadRequest<T>, Conflict<T>, UnprocessableEntity<T>, ValidationProblem, Problem (ProblemHttpResult), Unauthorized ve Forbid, StatusCode, dosya ve akış için File, PhysicalFile, Stream, Bytes, Redirect, düz metin için Text, .NET 10 ile gelen ServerSentEvents ve .NET 9 ile gelen InternalServerError. Her biri IResult'ı implement ediyor ve çoğu IEndpointMetadataProvider aracılığıyla kendi status code'unu ve body tipini endpoint metadata'sına yazıyor. Handler birden fazla sonuç dönebiliyorsa Results<T1, …, T6> union tipini kullanıyorsun:
public static Results<NoContent, NotFound, ProblemHttpResult> Cancel(int id, ClaimsPrincipal user, OrderStore store)
{
if (store.Find(id) is not { } order || order.Customer != user.Identity!.Name)
{
return TypedResults.NotFound();
}
if (order.Status == OrderStatus.Shipped)
{
return TypedResults.Problem(
title: "Shipped orders cannot be cancelled.",
statusCode: StatusCodes.Status409Conflict);
}
store.Update(order with { Status = OrderStatus.Cancelled });
return TypedResults.NoContent();
}
Burada bir boşluk var. ProblemHttpResult hangi status code'u döneceğini çalışma anında biliyor, derleme anında değil; bu yüzden metadata'ya bir şey yazamıyor. Belgede 409 görünsün diye grubu kurarken Cancel'a .ProducesProblem(StatusCodes.Status409Conflict) ekledik. Kural şu: TypedResults ve Results<…> belgeye kendiliğinden yansır; Problem, StatusCode gibi status'u değişken sonuçlar ve filter'ların döndüğü sonuçlar için Produces… çağrılarıyla elle tamamlarsın. Idempotency filter'ının replay'de döndüğü 200 de bu yüzden belgede yok.
Şimdi tahminin cevabı. /summary için belgede tek bir satır vardı:
"responses": {
"200": {
"description": "OK"
}
}
404 yok, şema da yok. Handler'ın dönüş tipi IResult ve Results.Ok(order)'ın içinde ne olduğunu framework derleme anında göremiyor; varsayılan bir 200 yazıp geçiyor. TypedResults ile yazılan /api/orders/{id} ise 200'ü OrderDto şemasıyla ve 404'ü ayrı bir response olarak belgeledi. Çalışma anındaki davranış aynı, contract farklı. Unit test tarafındaki fark da aynı yere çıkıyor: tipli sonuçlarda Assert.IsType<NotFound>(result.Result) yazabiliyorsun.
Kendi IResult'ın
Siparişin düz metin bir fişini dönen endpoint için hazır bir sonuç yok. Kendimiz yazıyoruz ve metadata'sını da biz veriyoruz:
public sealed class ReceiptResult(OrderDto order) : IResult, IEndpointMetadataProvider
{
public async Task ExecuteAsync(HttpContext httpContext)
{
StringBuilder text = new();
text.AppendLine($"Bookstore receipt #{order.Id}");
foreach (OrderLine line in order.Lines)
{
text.AppendLine(string.Create(CultureInfo.InvariantCulture, $"{line.Quantity} x {line.Title} @ {line.UnitPrice:0.00} TRY"));
}
text.Append(string.Create(CultureInfo.InvariantCulture, $"Total: {order.Total:0.00} TRY"));
httpContext.Response.ContentType = "text/plain; charset=utf-8";
await httpContext.Response.WriteAsync(text.ToString());
}
// OpenAPI'ye bu sonucun 200 ve text/plain olduğunu söyler
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) =>
builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, typeof(string), ["text/plain"]));
}
public static Results<ReceiptResult, NotFound> Receipt(int id, ClaimsPrincipal user, OrderStore store) =>
store.Find(id) is { } order && order.Customer == user.Identity!.Name
? new ReceiptResult(order)
: TypedResults.NotFound();
$ curl localhost:5238/api/orders/1001/receipt -H "Authorization: Bearer $TOKEN"
Bookstore receipt #1001
2 x Dune @ 450.00 TRY
1 x Emma @ 280.00 TRY
Total: 1180.00 TRY
IEndpointMetadataProvider'ın static PopulateMetadata method'u, endpoint oluşturulurken bir kez çağrılıyor; framework onu dönüş tipine bakarak buluyor ve Results<…>'un içindeki tiplere de uyguluyor. Belgede /receipt için 200 text/plain ve 404 göründü. Para tutarını InvariantCulture ile yazmamın sebebi önceki yazılardaki gibi bu Mac'in Türkçe kültürü: aksi halde fişte 450,00 yazıyordu.
OpenAPI Metadata'sı
.NET 9 yazısında AddOpenApi() ve MapOpenApi()'yi, .NET 10 yazısında da OpenAPI 3.1'i görmüştük. Belgeyi zenginleştiren araçlar endpoint tarafında şunlar: WithName operation id'yi verir ve CreatedAtRoute ile link üretirken de kullanılır; WithSummary ve WithDescription açıklamaları; Produces<T>, ProducesProblem ve ProducesValidationProblem elle eklenen response'ları; WithTags gruplamayı; ExcludeFromDescription endpoint'i belgeden çıkarmayı sağlar. Bookstore'da önceki yazıların debug ve antiforgery token endpoint'lerine ExcludeFromDescription() ekledik; anonim tip dönen debug endpoint'i belgede AnonymousTypeOfstringAndstring... diye uzayıp giden bir şema adı üretiyordu.
.NET 10'un yeniliklerinden biri XML comment desteği. Csproj'a GenerateDocumentationFile eklediğinde bir source generator, adlandırılmış handler method'larının <summary>, <remarks> ve <param> açıklamalarını belgeye taşıyor:
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
/// <summary>Places a new order.</summary>
/// <remarks>Send an Idempotency-Key header; a repeated key returns the first order instead of creating a new one.</remarks>
[Idempotent]
public static Results<Created<OrderDto>, ValidationProblem> Create(
CreateOrderRequest request, ClaimsPrincipal user, BookCatalog catalog, OrderStore store)
1591 public üyelerde eksik comment uyarısını, 1573 de bazı parametreleri belgeleyip ClaimsPrincipal gibi diğerlerini belgelemediğinde gelen uyarıyı susturuyor. Geri kalanını transformer'lar yapıyor. Document transformer belgenin bütününe bir kez dokunuyor; operation transformer her operation için çalışıyor ve endpoint metadata'sını görebiliyor:
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
options.AddOperationTransformer<OrderOperationTransformer>();
});
// ...
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
public sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider schemes) : IOpenApiDocumentTransformer
{
public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
document.Info.Title = "Bookstore API";
document.Info.Description = "Catalog, cart and order endpoints of Bookstore.";
IEnumerable<AuthenticationScheme> registered = await schemes.GetAllSchemesAsync();
if (registered.Any(s => s.Name == "Bearer"))
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes = new Dictionary<string, IOpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
},
};
}
}
}
public sealed class OrderOperationTransformer : IOpenApiOperationTransformer
{
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
IList<object> metadata = context.Description.ActionDescriptor.EndpointMetadata;
// Endpoint token istiyorsa belgeye de yaz
if (metadata.OfType<IAuthorizeData>().Any() && !metadata.OfType<IAllowAnonymous>().Any())
{
operation.Security ??= [];
operation.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", context.Document)] = [],
});
}
// Idempotency filter'ın eklediği işareti header parametresine çevir
if (metadata.OfType<IdempotentAttribute>().Any())
{
operation.Parameters ??= [];
operation.Parameters.Add(new OpenApiParameter
{
Name = IdempotencyFilter.HeaderName,
In = ParameterLocation.Header,
Required = true,
Description = "Repeating a key returns the first result.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
}
return Task.CompletedTask;
}
}
[Idempotent] attribute'u handler method'unda durduğu için endpoint metadata'sına da giriyor; böylece aynı işaret hem filter factory'ye hem de operation transformer'a yetiyor. CreateOrder operation'ı belgede şöyle çıktı (kısaltılmış):
{
"tags": [
"Orders"
],
"summary": "Places a new order.",
"description": "Send an Idempotency-Key header; a repeated key returns the first order instead of creating a new one.",
"operationId": "CreateOrder",
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"description": "Repeating a key returns the first result.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": { ... "$ref": "#/components/schemas/CreateOrderRequest" ... },
"responses": {
"201": { ... "$ref": "#/components/schemas/OrderDto" ... },
"400": { ... "application/problem+json": { "schema": { "$ref": "#/components/schemas/HttpValidationProblemDetails" } } ... }
},
"security": [
{
"Bearer": []
}
]
}
MapScalarApiReference() Development'ta /scalar/v1 adresinde bu belgeyi okuyan arayüzü açıyor; Orders tag'i, security şeması ve Idempotency-Key parametresi oradaki formlara da bu JSON'dan geliyor. Transformer'ların sırası da belli: Learn'e göre önce schema, sonra operation, en son document transformer'ları çalışıyor.
Controller Karşılığı
Controller'da grup class'ın kendisi: [Route("api/orders")], [Authorize] ve [Tags("Orders")] class seviyesinde yazılır. Endpoint filter'ın karşılığı filter'lar yazısındaki action filter'lar; IAsyncActionFilter da argümanları görür ve kısa devre yapabilir, ama filter pipeline'ı (authorization, resource, action, exception, result) daha katmanlıdır. Filter factory'nin karşılığı IFilterFactory ve ServiceFilter/TypeFilter. Results<…>'un karşılığı ActionResult<T> artı [ProducesResponseType] attribute'ları; controller'da status code'ları belgeye çoğunlukla bu attribute'larla elle yazıyorsun. OpenAPI transformer'ları ise iki tarafta aynı, çünkü AddOpenApi() controller'ları da aynı ApiExplorer üzerinden okuyor.
Sık Düşülen Tuzaklar
Birincisi, Results.Ok ile TypedResults.Ok farkını bilmeden metadata kaybetmek. Tahminde gördük: aynı davranış, eksik contract. Angular tarafında belgeden client üreten bir ekip /summary için OrderDto tipini hiç görmez.
İkincisi, filter'da exception yutmak. Şöyle bir "güvenlik ağı" filter'ı yazdığımda:
.AddEndpointFilter(async (context, next) =>
{
try
{
return await next(context);
}
catch (Exception)
{
return TypedResults.Ok();
}
});
Handler'ın fırlattığı Order 1001 is locked by the warehouse. exception'ı hiçbir yere loglanmadı, istemci body'siz bir 200 OK aldı. Exception'ları global hata yönetimi yazısındaki exception handler'a bırak; filter'da yakalıyorsan en azından logla ve bir Problem dön.
Üçüncüsü, .NET 10.0.15'te [AsParameters] ile bir sürpriz. Bir önceki yazıdaki BookQuery record'unda PageSize parametresinin üstünde [Range(1, 50)] vardı. AddOpenApi()'yi ekleyince /openapi/v1.json 500 döndü ve log'da şu vardı:
System.InvalidCastException: Unable to cast object of type 'System.Attribute[]' to type 'System.Collections.Generic.IEnumerable`1[System.ComponentModel.DataAnnotations.ValidationAttribute]'.
Aynı kodu .NET 11 GA ile çalıştırdığımda belge sorunsuz üretildi. .NET 10'da çözüm attribute'u property'ye hedeflemek: [property: Range(1, 50)] int PageSize = 10. Validation çalışmaya devam etti, belgede de "minimum": 1, "maximum": 50 göründü.
Dördüncüsü, grubun filter'larının sırasını unutmak. Grup filter'ı her zaman endpoint filter'ının dışında çalışır. Endpoint'e özel bir kontrolü grubun log filter'ından önce çalıştırmak istiyorsan onu gruba, daha önce eklenecek şekilde koymalısın.
Küçük Bir Deneme
★☆☆ Önce Tahmin Et'teki /summary endpoint'ini gruba ekle, /openapi/v1.json'daki halini not al, sonra Results<Ok<OrderDto>, NotFound> dönecek şekilde yeniden yaz ve /openapi/v1.json'daki farkı gör.
★★☆ Idempotency filter'ını IDistributedCache kullanacak şekilde değiştir: key'i 24 saatlik TTL ile sakla, AddDistributedMemoryCache() ile dene. Replay yanıtının belgeye de girmesi için Create'e .Produces<OrderDto>(StatusCodes.Status200OK) ekle.
★★★ Bir /api grubu kur ve orders ile books gruplarını onun altına taşı. /api grubuna bir filter ekle; iç içe gruplarda filter sırasının dıştan içe nasıl ilerlediğini log'larla doğrula.
Bu yazıda sipariş JSON'unda "status":0 gördük; enum sayı olarak dışarı çıkıyor. Bir sonraki yazıda bunun neden riskli olduğuna ve JSON ayarlarına, content negotiation'a, CSV formatter'ına ve JSON Patch'e bakıyoruz.