Back to "Een nieuwsbrief Abonnement Service Deel 2 - Refactoring van de Diensten (en een beetje 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

Een nieuwsbrief Abonnement Service Deel 2 - Refactoring van de Diensten (en een beetje Hangfire)

Monday, 23 September 2024

Inleiding

In deel 1 van deze serie Ik heb je laten zien hoe ik een nieuwe nieuwsbrief abonnement pagina heb gemaakt. In dit deel zal ik ingaan op de manier waarop ik de oplossing heb geherstructureerd om het delen van Diensten en Modellen mogelijk te maken tussen het Websiteproject (Meestallucid) en het Planner Service project (Meestallucid.SchedulerService).

De projecten

Oorspronkelijk had ik maar één monolithisch project dat alle code voor de website bevatte. Dit is een fatsoenlijke aanpak voor kleinere toepassingen, het geeft u een eenvoudig te navigeren, bouwen en implementeren oplossing; die zijn allemaal redelijke overwegingen. Maar als je een oplossing opschaalt, wil je je project opsplitsen om de problemen te isoleren en om het testen te vergemakkelijken, navigatie (groot project met veel bits kan lastig zijn om te navigeren). Bovendien het splitsen van de scheduler service is zinvol voor mij als ik dit kan implementeren als een aparte docker container waardoor ik om de website te updaten zonder dat de scheduler te herstarten.

Om dit te doen heb ik de zorgen logisch in 5 projecten gegroepeerd. Dit is een gemeenschappelijke aanpak in ASP.NET applicaties.

Ik kan nu proefprojecten voor elk van deze projecten toevoegen en ze afzonderlijk testen. Dit is een groot voordeel omdat het me in staat stelt om de diensten te testen zonder de hele toepassing te hoeven draaien.

Meestal lucid

Dit project is een ASP.NET Core (8) webproject, het bevat alle Controllers & Views om pagina's weer te geven aan de gebruiker.

Meestallucid.DbContext

Dit houdt mijn belangrijkste context definitie gebruikt om te interageren met de database. Inclusief de EF Core DbContext.

Meestal lucid.Diensten

Dit is het hoofdklasse bibliotheek project dat alle diensten die interactie met de database / Markdown bestanden / E-mail diensten etc.

Meestal lucid. Gedeeld

Dit is een klasse bibliotheek project dat alle gedeelde modellen die worden gebruikt door zowel de Mostlylucid en Mostlylucid.SchedulerService projecten bevat.

Meestal lucid.SchedulerService

Dit is een webtoepassing die de Hangfire-service bestuurt die zowel de geplande taken als de eindpunten uitvoert om e-mails te versturen.

De structuur hiervan is hieronder weergegeven. Je kunt zien dat SchedulerService en de belangrijkste Meestlucid alleen de DbContext klasse gebruiken voor de initiële setup.

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]

Zoals gebruikelijk gebruik ik een extensie methode als het entryppoint voor het opzetten van de database.

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);
        });
    }
}

Naast het aanroepen van deze methode heeft geen van beide topniveau projecten afhankelijkheden van het DbContext project.

OPMERKING: Het is belangrijk om een racevoorwaarde te vermijden bij het initialiseren van DbContext (vooral als je migraties uitvoert) om dit simpelweg hier te doen Ik heb net een afhankelijkheid toegevoegd in mijn Docker Compose bestand om ervoor te zorgen dat het belangrijkste Mostlylucid project draait voordat ik mijn ShcedulerService start. Dit wordt toegevoegd aan de service definitie voor mijn SchedulerService in het docker-compose bestand.

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

U kunt dit ook op andere manieren bereiken, bijvoorbeeld in het verleden bij het uitvoeren van meerdere instanties van dezelfde app heb ik Redis gebruikt om een vergrendelingsvlag in te stellen die elke instantie controleert voordat migraties / andere taken uitgevoerd worden, maar slechts één instantie per keer.

