【问题标题】:Best way to send multiple email types in ASP.NET MVC在 ASP.NET MVC 中发送多种电子邮件类型的最佳方式
【发布时间】:2020-12-20 22:55:55
【问题描述】:

嗨,SO 的好朋友们!

这更像是一个设计问题,所以我将进入一个详细的示例。

让我解释一下我们发送电子邮件的方式。 在应用程序的各个部分,我们在Notification 表中为我们可能需要发送的不同类型的电子邮件创建条目。 例如:NotificationQueue 表如下所示:

NotificationQueueID OrderID         EmailType           Notes       SentDatetime
1                   461196          OrderUpdate         SomeNote1   2020-09-01 14:45:13.153
2                   461194          OrderCancellation   SomeNote2   2020-09-01 14:45:13.153

使用 DbContext 中的属性访问它:

public DbSet<NotificationQueue> NotificationQueues { get; set; }

不同类型的电子邮件在enum 中建模:

public enum TypeOfEmail
{
    OrderCancellation,
    OrderUpdate
}

我们有一个 EmailModel 类,它有一个 TicketsInNotificationQueue 属性,其中包含我们拥有的任何电子邮件类型的列表。例如:在任何给定时间,它都可以有UpdatedTicketsCancelledTickets 的列表。电子邮件类型说明TicketsInNotificationQueue 属性中的票证类型。

public class EmailModel
{
    public EmailModel(TypeOfEmail emailType, TicketsInNotificationQueue ticketsInNotificationQueue)
    {
        EmailType = emailType;
        TicketsInNotificationQueue = ticketsInNotificationQueue;
    }

    public TypeOfEmail EmailType { get; set; }
    public TicketsInNotificationQueue TicketsInNotificationQueue { get; set; }
}

public class TicketsInNotificationQueue
{
    public List<OrderCancellation> CancelledTickets { get; set; }
    public List<OrderUpdate> UpdatedTickets { get; set; }
}

public class OrderCancellation : CommonOrderInformation
{
    public string SomeOrderId { get; set; }
}

public class OrderUpdate: CommonOrderInformation
{
    public string SomeUpdateRelatedProperty { get; set; }
}

public class CommonOrderInformation
{
    public int NotificationQueueId { get; set; }
    public string ReferenceNumber { get; set; }
}

有一种方法可以从Notification 表中检索票证:

public async Task<TicketsInNotificationQueue> GetTicketsfromNotificationQueueAsync(TypeOfEmail emailType)
{
    var ticketsInNotificationQueue = new TicketsInNotificationQueue();

    using (var dbCon = GetSomeDbContext())
    {
        var notifications = dbCon.NotificationQueues.Where(x => x.EmailType == emailType.ToString()).ToList();

        foreach (var ntf in notifications)
        {
            if (ntf.EmailType == TypeOfEmail.OrderCancellation.ToString())
            {
                if (ticketsInNotificationQueue.CancelledTickets == null)
                {
                    ticketsInNotificationQueue.CancelledTickets = new List<OrderCancellation>();
                }
                
                ticketsInNotificationQueue.CancelledTickets.Add(new OrderCancellation()
                {
                    NotificationQueueId = ntf.NotificationQueueID,
                    ReferenceNumber = ntf.OrderID,
                    SomeOrderId = "Something from a table."
                });
            }
            else if (ntf.EmailType == TypeOfEmail.OrderUpdate.ToString())
            {
                if (ticketsInNotificationQueue.UpdatedTickets == null)
                {
                    ticketsInNotificationQueue.UpdatedTickets = new List<OrderUpdate>();
                }

                var notes = dbCon.NotificationQueues.FirstOrDefault(x => x.NotificationQueueID == ntf.NotificationQueueID)?.Notes;

                ticketsInNotificationQueue.UpdatedTickets.Add(new OrderUpdate()
                {
                    NotificationQueueId = ntf.NotificationQueueID,
                    ReferenceNumber = ntf.OrderID,
                    SomeUpdateRelatedProperty = "Something from a table."
                });
            }
        }
    }
    return ticketsInNotificationQueue;
}

