Back to "Ένα Newsletter Υπηρεσία Συνδρομής Μέρος 2 - Αναπαράσταση των Υπηρεσιών (και λίγο 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

Ένα Newsletter Υπηρεσία Συνδρομής Μέρος 2 - Αναπαράσταση των Υπηρεσιών (και λίγο Hangfire)

Monday, 23 September 2024

Εισαγωγή

Το Μέρος 1 της παρούσας σειράς Σας έδειξα πώς δημιούργησα μια νέα σελίδα συνδρομής Newsletter. Σε αυτό το μέρος θα καλύψω τον τρόπο με τον οποίο αναδιοργάνωσα τη λύση για να επιτρέψω την ανταλλαγή των Υπηρεσιών και Μοντέλων μεταξύ του έργου της Ιστοσελίδας (Περισσότερο διαυγής) και του έργου της Υπηρεσίας Προγραμματιστών (Περισσότερο διαυγής.SchedulerService).

Τα Προγράμματα

Αρχικά είχα μόνο ένα μονολιθικό έργο που περιείχε όλο τον κώδικα για την ιστοσελίδα. Αυτή είναι μια αξιοπρεπής προσέγγιση για μικρότερες εφαρμογές, σας δίνει μια απλή πλοήγηση, κατασκευή και ανάπτυξη λύσης. Όλα αυτά είναι λογικές σκέψεις. Ωστόσο, καθώς κλιμακώνετε μια λύση θα θέλετε να αρχίσετε να διαχωρίζετε το πρόγραμμά σας για να επιτρέψετε την απομόνωση των ανησυχιών και να επιτρέψετε την ευκολότερη δοκιμή, πλοήγηση (μεγάλο έργο με πολλά κομμάτια μπορεί να είναι δύσκολο να πλοηγηθείτε). Επιπλέον, ο διαχωρισμός της υπηρεσίας προγραμματισμού έχει νόημα για μένα καθώς μπορώ να αναπτύξω αυτό ως ένα ξεχωριστό δοχείο docker που μου επιτρέπει να ενημερώσω την ιστοσελίδα χωρίς να προκαλέσω την επανεκκίνηση του προγραμματιστή.

Για να το κάνω αυτό συγκέντρωσα τις ανησυχίες λογικά σε 5 σχέδια. Πρόκειται για μια κοινή προσέγγιση στις εφαρμογές ASP.NET.

Κυρίως διαυγής

Αυτό το έργο είναι ένα web project ASP.NET Core (8), διαθέτει όλα τα Controllers & Views για να εμφανίσει τις σελίδες στο χρήστη.

Κυρίως διαυγής.DbContext

Αυτό κρατά τον κύριο ορισμό του πλαισίου που χρησιμοποιείται για να αλληλεπιδράσει με τη βάση δεδομένων. Συμπεριλαμβανομένου του DbContext πυρήνα EF.

Κυρίως διαυγής.Υπηρεσίες

Αυτό είναι το κύριο έργο βιβλιοθήκης κατηγορίας που κατέχει όλες τις υπηρεσίες που αλληλεπιδρούν με τη βάση δεδομένων / αρχεία Markdown / υπηρεσίες ηλεκτρονικού ταχυδρομείου κ.λπ.

Κυρίως διαυγής.

Αυτό είναι ένα έργο βιβλιοθήκης κατηγορίας που κατέχει όλα τα κοινά μοντέλα που χρησιμοποιούνται τόσο από τα πιο διαυγή και πιο διαυγή.SchedulerService έργα.

Κυρίως διαυγής.SchedulerService

Αυτή είναι μια εφαρμογή Web που ελέγχει την υπηρεσία Hangfire που εκτελεί τις προγραμματισμένες εργασίες καθώς και τα τελικά σημεία για να χειριστεί την αποστολή μηνυμάτων ηλεκτρονικού ταχυδρομείου.

