Przejdź do treści
Frameworki

.NET API - Wersjonowanie i najlepsze praktyki

Opublikowano:
·
Zaktualizowano:
·Autor: MDS Software Solutions Group
.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!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

.NET API - Wersjonowanie i najlepsze praktyki | MDS Software Solutions Group | MDS Software Solutions Group