Back to "Newsletter Subscribe Service Osa 2 - Palveluiden korjaus (ja pieni 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 Subscribe Service Osa 2 - Palveluiden korjaus (ja pieni hangfire)

Monday, 23 September 2024

Johdanto

Sisään tämän sarjan 1. osa Näytin, miten loin uuden uutiskirjeen tilaussivun. Tässä osassa selvitän, miten uudistin ratkaisun, jotta www-sivustoprojektin (Mostlylucid) ja Scheduler Service -projektin (Mostlylucid.SchedulerService) välillä voidaan jakaa palveluita ja malleja.

Hankkeet

Alun perin minulla oli vain yksi monoliittinen projekti, joka sisälsi kaikki sivuston koodit. Tämä on kunnollinen lähestymistapa pienempiin sovelluksiin, se mahdollistaa helpon navigoinnin, rakentamisen ja ratkaisun käyttöönoton. Kaikki nämä ovat järkeviä näkökohtia. Kun kuitenkin laajennat ratkaisua, haluat alkaa pilkkoa projektiasi, jotta voit eristää huolet ja helpottaa testausta, navigointi (iso projekti, jossa on paljon osia, voi olla hankalaa navigoida). Lisäksi aikataulupalvelun jakaminen on minulle järkevää, koska voin ottaa sen käyttöön erillisenä konttina, jonka avulla voin päivittää sivuston ilman, että aikatauluttaja käynnistyy uudelleen.

Tätä varten ryhmittelin huolet loogisesti viiteen hankkeeseen. Tämä on yhteinen lähestymistapa ASP.net-sovelluksissa.

Voin nyt lisätä testiprojektit jokaiselle näistä ja testata niitä eristyksissä. Tämä on iso etu, koska sen avulla voin testata palveluita ilman, että minun tarvitsee pyörittää koko sovellusta.

Enimmäkseen lusidi

Tämä projekti on ASP.NET Core (8) -verkkoprojekti, joka pitää sisällään kaikki ohjaimet ja näkymät näyttääkseen sivut käyttäjälle.

Enimmäkseen lucid.DbContext

Tämä pitää sisällään pääasiallisen kontekstimääritelmäni, jota on käytetty vuorovaikutuksessa tietokannan kanssa. Mukaan lukien EF Core DbContext.

Enimmäkseen lucid.Palvelut

Tämä on pääluokan kirjastoprojekti, jossa on kaikki tietokannan / Markdown-tiedostojen / sähköpostipalveluiden kanssa vuorovaikutuksessa olevat palvelut.

Enimmäkseen lusidi. Jaettu

Tämä on luokkakirjastoprojekti, jossa ovat mukana kaikki yhteiset mallit, joita sekä Moleislucid että Moleislucid käyttävät.SchedulerService-projektit.

Enimmäkseen lucid.SchedulerService

Tämä on verkkosovellus, joka ohjaa Hangfire-palvelua, joka hoitaa suunnitellut tehtävät sekä päätetapahtumat sähköpostien lähettämiseen.

Tämän rakenne esitetään alla. Voit nähdä, että SchedulerService ja suurin Enimmäkseenlucid käyttävät DbContext-luokkaa vain alkuasetukseen.

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]

Tavanomaiseen tapaan käytän laajennusmenetelmää tietokannan perustamisen hakupisteenä.

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

Tämän menetelmän lisäksi kumpikaan huipputason projekti ei ole riippuvainen DbContext-projektista.

HUOMAUTUS: On tärkeää välttää kisakuntoa, kun käynnistät DbContextin (etenkin jos pyörität muuttoliikkeitä) tekemään tämän yksinkertaisesti täällä. Lisäsin Docker Composite -tiedoston riippuvuuteen varmistaakseni, että pääprojekti on käynnissä ennen kuin aloitat ShcedulerServiceni. Tämä on lisätty SchedulerServicen palvelumääritelmään docker-compose-tiedostossa.

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

Voit toteuttaa tämän myös muilla tavoin, esimerkiksi aiemmin, kun suoritan useita kertoja samaa sovellusta, jota olen käyttänyt Redis-sovelluksessa, asettaaksesi lukituslipun, jonka jokainen instanssi tarkistaa ennen muuttoa / muita tehtäviä, joita vain yhden instanssin pitäisi suorittaa kerrallaan.