Hangfire setup

Ik koos ervoor om Hangfire te gebruiken om mijn planning te verwerken omdat het handige integratie met ASP.NET Core heeft en makkelijk te gebruiken is.

Hiervoor heb ik in het PlannerService project de volgende NuGet pakketten toegevoegd.

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

Dit betekent dat ik Postgres kan gebruiken als mijn winkel voor de geplande taken.

Nu heb ik Hangfire seetup Ik kan beginnen met het toevoegen van mijn geplande banen.

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 * * *");
    }
}

Hier zie je dat ik een baan heb voor elk SubscriptionType Dat is een vlag die bepaalt hoe vaak de nieuwsbrief wordt verzonden. Ik gebruik de Cron klasse om de frequentie van de taak in te stellen.

Aangezien ik de optie voor dag voor zowel maandelijkse en wekelijkse abonnementen Ik heb de tijd ingesteld op 17:00 (5pm) omdat dit een goed moment om nieuwsbrieven te verzenden (eind van de dag in het Verenigd Koninkrijk en beginnen in de VS). Ik heb ook een baan die elk uur loopt om de nieuwsbrief te sturen naar degenen die zich hebben ingeschreven op elke post.

Dit roept dan op tot een SendNewsletter methode in de NewsletterSendingService Klas. Wat ik in een latere post nader zal toelichten.

Dtos en Mapping

Ik gebruik DTO's om gegevens tussen de lagen van mijn toepassing te dragen. Dit voegt complexiteit toe omdat ik (vaak twee keer) de entiteiten in kaart moet brengen naar Dtos en vervolgens de Dtos naar ViewModels (en terug). Ik vind echter dat deze scheiding van zorgen de moeite waard is omdat het me in staat stelt om de onderliggende gegevensstructuur te veranderen zonder de UI te beïnvloeden.

U kunt benaderingen zoals AutoMapper / Mapster etc gebruiken om dit in kaart te brengen. Het gebruik van deze kan ook aanzienlijke prestaties voordelen voor.AsNoTracking() queries als u direct kunt in kaart brengen naar de Dto en voorkomen dat de overhead van tracking veranderingen. Bijvoorbeeld AutoMapper heeft een Onbetrouwbare uitbreiding methode waarmee u direct naar de Dto in de query kunt in kaart brengen.

Maar in dit geval besloot ik alleen maar om mapper extensies toe te voegen waar ik nodig had. Dit stelt me in staat om meer controle te hebben over de mapping voor elk niveau (maar is meer werk, vooral als je veel entiteiten).

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>()
        };
    }
}

Hier kun je zien dat ik de BlogPostEntity naar mijn hoofd BlogPostDto Dat is mijn transfer object.
Het doel is dat de front end services niets weten over het Entity object en 'abstracted' zijn van de onderliggende data structuur.

In deze top level services heb ik dan code om deze Dtos in kaart te brengen naar ViewModels die vervolgens worden gebruikt in de 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
        };
    }

Nogmaals, met behulp van een Mapper tool kan voorkomen dat deze ketelplaat code (en verminderen fouten) maar ik vind deze aanpak werkt goed voor mij in dit geval.

Nu heb ik al deze modellen opgezet Ik kan beginnen met het toevoegen van controller methoden om ze te gebruiken. Ik bedek deze stroom in het volgende deel.

Conclusie

We hebben nu de structuur waarmee we beginnen met het aanbieden van nieuwsbrief abonnementen aan onze gebruikers. Deze refactoring was enigszins pijnlijk, maar het is een feit van het leven voor de meeste projecten. Start eenvoudig en voeg architectonische complicaties wanneer nodig. Toekomstige posten zullen de rest van dit, met inbegrip van een follow-up van mijn gebruik van [FluentMail] om de e-mails en de Hosted Service te versturen...en misschien een beetje meer Hangfire.

logo

©2024 Scott Galloway