Back to "Ein Newsletter Subscription Service Teil 2 - Refactoring der Dienste (und ein wenig 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

Ein Newsletter Subscription Service Teil 2 - Refactoring der Dienste (und ein wenig Hangfire)

Monday, 23 September 2024

Einleitung

Im Teil 1 dieser Serie Ich habe Ihnen gezeigt, wie ich eine neue Newsletter-Abonnement-Seite erstellt habe. In diesem Teil werde ich behandeln, wie ich die Lösung umstrukturiert, um den Austausch von Diensten und Modellen zwischen dem Website-Projekt (Mostlylucid) und dem Scheduler-Service-Projekt (Mostlylucid.SchedulerService) zu ermöglichen.

Die Projekte

Ursprünglich hatte ich nur ein einziges, monolithisches Projekt, das den ganzen Code für die Website enthielt. Dies ist ein anständiger Ansatz für kleinere Anwendungen, es gibt Ihnen eine einfache zu navigieren, bauen und implementieren Lösung; alle von denen sind vernünftige Überlegungen. Während Sie jedoch eine Lösung skalieren, möchten Sie beginnen, Ihr Projekt aufzuspalten, um die Isolation von Bedenken zu ermöglichen und für einfacheres Testen zu ermöglichen, Navigation (großes Projekt mit vielen Bits kann schwierig sein zu navigieren). Zusätzlich macht die Aufteilung des Scheduler-Dienstes Sinn für mich, da ich dies als separaten Docker-Container bereitstellen kann, der es mir erlaubt, die Website zu aktualisieren, ohne dass der Scheduler neu gestartet wird.

Um dies zu tun, gruppierte ich die Bedenken logischerweise in 5 Projekte. Dies ist ein gemeinsamer Ansatz in ASP.NET-Anwendungen.

Ich kann nun Testprojekte für jedes dieser Projekte hinzufügen und sie isoliert testen. Dies ist ein großer Vorteil, da es mir erlaubt, die Dienste zu testen, ohne die gesamte Anwendung zu drehen.

Meistlyluzid

Dieses Projekt ist ein ASP.NET Core (8) Web-Projekt, es enthält alle Controller & Views, um Seiten für den Benutzer anzuzeigen.

Meistlucid.DbContext

Dies hält meine wichtigste Kontextdefinition, die verwendet wird, um mit der Datenbank zu interagieren. Einschließlich des EF Core DbContext.

Meistlucid.Dienstleistungen

Dies ist das Hauptklassen-Bibliotheksprojekt, das alle Dienste enthält, die mit der Datenbank interagieren / Markdown-Dateien / E-Mail-Dienste usw.

Meistlilucid.Teilt

Dies ist ein Klassenbibliotheksprojekt, das alle gemeinsam genutzten Modelle enthält, die sowohl von Mostlylucid als auch von Mostlylucid.SchedulerService-Projekten verwendet werden.

Meistlucid.SchedulerService

Dies ist eine Web-Anwendung, die den Hangfire-Dienst steuert, der die geplanten Aufgaben sowie die Endpunkte zum Senden von E-Mails ausführt.

Die Struktur davon ist unten dargestellt. Sie können sehen, dass SchedulerService und das Main Mostlylucid nur die DbContext-Klasse für das erste Setup verwenden.

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]

Wie üblich verwende ich als Eintragspunkt eine Erweiterungsmethode für die Einrichtung der Datenbank.

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

Neben dem Aufruf dieser Methode hat kein Top-Level-Projekt irgendwelche Abhängigkeiten vom DbContext-Projekt.

HINWEIS: Es ist wichtig, bei der Initialisierung von DbContext (besonders wenn Sie Migrationen ausführen) eine Race Condition zu vermeiden, um dies einfach hier zu tun.Ich habe nur eine Abhängigkeit in meiner Docker Compose-Datei hinzugefügt, um sicherzustellen, dass das Hauptprojekt Mostlylucid läuft, bevor mein ShcedulerService gestartet wird. Dies wird zur Servicedefinition für meinen SchedulerService in der docker-compose-Datei hinzugefügt.

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

Dies können Sie auch auf andere Weise erreichen, zum Beispiel in der Vergangenheit, wenn ich mehrere Instanzen der gleichen App ausgeführt habe, die ich Redis verwendet habe, um ein Lock Flag zu setzen, das jede Instanz überprüft, bevor Migrationen / andere Aufgaben ausgeführt werden, die nur eine Instanz gleichzeitig ausführen sollte.