现在我只需要这个列表,并根据我刚刚收到的票的类型过滤掉notificationIds,然后继续处理它们。 (通知发送后我需要那些notificationIds 来设置SentDatetime)。

    var ticketsReceived = false;
    notificationIds = new List<int>();

    if (ticketsInNotificationQueue.CancelledTickets != null && ticketsInNotificationQueue.CancelledTickets.Any())
    {
        ticketsReceived = true;
        notificationIds = ticketsInNotificationQueue.CancelledTickets.Select(x => x.NotificationQueueId).ToList();
    }
    else if (ticketsInNotificationQueue.UpdatedTickets != null && ticketsInNotificationQueue.UpdatedTickets.Any())
    {
        ticketsReceived = true;
        notificationIds = ticketsInNotificationQueue.UpdatedTickets.Select(x => x.NotificationQueueId).ToList();
    }

    if (ticketsReceived)
    {
        // Proceed with the process of sending the email, and setting the `SentDateTime`
    }

我在这里看到的问题是,随着emails 的类型变大,比如说10-20,检索票证并稍后过滤掉票证的方法需要增长得如此之大,以至于它会失控我根本不喜欢的可读性和代码可管理性方面。我需要检查获取中请求的emailType 以及已收到emailType 的部分(以获取SentDateTime 更新的相应notificationIds)。 那么有没有其他方法来设计这个工作流程(我什至愿意使用反射等)以使其更易于管理和简洁?

任何帮助将不胜感激!

