Back to "Un service d'abonnement à la newsletter Partie 2 - Refactoring the Services (et un peu Hangfire)"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Email Newsletter Hangfire

Un service d'abonnement à la newsletter Partie 2 - Refactoring the Services (et un peu Hangfire)

Monday, 23 September 2024

Présentation

Dans Partie 1 de cette série Je vous ai montré comment j'ai créé une nouvelle page d'abonnement à la newsletter. Dans cette partie, je traiterai de la façon dont j'ai restructuré la solution pour permettre le partage des services et des modèles entre le projet Web (Mostlylucid) et le projet Service Scheduler (Mostlylucid.SchedulerService).

Les projets

À l'origine, je n'avais qu'un seul projet monolithique qui contenait tout le code du site Web. Il s'agit d'une approche décente pour les applications plus petites, elle vous donne une solution simple à naviguer, construire et déployer, qui sont toutes des considérations raisonnables. Cependant, à mesure que vous mettez à l'échelle une solution, vous voudrez commencer à diviser votre projet pour permettre l'isolement des préoccupations et pour permettre des tests plus faciles, la navigation (un grand projet avec beaucoup de bits peut être difficile à naviguer). En outre, diviser le service de programmeur est logique pour moi car je peux déployer ceci comme un conteneur de docker séparé me permettant de mettre à jour le site Web sans faire redémarrer le programmeur.

Pour ce faire, j'ai regroupé les préoccupations logiquement en cinq projets. Il s'agit d'une approche commune dans les applications ASP.NET.

Je peux maintenant ajouter des projets de test pour chacun de ces projets et les tester isolément. C'est un grand avantage car il me permet de tester les services sans avoir besoin de faire tourner toute l'application.

Les plus lylucides

Ce projet est un projet web ASP.NET Core (8), il contient tous les Contrôleurs & Vues pour afficher des pages à l'utilisateur.

La plupart du temps, c'est DbContext.

Ceci tient ma principale définition de contexte utilisée pour interagir avec la base de données. Y compris le contenu de base DbContext de l'EF.

Les services les plus lylucides

C'est le principal projet de bibliothèque de classe qui détient tous les services qui interagissent avec la base de données / fichiers Markdown / services d'email etc.

La plupart du temps, c'est-à-dire partagé.

Il s'agit d'un projet de bibliothèque de classe qui contient tous les modèles partagés utilisés par les projets Mostlylucid et Mostlylucid.SchedulerService.

Le plus lylucide.SchedulerService

Il s'agit d'une application Web qui contrôle le service Hangfire qui exécute les tâches programmées ainsi que les paramètres pour gérer l'envoi d'emails.

La structure de ce système est présentée ci-dessous. Vous pouvez voir que SchedulerService et le principal Mostlylucid n'utilisent que la classe DbContext pour la configuration initiale.

graph LR Mostlylucid[Mostlylucid] --> |Intialize| Mostlylucid_DbContext[Mostlylucid.DbContext] Mostlylucid[Mostlylucid] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid[Mostlylucid] --> Mostlylucid_Services[Mostlylucid.Services] Mostlylucid_DbContext[Mostlylucid.DbContext] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid_Services[Mostlylucid.Services] --> Mostlylucid_DbContext[Mostlylucid.DbContext] Mostlylucid_Services[Mostlylucid.Services] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid_SchedulerService[Mostlylucid.SchedulerService] --> Mostlylucid_Shared[Mostlylucid.Shared] Mostlylucid_SchedulerService[Mostlylucid.SchedulerService] --> |Intialize|Mostlylucid_DbContext[Mostlylucid.DbContext] Mostlylucid_SchedulerService[Mostlylucid.SchedulerService] --> Mostlylucid_Services[Mostlylucid.Services]

Comme d'habitude, j'utilise une méthode d'extension comme point d'entrée pour configurer la base de données.

