NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Monday, 23 September 2024
//Less than a minute
Вхід перша частина цієї серії Я показав вам, як я створив нову сторінку передплати на Newletter. У цій частині я розповім про те, як я перебудував рішення, за допомогою якого можна обмінюватися службами і моделями між проектом Веб- сайта (здебільшого за все) і проектом Служба планування (Службовцем планування. SchedulerService).
Спочатку у мене був лише один монолітний проект, який містив весь код веб-сайту. Це належний підхід до менших програм, він надає вам можливість просто пересуватися, будувати і розробляти рішення. Все це є розсудливими міркуваннями. Але, оскільки ви збільшуєте розв' язок, вам слід почати роз' єднання вашого проекту, щоб дозволити усамітненню проблем, а також уможливити простіше тестування, навігації (Великий проект з великою кількістю бітів може бути дуже складним для навігації). Крім того, мені було б розумно розділяти роботу планувальника, оскільки я можу розкрити це як окремий контейнер докерів, що дозволяє мені оновити веб-сайт, не спричиняючи планувальника перезапустити.
Щоб зробити це, я згрупував ці турботи логічно в 5 проектах. Це поширений підхід у програмах ASP.NET.
Тепер я можу додати тестові проекти для кожного з них і випробувати їх у ізоляції. Це велика перевага, оскільки це дозволяє мені протестувати послуги без потреби прокручувати всю заявку.
Цей проект є ядром мережі ASP. NET (8), у ньому містяться всі регулятори і перегляди, призначені для показу сторінок користувачеві.
Це містить моє основне визначення контексту, яке використовується для взаємодії з базою даних. Включая EC Core DbContext.
Це основний проект бібліотеки класів, у якому зберігаються всі служби, які взаємодіють з файлами бази даних / Markdown / Служби пошти тощо.
Це проект бібліотеки класів, у якому містяться всі спільні моделі, які використовуються проектами "Маклюцід" та "Майселцид." SchedulerService.
Це веб- програма, яка керує службою Hangfire, яка запускає заплановані завдання, а також кінцеві пункти для роботи з електронними листами.
Будова цього зображення показана нижче. Ви можете бачити, що планувальник (Service) і головний програмний центр використовують лише клас DbContext для початкового налаштування.
Як завжди, я використовую метод розширення як точку запису для налаштування бази даних.
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 щоб передати дані між шарами моєї програми. Це збільшує складність як мені потрібно картувати (часто два рази) 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] Послать письмо и Службу Хранителя... и, возможно, еще немного поджигательства.