Back to "Служба підписки новин Частина 2 - відшкодування служб (і невеличке погасання)"

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

Служба підписки новин Частина 2 - відшкодування служб (і невеличке погасання)

Monday, 23 September 2024

Вступ

Вхід перша частина цієї серії Я показав вам, як я створив нову сторінку передплати на Newletter. У цій частині я розповім про те, як я перебудував рішення, за допомогою якого можна обмінюватися службами і моделями між проектом Веб- сайта (здебільшого за все) і проектом Служба планування (Службовцем планування. SchedulerService).

Проекти

Спочатку у мене був лише один монолітний проект, який містив весь код веб-сайту. Це належний підхід до менших програм, він надає вам можливість просто пересуватися, будувати і розробляти рішення. Все це є розсудливими міркуваннями. Але, оскільки ви збільшуєте розв' язок, вам слід почати роз' єднання вашого проекту, щоб дозволити усамітненню проблем, а також уможливити простіше тестування, навігації (Великий проект з великою кількістю бітів може бути дуже складним для навігації). Крім того, мені було б розумно розділяти роботу планувальника, оскільки я можу розкрити це як окремий контейнер докерів, що дозволяє мені оновити веб-сайт, не спричиняючи планувальника перезапустити.

Щоб зробити це, я згрупував ці турботи логічно в 5 проектах. Це поширений підхід у програмах ASP.NET.

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

У більшості випадків

Цей проект є ядром мережі ASP. NET (8), у ньому містяться всі регулятори і перегляди, призначені для показу сторінок користувачеві.

У більшості випадків locid.DbContext

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

Більшість програм. Послуги

Це основний проект бібліотеки класів, у якому зберігаються всі служби, які взаємодіють з файлами бази даних / Markdown / Служби пошти тощо.

У більшості випадків. Зроблено

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

usiclucid.SchedulerService

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

Будова цього зображення показана нижче. Ви можете бачити, що планувальник (Service) і головний програмний центр використовують лише клас 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, щоб переконатися, що головний проект з великими значеннями працює і виконується перед запуском програми ShederService. Цей пункт додано до визначення служби для мого Планувальника (Service) у файлі docker- compose.

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

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

Налаштування подачі вогню

Я вирішив використати Hangfire, щоб виконати свій розклад, оскільки це зручно об'єднати з ядром ASPNET, і ним легко користуватися.

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

Оскільки у мене є можливість на день для як щомісячних, так і тижневих підписок, я визначила час у 17: 00 (5pm), оскільки зараз слушний час для надсилання інформаційних бюлетелів (кінця дня у Великобританії і запуску в США). У мене також є робота, яка виконується щогодини, щоб надіслати інформаційний бюлетень тим, хто підписувався на кожну пошту.

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

Dtos і відображення

Я користуюся DTOs щоб передати дані між шарами моєї програми. Це збільшує складність як мені потрібно картувати (часто два рази) Entities to Dtos, а потім Dtos до ViewModels (і назад). Проте, я вважаю, що таке відокремлення занепокоєння варте того, як воно дозволяє мені змінювати основну структуру даних, не впливаючи на УІ.

Ви можете використовувати такі підходи, як AutoMapper / Mapster тощо, щоб виконати цю прив' язку. Використання цих запитів може також мати значні переваги для запитів.AsNoTracking} так само, як ви можете покластися безпосередньо на Dto і уникнути накладних змін у відстеження. Наприклад, AutoMapper має a Пов' язаний з написанням суфікс метод, за допомогою якого ви можете побудувати безпосередню прив' язку до Dto у запиті.

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

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

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

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

Включення

Тепер у нас є структура, яка дозволяє нам пропонувати електронні підписки нашим користувачам. Ця реорганізація була трохи болісною, але це факт життя для більшості проектів. Почніть з простого і додайте архітектурні труднощі, якщо це потрібно. Майбутні дописи покриватимуть все це, включно з моїм застосуванням [FluentMail] Послать письмо и Службу Хранителя... и, возможно, еще немного поджигательства.

logo

©2024 Scott Galloway