.NET API - Wersjonowanie i najlepsze praktyki

.NET API: Wersjonowanie i najlepsze praktyki
Wersjonowanie API to kluczowy aspekt rozwoju aplikacji enterprise. Pozwala na wprowadzanie zmian bez przerywania działania istniejących klientów. W tym artykule przedstawiamy kompleksowy przewodnik po wersjonowaniu API w .NET z wykorzystaniem ASP.NET Core.
Dlaczego wersjonowanie API jest ważne?
W miarę rozwoju aplikacji API ewoluuje:
- Nowe funkcje - dodawanie nowych endpointów
- Zmiany breaking - modyfikacja kontraktów
- Optymalizacja - poprawa wydajności
- Deprecation - wycofywanie starych funkcji
- Kompatybilność wsteczna - wsparcie dla starszych klientów
Strategie wersjonowania API
1. URL Path Versioning
Najpopularniejsza metoda - wersja w ścieżce URL:
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public ActionResult<IEnumerable<ProductV1>> GetV1()
{
return Ok(new[] { new ProductV1 { Id = 1, Name = "Product" } });
}
[HttpGet]
[MapToApiVersion("2.0")]
public ActionResult<IEnumerable<ProductV2>> GetV2()
{
return Ok(new[] {
new ProductV2 {
Id = 1,
Name = "Product",
Description = "New field in v2"
}
});
}
}
Zalety:
- Łatwe w użyciu i zrozumieniu
- Widoczne w URL
- Dobre dla dokumentacji
- Cache-friendly
Wady:
- URL może się wydłużyć
- Trzeba duplikować kod dla każdej wersji
2. Header Versioning
Wersja w nagłówku HTTP:
[ApiController]
[Route("api/products")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public ActionResult<IEnumerable<ProductV1>> GetV1()
{
// v1 logic
}
[HttpGet]
[MapToApiVersion("2.0")]
public ActionResult<IEnumerable<ProductV2>> GetV2()
{
// v2 logic
}
}
Klient wysyła:
GET /api/products
api-version: 2.0
Zalety:
- Czysty URL
- RESTful approach
- Łatwe w cache'owaniu
Wady:
- Mniej intuicyjne
- Trudniejsze do testowania w przeglądarce
3. Query String Versioning
Wersja jako parametr query:
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public ActionResult<IEnumerable<ProductV1>> GetV1()
{
// v1 logic
}
}
Wywołanie: GET /api/products?api-version=2.0
4. Media Type Versioning (Content Negotiation)
Wersja w nagłówku Accept:
[HttpGet]
[Produces("application/vnd.company.v1+json")]
public ActionResult<ProductV1> GetV1()
{
// v1 logic
}
[HttpGet]
[Produces("application/vnd.company.v2+json")]
public ActionResult<ProductV2> GetV2()
{
// v2 logic
}
Konfiguracja wersjonowania w ASP.NET Core
Instalacja pakietu
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
Podstawowa konfiguracja
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// URL Path versioning
options.ApiVersionReader = new UrlSegmentApiVersionReader();
// Multiple version readers
// options.ApiVersionReader = ApiVersionReader.Combine(
// new UrlSegmentApiVersionReader(),
// new HeaderApiVersionReader("api-version"),
// new QueryStringApiVersionReader("api-version")
// );
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
Najlepsze praktyki
1. Semantic Versioning
Używaj wersjonowania semantycznego (MAJOR.MINOR.PATCH):
[ApiVersion("1.0")] // Initial release
[ApiVersion("1.1")] // New features, backward compatible
[ApiVersion("2.0")] // Breaking changes
2. API Version Deprecation
Oznaczaj przestarzałe wersje:
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
[Obsolete("Use v2.0 instead")]
public ActionResult<ProductV1> GetV1()
{
Response.Headers.Add("X-API-Warn",
"This version is deprecated. Use v2.0");
// v1 logic
}
}
3. Shared Code Between Versions
Wykorzystuj dziedziczenie i dependency injection:
public interface IProductService
{
Task<IEnumerable<ProductDto>> GetProductsAsync(int version);
}
public class ProductService : IProductService
{
public async Task<IEnumerable<ProductDto>> GetProductsAsync(int version)
{
var products = await _repository.GetAllAsync();
return version switch
{
1 => _mapper.Map<IEnumerable<ProductV1>>(products),
2 => _mapper.Map<IEnumerable<ProductV2>>(products),
_ => throw new NotSupportedException($"Version {version} not supported")
};
}
}
4. Documentation with Swagger
Integracja z Swagger dla każdej wersji:
builder.Services.AddSwaggerGen(options =>
{
var provider = builder.Services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(
description.GroupName,
new OpenApiInfo
{
Title = $"My API {description.ApiVersion}",
Version = description.ApiVersion.ToString(),
Description = description.IsDeprecated
? "This API version is deprecated"
: ""
});
}
});
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
});
Zaawansowane techniki
Version-specific middleware
public class ApiVersionMiddleware
{
private readonly RequestDelegate _next;
public ApiVersionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var apiVersion = context.GetRequestedApiVersion();
if (apiVersion?.MajorVersion < 2)
{
context.Response.Headers.Add("X-API-Deprecated", "true");
}
await _next(context);
}
}
Version-specific validation
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator(int apiVersion)
{
RuleFor(x => x.Name).NotEmpty();
if (apiVersion >= 2)
{
RuleFor(x => x.Description)
.NotEmpty()
.WithMessage("Description is required in v2+");
}
}
}
Conditional responses based on version
[HttpGet("{id}")]
public ActionResult<object> Get(int id)
{
var product = _service.GetById(id);
var version = HttpContext.GetRequestedApiVersion();
return version?.MajorVersion switch
{
1 => Ok(new ProductV1
{
Id = product.Id,
Name = product.Name
}),
2 => Ok(new ProductV2
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
CreatedAt = product.CreatedAt
}),
_ => BadRequest("Unsupported API version")
};
}
Testing różnych wersji
public class ProductsControllerTests
{
[Fact]
public async Task GetV1_ReturnsProductsWithoutDescription()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/v1/products");
// Assert
response.EnsureSuccessStatusCode();
var products = await response.Content
.ReadFromJsonAsync<List<ProductV1>>();
Assert.NotNull(products);
Assert.All(products, p => Assert.Null(p.Description));
}
[Fact]
public async Task GetV2_ReturnsProductsWithDescription()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/v2/products");
// Assert
response.EnsureSuccessStatusCode();
var products = await response.Content
.ReadFromJsonAsync<List<ProductV2>>();
Assert.NotNull(products);
Assert.All(products, p => Assert.NotNull(p.Description));
}
}
Checklista wersjonowania API
Przed wdrożeniem wersjonowania upewnij się, że:
- [ ] Strategia wersjonowania wybrana (URL/Header/Query)
- [ ] Semantic versioning stosowane
- [ ] Default version skonfigurowana
- [ ] Deprecated versions oznaczone
- [ ] Dokumentacja Swagger dla każdej wersji
- [ ] Testy dla wszystkich wersji
- [ ] Breaking changes udokumentowane
- [ ] Migration guide przygotowany
- [ ] Monitoring per-version włączony
- [ ] Sunset policy zdefiniowana
- [ ] Client libraries zaktualizowane
- [ ] Komunikacja z klientami o zmianach
Migracja między wersjami
Sunset header
Informuj klientów o wycofaniu wersji:
[ApiVersion("1.0", Deprecated = true)]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public ActionResult<ProductV1> GetV1()
{
Response.Headers.Add("Sunset",
"Sat, 31 Dec 2024 23:59:59 GMT");
Response.Headers.Add("Link",
"</api/v2/products>; rel=\"successor-version\"");
return Ok(/* ... */);
}
}
Graceful degradation
public ActionResult<object> Get(int id)
{
var version = HttpContext.GetRequestedApiVersion();
var product = _service.GetById(id);
if (version == null || version.MajorVersion < 1)
{
return BadRequest(new
{
error = "API version not specified",
message = "Please specify api-version header",
currentVersion = "2.0",
minimumVersion = "1.0"
});
}
// Return appropriate version
}
Podsumowanie
Dobre wersjonowanie API w .NET wymaga:
- Jasnej strategii - wybierz metodę i trzymaj się jej
- Dokumentacji - Swagger dla każdej wersji
- Testów - coverage dla wszystkich wersji
- Komunikacji - informuj klientów o zmianach
- Stopniowego wycofywania - daj czas na migrację
Prawidłowe wersjonowanie pozwala na ewolucję API bez przerywania działania istniejących integracji.
Potrzebujesz pomocy z API?
W MDS Software Solutions Group oferujemy:
- Projektowanie architektury API
- Implementację wersjonowania
- Audyt istniejących API
- Migrację do nowych wersji
- Wsparcie techniczne 24/7
Skontaktuj się z nami, aby omówić Twój projekt!
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.