En Newsletter-abonnementstjänsterna- -2, -, Serviceuppbyggnader, ( och lite Hangfire , ) (Svenska (Swedish))

En Newsletter-abonnementstjänsterna- -2, -, Serviceuppbyggnader, ( och lite Hangfire , )

Monday, 23 September 2024

//

7 minute read

Inleiding

I del 1 av denna serie Jag visade er hur jag skapade en ny Newsletter-abonnementssida.

Projekten

I början hade jag bara ett enda projekt, ett monolitiskt projekt som innehöll all koden för hemsidan.. This is a decent approach for smaller applications, , It gives you a simple to navigate,,, build and deploy solution, ;, all of which are reasonable considerations, M SK5 However as you scale up a solution you ', will you start splitting your project out to allow isolation of concerns and to allow for easier testing, MSC7, navigation. Dessutom är det logiskt att dela ut planens service för mig eftersom jag kan använda den som en separat dockercontainer som låter mig uppdatera webbsidan utan att tvinga planens att starta om igen.

För att göra detta så grupperade jag oroen logiskt i 5 projekt . Det här är en gemensam metod inom ASP

Jag kan nu lägga till testprojekt för var och en av dessa och testa dem isolerat.

De mest uppenbara

Det här projektet är ett ASP-projekt

Det mest tydliga.DbKontext

Det här håller min huvudsakliga kontextdefinition som används för att interagera med databasen. Inklusive EF Core DbKontext

De mest tydliga.tjänster

Det här är huvudklassens biblioteksprojekt som består av alla tjänster som interagerar med databasen / Markdown-filer / E-posttjänster och så vidare

Mostlylucid.Shared

Det här är ett klassbiblioteksprojekt som består av alla de delaktiga modellerna som används av både Mostlylucid och Mostly Lucid.

Mostlylucid.SchedulerService

Det här är en webapplikation som kontrollerar Hangfire-tjänsterna som driver avsedda uppgifter och även slutpunkterna för att hantera att skicka e-post

Strukturen av detta visas härunder. Ni kan se att SchedulerService och huvuden Mostlylucid bara använder DbContext-klassen för inledande konfiguration.

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]

Som vanligt använder jag en förlängningsmetod som ingångspunkt för att sätta upp databasen.

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

Förutom att kalla den här metoden har inget toppnivåprojekt några beroende av DbContext-projektet.

NOTA: Det är viktigt att avoidera ett rasstillstånd när man initialiserar DbContext ( speciellt om du kör migrationerM SK3 för att göra det här helt enkelt. Jag lade bara till en beroende i min Docker Compose file för att se till att det mesta Mostlylucid-projektet är upp och igång innan jag startar min SchedulerService.

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

Man kan också åstadkomma detta på andra sätt, till exempel i det förflutna när man kör flera instanser av samma app som jag Redis att sätta en lockflag som varje instans kontrollerar innan migrationer körs / andra uppgifter som garanterar att endast en instans kan fungera vid ett tillfälle .

Redis har förstås också många andra användningsområden, särskilt med ASP, ., NET och 8'. IDistributedCache gränssnitt som tillåter dig att använda Redis som ett kass-tillhandahållare med tagbaserad utgång

Hangfire-uppsättning

Jag valde att använda Hangfire för att sköta min avvägning eftersom det har en bekväm integration med ASP.NET Core och är enkelt att använda

För detta i SchedulerService-projektet lade jag in följande NuGet-paket.

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

Det betyder att jag kan använda Postgres som min affär för avsedda uppgifter.

Nu har jag Hangfire seetup och kan börja lägga till mina avsedda jobb

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

Här ser ni att jag har ett jobb för varje SubscriptionType som är en flagga som bestämmer hur ofta newslettern skickas Cron klass för att sätta Frequensen på jobbet.

Eftersom jag har möjligheten att använda dag för både Monthly och Weekly subscriptioner så kan jag sätta tid till 17:00 (5pm) eftersom det är en bra tid att skicka newslettern | ( | slutet av dagen i Storbritannien och börja i USA Jag har också ett jobb som pågår varje timme för att skicka newslettern till de som harbonnerat på varje skrift.

Detta leder till SendNewsletter metod i NewsletterSendingService klass. Vilket jag ska gå in i mer detaljer i en senare artikel

Dtos och kartläggning

Jag använder DTOs att bära data mellan lagerna i min application. Det här gör det mer komplext eftersom jag behöver kartlägga (often två gångerM SK2 enheterna till Dtos och sedan Dtos till ViewModel (och bakåtMska4 HursomhelstM Ska5 jag tycker att det är värt det att göra, eftersom det tillåter mig att ändra underliggande datastrukturen utan att påverka gränssnittet

Du kan använda tillvägagångssätt som AutoMapper / Mapster etc för att göra den här kartläggningen . Att använda dessa kan också ha signifikanta prestationsfördelen för | . | AsNoTracking | МSK3 | sökningar eftersom du kan kartlägga direkt till Dto och undvika överskott av spårningförändringar ♫ . | Till exempel har Automapper en Lyssliga utlängningar metod som tillåter dig att kartlägga direkt till Dto i queryn.

Men i det här fallet bestämde jag mig för att bara lägga till mapperextensioner där jag behövde.

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

Här kan ni se att jag kartlägger BlogPostEntity till min huvudstation BlogPostDto som är min överförelseobjekt.
Målet är att front-end-tjänsterna inte vet något om enhetens objekt och är abstrakerade från underliggande datastrukturen.

I dessa högt uppsatta tjänster har jag då koden för att kartlägga dessa Dtos till ViewModel som sedan används i Controllern.

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

Återigen, med hjälp av ett Mapper-verktyg kan man undvika den här boilerplatta-koden ( och minska fel ) men jag tycker att denna metod fungerar bra för mig i det här fallet

Nu har jag alla dessa modeller uppbyggt kan jag börja lägga till kontrollmetoder för att använda dem I' Jag kommer att täcka den här strömmen i nästa del

I slutsats

Vi har nu strukturen som tillåter oss att börja erbjuda newsletter ګډون till våra kunder. Den här omformningen var lite smärtsam men den är en livsvikt för de flesta projekt. Framstående postituser kommer att täcka resten av detta ",", inklusive en uppföljning av mitt användande [FluentMail] att skicka e-mail och den Hosted Service...och kanske lite mer Hangfire.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.