Η δομή αυτού φαίνεται παρακάτω. Μπορείτε να δείτε ότι η Υπηρεσία Προγραμματιστών και η κύρια Κυρίως διαυγής χρησιμοποιούν μόνο την κατηγορία DbContext για την αρχική εγκατάσταση.

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]

Ως συνήθως χρησιμοποιώ μια μέθοδο επέκτασης ως σημείο εισόδου για τη δημιουργία της βάσης δεδομένων.

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

Εκτός από το να καλέσετε αυτή τη μέθοδο, κανένα από τα κορυφαία έργα δεν έχει καμία εξάρτηση από το έργο DbContext.

ΣΗΜΕΙΩΣΗ: Είναι σημαντικό να αποφευχθεί μια κατάσταση αγώνα κατά την αρχικοποίηση DbContext (ειδικά αν τρέχετε μεταναστεύσεις) για να το κάνετε αυτό απλά εδώ πρόσθεσα μια εξάρτηση στο Docker Compose αρχείο μου για να διασφαλιστεί ότι το κύριο Κυρίως διαυγή έργο είναι επάνω και λειτουργεί πριν από την έναρξη της υπηρεσίας Shceduler μου. Αυτό προστίθεται στον ορισμό της υπηρεσίας για την υπηρεσία προγραμματισμού μου στο αρχείο Docker-compose.

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

Μπορείτε επίσης να το πετύχετε αυτό με άλλους τρόπους, για παράδειγμα στο παρελθόν, όταν εκτελούσα πολλαπλές περιπτώσεις της ίδιας εφαρμογής Έχω χρησιμοποιήσει Redis για να θέσει μια σημαία κλειδαριά που κάθε περίπτωση ελέγχει πριν από την εκτέλεση των μεταναστευτικών εργασιών / άλλες εργασίες μόνο μία περίπτωση θα πρέπει να τρέχει σε μια στιγμή.

Ρύθμιση πυρός κρεμαστής

Επέλεξα να χρησιμοποιήσω Hangfire για να χειριστεί τον προγραμματισμό μου, καθώς έχει βολική ενσωμάτωση με ASP.NET Core και είναι εύκολο στη χρήση.

Για αυτό στο πρόγραμμα SchedulerService πρόσθεσα τα ακόλουθα πακέτα NuGet.

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

Αυτό σημαίνει ότι μπορώ να χρησιμοποιήσω το Postgres ως κατάστημα μου για τις προγραμματισμένες εργασίες.

Τώρα έχω Hangfire seetup μπορώ να αρχίσω να προσθέτω τις προγραμματισμένες δουλειές μου.

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

Εδώ βλέπετε έχω μια δουλειά για κάθε SubscriptionType η οποία είναι μια σημαία που καθορίζει πόσο συχνά αποστέλλεται το ενημερωτικό δελτίο. Χρησιμοποιώ το Cron Τάξη για να ρυθμίσετε τη συχνότητα της εργασίας.

Καθώς έχω την επιλογή για την ημέρα τόσο για τις μηνιαίες όσο και εβδομαδιαίες συνδρομές ορίζω την ώρα στις 17:00 (5μμ) καθώς αυτή είναι μια καλή στιγμή για να στείλετε ενημερωτικά δελτία (τέλος της ημέρας στο Ηνωμένο Βασίλειο και να ξεκινήσετε στις ΗΠΑ). Έχω επίσης μια δουλειά που τρέχει κάθε ώρα για να στείλω το ενημερωτικό δελτίο σε όσους έχουν εγγραφεί σε κάθε θέση.

Αυτό στη συνέχεια καλεί σε ένα SendNewsletter μέθοδος στην NewsletterSendingService Μαθήματα. Το οποίο θα μπω σε περισσότερες λεπτομέρειες αργότερα.

Dtos και Mapping

