WebAPI versioning

Découvrez les concepts essentiels de WebAPI versioning avec des exemples pratiques et des bonnes pratiques.

Olivier Dupuy
21 juillet 2025

10

Vues

0

Commentaires

5

Min de lecture

Bonnes pratiques de développement

Les bonnes pratiques sont essentielles pour maintenir la qualité du code et la productivité de l'équipe. Cet article couvre WebAPI versioning et les standards de développement incontournables.

Qualité du code

Principes SOLID

// S - Single Responsibility Principle
// Mauvais exemple
public class UserService
{
    public void CreateUser(User user) { /* ... */ }
    public void SendEmail(string email, string message) { /* ... */ }
    public void LogActivity(string activity) { /* ... */ }
}

// Bon exemple - responsabilités séparées
public class UserService
{
    private readonly IUserRepository _userRepository;
    private readonly IEmailService _emailService;
    private readonly ILogger<UserService> _logger;
    
    public UserService(IUserRepository userRepository, IEmailService emailService, ILogger<UserService> logger)
    {
        _userRepository = userRepository;
        _emailService = emailService;
        _logger = logger;
    }
    
    public async Task<Result> CreateUserAsync(CreateUserRequest request)
    {
        try
        {
            var user = new User(request.Email, request.Name);
            await _userRepository.AddAsync(user);
            
            await _emailService.SendWelcomeEmailAsync(user.Email);
            _logger.LogInformation("User created: {UserId}", user.Id);
            
            return Result.Success();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating user");
            return Result.Failure("User creation failed");
        }
    }
}

// O - Open/Closed Principle
public abstract class PaymentProcessor
{
    public abstract Task<PaymentResult> ProcessAsync(PaymentRequest request);
    
    protected PaymentResult CreateSuccessResult(string transactionId)
    {
        return new PaymentResult { Success = true, TransactionId = transactionId };
    }
}

public class CreditCardProcessor : PaymentProcessor
{
    public override async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // Logique spécifique aux cartes de crédit
        var transactionId = await ProcessCreditCardAsync(request);
        return CreateSuccessResult(transactionId);
    }
}

public class PayPalProcessor : PaymentProcessor
{
    public override async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // Logique spécifique à PayPal
        var transactionId = await ProcessPayPalAsync(request);
        return CreateSuccessResult(transactionId);
    }
}

Gestion des erreurs robuste

// Pattern Result pour éviter les exceptions
public class Result<T>
{
    public bool IsSuccess { get; private set; }
    public T? Value { get; private set; }
    public string? Error { get; private set; }
    public Exception? Exception { get; private set; }
    
    private Result(T value)
    {
        IsSuccess = true;
        Value = value;
    }
    
    private Result(string error, Exception? exception = null)
    {
        IsSuccess = false;
        Error = error;
        Exception = exception;
    }
    
    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(string error, Exception? exception = null) => new(error, exception);
    
    public TResult Match<TResult>(Func<T, TResult> onSuccess, Func<string, TResult> onFailure)
    {
        return IsSuccess ? onSuccess(Value!) : onFailure(Error!);
    }
}

// Service avec gestion d'erreurs
public class ArticleService
{
    private readonly IArticleRepository _repository;
    private readonly ILogger<ArticleService> _logger;
    
    public ArticleService(IArticleRepository repository, ILogger<ArticleService> logger)
    {
        _repository = repository;
        _logger = logger;
    }
    
    public async Task<Result<ArticleDto>> GetArticleAsync(int id)
    {
        try
        {
            if (id <= 0)
                return Result<ArticleDto>.Failure("Invalid article ID");
            
            var article = await _repository.GetByIdAsync(id);
            
            if (article == null)
                return Result<ArticleDto>.Failure("Article not found");
            
            var dto = new ArticleDto
            {
                Id = article.Id,
                Title = article.Title,
                Content = article.Content
            };
            
            return Result<ArticleDto>.Success(dto);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving article {ArticleId}", id);
            return Result<ArticleDto>.Failure("An error occurred while retrieving the article", ex);
        }
    }
}

// Utilisation dans un contrôleur
[ApiController]
[Route("api/[controller]")]
public class ArticleController : ControllerBase
{
    private readonly ArticleService _articleService;
    
    public ArticleController(ArticleService articleService)
    {
        _articleService = articleService;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<ArticleDto>> GetArticle(int id)
    {
        var result = await _articleService.GetArticleAsync(id);
        
        return result.Match<ActionResult<ArticleDto>>(
            onSuccess: article => Ok(article),
            onFailure: error => error.Contains("not found") ? NotFound(error) : BadRequest(error));
    }
}

Tests automatisés

Tests unitaires avec mocks

[TestFixture]
public class ArticleServiceTests
{
    private Mock<IArticleRepository> _mockRepository;
    private Mock<ILogger<ArticleService>> _mockLogger;
    private ArticleService _service;
    
    [SetUp]
    public void Setup()
    {
        _mockRepository = new Mock<IArticleRepository>();
        _mockLogger = new Mock<ILogger<ArticleService>>();
        _service = new ArticleService(_mockRepository.Object, _mockLogger.Object);
    }
    
    [Test]
    public async Task GetArticleAsync_WithValidId_ReturnsArticle()
    {
        // Arrange
        var articleId = 1;
        var expectedArticle = new Article
        {
            Id = articleId,
            Title = "Test Article",
            Content = "Test Content"
        };
        
        _mockRepository.Setup(r => r.GetByIdAsync(articleId))
            .ReturnsAsync(expectedArticle);
        
        // Act
        var result = await _service.GetArticleAsync(articleId);
        
        // Assert
        Assert.IsTrue(result.IsSuccess);
        Assert.AreEqual(expectedArticle.Title, result.Value.Title);
        Assert.AreEqual(expectedArticle.Content, result.Value.Content);
        
        _mockRepository.Verify(r => r.GetByIdAsync(articleId), Times.Once);
    }
    