【问题讨论】:

    标签: c# asp.net asp.net-mvc email architecture


    【解决方案1】:

    您可以对现有系统和现有代码进行重大改进。为了获得更完整的答案,我将推荐一个不太昂贵的系统大修,然后继续您的确切答案。

    一种不同的行业标准方法

    您已经拥有正确的数据结构,这是分布式持久队列的完美工作,您无需担心查询数据库;相反,您只需将消息排入队列并拥有一个处理它们的处理器。由于您使用的是 C# 和 .net,我强烈建议您查看 Azure Service Bus。这实际上是一个大队列,您可以在其中发送消息(在您的情况下发送电子邮件请求),并且您可以根据类型将消息排入服务总线中的不同通道。

    您还可以考虑创建一个队列处理器 / Azure Functions have a trigger 开箱即用。发送电子邮件后,您就可以写信给您的数据库了,我们已经发送了这封电子邮件。

    所以,好的设计看起来像

    • 拥有分布式持久队列、通道/将电子邮件请求直接排入队列。
    • 如果您想按节奏处理它们,请使用大多数行业解决方案都支持的 cron 运行您的处理器。
    • 如果您想在它们最终进入队列时对其进行处理,请使用触发器。

    您可以根据您的场景丰富您的处理器,它看起来与订单有关,因此您可能需要处理诸如取消订单后不发送已排队的电子邮件等情况。

    改善你所拥有的

    由于某些情况,您可能无法使用上述解决方案 - 所以让我们开始吧。

    看看如何重构 switch 语句(因为你有一个 if / else ifs)

    您可以通过多态来实现这一点,只需创建一个基本邮件类型并覆盖子类中的行为。这样您就可以将正确的队列与正确的电子邮件类型相关联。

    例子:

    var results = await getSomeEmails(OrderMail);
    // returns a separate processor inherited from the base one, implemented in different ways.
    var processor = ProcessorFactory.Create(OrderMail);
    await processor.Send(results);
    

    更多改进

     foreach (var ntf in notifications)
            {
                if (ntf.EmailType == TypeOfEmail.OrderCancellation.ToString())
    

    您在此循环中一遍又一遍地检查电子邮件类型,这是不必要的,您应该考虑将这些语句移到 for 上方并检查传入的参数,因为您已经知道要查询的类型。

    【讨论】:

    • @Ash-K 你有机会看看这个答案吗?
    • 非常感谢您花时间回答。我将探讨您提到的行业标准方法,但此时,我在上面刚刚发布的答案中简化了emailRecords 的获取和处理。如果你有时间,我想得到你的反馈。谢谢!
    【解决方案2】:

    感谢@Mavi Domates 的回答。

    但这就是我最终要做的: 我修改了EmailModelTicketsInNotificationQueue 属性,这样我们就没有为不同类型的电子邮件提供不同类型的类,而是只有一种类型的公共类。这将避免让我们检查在获取逻辑中请求的电子邮件类型以及检索notification Ids(在发送电子邮件后更新SentDateTime),如原始问题所示。

    public class EmailModel
    {
        public EmailModel(TypeOfEmail emailType, IEnumerable<CommonEmailModel> ticketsInNotificationQueue)
        {
            EmailType = emailType;
            TicketsInNotificationQueue = ticketsInNotificationQueue;
        }
    
        public TypeOfEmail EmailType { get; set; }
        public IEnumerable<CommonEmailModel> TicketsInNotificationQueue { get; set; }
    }
    
    public enum TypeOfEmail
    {
        OrderCancellation,
        OrderUpdate
    }
    

    我添加了一个名为 CommonEmailModel 的新类,并删除了所有这些不同的电子邮件类型类(OrderCancellationOrderUpdate 等的类)。

    public class CommonEmailModel
    {
        // Common to all email types. A lot of email types only need these first 4 properties
        public string EmailType { get; set; }
        public int NotificationQueueId { get; set; }
        public string OrderId { get; set; }
        public string Notes { get; set; }
    
        // Cancellation related
        public string SomeOrderId { get; set; }
    
        // Update related
        public string SomeUpdateRelatedProperty { get; set; }
    
        public static async Task<IEnumerable<CommonEmailModel>> GetEmailBodyRecordsAsync(TypeOfEmail emailType)
        {
            var emailModels = new List<CommonEmailModel>();
            var emailEntries = await EmailNotificationQueue.GetEmailEntriesAsync(emailType);
            var relevantOrdIds = emailEntries.Select(x => x.OrderID).Distinct().ToList();
    
            using (var dbCon = GetSomeDbContext())
            {
                orders = dbCon.Orders.Where(x => relevantOrdIds.Contains(x.OrdNumber)).ToList();
            }
    
            foreach (var record in emailEntries)
            {
                var emailModel = new CommonEmailModel
                {
                    EmailType = emailType,
                    NotificationQueueId = record.NotificationQueueID,
                    OrderId = record.OrderID,
                    Notes = record.Notes,
                    SomeOrderId = orders?.FirstOrDefault(o => o.OrdNumber == record.OrderID)?.SomeOrderIdINeed,
                    SomeUpdateRelatedProperty = orders?.FirstOrDefault(o => o.OrdNumber == record.OrderID)?.UpdateRelatedPropertyINeed
                };
    
                emailModels.Add(emailModel);
            }
    
            return emailModels;
        }
    }
    

    我只是通过以下方式获取记录:

    var emailRecords = await CommonEmailModel.GetEmailBodyRecordsAsync(emailType);
    

    然后简单地将其作为ticketsInNotificationQueue 参数传递给EmailModel 构造函数。无需进行所有额外检查来确定是否请求了某些 emailType 的记录。 OrderCancellationOrderUpdate 的视图将使用 CommonEmailModel 类中存在的公共属性及其各自的相关属性。

    if (emailRecords.Any())
    {
        var emailModel = new EmailModel(emailType, emailRecords);
    }
    

    现在我要做的就是将notification Ids 传递给一个方法,该方法通过简单地调用用当前时间戳标记SentDateTime 列:

    if (emailWasSent)
    {
        await UpdateNotificationSentTimeAsync(emailRecords.Select(t => t.NotificationQueueId));
    }
    

    将来如果我们继续添加新的emailType(很可能他们会在CommonEmailModel 中的这4 个第一个常见属性中携带信息),我们可以简单地向CommonEmailModel 添加新属性以适应它并创建一个新视图。这样我可以避免代码重复和提取过程中的复杂性以及最后更新SentDateTime

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-06-14
      • 2010-09-08
      • 2014-10-30
      • 2013-04-28
      • 2011-08-20
      • 2016-02-09
      • 2012-11-14
      • 2011-09-07
      相关资源
      最近更新 更多