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
内 本系列第1部分 我向你们展示了我如何创建了一个新的《通讯》订阅网页。 在本部分,我将介绍我如何调整解决方案,以便在网站项目(Mostlylucid)和调度服务项目(Mostlylucid.Scheduler Service)之间共享服务和模型。
最初我只有一个单一的、单一的, 包含网站所有代码的项目。 对于较小的应用来说,这是一个体面的方法,它使你们能够简单地导航、构建和部署解决方案;所有这些都是合理的考虑。 然而,当你扩大一个解决方案, 你会想要开始拆散你的项目, 以便孤立关注事项, 并允许更容易的测试, 导航(拥有很多位数的大型项目很难导航 ) 。 此外,将调度器服务分开对我来说是有道理的,因为我可以将它作为一个单独的docker容器来部署,这样我就可以更新网站,而不会让调度器重新启动。
为此,我将关切按逻辑分为5个项目。 这是ASP.NET应用中的一种共同做法。
我现在可以为其中每一项添加测试项目,并单独进行测试。 这是一个很大的优势,因为它使我能够测试服务,而不需要将整个应用程序推开。
该项目是一个 ASP.NET Core (8) 网络项目,它持有所有主计长和视图向用户显示页面。
这正是我用来与数据库互动的主要背景定义。 包括EF核心DbContext。
这是主类图书馆项目, 保存与数据库/ 标记文件/ 电子邮件服务等进行互动的所有服务 。
这是一个班级图书馆项目, 持有所有共享模式, 都同时用于Mostlylucid和Mostlylucid.Scheduler Servicice项目。
这是一个网络应用程序, 控制执行预定任务的“ 挂机” 服务, 以及处理邮件发送的端点 。
其结构如下所示。 您可以看到,调度服务与主要顺畅度最高者在初始设置时只使用 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 Compostee 文件中添加了一个依赖性, 以确保在启动我的 Shceduler Service 之前, 主要的 Mostlylucid 项目已经启动并运行 。 此选项被添加到 Docker- complace 文件中我的调度服务的服务定义中 。
depends_on:
- mostlylucid
healthcheck:
test: ["CMD", "curl", "-f", "http://mostlylucid:80/healthy"]
interval: 30s
timeout: 10s
retries: 5
您也可以以其他方式完成此任务, 例如, 过去运行多个相同应用程序的多个实例时, 我用 Redis 设置一个锁定标记, 每个实例在运行迁移前检查 / 其它任务时只应运行一个实例 。
我选择使用Hangfire来处理我的日程安排,因为它与ASP.NET核心有方便的结合,而且很容易使用。
为此,在调度服务项目中,我添加了以下NuGet软件包。
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.PostgreSql
这意味着我可以用Postgres 做我的商店 来完成预定的任务
现在我有捕火发现,我可以开始 增加我预定的工作。
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(下午5:00), 因为这是发通讯的好时机(在英国, 我每小时都有一份工作, 将通讯寄给订阅每篇文章的人。
然后,这等于是 SendNewsletter
方法中 NewsletterSendingService
类。 稍后我会在一篇文章里更详细地讲这些。
我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用 地区贸易机会组织 以在我申请的两层之间传输数据。 这个DOES增加了复杂性, 因为我需要将实体映射为 Dtos (往往两次), 然后再将 Dtos 映射为 ViewModel (和后向) 。 然而,我认为这种将关切分开的做法值得考虑,因为它允许我改变基本数据结构,而不影响UI。
您可以使用 AutoMapper / Master 等方法进行此映射 。 使用这些功能还可以在.AsAsNoTracking()查询方面产生很大的性能优势,因为您可以直接绘制到Dto,避免跟踪变化的间接费用。 例如 AUUMapper 有 无法查询的扩展名 允许您在查询中直接映射到 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 用于查看模式,然后在主计长中使用。
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
};
}
使用地图仪工具可以避免使用锅炉牌代码(并减少错误),
现在我已经设置了所有这些模型, 我可以开始添加控制器方法来使用它们。 我会在接下来的部分 覆盖这种流动。
我们现在有了一种结构,使我们能够开始向用户提供通讯订阅服务。 这种重构虽然有点痛苦, 但对于大多数项目来说,这是生命中的一个事实。 开始简单化, 必要时添加建筑复杂化 。 未来职位将涵盖其余职位,包括跟进我使用 [流利邮件] 发送电子邮件 和主机服务... 和也许多一点点 挂起火。