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

# Введение

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

[TOC]

<!--category-- ASP.NET, Hangfire, Email Newsletter -->
<datetime class="hidden">2024-09-23T10:45</datetime>

# Проекти

Спочатку у мене був лише один монолітний проект, який містив весь код для веб-сайту. Це пристойний підхід для менших застосувань , він дає вам просту навігацію М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 для початкового налаштування.

```mermaid
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]

```

Як зазвичай, я використовую метод розширення як вхідну точку для створення бази даних.

```csharp
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

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

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

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

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

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

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

```bash
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.PostgreSql
```

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

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

```csharp
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 розповім детальніше пізніше

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

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

Ви можете використати такі підходи, як AutoMapper / Mapster і т.д. для цього мапування . Використовуючи ці підходи також можете мати значні переваги в результатах для МSK2AsNoTrackingM SK3 запитів, тому що ви можете мапувати напряму до Dto і уникнути накладних витрат на зміни відслідковування МSK4 Наприклад, Automapper має [Поширення, яка доступна для пошуку](https://docs.automapper.org/en/stable/Queryable-Extensions.html) метод, який дозволяє відображати напряму на карті Dto у запиті.

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

```csharp
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, які потім будуть використані в контроллерах.

```csharp
    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.