Χρησιμοποιώ DTOsCity name (optional, probably does not need a translation) να μεταφέρω δεδομένα μεταξύ των στρωμάτων της αίτησής μου. Αυτό προσθέτει πολυπλοκότητα καθώς πρέπει να χαρτογραφώ (συχνά δύο φορές) τους Φορείς στο Dtos και στη συνέχεια το Dtos στο ViewModels (και πίσω). Ωστόσο, θεωρώ ότι αυτός ο διαχωρισμός των ανησυχιών αξίζει τον κόπο, καθώς μου επιτρέπει να αλλάξω την υποκείμενη δομή των δεδομένων χωρίς να επηρεάσω την UI.

Μπορείτε να χρησιμοποιήσετε προσεγγίσεις όπως AutoMapper / Mapster κ.λπ. για να κάνετε αυτή τη χαρτογράφηση. Χρησιμοποιώντας αυτά μπορεί επίσης να έχουν σημαντικά πλεονεκτήματα απόδοσης για.AsNoTracking() ερωτήματα, όπως μπορείτε να χαρτογραφήσετε απευθείας στο Dto και να αποφύγετε το πάνω μέρος των αλλαγών παρακολούθησης. Για παράδειγμα AutoMapper έχει ένα IQueryable extension μέθοδος που σας επιτρέπει να χαρτογραφήσετε απευθείας στο Dto στο ερώτημα.

Ωστόσο, σε αυτή την περίπτωση αποφάσισα απλά να προσθέσω επεκτάσεις Mapper όπου χρειαζόμουν. Αυτό μου επιτρέπει να έχω περισσότερο έλεγχο στη χαρτογράφηση για κάθε επίπεδο (αλλά είναι περισσότερη δουλειά, ειδικά αν έχετε πολλές οντότητες).

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

Εδώ μπορείτε να δείτε ότι χαρτογράφω το BlogPostEntity προς το κύριο μου BlogPostDto Το οποίο είναι το αντικείμενο μεταφοράς μου.
Ο στόχος είναι ότι οι υπηρεσίες front end δεν γνωρίζουν τίποτα για το αντικείμενο της οντότητας και είναι "αποσπασμένοι" από την υποκείμενη δομή δεδομένων.

Σε αυτές τις κορυφαίες υπηρεσίες έχω στη συνέχεια τον κωδικό για να χαρτογραφήσει αυτά τα Dtos να ViewModels που στη συνέχεια χρησιμοποιούνται στα χειριστήρια.

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

Και πάλι, χρησιμοποιώντας ένα εργαλείο Mapper μπορεί να αποφύγει αυτόν τον κώδικα λεβήτων (και να μειώσει τα λάθη) αλλά βρίσκω αυτή η προσέγγιση λειτουργεί καλά για μένα σε αυτή την περίπτωση.

Τώρα έχω ετοιμάσει όλα αυτά τα μοντέλα μπορώ να αρχίσω να προσθέτω μεθόδους ελέγχου για να τα χρησιμοποιήσω. Θα καλύψω αυτή τη ροή στο επόμενο μέρος.

Συμπέρασμα

Τώρα έχουμε τη δομή που μας επιτρέπει να αρχίσουμε να προσφέρουμε συνδρομές σε ενημερωτικά δελτία στους χρήστες μας. Αυτός ο αναπροσανατολισμός ήταν ελαφρώς επώδυνος, αλλά είναι γεγονός για τα περισσότερα σχέδια. Ξεκινήστε απλά και προσθέστε αρχιτεκτονικές επιπλοκές όταν απαιτείται. Οι μελλοντικές θέσεις θα καλύψουν τα υπόλοιπα, συμπεριλαμβανομένης της παρακολούθησης σχετικά με τη χρήση μου [FluentMailCity name (optional, probably does not need a translation)] να στείλω τα email και την υπηρεσία φιλοξενίας... και ίσως λίγο περισσότερο Hangfire.

logo

©2024 Scott Galloway