Hangfire-Einrichtung

Ich entschied mich für Hangfire, um meine Terminplanung zu handhaben, da es eine bequeme Integration mit ASP.NET Core hat und einfach zu bedienen ist.

Dazu habe ich im SchedulerService-Projekt die folgenden NuGet-Pakete hinzugefügt.

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

Das bedeutet, dass ich Postgres als Store für die geplanten Aufgaben verwenden kann.

Jetzt habe ich Hangfire seatup Ich kann anfangen, meine geplanten Jobs hinzuzufügen.

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 sehen Sie, ich habe einen Job für jeden SubscriptionType welches eine Flagge ist, die bestimmt, wie oft der Newsletter gesendet wird. Ich benutze die Cron Klasse, um die Häufigkeit des Auftrages einzustellen.

Da ich die Option für den Tag sowohl für monatliche als auch für wöchentliche Abonnements habe, stelle ich die Zeit auf 17:00 (5pm) fest, da dies eine gute Zeit ist, Newsletter zu versenden (Ende des Tages in Großbritannien und in den USA zu starten). Ich habe auch einen Job, der jede Stunde läuft, um den Newsletter an diejenigen zu senden, die jeden Beitrag abonniert haben.

Dies führt dann zu einer SendNewsletter Verfahren in der NewsletterSendingService Unterricht. Was ich in einem späteren Post ins Detail gehen werde.

Dtos und Mapping

Ich verwende DTOs Daten zwischen den Schichten meiner Anwendung zu tragen. Diese DOES fügen Komplexität hinzu, da ich (oft zweimal) die Entities auf Dtos und dann die Dtos auf ViewModels (und zurück) abbilden muss. Allerdings finde ich diese Trennung von Bedenken wert zu sein, da es mir erlaubt, die zugrunde liegende Datenstruktur zu ändern, ohne die Benutzeroberfläche zu beeinflussen.

Sie können Ansätze wie AutoMapper / Mapster etc. verwenden, um dieses Mapping durchzuführen. Diese können auch erhebliche Leistungsvorteile für.AsNoTracking()-Abfragen haben, da Sie direkt auf das Dto mappen und den Overhead von Tracking-Änderungen vermeiden können. Zum Beispiel hat AutoMapper eine IQueryable Erweiterung Methode, die es Ihnen ermöglicht, in der Abfrage direkt dem Dto zuzuordnen.

Jedoch in diesem Fall entschied ich mich, nur Mapper-Erweiterungen hinzuzufügen, wo ich brauchte. Dies erlaubt mir, mehr Kontrolle über das Mapping für jede Ebene zu haben (aber ist mehr Arbeit, vor allem, wenn Sie eine Menge von Entitäten haben).

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 kann man sehen, dass ich die BlogPostEntity zu meinem Haupt BlogPostDto Das ist mein Übertragungsobjekt.
Das Ziel ist, dass die Frontend-Dienste nichts über das Entity-Objekt wissen und von der zugrunde liegenden Datenstruktur "abgezogen" werden.

In diesen Top-Level-Diensten habe ich dann Code, um diese Dtos auf ViewModels zu mappen, die dann in den Controllern verwendet werden.

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

Auch hier kann die Verwendung eines Mapper-Tools diesen Boilerplate-Code vermeiden (und Fehler reduzieren), aber ich finde, dieser Ansatz funktioniert gut für mich in diesem Fall.

Jetzt habe ich alle diese Modelle eingerichtet Ich kann mit dem Hinzufügen von Controller-Methoden beginnen, um sie zu verwenden. Ich decke diesen Fluss im nächsten Teil.

Schlussfolgerung

Wir haben jetzt die Struktur, die es uns ermöglicht, mit dem Anbieten von Newsletter-Abonnements für unsere Nutzer zu beginnen. Diese Refactoring war etwas schmerzhaft, aber es ist eine Tatsache des Lebens für die meisten Projekte. Starten Sie einfach und fügen Sie architektonische Komplikation, wenn erforderlich. Zukünftige Stellen werden den Rest dieses Themas abdecken, einschließlich eines Follow-ups zu meiner Nutzung von [FluentMail] um die E-Mails und den Hosted Service zu senden...und vielleicht ein wenig mehr Hangfire.

logo

©2024 Scott Galloway