Уявіть, що ви керуєте мобільним додатком з мільйоном користувачів. Ви випустили версію 1.0 рік тому, і тисячі користувачів досі використовують її, не оновлюючись. Тепер вам потрібно змінити структуру API — додати нові поля, перейменувати властивості, змінити логіку валідації. Якщо ви просто оновите API, старі версії додатка перестануть працювати. Користувачі побачать помилки, залишать негативні відгуки, а ваш бізнес втратить довіру.
Це класична проблема breaking changes — змін, що порушують сумісність з існуючими клієнтами. Рішення цієї проблеми — API Versioning (версіонування API) — практика підтримки кількох версій API одночасно, що дозволяє:
Версіонування — це не технічна деталь, а стратегічне рішення, що впливає на архітектуру, документацію та життєвий цикл вашого API. Неправильний вибір стратегії може призвести до технічного боргу, складності підтримки та фрустрації розробників.
Ми побудуємо E-commerce API з трьома версіями, що демонструють реальну еволюцію продукту:
Версія 1.0 (2023) — базовий функціонал:
{
"id": 1,
"name": "Laptop",
"price": 1499.99
}
Версія 2.0 (2024) — додано категорії та рейтинг:
{
"id": 1,
"name": "Laptop",
"price": 1499.99,
"category": "Electronics",
"rating": 4.5
}
Версія 3.0 (2025) — breaking change (price → pricing object):
{
"id": 1,
"name": "Laptop",
"pricing": {
"amount": 1499.99,
"currency": "USD",
"discount": 10
},
"category": "Electronics",
"rating": 4.5
}
Ми реалізуємо 4 стратегії версіонування:
/api/v1/products, /api/v2/products/api/products?api-version=1.0X-Api-Version: 2.0Accept: application/vnd.myapi.v2+jsonДо кінця статті ви зможете:
Asp.Versioning.MvcНе всі зміни API вимагають нової версії. Розуміння різниці між breaking та non-breaking змінами критично важливе.
❌ Видалення полів
// v1
{ "id": 1, "name": "Laptop", "oldField": "value" }
// v2 (breaking!)
{ "id": 1, "name": "Laptop" }
Клієнти, що очікують oldField, отримають помилку.
❌ Перейменування полів
// v1
{ "productName": "Laptop" }
// v2 (breaking!)
{ "name": "Laptop" }
Клієнти шукатимуть productName, якого більше немає.
❌ Зміна типу даних
// v1
{ "price": 1499.99 }
// v2 (breaking!)
{ "price": "1499.99 USD" }
Клієнти очікують число, отримують рядок.
❌ Зміна структури
// v1
{ "price": 1499.99 }
// v2 (breaking!)
{ "pricing": { "amount": 1499.99, "currency": "USD" } }
Повна зміна структури даних.
❌ Зміна поведінки
// v1: POST створює і повертає 201
// v2: POST створює асинхронно і повертає 202
Клієнти очікують синхронну обробку.
❌ Зміна валідації
// v1: email опціональний
// v2: email обов'язковий
Старі запити без email стануть невалідними.
✅ Додавання нових полів
// v1
{ "id": 1, "name": "Laptop" }
// v1.1 (non-breaking)
{ "id": 1, "name": "Laptop", "category": "Electronics" }
Клієнти ігнорують невідомі поля.
✅ Додавання нових endpoints
// v1
GET /api/products
// v1.1 (non-breaking)
GET /api/products
GET /api/products/search ← новий endpoint
Існуючі endpoints не змінюються.
✅ Розширення enum
// v1: status = "active" | "inactive"
// v1.1: status = "active" | "inactive" | "pending"
Якщо клієнт обробляє unknown values gracefully.
✅ Пом'якшення валідації
// v1: email обов'язковий
// v1.1: email опціональний
Старі запити продовжують працювати.
Існує 4 основні стратегії версіонування API, кожна з яких має свої переваги та недоліки.
Версія вказується безпосередньо в URL-шляху:
GET /api/v1/products
GET /api/v2/products
GET /api/v3/products
Переваги:
Недоліки:
Коли використовувати:
Версія передається як query-параметр:
GET /api/products?api-version=1.0
GET /api/products?api-version=2.0
Переваги:
Недоліки:
Коли використовувати:
Версія передається через кастомний HTTP-заголовок:
GET /api/products
X-Api-Version: 2.0
Переваги:
Недоліки:
Коли використовувати:
Версія вказується в Accept header через кастомний media type:
GET /api/products
Accept: application/vnd.myapi.v2+json
Переваги:
Недоліки:
Коли використовувати:
| Критерій | URL Path | Query String | HTTP Header | Media Type |
|---|---|---|---|---|
| Простота | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| REST-сумісність | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Тестування | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Документація | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Кешування | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Гнучкість | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Популярність | 🥇 70% | 🥈 20% | 🥉 8% | 2% |
Настав час створити реальний API з підтримкою кількох версій. Використаємо пакет Asp.Versioning.Mvc від Microsoft.
Створіть файл Models/ProductModels.cs:
namespace EcommerceApi.Models;
// Базова модель (внутрішня, для БД)
public class Product
{
public int Id { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
public string? Category { get; set; }
public double Rating { get; set; }
public string Currency { get; set; } = "USD";
public decimal Discount { get; set; }
}
// DTO для версії 1.0 (2023)
public record ProductV1Dto
{
public int Id { get; init; }
public required string Name { get; init; }
public decimal Price { get; init; }
}
// DTO для версії 2.0 (2024)
public record ProductV2Dto
{
public int Id { get; init; }
public required string Name { get; init; }
public decimal Price { get; init; }
public string? Category { get; init; }
public double Rating { get; init; }
}
// DTO для версії 3.0 (2025) - breaking change
public record ProductV3Dto
{
public int Id { get; init; }
public required string Name { get; init; }
public required PricingInfo Pricing { get; init; }
public string? Category { get; init; }
public double Rating { get; init; }
}
public record PricingInfo
{
public decimal Amount { get; init; }
public string Currency { get; init; } = "USD";
public decimal Discount { get; init; }
}
Створіть файл Data/EcommerceDbContext.cs:
using Microsoft.EntityFrameworkCore;
using EcommerceApi.Models;
namespace EcommerceApi.Data;
public class EcommerceDbContext : DbContext
{
public EcommerceDbContext(DbContextOptions<EcommerceDbContext> options)
: base(options)
{
}
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Price).HasPrecision(18, 2);
entity.Property(e => e.Discount).HasPrecision(5, 2);
// Seed data
entity.HasData(
new Product
{
Id = 1,
Name = "Laptop Dell XPS 15",
Price = 1499.99m,
Category = "Electronics",
Rating = 4.7,
Currency = "USD",
Discount = 10
},
new Product
{
Id = 2,
Name = "Wireless Mouse",
Price = 29.99m,
Category = "Accessories",
Rating = 4.3,
Currency = "USD",
Discount = 0
},
new Product
{
Id = 3,
Name = "USB-C Hub",
Price = 49.99m,
Category = "Accessories",
Rating = 4.5,
Currency = "USD",
Discount = 5
}
);
});
}
}
using Microsoft.EntityFrameworkCore;
using EcommerceApi.Data;
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
// Реєстрація DbContext
builder.Services.AddDbContext<EcommerceDbContext>(options =>
options.UseInMemoryDatabase("EcommerceDb"));
// Налаштування API Versioning
builder.Services.AddApiVersioning(options =>
{
// Версія за замовчуванням (якщо клієнт не вказав)
options.DefaultApiVersion = new ApiVersion(1, 0);
// Використовувати версію за замовчуванням, якщо не вказано
options.AssumeDefaultVersionWhenUnspecified = true;
// Повертати підтримувані версії у заголовках відповіді
options.ReportApiVersions = true;
// Стратегії читання версії (можна комбінувати!)
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // /api/v1/products
new QueryStringApiVersionReader("api-version"), // ?api-version=1.0
new HeaderApiVersionReader("X-Api-Version"), // X-Api-Version: 2.0
new MediaTypeApiVersionReader("version") // Accept: application/json;version=3.0
);
})
.AddApiExplorer(options =>
{
// Формат версії у URL: 'v'major[.minor][-status]
options.GroupNameFormat = "'v'VVV";
// Замінювати {version} у маршрутах на фактичну версію
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Ініціалізація бази даних
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<EcommerceDbContext>();
db.Database.EnsureCreated();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Декомпозиція налаштувань:
DefaultApiVersion — версія, що використовується, якщо клієнт не вказав (зазвичай найстаріша стабільна)AssumeDefaultVersionWhenUnspecified — якщо true, використовувати default версію; якщо false — повертати 400 Bad RequestReportApiVersions — додає заголовки api-supported-versions та api-deprecated-versions у відповідьApiVersionReader.Combine() — дозволяє клієнту вказувати версію будь-яким способом (URL, query, header, media type)GroupNameFormat — формат версії для Swagger UI (v1, v2, v3)SubstituteApiVersionInUrl — замінює {version:apiVersion} у маршрутах на фактичну версіюApiVersionReader.Combine() дозволяє клієнтам вибирати зручний спосіб вказання версії. Це особливо корисно під час міграції між стратегіями.Тепер створимо три контролери для трьох версій API.
Створіть файл Controllers/V1/ProductsV1Controller.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using EcommerceApi.Data;
using EcommerceApi.Models;
using Asp.Versioning;
namespace EcommerceApi.Controllers.V1;
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
public class ProductsV1Controller : ControllerBase
{
private readonly EcommerceDbContext _db;
private readonly ILogger<ProductsV1Controller> _logger;
public ProductsV1Controller(EcommerceDbContext db, ILogger<ProductsV1Controller> logger)
{
_db = db;
_logger = logger;
}
/// <summary>
/// Отримати всі продукти (v1.0)
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ProductV1Dto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ProductV1Dto>>> GetAll()
{
_logger.LogInformation("V1: Fetching all products");
var products = await _db.Products.ToListAsync();
var response = products.Select(p => new ProductV1Dto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
});
return Ok(response);
}
/// <summary>
/// Отримати продукт за ID (v1.0)
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductV1Dto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductV1Dto>> GetById(int id)
{
var product = await _db.Products.FindAsync(id);
if (product is null)
return NotFound();
var response = new ProductV1Dto
{
Id = product.Id,
Name = product.Name,
Price = product.Price
};
return response;
}
}
Ключові моменти:
[ApiVersion("1.0")] — вказує версію контролера[Route("api/v{version:apiVersion}/products")] — {version:apiVersion} автоматично замінюється на v1ProductV1Dto — спрощену версію без категорій та рейтингуСтворіть файл Controllers/V2/ProductsV2Controller.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using EcommerceApi.Data;
using EcommerceApi.Models;
using Asp.Versioning;
namespace EcommerceApi.Controllers.V2;
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("2.0")]
public class ProductsV2Controller : ControllerBase
{
private readonly EcommerceDbContext _db;
private readonly ILogger<ProductsV2Controller> _logger;
public ProductsV2Controller(EcommerceDbContext db, ILogger<ProductsV2Controller> logger)
{
_db = db;
_logger = logger;
}
/// <summary>
/// Отримати всі продукти (v2.0) - з категоріями та рейтингом
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ProductV2Dto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ProductV2Dto>>> GetAll()
{
_logger.LogInformation("V2: Fetching all products with categories and ratings");
var products = await _db.Products.ToListAsync();
var response = products.Select(p => new ProductV2Dto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Category = p.Category,
Rating = p.Rating
});
return Ok(response);
}
/// <summary>
/// Отримати продукт за ID (v2.0)
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductV2Dto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductV2Dto>> GetById(int id)
{
var product = await _db.Products.FindAsync(id);
if (product is null)
return NotFound();
var response = new ProductV2Dto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Category = product.Category,
Rating = product.Rating
};
return response;
}
/// <summary>
/// Фільтрувати продукти за категорією (нова функція у v2.0)
/// </summary>
[HttpGet("by-category/{category}")]
[ProducesResponseType(typeof(IEnumerable<ProductV2Dto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ProductV2Dto>>> GetByCategory(string category)
{
var products = await _db.Products
.Where(p => p.Category == category)
.ToListAsync();
var response = products.Select(p => new ProductV2Dto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Category = p.Category,
Rating = p.Rating
});
return Ok(response);
}
}
Створіть файл Controllers/V3/ProductsV3Controller.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using EcommerceApi.Data;
using EcommerceApi.Models;
using Asp.Versioning;
namespace EcommerceApi.Controllers.V3;
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("3.0")]
public class ProductsV3Controller : ControllerBase
{
private readonly EcommerceDbContext _db;
private readonly ILogger<ProductsV3Controller> _logger;
public ProductsV3Controller(EcommerceDbContext db, ILogger<ProductsV3Controller> logger)
{
_db = db;
_logger = logger;
}
/// <summary>
/// Отримати всі продукти (v3.0) - з новою структурою ціни
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ProductV3Dto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ProductV3Dto>>> GetAll()
{
_logger.LogInformation("V3: Fetching all products with new pricing structure");
var products = await _db.Products.ToListAsync();
var response = products.Select(MapToV3Dto);
return Ok(response);
}
/// <summary>
/// Отримати продукт за ID (v3.0)
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductV3Dto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductV3Dto>> GetById(int id)
{
var product = await _db.Products.FindAsync(id);
if (product is null)
return NotFound();
return MapToV3Dto(product);
}
/// <summary>
/// Фільтрувати продукти за категорією (v3.0)
/// </summary>
[HttpGet("by-category/{category}")]
[ProducesResponseType(typeof(IEnumerable<ProductV3Dto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ProductV3Dto>>> GetByCategory(string category)
{
var products = await _db.Products
.Where(p => p.Category == category)
.ToListAsync();
var response = products.Select(MapToV3Dto);
return Ok(response);
}
// Допоміжний метод для маппінгу
private static ProductV3Dto MapToV3Dto(Product product) => new()
{
Id = product.Id,
Name = product.Name,
Pricing = new PricingInfo
{
Amount = product.Price,
Currency = product.Currency,
Discount = product.Discount
},
Category = product.Category,
Rating = product.Rating
};
}
Ключова відмінність v3.0: Замість простого поля price, тепер використовується об'єкт pricing з детальною інформацією про валюту та знижку. Це breaking change, тому потрібна нова major версія.
Запустіть проєкт та протестуйте всі стратегії версіонування:
api-supported-versions: Завдяки ReportApiVersions = true, кожна відповідь містить інформацію про всі підтримувані версії. Це допомагає клієнтам виявляти доступні версії.Підтримка всіх версій назавжди неможлива. Потрібен процес виведення з експлуатації (deprecation) старих версій.
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0", Deprecated = true)] // Позначаємо як deprecated
[ApiVersion("2.0")] // Поточна стабільна версія
public class ProductsV1Controller : ControllerBase
{
// ...
}
Тепер відповіді v1.0 містять заголовок:
HTTP/1.1 200 OK
api-supported-versions: 1.0, 2.0, 3.0
api-deprecated-versions: 1.0
Вкажіть дату видалення версії через кастомний middleware:
// Middleware для додавання Sunset header
public class DeprecationMiddleware
{
private readonly RequestDelegate _next;
public DeprecationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
// Якщо використана deprecated версія
if (context.Response.Headers.ContainsKey("api-deprecated-versions"))
{
var deprecatedVersions = context.Response.Headers["api-deprecated-versions"].ToString();
if (deprecatedVersions.Contains("1.0"))
{
// Sunset header (RFC 8594) - дата видалення
context.Response.Headers.Append("Sunset", "Sat, 31 Dec 2024 23:59:59 GMT");
// Link на migration guide
context.Response.Headers.Append("Link",
"</docs/migration/v1-to-v2>; rel=\"deprecation\"; type=\"text/html\"");
// Кастомне повідомлення
context.Response.Headers.Append("X-Api-Warn",
"API v1.0 is deprecated and will be removed on 2024-12-31. Please migrate to v2.0.");
}
}
}
}
// Реєстрація у Program.cs
app.UseMiddleware<DeprecationMiddleware>();
Створіть документ docs/migration/v1-to-v2.md:
# Migration Guide: v1.0 → v2.0
## Зміни
### Додані поля (non-breaking)
- `category` (string, nullable) — категорія продукту
- `rating` (number) — рейтинг від 0 до 5
### Приклад
**v1.0:**
```json
{
"id": 1,
"name": "Laptop",
"price": 1499.99
}
v2.0:
{
"id": 1,
"name": "Laptop",
"price": 1499.99,
"category": "Electronics",
"rating": 4.7
}
/api/v1/products на /api/v2/productsv2.0 повністю зворотно сумісна з v1.0. Нові поля опціональні.
### Крок 4: Видалення старої версії
Після закінчення deprecation period (зазвичай 6-12 місяців):
1. Видаліть контролер `ProductsV1Controller`
2. Видаліть DTO `ProductV1Dto`
3. Оновіть документацію
4. Повідомте клієнтів через email/блог
Замість створення окремих контролерів для кожної версії, можна використовувати [MapToApiVersion]:
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// Метод для v1.0
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<ProductV1Dto>>> GetAllV1()
{
// Логіка для v1.0
}
// Метод для v2.0
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<IEnumerable<ProductV2Dto>>> GetAllV2()
{
// Логіка для v2.0
}
// Спільний метод для обох версій
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id, ApiVersion version)
{
var product = await _db.Products.FindAsync(id);
if (product is null) return NotFound();
// Повертаємо різні DTO залежно від версії
return version.MajorVersion switch
{
1 => Ok(new ProductV1Dto { /* ... */ }),
2 => Ok(new ProductV2Dto { /* ... */ }),
_ => StatusCode(406)
};
}
}
Коли використовувати:
Коли НЕ використовувати:
Деякі endpoints не потребують версіонування (health checks, metrics):
[ApiController]
[Route("api/health")]
[ApiVersionNeutral] // Доступний для всіх версій
public class HealthController : ControllerBase
{
[HttpGet]
public IActionResult Check()
{
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
}
}
Підтримка діапазону версій:
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// Доступний для v1.0, v1.1, v2.0
[HttpGet]
public async Task<IActionResult> GetAll()
{
// ...
}
// Доступний тільки для v2.0
[HttpGet("advanced")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetAdvanced()
{
// ...
}
}
✅ Семантичне версіонування
Використовуйте SemVer (Semantic Versioning):
Для API зазвичай достатньо Major.Minor.
✅ Документуйте зміни
Для кожної версії створюйте:
✅ Deprecation Period
Встановіть чіткий термін підтримки:
✅ Backward Compatibility
Намагайтеся уникати breaking changes:
✅ Версіонуйте контракт, не код
Версія — це контракт з клієнтом:
✅ Тестуйте всі версії
Кожна версія має свої тести:
Визначте, чи є наступні зміни breaking чи non-breaking:
description до Productname на productNameprice з number на stringGET /api/products/searchemail стає обов'язковимname, якого більше немаєДля кожного сценарію оберіть найкращу стратегію версіонування:
Змініть проєкт так, щоб версія читалася тільки з header X-Api-Version:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// Тільки header versioning
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
Оновіть маршрути контролерів:
[Route("api/products")] // Без {version}
[ApiVersion("1.0")]
public class ProductsV1Controller : ControllerBase
{
// ...
}
Тестування:
curl -H "X-Api-Version: 1.0" https://localhost:5001/api/products
curl -H "X-Api-Version: 2.0" https://localhost:5001/api/products
Створіть generic метод для автоматичного маппінгу між версіями DTO:
public static TTarget MapVersion<TSource, TTarget>(TSource source)
{
// Реалізуйте автоматичний маппінг через рефлексію
}
using System.Reflection;
public static class VersionMapper
{
public static TTarget MapVersion<TSource, TTarget>(TSource source)
where TTarget : new()
{
var target = new TTarget();
var sourceProps = typeof(TSource).GetProperties();
var targetProps = typeof(TTarget).GetProperties();
foreach (var targetProp in targetProps)
{
var sourceProp = sourceProps.FirstOrDefault(p =>
p.Name == targetProp.Name &&
p.PropertyType == targetProp.PropertyType);
if (sourceProp != null)
{
var value = sourceProp.GetValue(source);
targetProp.SetValue(target, value);
}
}
return target;
}
}
// Використання
var v1Dto = VersionMapper.MapVersion<Product, ProductV1Dto>(product);
var v2Dto = VersionMapper.MapVersion<Product, ProductV2Dto>(product);
::
::
Налаштуйте Swagger UI для відображення окремих документів для кожної версії:
using Asp.Versioning.ApiExplorer;
var builder = WebApplication.CreateBuilder(args);
// ... (налаштування API Versioning)
builder.Services.AddSwaggerGen(options =>
{
var provider = builder.Services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
// Створюємо документ для кожної версії
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(
description.GroupName,
new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = $"E-commerce API {description.ApiVersion}",
Version = description.ApiVersion.ToString(),
Description = description.IsDeprecated
? "⚠️ This version is deprecated"
: "Current version"
});
}
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
// Dropdown для вибору версії
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
});
}
Результат: Swagger UI з dropdown "Select a definition" → v1, v2, v3
У цій статті ми опанували мистецтво версіонування API — критично важливу практику для еволюції сервісів без порушення роботи існуючих клієнтів. Ви навчилися не просто додавати версії до URL, а стратегічно керувати життєвим циклом API.
Ключові висновки:
/api/v1/products) через простоту та явність. Це найкращий вибір для більшості проєктів.У наступній статті ми розглянемо ProblemDetails та структуровану обробку помилок — як повертати консистентні та інформативні помилки згідно з RFC 9457.
Content Negotiation - JSON, XML та власні форматери
Механізм узгодження формату відповіді між клієнтом та сервером. System.Text.Json vs Newtonsoft.Json, XML-серіалізація та створення кастомних форматерів для CSV, YAML, MessagePack.
ProblemDetails та структурована обробка помилок
Реалізація RFC 9457 для консистентних помилок API. GlobalExceptionHandler, IExceptionHandler, IProblemDetailsService, кастомні error codes та traceability.