    [Test]
    public async Task GetArticleAsync_WithInvalidId_ReturnsFailure()
    {
        // Arrange
        var invalidId = -1;
        
        // Act
        var result = await _service.GetArticleAsync(invalidId);
        
        // Assert
        Assert.IsFalse(result.IsSuccess);
        Assert.AreEqual("Invalid article ID", result.Error);
        
        _mockRepository.Verify(r => r.GetByIdAsync(It.IsAny<int>()), Times.Never);
    }
    
    [Test]
    public async Task GetArticleAsync_WithRepositoryException_ReturnsFailure()
    {
        // Arrange
        var articleId = 1;
        var exception = new Exception("Database error");
        
        _mockRepository.Setup(r => r.GetByIdAsync(articleId))
            .ThrowsAsync(exception);
        
        // Act
        var result = await _service.GetArticleAsync(articleId);
        
        // Assert
        Assert.IsFalse(result.IsSuccess);
        Assert.AreEqual("An error occurred while retrieving the article", result.Error);
        Assert.AreEqual(exception, result.Exception);
        
        // Vérifier que l'erreur a été loggée
        _mockLogger.Verify(
            x => x.Log(
                LogLevel.Error,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Error retrieving article")),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Once);
    }
}

Tests d'intégration

[TestFixture]
public class ArticleIntegrationTests
{
    private WebApplicationFactory<Program> _factory;
    private HttpClient _client;
    private ApplicationDbContext _context;
    
    [SetUp]
    public void Setup()
    {
        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // Remplacer la base de données par une base en mémoire
                    var descriptor = services.SingleOrDefault(
                        d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
                    
                    if (descriptor != null)
                        services.Remove(descriptor);
                    
                    services.AddDbContext<ApplicationDbContext>(options =>
                        options.UseInMemoryDatabase("TestDb"));
                });
            });
        
        _client = _factory.CreateClient();
        
        using var scope = _factory.Services.CreateScope();
        _context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        _context.Database.EnsureCreated();
    }
    
    [TearDown]
    public void TearDown()
    {
        _context?.Dispose();
        _client?.Dispose();
        _factory?.Dispose();
    }
    
    [Test]
    public async Task GetArticle_WithExistingId_ReturnsArticle()
    {
        // Arrange
        var article = new Article
        {
            Title = "Integration Test Article",
            Content = "Test content for integration test"
        };
        
        _context.Articles.Add(article);
        await _context.SaveChangesAsync();
        
        // Act
        var response = await _client.GetAsync($"/api/articles/{article.Id}");
        
        // Assert
        response.EnsureSuccessStatusCode();
        
        var json = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<ArticleDto>(json, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });
        
        Assert.IsNotNull(result);
        Assert.AreEqual(article.Title, result.Title);
        Assert.AreEqual(article.Content, result.Content);
    }
}

Configuration et déploiement

Configuration par environnement

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=BlogMagasine;Trusted_Connection=true;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

// appsettings.Production.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=prod-server;Database=BlogMagasine;User Id=sa;Password={DB_PASSWORD};"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "System": "Error",
      "Microsoft": "Error"
    }
  }
}

// Configuration des options fortement typées
public class DatabaseOptions
{
    public const string SectionName = "Database";
    
    public string ConnectionString { get; set; } = string.Empty;
    public int CommandTimeout { get; set; } = 30;
    public bool EnableSensitiveDataLogging { get; set; } = false;
}

// Enregistrement des options
builder.Services.Configure<DatabaseOptions>(
    builder.Configuration.GetSection(DatabaseOptions.SectionName));

Pipeline CI/CD avec GitHub Actions

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 8.0.x
        
    - name: Restore dependencies
      run: dotnet restore
      
    - name: Build
      run: dotnet build --no-restore
      
    - name: Test
      run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
      
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      
  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 8.0.x
        
    - name: Publish
      run: dotnet publish -c Release -o ./publish
      
    - name: Deploy to Azure
      uses: azure/webapps-deploy@v2
      with:
        app-name: 'blog-magazine-app'
        publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
        package: ./publish

Monitoring et observabilité

// Configuration d'Application Insights
builder.Services.AddApplicationInsightsTelemetry();

// Middleware de monitoring personnalisé
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;
    
    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            
            _logger.LogInformation(
                "HTTP {Method} {Path} responded {StatusCode} in {Duration}ms",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

// Health checks détaillés
builder.Services.AddHealthChecks()
    .AddDbContext<ApplicationDbContext>("database")
    .AddRedis(builder.Configuration.GetConnectionString("Redis"), "redis")
    .AddUrlGroup(new Uri("https://api.external-service.com/health"), "external-api");

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

Conclusion

L'adoption de bonnes pratiques avec WebAPI versioning est essentielle pour maintenir la qualité et l'évolutivité de vos applications. Les principes SOLID, les tests automatisés, la configuration appropriée et le monitoring constituent les piliers d'un développement professionnel et durable.

Partager cet article
42
12

Commentaires (0)

Rejoignez la discussion

Connectez-vous pour partager votre avis et échanger avec la communauté

Première discussion

Soyez le premier à partager votre avis sur cet article !

À propos de l'auteur
Olivier Dupuy

Développeur passionné et créateur de contenu technique. Expert en développement web moderne avec ASP.NET Core, JavaScript, et technologies cloud.

Profil
Articles similaires
API versioning strategies
02 août 2025 0
C# & .NET
Cryptographie post-quantique
02 août 2025 0
C# & .NET
Géolocalisation et cartes interactives
02 août 2025 0
C# & .NET
Navigation rapide
Commentaires (0)