[英]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
.例如:在任何给定时间,它都可以有
UpdatedTickets
或CancelledTickets
的列表。 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.
为了获得更完整的答案,我将推荐一个不太昂贵的系统检修,然后继续您的确切答案。
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所以,好的设计看起来像
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等情况。
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);
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.但这就是我最终做的:我修改了
EmailModel
的TicketsInNotificationQueue
属性,这样我们就没有为不同类型的 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 类型类( OrderCancellation
、 OrderUpdate
等类)。
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. OrderCancellation
和OrderUpdate
的视图将使用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.