ASP.NET Core Web API’de Temiz Pagination (Header + Link)
Response body’de yalnızca öğeleri döndürün, pagination metadatasını header’lara taşıyın. RFC 5988 Link header ile next/prev sayfalarını yönetin.
Bir REST API geliştirirken sayfalama (pagination) bilgileri çoğu zaman response body’nin içine eklenir. Bu yöntem çalışır; ancak veri ile gezinti metadatasını aynı payload içinde karıştırır. Daha temiz bir yaklaşım, response body’yi yalnızca öğelere odaklamak ve sayfalama bilgisini HTTP header’larına taşımaktır.
Bu örnekte API, müşterileri basit bir JSON dizi olarak döndürürken sayfalama metadatasını
X-Total-Count, X-Page, X-Page-Size, X-Total-Pages ve
next/prev gezinimi için RFC 5988 Link header’ı ile sunar.
Amaç
- Response body’yi temiz tutmak: sadece öğeleri döndürmek.
- Sayfalama metadatasını header’larda sunmak.
- İstemcinin
next/prevsayfalarını keşfedebilmesi içinLinkheader’ı eklemek. - Sayfalama linkleri üretilirken mevcut query parametrelerini (filter/sort/search) korumak.
Controller Örneği (Body’de Öğeler, Header’da Metadata)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
[HttpGet("api/customers")]
public async Task<ActionResult<IEnumerable<Customer>>> GetCustomers(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
// Sunucu tarafı limitler (kötüye kullanımı önler)
page = Math.Max(1, page);
pageSize = Math.Clamp(pageSize, 1, 100);
// Servis: items + totalCount döndürür
var (items, totalCount) = await _customerService.ListPagedAsync(page, pageSize);
// toplam sayfa (totalCount = 0 olduğunda 0 olabilir)
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Sayfalama metadatası header'larda
Response.Headers["X-Total-Count"] = totalCount.ToString();
Response.Headers["X-Page"] = page.ToString();
Response.Headers["X-Page-Size"] = pageSize.ToString();
Response.Headers["X-Total-Pages"] = totalPages.ToString();
// Link header: next/prev (RFC 5988)
var links = BuildLinkHeader(page, pageSize, totalPages);
if (!string.IsNullOrWhiteSpace(links))
Response.Headers["Link"] = links;
return Ok(items);
}
// Şöyle Link header üretir:
// <https://host/api/customers?page=1&pageSize=20&sort=name>; rel="prev", <...>; rel="next"
private string BuildLinkHeader(int page, int pageSize, int totalPages)
{
// Sayfa yoksa (totalCount = 0) veya tek sayfaysa gezinim linki gerekmez.
if (totalPages <= 1)
return string.Empty;
var baseUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}";
// Var olan query parametrelerini (filter/sort/search...) koru
var query = Request.Query
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString());
// Mevcut pageSize'ı zorunlu/uyumlu tut
query["pageSize"] = pageSize.ToString();
var parts = new List<string>();
if (page > 1)
{
query["page"] = (page - 1).ToString();
var prevUrl = QueryHelpers.AddQueryString(baseUrl, query);
parts.Add($"<{prevUrl}>; rel=\"prev\"");
}
if (page < totalPages)
{
query["page"] = (page + 1).ToString();
var nextUrl = QueryHelpers.AddQueryString(baseUrl, query);
parts.Add($"<{nextUrl}>; rel=\"next\"");
}
return string.Join(", ", parts);
}
// Opsiyonel (örneğin copy/paste ile çalışması için)
public sealed record Customer(string Name);
Response body minimal kalır (sadece items), istemci ise sayfalama metadatasını header’lardan okur.
Ayrıca sayfalama linkleri, mevcut query parametrelerini (sort, filter, search)
koruduğu için gezinim her zaman tutarlı olur.
Örnek Response Body
[
{ "name": "John" },
{ "name": "Hans" },
{ "name": "Alex" },
{ "name": "Pearl" }
]
Örnek Response Header’ları
X-Total-Count: 542
X-Page: 2
X-Page-Size: 20
X-Total-Pages: 28
Link: <https://api.site.com/api/customers?page=1&pageSize=20&sort=name>; rel="prev",
<https://api.site.com/api/customers?page=3&pageSize=20&sort=name>; rel="next"
Bu örnekte sort=name (ve diğer query parametreleri) üretilen Link URL’lerinde korunur.
Fetch Örneği (JavaScript)
const response = await fetch(`/api/customers?page=${page}&pageSize=${pageSize}&sort=name`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const items = await response.json();
// Header'lardan sayfalama metadatası
const totalCount = Number(response.headers.get("X-Total-Count"));
const currentPage = Number(response.headers.get("X-Page"));
const pageSize = Number(response.headers.get("X-Page-Size"));
const totalPages = Number(response.headers.get("X-Total-Pages"));
// Link header ile gezinim (next/prev)
const linkHeader = response.headers.get("Link");
// Link header'ı { next, prev } şeklinde parse edin
const links = parseLinkHeader(linkHeader);
console.log(links.next, links.prev);
function parseLinkHeader(value) {
if (!value) return {};
// Örnek:
// <https://...page=1&pageSize=20>; rel="prev", <https://...page=3&pageSize=20>; rel="next"
const result = {};
const parts = value.split(",");
for (const part of parts) {
const section = part.trim();
const urlMatch = section.match(/<([^>]+)>/);
const relMatch = section.match(/rel="([^"]+)"/);
if (!urlMatch || !relMatch) continue;
const url = urlMatch[1];
const rel = relMatch[1];
result[rel] = url;
}
return result;
}
Küçük bir parseLinkHeader yardımcı fonksiyonu sayesinde UI tarafında,
response body’ye metadata eklemeden links.next ve links.prev üzerinden sayfa gezintisi yapılabilir.
CORS Notu (Özel Header’ları Okumak)
API ve frontend farklı domainlerde çalışıyorsa, tarayıcı CORS nedeniyle
JavaScript tarafında tüm response header’larını okumaya izin vermez.
Bu durumda response.headers.get("X-Total-Count") gibi çağrılar null dönebilir.
Bu header’ları okuyabilmek için API tarafında CORS ayarına Access-Control-Expose-Headers
eklenmelidir. Aşağıdaki örnek, ASP.NET Core Program.cs içinde CORS policy tanımlar ve uygular:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("Frontend", policy =>
policy.WithOrigins("https://frontend.site.com")
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders(
"X-Total-Count",
"X-Page",
"X-Page-Size",
"X-Total-Pages",
"Link"));
});
var app = builder.Build();
// ... diğer middleware'ler (ör. UseRouting)
// CORS'u endpoint'lerden önce uygula
app.UseCors("Frontend");
// ... UseAuthentication / UseAuthorization vb.
app.MapControllers();
app.Run();
Böylece pagination metadatası header’ları JavaScript tarafında okunabilir hale gelir.
Neden Link Header?
Link header, sayfalama gezinimini standart bir şekilde keşfedilebilir hale getirir:
rel="next"bir sonraki sayfanın URL’ini gösterir.rel="prev"bir önceki sayfanın URL’ini gösterir.- İstemci, URL formatını tahmin etmek zorunda kalmadan gezinebilir.
Yaygın İyileştirmeler
- Daha iyi gezinim için
rel="first"verel="last"linkleri ekleyin. - Kötüye kullanımı önlemek için maksimum
pageSizesınırı uygulayın (Math.Clamppratik bir çözümdür). - Link üretirken tüm query parametrelerini (filter/sort/search) dahil edin (bu örnek zaten bunu yapıyor).
TL;DR
- Response body’de sadece öğeleri döndürün.
- Sayfalama metadatasını header’larda sunun:
X-Total-Count,X-Page,X-Page-Size. next/previçin RFC 5988Linkheader ekleyin.- Mevcut query parametrelerini koruyarak sayfalama linklerinin doğru kalmasını sağlayın.