نسخهبندی و مستندسازی APIها
در بخش پیش دیدیم که چگونه CORS را پیکربندی کنیم. شکل ۴-۴۶ نمونهای از شکست درخواست را نشان میدهد؛ زیرا Origin درخواست در فهرست مبدأهای مجاز قرار ندارد.
شکل ۴-۴۶ — شکست CORS بهدلیل مجاز نبودن Origin درخواست
API Versioning (نسخهبندی API)
وقتی API در طول زمان تکامل پیدا میکند، ممکن است قراردادها، مدلها یا رفتارهای آن تغییر کنند. نسخهبندی کمک میکند چند نسخه از یک API همزمان در دسترس بمانند تا مصرفکنندگان قدیمی مجبور نباشند فوراً به قرارداد جدید مهاجرت کنند.
سه روش رایج برای اعلام نسخه عبارتاند از:
- فرستادن نسخه در Header، برای مثال
api-version: 2.
- قرار دادن نسخه در مسیر، برای مثال
/v2/countries.
- قرار دادن نسخه در Query String، برای مثال
/countries?api-version=2.
روش Query String از نظر نویسنده انتخاب مطلوبی نیست. نسخهبندی در مسیر بسیار آشکار و قابلفهم است و نسخهبندی در Header نیز URL را تمیز نگه میدارد. در ادامه هر دو رویکرد بررسی میشوند.
نسخهبندی با Header
برای این کار بستهٔ NuGet با نام Asp.Versioning.Http را به پروژه اضافه کنید. سپس سرویس نسخهبندی را مانند Listing 4-40 ثبت کنید.
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
var app = builder.Build();
Listing 4-40 — پیکربندی API Versioning با Header
DefaultApiVersion نسخهٔ پیشفرض را تعیین میکند. ReportApiVersions باعث میشود نسخههای پشتیبانیشده و منسوخشده در Headerهای پاسخ گزارش شوند. با AssumeDefaultVersionWhenUnspecified اگر مصرفکننده نسخهای نفرستد، نسخهٔ پیشفرض در نظر گرفته میشود. در پایان، HeaderApiVersionReader مشخص میکند نسخه از Header با نام api-version خوانده شود.
اکنون یک مجموعه نسخه (Version Set) برای نسخههای ۱ و ۲ ایجاد میکنیم.
var versionSet = app.NewApiVersionSet()
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.Build();
Listing 4-41 — ایجاد Version Set
سپس Endpointها را به نسخههای مشخص نگاشت میکنیم. یک Endpoint فقط نسخهٔ ۱، یکی نسخهٔ ۲، یک Endpoint فقط نسخهٔ ۲ و نمونهٔ آخر بدون وابستگی به نسخه است.
app.MapGet("/version", () => "Hello version 1")
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
app.MapGet("/version", () => "Hello version 2")
.WithApiVersionSet(versionSet)
.MapToApiVersion(2.0);
app.MapGet("/version2only", () => "Hello version 2 only")
.WithApiVersionSet(versionSet)
.MapToApiVersion(2.0);
app.MapGet("/versionneutral", () => "Hello neutral version")
.WithApiVersionSet(versionSet)
.IsApiVersionNeutral();
Listing 4-42 — نگاشت Endpointها به نسخهها
درخواست به /version با Header نسخهٔ ۱ پاسخ نسخهٔ ۱ را میگیرد و همان URL با نسخهٔ ۲ پاسخ نسخهٔ ۲ را برمیگرداند. اگر نسخه ارسال نشود، بهدلیل تنظیمات قبلی نسخهٔ پیشفرض ۱ اعمال میشود. Endpoint خنثی نسبت به نسخه، مستقل از مقدار نسخه قابل فراخوانی است.
شکل ۴-۴۷ — فراخوانی نسخهٔ ۱
شکل ۴-۴۸ — پاسخ Endpoint نسخهٔ ۱
شکل ۴-۴۹ — فراخوانی نسخهٔ ۲
شکل ۴-۵۰ — پاسخ Endpoint نسخهٔ ۲
شکل ۴-۵۱ — Endpoint فقط مخصوص نسخهٔ ۲
شکل ۴-۵۲ — فراخوانی Endpoint خنثی از نسخه
اگر نسخهای مانند ۳ درخواست شود که در مجموعه نسخهها تعریف نشده است، چارچوب درخواست را رد میکند و پاسخ 400 Bad Request برمیگرداند.
شکل ۴-۵۳ — درخواست نسخهٔ پشتیبانینشده
شکل ۴-۵۴ — پاسخ خطا برای نسخهٔ پشتیبانینشده
نسخهبندی با Route
راه دیگر آن است که نسخه جزئی از مسیر باشد. در این حالت میتوان Route Groupهای جداگانه برای هر نسخه ساخت و Endpointهای هر نسخه را در گروه مربوط قرار داد.
public static class VersionGroup
{
public static RouteGroupBuilder GroupVersion1(this RouteGroupBuilder group)
{
group.MapGet("/version", () => "Hello version 1");
return group;
}
public static RouteGroupBuilder GroupVersion2(this RouteGroupBuilder group)
{
group.MapGet("/version", () => "Hello version 2");
group.MapGet("/version2only", () => "Hello version 2 only");
return group;
}
}
Listing 4-43 — گروهبندی Endpointها بر اساس نسخه
در Program.cs کافی است هر گروه را زیر Prefix نسخهٔ خودش نگاشت کنید.
app.MapGroup("/v1").GroupVersion1();
app.MapGroup("/v2").GroupVersion2();
app.Run();
Listing 4-44 — نگاشت Route Groupهای نسخهٔ ۱ و ۲
اکنون URLهایی مانند /v1/version و /v2/version نسخه را بهوضوح در خود مسیر نشان میدهند. نویسنده این روش را ترجیح میدهد؛ زیرا درخواست نسخهای که Route آن وجود ندارد بهطور طبیعی با 404 Not Found پاسخ داده میشود، نه خطای نسخهبندی ۴۰۰.
شکل ۴-۵۵ — فراخوانی نسخهٔ ۱ از مسیر
شکل ۴-۵۶ — فراخوانی نسخهٔ ۲ از مسیر
Documentation (مستندسازی)
یک REST API خوب فقط باید درست کار کند؛ مصرفکننده باید بتواند قرارداد، ورودیها، خروجیها، نسخهها و وضعیتهای پاسخ آن را نیز بفهمد. در اکوسیستم ASP.NET Core، OpenAPI و Swagger ابزارهای اصلی این کار هستند.
برای نمونهٔ این بخش بستههای زیر استفاده میشوند:
Asp.Versioning.Http
Asp.Versioning.Mvc.ApiExplorer
Microsoft.AspNetCore.OpenApi
Swashbuckle.AspNetCore
پیکربندی Swagger برای چند نسخه
کلاس زیر تنظیمات Swagger را بر اساس نسخههایی که ApiExplorer گزارش میکند ایجاد میکند.
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace AspNetCore8MinimalApis.Swagger;
public class SwaggerConfigurationsOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
public SwaggerConfigurationsOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = "Minimal APIs",
Version = description.ApiVersion.ToString()
});
}
}
}
Listing 4-45 — تنظیم Swagger برای نسخههای API
سپس سرویسهای ApiExplorer، نسخهبندی و Swagger را در Program.cs ثبت میکنیم و Swagger UI را برای دو سند نسخه پیکربندی میکنیم.
using Asp.Versioning;
using Asp.Versioning.Conventions;
using AspNetCore8MinimalApis.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VV";
});
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IConfigureOptions<SwaggerGenOptions>,
SwaggerConfigurationsOptions>();
var app = builder.Build();
app.UseSwagger().UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/v1.0/swagger.json", "Version 1.0");
c.SwaggerEndpoint($"/swagger/v2.0/swagger.json", "Version 2.0");
});
var versionSet = app.NewApiVersionSet()
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.Build();
app.MapGet("/version", () => "Hello version 1")
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
app.MapGet("/version", () => "Hello version 2")
.WithApiVersionSet(versionSet)
.MapToApiVersion(2.0);
app.MapGet("/version2only", () => "Hello version 2 only")
.WithApiVersionSet(versionSet)
.MapToApiVersion(2.0);
app.MapGet("/versionneutral", () => "Hello neutral version")
.WithApiVersionSet(versionSet)
.IsApiVersionNeutral();
app.Run();
Listing 4-46 — پیکربندی کامل API Versioning و Swagger
AddEndpointsApiExplorer اطلاعات Endpointهای Minimal API را در اختیار OpenAPI قرار میدهد. AddSwaggerGen مولد سند Swagger را ثبت میکند و AddApiExplorer نسخهها را برای ApiExplorer قابل مشاهده میسازد. کلاس تنظیمات نیز یک سند جداگانه برای هر نسخه ایجاد میکند.
UseSwagger JSONهای OpenAPI را منتشر میکند و UseSwaggerUI رابط کاربری Swagger را فعال میکند. آدرس پیشفرض رابط کاربری /swagger/index.html است. در نسخهٔ Preview مورد استفاده هنگام نگارش کتاب، بهدلیل یک مشکل موقت، Endpointهای نسخه در UI بهصورت صریح تنظیم شدهاند.
شکل ۴-۵۷ — صفحهٔ Swagger UI برای نسخهٔ ۱
شکل ۴-۵۸ — صفحهٔ Swagger UI برای نسخهٔ ۲
شکل ۴-۵۹ — جزئیات یک Endpoint در Swagger
شکل ۴-۶۰ — نمایش خودکار Header نسخه در Swagger
افزودن توضیحات و Commentها
Swagger میتواند Commentهای XML را در مستندات نمایش دهد. برای Lambdaهای Endpoint، مستندسازی XML به همان شکلی که برای متدهای معمولی وجود دارد در دسترس نیست، اما میتوان انواع پیچیدهٔ ورودی را مستند کرد. توضیحات سرویسهایی که از DI به Endpoint تزریق میشوند نیز نباید بهعنوان پارامتر عمومی API نمایش داده شوند.
ابتدا Extension زیر مسیر فایل XML تولیدشده برای اسمبلی را پیدا کرده و آن را به Swagger معرفی میکند.
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
namespace AspNetCore8MinimalApis.Swagger;
public static class SwaggerXmlComments
{
public static void AddXmlComments(this SwaggerGenOptions options)
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
}
}
Listing 4-47 — افزودن XML Comments به Swagger
در فایل پروژه نیز تولید فایل Documentation XML را فعال و هشدار ۱۵۹۱ را غیرفعال کنید.
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Listing 4-48 — فعالسازی فایل مستندات XML
سپس Extension را هنگام ثبت Swagger فراخوانی کنید.
builder.Services.AddSwaggerGen(options =>
{
options.AddXmlComments();
});
Listing 4-49 — ثبت XML Comments در SwaggerGen
برای مثال، Endpoint ایجاد کشور یک مدل Country میپذیرد و نتیجه را از سرویس مربوط دریافت میکند. مدل را میتوان با Commentهای XML مستند کرد تا توضیح Propertyها در OpenAPI ظاهر شود.
app.MapPost("/countries", async (Country country,
ICountryService countryService) =>
{
var result = await countryService.AddCountry(country);
return Results.Created($"/countries/{result.Id}", result);
});
Listing 4-50 — Endpoint نمونه برای نمایش مستندسازی مدل ورودی
public class Country
{
/// <summary>
/// Country identifier
/// </summary>
public int Id { get; set; }
/// <summary>
/// Country name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Country description
/// </summary>
public string Description { get; set; }
/// <summary>
/// Country flag URI
/// </summary>
public string FlagUri { get; set; }
}
Listing 4-51 — مستندسازی Propertyهای Country با XML Comment
شکل ۴-۶۱ — نمایش توضیحات مدل Country در Swagger UI
Swagger Annotations
روش دیگر استفاده از بستهٔ Swashbuckle.AspNetCore.Annotations است. پس از نصب بسته، Annotationها را فعال کنید.
builder.Services.AddSwaggerGen(options =>
{
options.EnableAnnotations();
options.AddXmlComments();
});
Listing 4-52 — فعالسازی Swagger Annotations
اکنون میتوان با SwaggerOperation خلاصه و توضیح Endpoint را مشخص کرد.
app.MapGet("/versionneutral",
[SwaggerOperation(Summary = "Neutral version",
Description = "This version is neutral")] ()
=> "Hello neutral version")
.WithApiVersionSet(versionSet)
.IsApiVersionNeutral();
Listing 4-53 — مستندسازی Endpoint با SwaggerOperation
شکل ۴-۶۲ — نتیجهٔ SwaggerOperation در Swagger UI
راه دیگر، استفاده مستقیم از WithOpenApi است.
app.MapGet("/versionneutral",() => "Hello neutral version")
.WithApiVersionSet(versionSet)
.IsApiVersionNeutral()
.WithOpenApi(operation => new(operation)
{
Summary = "This is a summary",
Description = "This is a description"
});
Listing 4-54 — سفارشیسازی OpenAPI با WithOpenApi
گروهبندی Endpointها با Tag
برای خواناتر شدن رابط Swagger، میتوان Endpointهای هر Route Group را با یک Tag مشترک گروهبندی کرد.
app.MapGroup("/v1").GroupVersion1().WithTags("V1");
app.MapGroup("/v2").GroupVersion2().WithTags("V2");
Listing 4-55 — گروهبندی نسخهها با Tag
شکل ۴-۶۳ — گروههای V1 و V2 در Swagger UI
سفارشیسازیهای دیگر Swagger
میتوان Endpointی را از مستندات پنهان کرد، منسوخ بودن آن را نشان داد و پاسخهای ممکن آن را صریحاً توصیف کرد.
برای حذف یک Endpoint یا گروه از OpenAPI از ExcludeFromDescription استفاده کنید.
app.MapGroup("/v1")
.GroupVersion1()
.WithTags("V1")
.ExcludeFromDescription();
Listing 4-56 — حذف Endpointها از توضیحات OpenAPI
شکل ۴-۶۴ — Swagger UI پس از حذف گروه از مستندات
برای اعلام منسوخشدن یک عملیات، Property مربوط به Deprecated را در OpenAPI تنظیم کنید.
.WithOpenApi(operation => new(operation)
{
Deprecated = true
});
Listing 4-57 — علامتگذاری Endpoint بهعنوان Deprecated
شکل ۴-۶۵ — نمایش Endpoint منسوخ در Swagger UI
در نهایت، وضعیتهای پاسخ مهم را با Produces مستند کنید. نمونهٔ زیر Endpoint دانلود فایل را با پاسخ موفق، پیدا نشدن، خطای داخلی و Timeout توصیف میکند.
app.MapGet("/countries/download", (ICountryService countryService) =>
{
(byte[] fileContent, string mimeType, string fileName) =
countryService.GetFile();
if (fileContent is null || mimeType is null)
return Results.NotFound();
return Results.File(fileContent, mimeType, fileName);
})
.Produces<Stream>(StatusCodes.Status200OK, "video/mp4")
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status500InternalServerError)
.Produces(StatusCodes.Status408RequestTimeout);
Listing 4-58 — توصیف پاسخهای Endpoint با Produces
شکل ۴-۶۶ — پاسخهای مستندشده در Swagger UI
جمعبندی فصل
در این فصل حداقل دانش لازم برای ساخت REST APIهای تمیز با Minimal APIs در ASP.NET Core مرور شد: مسیریابی، اتصال پارامتر، اعتبارسنجی، نگاشت شیء، عملیات CRUD، وضعیتهای HTTP، کار با فایل و Streaming، CORS، نسخهبندی و مستندسازی با OpenAPI و Swagger. فصل بعد به قابلیتهای پیشرفتهتر Minimal API میپردازد.
تصاویر منبع مرتبط با این بخش