简体   繁体   English

在 ASP.NET MVC 中发送多个 email 类型的最佳方式

[英]Best way to send multiple email types in ASP.NET MVC

Hi there to the good friends of SO! SO 的好朋友们大家好!

This is more of a design question so I'll get into a detailed example.这更像是一个设计问题,所以我将进入一个详细的例子。

Let me explain the way we're sending emails.让我解释一下我们发送电子邮件的方式。 In various parts of the application, we create entries in our Notification table for different kinds of email we might have to send.在应用程序的各个部分,我们在Notification表中为我们可能必须发送的不同类型的 email 创建条目。 For eg: The NotificationQueue table looks like this:例如: 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

It's accessed using the property in the DbContext as:使用 DbContext 中的属性访问它:

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

The different types of email is modeled in an enum : email 的不同类型在enum中建模:

public enum TypeOfEmail
{
    OrderCancellation,
    OrderUpdate
}

We have a EmailModel class that has a TicketsInNotificationQueue property that has a list of any of the email types we have.我们有一个EmailModel class,它有一个TicketsInNotificationQueue属性,该属性包含我们拥有的任何 email 类型的列表。 For eg: At any given time, it can have list of either UpdatedTickets or CancelledTickets .例如:在任何给定时间,它都可以有UpdatedTicketsCancelledTickets的列表。 The email type says what type of tickets are in the TicketsInNotificationQueue property. email 类型表示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; }
}

There's a method that retrieves tickets from Notification table:有一种方法可以从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;
}

