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

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

本系列第1部分 我向你们展示了我如何创建了一个新的《通讯》订阅网页。 在本部分,我将介绍我如何调整解决方案,以便在网站项目(Mostlylucid)和调度服务项目(Mostlylucid.Scheduler Service)之间共享服务和模型。

项目

最初我只有一个单一的、单一的, 包含网站所有代码的项目。 对于较小的应用来说,这是一个体面的方法,它使你们能够简单地导航、构建和部署解决方案;所有这些都是合理的考虑。 然而,当你扩大一个解决方案, 你会想要开始拆散你的项目, 以便孤立关注事项, 并允许更容易的测试, 导航(拥有很多位数的大型项目很难导航 ) 。 此外,将调度器服务分开对我来说是有道理的,因为我可以将它作为一个单独的docker容器来部署,这样我就可以更新网站,而不会让调度器重新启动。

为此,我将关切按逻辑分为5个项目。 这是ASP.NET应用中的一种共同做法。

我现在可以为其中每一项添加测试项目,并单独进行测试。 这是一个很大的优势,因为它使我能够测试服务,而不需要将整个应用程序推开。

最美味的

该项目是一个 ASP.NET Core (8) 网络项目,它持有所有主计长和视图向用户显示页面。

最流利的 DbContext

这正是我用来与数据库互动的主要背景定义。 包括EF核心DbContext。

服务

这是主类图书馆项目, 保存与数据库/ 标记文件/ 电子邮件服务等进行互动的所有服务 。

最美味的。 分享

这是一个班级图书馆项目, 持有所有共享模式, 都同时用于Mostlylucid和Mostlylucid.Scheduler Servicice项目。

最流利的。 Scheduler Services

这是一个网络应用程序, 控制执行预定任务的“ 挂机” 服务, 以及处理邮件发送的端点 。

其结构如下所示。 您可以看到,调度服务与主要顺畅度最高者在初始设置时只使用 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 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 类。 稍后我会在一篇文章里更详细地讲这些。

Dtos和绘图

我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用,我用 地区贸易机会组织 以在我申请的两层之间传输数据。 这个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
        };
    }

使用地图仪工具可以避免使用锅炉牌代码(并减少错误),

现在我已经设置了所有这些模型, 我可以开始添加控制器方法来使用它们。 我会在接下来的部分 覆盖这种流动。

在结论结论中

我们现在有了一种结构,使我们能够开始向用户提供通讯订阅服务。 这种重构虽然有点痛苦, 但对于大多数项目来说,这是生命中的一个事实。 开始简单化, 必要时添加建筑复杂化 。 未来职位将涵盖其余职位,包括跟进我使用 [流利邮件] 发送电子邮件 和主机服务... 和也许多一点点 挂起火。

logo

©2024 Scott Galloway