# 通讯订阅服务部分 @2_#@-}重新描述服务内容 @MS K2}和点燃的一点

# 一. 导言导言 导言 导言 导言

内 [本系列的 1部分](/blog/anewslettersubscriptionservicept1) 我向大家展示了我如何创建新的通讯订阅页面 . 。 在这部分, I' 会介绍我是如何调整解决方案的,以便分享网站项目“(”和调度服务项目“(”之间的服务和模型共享。

[TOC]

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

# 项目

最初我只有一个包含网站所有代码的“,”单一语言项目。 这是用于较小应用程序的一个体面的方法 @,} 它使您能够轻松地浏览\, 构建和部署解决方案@; 所有这些都是合理的考虑. 然而当你扩大一个解决方案时,你'将想要开始拆开您的工程以孤立关注事项并允许更容易测试\MS K7 导航\ (Big 项目与许多位元可能难以浏览\).
此外,拆分调度器服务对于我来说是有道理的,因为我可以将它作为一个单独的 docker 容器来部署,让我能够更新网站,而不会让排程器重新启动@. @

为了做到这一点,我按逻辑将关注问题归为“5”项目 .}这是ASP=.}NET应用程序中的一种常见做法

现在,我可以为其中每一个添加测试项目 并单独进行测试..."."这是一个很大的优势 它允许我测试服务

## 最美味的

此工程是一个 ASP.NET 核心 &(8)Web Project < ,\} 它持有所有控制器{&View> 以显示用户的页面@.}

## 最优lucid.}DbContext

包括EF Core DbContext@.}。

## 多数服务

这是主类图书馆项目, 保存所有与数据库 {/} Markdown 文件 {/} email service et .} 互动的服务 。

## 最美味的 . 分享

这是一个班级图书馆项目, 持有Mostlylucid 和 Mostlylucid.+Scheduler Service项目使用的所有共享模型@.}

## Mostlylucid.Scheduler Service

这是一个网络应用程序, 用于控制运行指定任务的“ 挂牌火警服务” 以及处理邮件发送端点@ . @ {}

此结构的结构显示于下@. @%
您可以看到,调度服务以及主要最精密的服务器在初始设置时只使用 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 项目有任何依赖性@.

在初始化 DbContext 时, 它对于避免种族条件很重要, 尤其是如果您在此运行 Migrations@ ) 仅此而已, 我刚刚在我的 Docker Cropomee 文件中添加了一个依赖性, 以确保主要的 Mostlylucid 项目在开始我的调度服务前已经启动并正在运行 @ MS K4\ 。 这被加入到 Docker *-compositice 文件中的我的调度服务的服务定义中 =.\ }

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

您也可以以其他方式完成此任务@ , @% 例如过去, 当运行同一应用程序 I' @ ve 的多个实例时 [重新重订](https://redis.io/) 以设置一个锁定标记, 每个实例在运行迁移前先检查该标记@ @ / 其它任务确保只有一个例子可以在时间上运行@ MS K1 _

当然 Redis 还有其他许多用途@;, `IDistributedCache` 允许您使用 Redis 作为基于标签过期的缓存提供者@ . @ {}

# 防火挂机装置

我选择使用Hangfire来处理我的日程安排,因为它方便地与 ASP.NET Core 连接,而且很容易使用@.

为此,我在调度服务项目中添加了以下Nuget 软件包@.

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

这意味着我可以用Postgres做我的商店 来完成预定的任务

现在我有挂火渗漏,我可以开始 增加我的预定工作了

```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` 用来决定通讯多久发一次的旗帜 `Cron` 类以设定任务频率@. @%

由于我可以选择月刊和周刊的订费, 我设定了时间给 @17:00@ (5_pm)}因为现在是时候在英国寄发每日末日通讯@(+%
我每小时都有一份工作,

然后,这等于 `SendNewsletter` 方法中 `NewsletterSendingService` @._}I'}在稍后的一篇文章中,

# Dtos和绘图

我用的是 [地区贸易机会组织](https://en.wikipedia.org/wiki/Data_transfer_object) 以在应用程序的层层之间传送数据@.}这个DOES增加了复杂性,因为我需要为 Dtos 绘制“(” 地图时, 我经常两次将实体 ) 映射到 dtos, 然后又把“Dtos” 映像“Z(” 和回溯“MS K4 ” 。 然而, “, ” 我认为这种关注分离是值得的,因为它允许我在不影响 UI @.的情况下改变基本的数据结构

您可以使用 AutoMapper {/} Master 等方法来进行此映射@ .}使用这些选项也可以为 @ MS K2_AsNoTracking}()}查询提供显著的性能优势, 您可以直接绘制到 Dto 并避免跟踪更改的间接操作@.}例如 Automapper 有 [无法查询的扩展名](https://docs.automapper.org/en/stable/Queryable-Extensions.html) 允许您直接映射到查询中的 Dto 的方法@ .}{}

但在此情况下, 我决定只要在需要的地方添加映像师扩展名, 这样我就可以对每个级别 的绘图有更多的控制了 @ ( @but is more work@,}(尤其是如果你有很多实体的话) @).#

```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` 是我的传输对象@. @  
目的是让前端服务对实体对象有所了解,

在这些顶级服务中,我然后有代码来绘制这些 Dtos 用于查看模式的图形。 这些代码随后被控制器@.}使用

```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 工具可以避免此锅炉牌代码 @ ( @ 并减少错误@ MS K2}( AgainMSMK0),

现在我已经设置了所有这些模型, 我可以开始添加控制器方法 来使用它们@.
I'将覆盖下一部分的流量 .

# 在结论中的结论

我们现在有了一个结构 允许我们开始向用户提供通讯订阅服务
开始简单,在需要时添加建筑复杂因素.
包括跟进我使用 [流利邮件] 发送电子邮件和主机服务... ..."...,也许再多发一点 "Hangfire"