Now I just take this list, and filter out the notificationIds for the type of tickets that I just received, and work on them down the line.现在我只需要这个列表,过滤掉我刚收到的票证类型的notificationIds ,然后继续处理它们。 (I need those notificationIds to set the SentDatetime after the notification has been sent). (我需要那些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`
    }

The problem I see here is that as the type of emails grows bigger, let's say 10-20 , the method to retrieve tickets and filter them out later needs to grow so big that it's going to spin out of control in terms of readability and code manageability which I'm not liking at all.我在这里看到的问题是,随着emails类型的增加,比方说10-20 ,检索票证并在以后过滤掉它们的方法需要变得如此之大,以至于它在可读性和代码方面会失控我根本不喜欢的可管理性。 The part where I need to check what emailType is requested in the fetch and what emailType has been received(to get the corresponding notificationIds for SentDateTime update).我需要检查提取中请求的emailType以及已收到的emailType的部分(以获取SentDateTime更新的相应notificationIds )。 So is there some other way to design this workflow (I'm even open to using reflection and such) to make it more manageable and concise?那么是否有其他方法来设计此工作流(我什至愿意使用反射等)以使其更易于管理和简洁?

Any help would be greatly appreciated!任何帮助将不胜感激!

There is significant improvements that you can make to the existing system and the existing code.您可以对现有系统和现有代码进行重大改进。 In the interest of having a more complete answer I'm going to recommend a not-too-expensive system overhaul and then proceed to your exact answer.为了获得更完整的答案,我将推荐一个不太昂贵的系统检修,然后继续您的确切答案。

A different and industry standard approach一种不同的行业标准方法

You already have the data structure correct, this is a perfect job for distributed persistent queues, where you don't need to worry about querying the database as much;您已经拥有正确的数据结构,这对于分布式持久队列来说是一项完美的工作,您无需担心查询数据库; instead you just enqueue the messages and have a processor that deals with them.相反,您只需将消息排入队列并有一个处理它们的处理器。 Since you're using C# and .net, I strongly encourage you to check out Azure Service Bus .由于您使用的是 C# 和 .net,我强烈建议您查看Azure 服务总线 This is effectively a large queue where you can send messages (in your case send email requests) and you can enqueue your messages to different channels in the service bus depending on their type.这实际上是一个大队列,您可以在其中发送消息(在您的情况下发送 email 请求),并且您可以根据消息的类型将消息排队到服务总线中的不同通道。

You could also look into creating a queue processor / which Azure Functions have a trigger out of the box.您还可以考虑创建一个队列处理器/哪个Azure 函数具有开箱即用的触发器 Once your email is sent, then you can write to your DB, we've sent this email.发送您的 email 后,您就可以写入您的数据库,我们已经发送了这个 email。

So, the good design looks like所以,好的设计看起来像

  • Have distributed persistent queues, channels / enqueue the email requests to them directly.有分布式持久队列,通道/直接将 email 请求排入队列。
  • If you want to process them at a cadence, run your processor using cron - which most industry solutions support.如果您想按节奏处理它们,请使用 cron 运行您的处理器——大多数行业解决方案都支持。
  • If you want to process them as they are ending up in the queue, use a trigger.如果您想在它们进入队列时处理它们,请使用触发器。

You can enrich your processor based on your scenario, it looks like it has something to do with orders, so you may need to handle cases like not sending an already queued email after an order in cancelled, etc..你可以根据你的场景丰富你的处理器,看起来它与订单有关,所以你可能需要处理在取消订单后不发送已经排队的email等情况。

Improving what you have改善你所拥有的

Due to some circumstances, the solution above might not be available to you - so let's get to it.由于某些情况,您可能无法使用上述解决方案 - 让我们开始吧。

See how to refactor switch statements (since you have one with if / else if s)查看如何重构 switch 语句(因为你有一个带有if / else if的语句)

You could get this through polymorphism, just create a base mail type and override the behaviors in subclasses.您可以通过多态性来实现这一点,只需创建一个基本邮件类型并覆盖子类中的行为。 This way you can associate the correct queue with the correct email type.这样您就可以将正确的队列与正确的 email 类型相关联。

Example:例子:

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);

Some more improvements更多改进

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

You are checking the email type over and over again unnecessarily in this loop, you should look into moving those statements above the for and check through the passed-in parameter, since you already know the type you're querying for.您在此循环中不必要地一遍又一遍地检查 email 类型,您应该考虑将这些语句移到 for 上方并检查传入的参数,因为您已经知道要查询的类型。

Thank you for the answer @Mavi Domates.感谢@Mavi Domates 的回答。

But this is what I ended up doing: I modified the EmailModel 's TicketsInNotificationQueue property so that instead of having different types of classes for different types of email, we just have one type of common class. This will avoid having us to put those checks for checking what kind of email was requested in the fetch logic and also to retrieve notification Ids down the line (to update SentDateTime after email is sent) as indicated in the original question.但这就是我最终做的:我修改了EmailModelTicketsInNotificationQueue属性,这样我们就没有为不同类型的 email 使用不同类型的类,我们只有一种常见的 class。这将避免让我们进行这些检查用于检查在获取逻辑中请求了哪种类型的 email 以及检索notification Ids (在发送SentDateTime后更新 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
}

I added a new class called: CommonEmailModel and removed all those different email type classes (classes for OrderCancellation , OrderUpdate etc.).我添加了一个名为: CommonEmailModel的新 class 并删除了所有那些不同的 email 类型类( 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;
    }
}

I just get the records the following way:我只是通过以下方式获取记录:

var emailRecords = await CommonEmailModel.GetEmailBodyRecordsAsync(emailType);

And simply pass this to EmailModel constructor as the ticketsInNotificationQueue parameter.只需将其作为ticketsInNotificationQueue参数传递给EmailModel构造函数。 No need to do all that extra check of figuring out if records of certain emailType was requested.无需执行所有额外检查以确定是否请求了特定emailType的记录。 The views for OrderCancellation and OrderUpdate will use the common properties and their respective relevant properties that are present in the CommonEmailModel class. OrderCancellationOrderUpdate的视图将使用CommonEmailModel class 中存在的公共属性及其各自的相关属性。

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

Now all I have to do is pass the notification Ids to a method that marks the SentDateTime column with the current timestamp by simply calling:现在我所要做的就是将notification Ids传递给一个方法,该方法通过简单地调用以下方法将SentDateTime列标记为当前时间戳:

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

In the future if we keep on adding new emailType (most probably they'll carry the information in those 4 first common properties in CommonEmailModel ), we can simply add new properties to the CommonEmailModel to accommodate that and just create a new view.将来,如果我们继续添加新的emailType (它们很可能会在CommonEmailModel的前 4 个通用属性中携带信息),我们可以简单地向CommonEmailModel添加新属性以适应它并创建一个新视图。 This way I can avoid code repetition and complexity in the fetch and also at the end while updating the SentDateTime .通过这种方式,我可以在更新SentDateTime时避免获取代码的重复和复杂性,也可以在最后避免代码重复和复杂性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM