Microservice de Panier : Gestion et Persistance avec ASP.NET Core et Redis

Le microservice de panier est responsable de la gestion des opérations relatives au panier d'achats, notamment :

  • CRUD (Création, Lecture, Mise à jour, Suppression) des articles dans le panier.
  • Synchronisation des articles du panier avec les mises à jour de prix des produits via des événements.
  • Publication d'événements lors de la finalisation du panier.
  • Gestion de la suppression des articles du panier suite à la création réussie d'une commande.

Architecture et Tcehnologies

Ce microservice adopte une architecture CRUD pilotée par les données, implémentée au sein d'un projet ASP.NET Core Web API unique. La persistance des données est assurée par Redis.

Les principales technologies utilisées sont :

  • ASP.NET Core Web API
  • Entity Framework Core (pour la gestion du contexte de données, bien que Redis soit utilisé pour la persistance principale)
  • Redis (pour le stockage des données du panier)
  • Swashbuckle (pour la génération de documentation OpenAPI/Swagger)
  • Autofac (pour l'injection de dépendances)
  • Un Bus d'Événements (pour la communication inter-services via des événements)
  • Newtonsoft.Json (pour la sérialisation/désérialisation JSON)

Modélisation et Persistance des Données

L'entité principale est CustomerBasket, qui entretient une relation un-à-pluiseurs avec BasketItem. Le stockage dans Redis s'effectue en sérialisant et désérialisant ces objets en format JSON.

La connexion à Redis est gérée via un singleton ConnectionMultiplexer, injecté dans le dépôt Redis (RedisBasketRepository).


services.AddSingleton<ConnectionMultiplexer>(sp =>
{
    var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
    var configurationOptions = ConfigurationOptions.Parse(settings.ConnectionString, true);

    configurationOptions.ResolveDns = true; // Résolution DNS activée

    return ConnectionMultiplexer.Connect(configurationOptions);
});

Gestion des Événements

Le microservice gère la publication et la souscription aux événements suivants :

  • Publication d'un événement de finalisation du panier lorsqu'un utilisateur procède au paiement.
  • Souscription à un événement de création de commande réussie pour vider le panier de l'utilisateur.
  • Souscription à un événement de changement de prix de produit pour mettre à jour les prix des articles correspondants dans le panier.

private void ConfigureEventBus(IApplicationBuilder app)
{
    var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();

    // S'abonner aux événements pertinents
    eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
    eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
}

Toutes ces interactions sont médiatisées par le bus d'événements.

Authentification et Autorisation

L'accès aux fonctionnalités de gestion du panier est protégé. L'intégration avec le microservice d'identité (Identity Microservice) est configurée au démarrage de l'application.


private void ConfigureAuthService(IServiceCollection services)
{
    // Empêcher le mappage du claim "sub" vers NameIdentifier
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    var identityUrl = Configuration.GetValue<string>("IdentityUrl");

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
        options.Authority = identityUrl;
        options.RequireHttpsMetadata = false; // Pour les environnements de développement
        options.Audience = "basket"; // Audience du token JWT
    });
}

protected virtual void ConfigureAuth(IApplicationBuilder app)
{
    // Utilisation d'un middleware pour contourner l'authentification dans les scénarios de test de charge
    if (Configuration.GetValue<bool>("UseLoadTest"))
    {
        app.UseMiddleware<ByPassAuthMiddleware>();
    }
    app.UseAuthentication(); // Activation du middleware d'authentification
}

Simulation de Disjoncteur (Circuit Breaker)

Un middleware personnalisé, FailingMiddleware, permet de simuler manuellement l'état "ouvert" du disjoncteur. En accédant à l'endpoint /failing avec les paramètres ?enable ou ?disable, on peut activer ou désactiver cette simulation.

Lorsque le disjoncteur est ouvert, les tentatives d'accès au service de panier échouent. Si des mécanismes de résilience comme Polly sont utilisés pour les appels HTTP, une BrokenCircuitException sera levée après un certain nombre d'échecs, permettant d'informer l'utilisateur.


public class CartController : Controller
{
    // ... autres dépendances et méthodes ...