Hangfire-asetus

Päätin käyttää Hangfireä aikataulujeni hoitamiseen, koska se on kätevästi integroitu ASP.NET Coreen ja sitä on helppo käyttää.

Tätä varten lisäsin SchedulerService-projektiin seuraavat NuGet-paketit.

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

Tämä tarkoittaa, että voin käyttää Postgresiä myymälänäni aikataulun mukaisissa tehtävissä.

Nyt minulla on Hangfire-seurue, jonka avulla voin alkaa lisätä suunniteltuja töitäni.

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

Tässä näet, että minulla on töitä jokaiselle SubscriptionType joka on lippu, joka määrää, kuinka usein tiedote lähetetään. Käytän Cron Luokka määrittää työn tiheyden.

Koska minulla on mahdollisuus päivälle sekä kuukausitilauksiin että Weekly-tilauksiin, määräsin ajan kello 17:00 (klo 17.00), koska nyt on hyvä aika lähettää tiedotteita (päivän päätteeksi Isossa-Britanniassa ja aloittaa Yhdysvalloissa). Minulla on myös työ, joka kulkee tunnin välein, jotta voin lähettää tiedotteen niille, jotka ovat kirjoittaneet jokaisen postin.

Tämä johtaa sitten siihen, että SendNewsletter Metodia in the that in the equipment NewsletterSendingService Luokka. Kerron siitä tarkemmin myöhemmin.

Dto- ja kartoitus

Käytän Ilmoitukset Kantaa dataa sovelluksen kerrosten välillä. Tämä ei lisää monimutkaisuutta, koska minun täytyy kartoittaa (usein kahdesti) Yksiköt Dtosiin ja sitten Dtos to ViewModelsiin (ja takaisin). Olen kuitenkin sitä mieltä, että tämä huolien erottaminen kannattaa, koska sen avulla voin muuttaa taustalla olevaa tietorakennetta vaikuttamatta käyttöliittymään.

Kartoituksessa voi käyttää lähestymistapoja, kuten AutoMapper/Mapster jne. Niiden käytöllä voi olla myös merkittäviä suoritusetuja.AsNoTracking () -kyselyille, joita voit kartoittaa suoraan Dto:lle ja välttää seurantamuutosten kulun. Esimerkiksi AutoMapperilla on IQueryable laajennus menetelmä, jonka avulla voit kartoittaa suoraan Dto:lle kyselyssä.

Tässä tapauksessa päätin kuitenkin vain lisätä karttalaajennuksia sinne, missä tarvitsin. Tämä antaa minulle mahdollisuuden kontrolloida enemmän kunkin tason kartoitusta (mutta on enemmän työtä, varsinkin jos on paljon kokonaisuuksia).

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

Tässä näet, että kartoitan BlogPostEntity Päällimmäiseksi BlogPostDto Se on siirtokohteeni.
Tavoitteena on, että etupään palvelut eivät tiedä Entiteetti-objektista mitään ja että ne "karkotetaan" taustalla olevasta tietorakenteesta.

Näissä huipputason palveluissa minulla on sitten koodi, jonka avulla voin kartoittaa nämä Dto:t ViewModelsiin, joita sitten käytetään ohjaimissa.

    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-työkalun avulla voidaan taas välttää tämä kattilalevykoodi (ja vähentää virheitä), mutta mielestäni tämä lähestymistapa toimii hyvin tässä tapauksessa.

Nyt minulla on kaikki nämä mallit valmiina, voin alkaa lisätä ohjainmenetelmiä niiden käyttöön. Kannan tämän virtauksen seuraavassa osassa.

Johtopäätöksenä

Meillä on nyt rakenne, jonka avulla voimme alkaa tarjota uutiskirjetilaajia käyttäjillemme. Uudistaminen oli hieman tuskallista, mutta se on tosiasia useimmille projekteille. Aloita yksinkertainen ja lisää tarvittaessa arkkitehtoninen monimutkaisuus. Tulevaisuuden virat kattavat loput tästä, mukaan lukien jatkotoimet, jotka koskevat [FluentMail] Lähettää sähköpostit ja Hosted Service...ja ehkä hieman enemmän Hangfire.

logo

©2024 Scott Galloway