Module Messaging

Module d'envoi multicanal - email SMTP, SMS AWS SNS, notifications push et templates MJML

Module Messaging

Le module Messaging fournit une infrastructure d'envoi de messages multicanal (email, SMS, push) avec persistance du suivi d'envoi, templates MJML pour les emails et nettoyage automatique des anciens messages.

Architecture du module

mermaid
flowchart TB
    subgraph Modules appelants
        ID[Module Identity]
    end

    subgraph Messaging.Contracts
        SEC[SendEmailCommand]
        SSC[SendSmsCommand]
        SPC[SendPushCommand]
    end

    subgraph Module Messaging
        PB[MessagingPersistenceBehavior<br/>Pipeline MediatR]
        EH[SendEmailCommandHandler]
        SH[SendSmsCommandHandler]
        PH[SendPushCommandHandler]
        TR[MjmlTemplateRenderer]
        DB[(MessagingContext<br/>schema: messaging)]
        CJ[MessageCleanupJob<br/>Hangfire]
    end

    subgraph Services externes
        SMTP[SMTP / MailKit]
        SNS[AWS SNS]
        PUSH[Service Push]
    end

    ID -->|MediatR Send| SEC
    ID -->|MediatR Send| SSC

    SEC --> PB
    SSC --> PB
    SPC --> PB
    PB -->|Persistance avant envoi| DB
    PB --> EH
    PB --> SH
    PB --> PH

    EH --> TR
    SH --> TR
    EH --> SMTP
    SH --> SNS
    PH --> PUSH

    CJ -->|Nettoyage 90j| DB

Contracts (interface publique)

Messages

Les messages sont definis dans Messaging.Contracts.Messages/ :

// Email
public sealed record EmailMessage(
    string To,
    string Subject,
    string TemplateName,
    IReadOnlyDictionary<string, string> Variables,
    string? ReplyTo = null) : IMessage;

// SMS
public sealed record SmsMessage(
    string PhoneNumber,
    string TemplateName,
    IReadOnlyDictionary<string, string> Variables) : IMessage;

// Push
public sealed record PushMessage(
    Guid UserId,
    string Title,
    string Body,
    IReadOnlyDictionary<string, string>? Data = null) : IMessage;

Commandes

Les commandes sont des wrappers autour de ICommand de BuildingBlocks via l'interface IMessagingCommand :

public sealed record SendEmailCommand(EmailMessage Message) : IMessagingCommand;
public sealed record SendSmsCommand(SmsMessage Message) : IMessagingCommand;
public sealed record SendPushCommand(PushMessage Message) : IMessagingCommand;

Evenements

  • MessageSentEvent : emis apres un envoi reussi
  • MessageFailedEvent : emis apres un echec d'envoi

Entites

SentMessage

Enregistrement de chaque message envoye pour le suivi et l'audit :

ProprieteTypeDescription
IdGuidIdentifiant unique
ChannelstringCanal d'envoi (email, sms, push)
RecipientstringDestinataire (email, telephone, userId)
TemplateNamestringNom du template utilise
SubjectstringSujet du message
StatusMessageStatusStatut courant
ErrorMessagestring?Message d'erreur en cas d'echec
AttemptsintNombre de tentatives d'envoi
CreatedAtDateTimeDate de creation
SentAtDateTime?Date d'envoi effectif
CorrelationIdstring?Identifiant de correlation

MessageStatus (Enum)

internal enum MessageStatus
{
    Pending,   // En attente d'envoi
    Sent,      // Envoye avec succes
    Failed,    // Echec d'envoi
    Bounced    // Rebond (email non delivre)
}

MessagingPersistenceBehavior

Ce behavior MediatR est specifique au module Messaging. Il s'active pour toute commande implementant IMessagingCommand :

  1. Avant l'envoi : cree un SentMessage avec le statut Pending et le persiste
  2. Pendant l'envoi : execute le handler reel (SMTP, SNS, push)
  3. Apres l'envoi : met a jour le statut en Sent avec la date d'envoi
  4. En cas d'echec : met a jour le statut en Failed avec le message d'erreur
  5. Toujours : persiste le resultat final (meme si la persistance echoue, un warning est logue)

