Back to "Час із сервісу подписки на newsletter 2 - Переробка сервісів МSK2 і маленьке полум 'яття МSK3"

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 - Переробка сервісів МSK2 і маленьке полум 'яття МSK3

Monday, 23 September 2024

Введение

В частина 1 цієї серії Я показала вам, як я створила нову сторінку Abonnування Newsletter. В цій частині яM SK1 розповім про те, як реструктуризувала рішення, щоб дозволити ділитися послугами та моделями між проектом Веб-сайту (Mostlylucid) і проектом Служби Планувальника МSK4MostslylucideMNK5SchedulerServiceMSC6

Проекти

Спочатку у мене був лише один монолітний проект, який містив весь код для веб-сайту. Це пристойний підхід для менших застосувань , він дає вам просту навігацію МSK3 побудувати та розмістити рішення M SK4 усі з яких є обґрунтованими consideraціями . Однак, коли ви масштабуєте рішення, ви' захочете почати розділяти ваш проект, щоб дозволити ізоляцію проблем і спрощувати тестування MSC7 навігування MSSK8 великий проект з великою кількістю бітів може бути складним для навігуї MСК9 Крім того, для мене це має сенс: я можу розмістити це як окремий контейнер для докерів, що дозволяє мені оновлювати вебсайт, не спричиняючи запуску програми.

Для цього я логічно склав ці проблеми в групи: 5 проекти . Це загальноприйнятий підхід у ASP

Тепер я можу додати тестові проекти для кожного з них і протестувати їх в ізоляції. Це велика перевага, тому що це дозволяє мені протестувати послуги без потреби запускати всю програмуM SK1

Найяскравіший

Цей проект - ASP.NET Core | МСК1 | веб-проект | , | він зберігає всі контролери |& | Просмотри, щоб відображати сторінки для користувача

Найяскравіший.DbКонтекст

Це є моїм основним контекстним визначенням для взаємодії з базою даних. Включно з ядром EF DbContext.

Найяскравіші.Сервиси

Це головна класна бібліотека, яка має всі послуги, які взаємодіють з базою даних. / Файлі відмітки / Услуги електронної пошти і т.д.

Найяскравіший.Shared

Це класний проект бібліотеки, який містить всі спільні моделі, які використовуються як Mostlylucid, так і Mostly Lucid.SchedulerService проекти.

Найяскравіший.Сервис планувальника

Це веб-приложение, яке контролює службу Hangfire, яка виконує заплановані завдання, а також кінцеві точки, які відповідають за відправлення електронних повідомлень.

Структура цього показана нижче. Ви бачите, що SchedulerService та головна Mostlylucid користуються лише класом 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.

NOTA: Для того, щоб уникнути умов гонки при запуску DbContext M SK2 особливо, якщо ви виконуєте міграції ) зробити це просто тут Я тільки-но додав залежність в файлі Docker Compose для того, аби переконатися, що головний Mostlylucid проект працює перед запуском мого Сервісу Скидальників . Це додається до визначення мого Serвісу Зкидаліків у докері MSC5 файла скидальних M SK6

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

Ви також можете зробити це іншими способами, наприклад, в минулому під час запуску декількох instanцій тієї ж програми, яку я використав Рідіс встановити значок замика, який перевіряє кожен экземпляр перед запуском міграції. / інші завдання, які гарантують, що один экземпляр може працювати одночасно.

Звичайно, Redis також має багато інших застосувань; особливо з ASP.NET M SK2с новими IDistributedCache інтерфейс, який дозволяє використовувати Redis як постачальникキャッシュу, що базується на значку.

Установлення вогню

Я вирішила використати Hangfire, щоб керувати моєю схемою, тому що він має удобну інтеграцію з ASP.NET Core і є легким для використання

Для цього в проекті SchedulerService я додав następujące пакети NuGet.

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

Це означає, що я можу використовувати Postgres як мій магазин для запланованих завдань.

Тепер у мене є налаштування Hangfire, я можу почати додавати свої заплановані роботи.

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 що є прапорцем, який визначає, як часто посилається newsletter. Я користуюсь Cron клас для набору частоти завдання.

Оскільки у мене є вибір на день для щомісяця та щотижневого abonamentу, я встановлю час на 17:00 (5pmM SK2 тому що це хороший час для відправлення повідомлень МSK3 закінчити день у Великобританії і розпочати в США МSK4 Я також маю роботу, яка працює щогодини, щоб надсилати бюллетень тим, хто підписувався на кожен лист.

Це потім викликає SendNewsletter метод в NewsletterSendingService клас. Який яM SK1 розповім детальніше пізніше

Дтоси та картографія

Я користуюсь ДПМ для передачі даних між шарами моєї додатки. Це НЕ додає складності, тому що мені потрібно відображати ( часто двічі МSK2 Ентіти до Dtos, а потім Dtos до ViewModels МSK3 і назад M SK4 Однак, Я вважаю, що це відокремлення занепокоєння варте того, адже воно дозволяє мені змінити базову структуру даних, не вплинувши на інтерфейс

Ви можете використати такі підходи, як AutoMapper / Mapster і т.д. для цього мапування . Використовуючи ці підходи також можете мати значні переваги в результатах для МSK2AsNoTrackingM SK3 запитів, тому що ви можете мапувати напряму до Dto і уникнути накладних витрат на зміни відслідковування МSK4 Наприклад, Automapper має Поширення, яка доступна для пошуку метод, який дозволяє відображати напряму на карті Dto у запиті.

Однак в цьому випадку я вирішив лише додати розширення картографа там, де мені потрібно.. Це дозволяє мені мати більше контролю над картографією для кожного рівня. M SK1 але це більше роботи. , особливо, якщо ви маєте багато об 'єктів.

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 який є моїм предметом передачі.
Мета полягає в тому, щоб фронтальні служби не знали нічого про об 'єкт Entity і були відокремлені від базової структури даних

В цих сервісах верхнього рівня я маю код, щоб відобразити ці 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, можна уникнути цього коду котла ( і зменшити помилки ), але я вважаю, що цей підхід працює добре для мене в цьому випадку

Тепер у мене є всі ці моделі, я можу почати додавати методи контроллера, щоб їх використовувати. I' Я покрию цей поток у наступній частині .

На завершення

Тепер у нас є структура, яка дає нам змогу почати надавати підписки на newsletter нашим користувачам. Це рефакторування було трохи боляче, але це - правда для більшості проектів. Наступні пости охоплять решту цього МСК0 , включно з подальшим розвитком мого використання [FluentMail] надіслати електронні листи та домашню службу...и, можливо, трохи більше Hangfire.

logo

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