Architecture : Services et Injection de Dépendance
Au début d’un projet ASP.NET Core, on a tendance à écrire toute la logique métier directement dans les méthodes des Contrôleurs. C’est ce qu’on appelle l’anti-pattern “Fat Controller”.
Pourquoi séparer le code ?
Les contrôleurs ont pour unique responsabilité de gérer le protocole HTTP :
- Recevoir la requête (et désérialiser le JSON).
- Valider les entrées basiques.
- Appeler la logique métier.
- Renvoyer une réponse HTTP appropriée (200 OK, 404 Not Found, etc.).
Ils ne devraient JAMAIS contenir :
- La logique de calcul (ex: formule du coût du reset).
- Les appels directs à la base de données (si possible).
- La logique de validation complexe (ex: vérifier si l’utilisateur a assez d’argent).
Cette séparation permet de :
- Tester la logique métier sans lancer un serveur web (Tests Unitaires).
- Réutiliser la logique métier ailleurs (ex: dans une tâche planifiée).
- Lire plus facilement le code.
Création d’un Service
Un service est une classe C# standard qui contient la logique métier.
Exemple : GameService.cs
using GameServerApi.Models;
using Microsoft.EntityFrameworkCore;
namespace GameServerApi.Services;
public class GameService
{
private readonly BDDContext _context;
public GameService(BDDContext context)
{
_context = context;
}
public async Task<Progression> ClickAsync(int userId)
{
var progression = await _context.Progressions.FirstOrDefaultAsync(p => p.UserId == userId);
if (progression == null)
{
throw new Exception("Progression introuvable");
}
progression.Count += (1 + progression.TotalClickValue) * progression.Multiplier;
await _context.SaveChangesAsync();
return progression;
}
}
Injection de Dépendance (DI)
Pour utiliser ce service dans un contrôleur, il faut l’enregistrer dans le conteneur de dépendances.
Dans Program.cs :
// Enregistrement du service
// AddScoped : Une nouvelle instance est créée pour chaque requête HTTP
builder.Services.AddScoped<GameService>();
Ensuite, on l’injecte dans le constructeur du contrôleur :
using System.Security.Claims;
using GameServerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GameServerApi.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class GameController : ControllerBase
{
private readonly GameService _gameService;
public GameController(GameService gameService)
{
_gameService = gameService;
}
[HttpGet("Click")]
public async Task<IActionResult> Click()
{
var idClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(idClaim, out int userId)) return Unauthorized();
var progression = await _gameService.ClickAsync(userId);
return Ok(new
{
count = progression.Count,
multiplier = progression.Multiplier,
totalClickValue = progression.TotalClickValue
});
}
}
Refactoring étape par étape
- Identifiez un bloc de logique dans votre contrôleur.
- Créez une méthode dans un Service correspondant (
UserService,GameService,InventoryService). - Déplacez le code (et les dépendances comme le
DbContext) dans ce Service. - Appelez le Service depuis le Contrôleur.
Votre contrôleur va maigrir et devenir beaucoup plus clair !