    public async Task<IActionResult> Index()
    {
        try
        {
            var user = _appUserParser.Parse(HttpContext.User);
            // Appel du client typé pour interagir avec le service de panier
            var vm = await _basketSvc.GetBasket(user);
            return View(vm);
        }
        catch (BrokenCircuitException)
        {
            // Gestion de l'exception lorsque le service de panier est indisponible (circuit ouvert)
            HandleBrokenCircuitException();
        }
        return View();
    }

    private void HandleBrokenCircuitException()
    {
        TempData["BasketInoperativeMsg"] = "Le service de panier est actuellement indisponible. Veuillez réessayer plus tard.";
    }
}

Filtres MVC

Deux filtres globaux sont configurés lors de la mise en place des services MVC : un filtre d'exception global et un filtre de validation de modèle.


// Ajout des services du framework
services.AddMvc(options =>
{
    options.Filters.Add(typeof(HttpGlobalExceptionFilter)); // Filtre d'exception global
    options.Filters.Add(typeof(ValidateModelStateFilter));   // Filtre de validation de modèle

}).AddControllersAsServices();

  • Le filtre d'exception global gère les exceptions personnalisées comme BasketDomainException via le filtre HttpGlobalExceptionFilter.
  • Le filtre ValidateModelStateFilter, hérité de ActionFilterAttribute, vérifie la validité du modèle d'entrée et retourne des erreurs de validation si nécessaire.

public class ValidateModelStateFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.ModelState.IsValid)
        {
            return; // Le modèle est valide, continuer l'exécution
        }

        // Récupération des erreurs de validation
        var validationErrors = context.ModelState
            .Keys
            .SelectMany(key => context.ModelState[key].Errors)
            .Select(error => error.ErrorMessage)
            .ToArray();

        // Création d'une réponse d'erreur JSON
        var jsonErrorResponse = new JsonErrorResponse
        {
            Messages = validationErrors
        };

        // Retourner une réponse BadRequest avec les détails des erreurs
        context.Result = new BadRequestObjectResult(jsonErrorResponse);
    }
}

Intégration de l'Authentification dans Swagger UI

Pour faciliter les tests via l'interface Swagger UI, l'authentification OAuth2 est intégrée.


services.AddSwaggerGen(options =>
{
    options.DescribeAllEnumsAsStrings();
    options.SwaggerDoc("v1", new Info
    {
        Title = "Basket HTTP API",
        Version = "v1",
        Description = "The Basket Service HTTP API",
        TermsOfService = "Terms Of Service"
    });

    // Configuration du schéma d'authentification OAuth2
    options.AddSecurityDefinition("oauth2", new OAuth2Scheme
    {
        Type = "oauth2",
        Flow = "implicit", // ou "accessCode" selon le besoin
        AuthorizationUrl = $"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize",
        TokenUrl = $"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token",
        Scopes = new Dictionary<string, string>()
        {
            { "basket", "Basket API" } // Portée pour l'API du panier
        }
    });

    // Application du filtre pour vérifier les autorisations nécessaires
    options.OperationFilter<AuthorizeCheckOperationFilter>();
});

Cette configuration inclut la définition des URLs d'autorisation et de token, ainsi que les portées requises. Le filtre AuthorizeCheckOperationFilter s'assure que les endpoints nécessitant une authentification sont correctement marqués.


public class AuthorizeCheckOperationFilter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        // Vérifier la présence de l'attribut [Authorize] sur la classe ou la méthode
        var hasAuthorizeAttribute = context.ApiDescription.ControllerAttributes().OfType<AuthorizeAttribute>().Any() ||
                                   context.ApiDescription.ActionAttributes().OfType<AuthorizeAttribute>().Any();

        if (hasAuthorizeAttribute)
        {
            operation.Responses.Add("401", new Response { Description = "Unauthorized" });
            operation.Responses.Add("403", new Response { Description = "Forbidden" });

            // Ajout de la sécurité OAuth2 à l'opération
            operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
            operation.Security.Add(new Dictionary<string, IEnumerable<string>>
            {
                { "oauth2", new [] { "basketapi" } } // Spécification de la portée
            });
        }
    }
}

Par rapport au microservice Catalog, le microservice Basket intègre des fonctionnalités d'authentification et utilise Redis pour la persistance des données.

Étiquettes: ASP.NET Core Web API Microservices Redis Cache

Publié le 18 septembre à 07h20