public static class Setup
{
    public static void SetupDatabase(this IServiceCollection services, IConfiguration configuration,
        IWebHostEnvironment env, string applicationName="mostlylucid")
    {
        services.AddDbContext<IMostlylucidDBContext, MostlylucidDbContext>(options =>
        {
            if (env.IsDevelopment())
            {
                options.EnableDetailedErrors(true);
                options.EnableSensitiveDataLogging(true);
            }
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString)
            {
                ApplicationName = applicationName
            };
            options.UseNpgsql(connectionStringBuilder.ConnectionString);
        });
    }
}

Outre l'appel à cette méthode, aucun projet de niveau supérieur n'a de dépendances sur le projet DbContext.

REMARQUE: Il est important d'éviter une condition de course lors de l'initialisation de DbContext (surtout si vous exécutez des migrations) pour le faire simplement ici J'ai juste ajouté une dépendance dans mon fichier Docker Compose pour m'assurer que le principal projet Mostlylucid est en cours d'exécution avant de démarrer mon ShcedulerService. Ceci est ajouté à la définition de service de mon Service Scheduler dans le fichier Docker-compose.

  depends_on:
      - mostlylucid 
  healthcheck:
      test: ["CMD", "curl", "-f", "http://mostlylucid:80/healthy"]
      interval: 30s
      timeout: 10s
      retries: 5

Vous pouvez également accomplir cela d'autres façons, par exemple dans le passé lors de l'exécution de plusieurs instances de la même application que j'ai utilisé Redis pour définir un drapeau de verrouillage que chaque instance vérifie avant d'exécuter des migrations / d'autres tâches qu'une seule instance devrait exécuter à la fois.

Configuration du feu de accrochage

J'ai choisi d'utiliser Hangfire pour gérer mon planning car il a une intégration pratique avec ASP.NET Core et est facile à utiliser.

Pour cela, dans le projet SchedulerService, j'ai ajouté les paquets suivants NuGet.

dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.PostgreSql

Cela signifie que je peux utiliser Postgres comme mon magasin pour les tâches programmées.

Maintenant, j'ai Hangfire seetup je peux commencer à ajouter mes emplois prévus.

public static class JobInitializer
{
    private const string AutoNewsletterJob = "AutoNewsletterJob";
    private const string DailyNewsletterJob = "DailyNewsletterJob";
    private const string WeeklyNewsletterJob = "WeeklyNewsletterJob";
    private const string MonthlyNewsletterJob = "MonthlyNewsletterJob";
    public static void InitializeJobs(this IApplicationBuilder app)
    {
      var scope=  app.ApplicationServices.CreateScope();
        var recurringJobManager = scope.ServiceProvider.GetRequiredService<RecurringJobManager>();
      
        recurringJobManager.AddOrUpdate<NewsletterSendingService>(AutoNewsletterJob,  x =>  x.SendNewsletter(SubscriptionType.EveryPost), Cron.Hourly);
        recurringJobManager.AddOrUpdate<NewsletterSendingService>(DailyNewsletterJob,  x =>  x.SendNewsletter(SubscriptionType.Daily), "0 17 * * *");
        recurringJobManager.AddOrUpdate<NewsletterSendingService>(WeeklyNewsletterJob,  x =>  x.SendNewsletter(SubscriptionType.Weekly), "0 17 * * *");
        recurringJobManager.AddOrUpdate<NewsletterSendingService>(MonthlyNewsletterJob,  x => x.SendNewsletter(SubscriptionType.Monthly), "0 17 * * *");
    }
}

Ici vous voyez que j'ai un travail pour chaque SubscriptionType qui est un drapeau qui détermine la fréquence d'envoi de la lettre d'information. J'utilise les Cron classe pour définir la fréquence de l'emploi.

Comme j'ai l'option de jour pour les abonnements mensuels et hebdomadaires, j'ai fixé l'heure à 17:00 (17:00) car c'est un bon moment pour envoyer des newsletters (fin de journée au Royaume-Uni et commencer aux États-Unis). J'ai également un travail qui fonctionne toutes les heures pour envoyer la lettre d'information à ceux qui ont souscrit à chaque poste.

Cela appelle alors à une SendNewsletter méthode dans le NewsletterSendingService En cours. Ce que j'entrerai plus en détail dans un post plus tard.

Dtos et cartographie