Le pattern garantit que chaque tentative d'envoi est tracee, qu'elle reussisse ou echoue.

Services internes

IEmailSender / SmtpEmailSender

Envoi d'emails via SMTP avec MailKit :

internal interface IEmailSender
{
    Task SendAsync(string to, string subject, string htmlBody, string? replyTo, CancellationToken cancellationToken);
}

Configuration via SmtpOptions :

  • Host : serveur SMTP
  • Port : port SMTP (587 par defaut)
  • Username / Password : authentification
  • FromEmail / FromName : expediteur
  • UseSsl : activer TLS/SSL

Un SmtpHealthCheck est enregistre pour verifier la connectivite SMTP.

ISmsSender / SnsSmsSender

Envoi de SMS via AWS Simple Notification Service :

internal interface ISmsSender
{
    Task SendAsync(string phoneNumber, string body, CancellationToken cancellationToken);
}

Configuration via SnsOptions :

  • Region : region AWS (ex: eu-north-1)
  • AccessKeyId / SecretAccessKey : credentials AWS (optionnel si IAM role)

Si aucune region SNS n'est configuree, un SmsServiceStub est enregistre a la place.

IPushSender / PushServiceStub

Interface pour les notifications push :

internal interface IPushSender
{
    Task SendAsync(Guid userId, string title, string body, IReadOnlyDictionary<string, string>? data, CancellationToken cancellationToken);
}

Actuellement implemente par un stub (PushServiceStub). L'implementation reelle est a venir.

ITemplateRenderer / MjmlTemplateRenderer

Rendu de templates email avec MJML :

internal interface ITemplateRenderer
{
    string Render(string templateName, IReadOnlyDictionary<string, string> variables);
}

L'implementation MjmlTemplateRenderer utilise la librairie Mjml.Net pour convertir les templates MJML en HTML responsive.

Handlers

SendEmailCommandHandler

  1. Rend le template MJML en HTML via ITemplateRenderer
  2. Envoie l'email via IEmailSender

SendSmsCommandHandler

  1. Rend le template en texte via ITemplateRenderer
  2. Envoie le SMS via ISmsSender

SendPushCommandHandler

  1. Envoie la notification push via IPushSender

Job de nettoyage

MessageCleanupJob est un job Hangfire qui supprime les messages de plus de 90 jours :

internal sealed class MessageCleanupJob(MessagingContext messagingContext, ILogger<MessageCleanupJob> logger)
{
    internal const int RetentionDays = 90;

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        DateTime cutoff = DateTime.UtcNow.AddDays(-RetentionDays);
        int deletedCount = await messagingContext.SentMessages
            .Where(message => message.CreatedAt < cutoff)
            .ExecuteDeleteAsync(cancellationToken);
    }
}

DbContext

internal sealed class MessagingContext : AppDbContextBase
{
    public DbSet<SentMessage> SentMessages => Set<SentMessage>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.HasDefaultSchema("messaging");
        builder.ApplyConfigurationsFromAssembly(typeof(MessagingContext).Assembly);
        builder.ToSnakeCaseTables();
    }
}

Schema : messaging, convention snake_case pour les tables et colonnes.

Enregistrement du module

extension(WebApplicationBuilder builder)
{
    public WebApplicationBuilder AddInfrastructure()
    {
        builder.AddCustomDbContext<MessagingContext>("Messaging");
        builder.Services.AddDatabaseMigration<MessagingContext>(2);
        builder.Services.AddMediatR(cfg =>
            cfg.RegisterServicesFromAssemblies(typeof(MessagingRoot).Assembly));
        builder.Services.AddTransient(typeof(IPipelineBehavior<,>),
            typeof(MessagingPersistenceBehavior<,>));
        // SmtpOptions, IEmailSender, ISmsSender, IPushSender, ITemplateRenderer
        // SmtpHealthCheck
    }
}

Developpement local

En developpement avec Aspire, Mailpit est utilise comme serveur SMTP local :

  • SMTP sur le port 1025
  • Interface web de visualisation sur le port 8025
  • Configuration injectee automatiquement via les variables d'environnement Aspire