J'utilise DTOs pour transporter des données entre les couches de mon application. Cette DOES ajoute de la complexité car j'ai besoin de mapper (souvent deux fois) les Entités à Dtos, puis les Dtos à ViewModels (et retour). Cependant, je trouve que cette séparation des préoccupations en vaut la peine car elle me permet de changer la structure des données sous-jacentes sans affecter l'assurance-chômage.

Vous pouvez utiliser des approches comme AutoMapper / Mapster etc pour faire cette cartographie. L'utilisation de ceux-ci peut également avoir d'importants avantages de performance pour les requêtes.AsNoTracking() car vous pouvez mapper directement vers le Dto et éviter les frais généraux de suivi des changements. Par exemple AutoMapper a un Extension IQueryable méthode qui vous permet de mapper directement vers le Dto dans la requête.

Cependant, dans ce cas, j'ai décidé juste d'ajouter des extensions de mapper là où j'en avais besoin. Cela me permet d'avoir plus de contrôle sur la cartographie pour chaque niveau (mais c'est plus de travail, surtout si vous avez beaucoup d'entités).

public static class BlogPostEntityMapper
{
   public static BlogPostDto ToDto(this BlogPostEntity entity, string[] languages = null)
    {
        return new BlogPostDto
        {
            Id = entity.Id.ToString(),
            Title = entity.Title,
            Language = entity.LanguageEntity?.Name,
            Markdown = entity.Markdown,
            UpdatedDate = entity.UpdatedDate.DateTime,
            HtmlContent = entity.HtmlContent,
            PlainTextContent = entity.PlainTextContent,
            Slug = entity.Slug,
            WordCount = entity.WordCount,
            PublishedDate = entity.PublishedDate.DateTime,
            Languages = languages ?? Array.Empty<string>()
        };
    }
}

Ici vous pouvez voir que j'ai map le BlogPostEntity à mon principal BlogPostDto qui est mon objet de transfert.
L'objectif est que les services de première ligne ne connaissent rien de l'objet Entité et soient « absents » de la structure de données sous-jacente.

Dans ces services de haut niveau, j'ai ensuite du code pour mapper ces Dtos vers ViewModels qui sont ensuite utilisés dans les Controllers.

    public static PostListModel ToPostListModel(this BlogPostDto dto)
    {
        return new PostListModel
        {
            Id = dto.Id,
            Title = dto.Title,
            Language = dto.Language,
            UpdatedDate = dto.UpdatedDate.DateTime,
            Slug = dto.Slug,
            WordCount = dto.WordCount,
            PublishedDate = dto.PublishedDate,
            Languages = dto.Languages
        };
    }
    
    public static BlogPostViewModel ToViewModel(this BlogPostDto dto)
    {
        return new BlogPostViewModel
        {
            Id = dto.Id,
            Title = dto.Title,
            Language = dto.Language,
            Markdown = dto.Markdown,
            UpdatedDate = dto.UpdatedDate.DateTime,
            HtmlContent = dto.HtmlContent,
            PlainTextContent = dto.PlainTextContent,
            Slug = dto.Slug,
            WordCount = dto.WordCount,
            PublishedDate = dto.PublishedDate,
            Languages = dto.Languages
        };
    }

Encore une fois, l'utilisation d'un outil Mapper peut éviter ce code de plaque de chaudière (et réduire les erreurs) mais je trouve que cette approche fonctionne bien pour moi dans cette instance.

Maintenant, j'ai tous ces modèles mis en place, je peux commencer à ajouter des méthodes de contrôleur pour les utiliser. Je couvrirai ce flux dans la partie suivante.

En conclusion

Nous avons maintenant la structure qui nous permet de commencer à offrir des abonnements à la newsletter à nos utilisateurs. Ce refactoring a été légèrement douloureux mais c'est un fait de la vie pour la plupart des projets. Commencez simplement et ajoutez des complications architecturales au besoin. Les futurs postes couvriront le reste de ceci, y compris un suivi de mon utilisation de [FluentMail] pour envoyer les e-mails et le service hébergé... et peut-être un peu plus de Hangfire.

logo

©2024 Scott Galloway