Email services are part of Infrastructure, Email templates? #685
-
Hi, I'm using this template for one of my projects based on .NET6 & Vuejs. I had run into issues initially (mostly with Automapper & Identity) but going through with discussion and issues reported here really helped me to overcome those issues. So thank you all for your contributions. I really appreciate it if someone could please share an opinion here! |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 7 replies
-
I have generated Emails and PDFs in this template using the razor view engine. For the most part I referenced this article.
This is where I divert a bit from the article.In your command or query ( var body = await _renderer.RenderPartialToStringAsync("_ContactEmailPartial", ContactForm); Then you can pass the body into the email service and send the email. Hope this was helpful |
Beta Was this translation helpful? Give feedback.
-
I'm separating email sending into 3 parts:
The way to do that, create I store the email templates in WebUI layer, because it's easier to get the template folder path (by relying on To get full illustration here public interface IEmailSender
{
Task SendAsync(CompiledEmailMessage message);
}
public interface IEmailTemplate
{
Task<EmailBody> CompileAsync(string bodyPath, IEnumerable<Placeholder> placeholders);
}
public interface IEmailService
{
Task SendAsync(EmailMessage message);
}
public interface ITemplatePathProvider
{
string Resolve(string relativePath);
}
public record EmailAttachment(Func<Stream> StreamFactory, string FileName, string MimeType = "application/octet-stream");
public record EmailBody(string Html, string PlainText);
public record Placeholder(string Name, string Value);
public record EmailAddress(string Address, string DisplayName)
{
public static implicit operator EmailAddress(string address) => new(address, string.Empty);
}
public record CompiledEmailMessage(string Subject, EmailBody Body, EmailAddress From,
IEnumerable<EmailAddress> To,
IEnumerable<EmailAddress>? Cc = null,
IEnumerable<EmailAddress>? Bcc = null,
IEnumerable<EmailAttachment>? Attachments = null);
public record EmailMessage(string Subject, string BodyPath, IEnumerable<Placeholder> Placeholders,
EmailAddress From, IEnumerable<EmailAddress> To,
IEnumerable<EmailAddress>? Cc = null,
IEnumerable<EmailAddress>? Bcc = null,
IEnumerable<EmailAttachment>? Attachments = null); Infrastructure layer public class TemplateOptions
{
public string LayoutPath { get; set; } = string.Empty;
}
public class RazorEmailTemplate : IEmailTemplate
{
private readonly ITemplatePathProvider _pathProvider;
private readonly TemplateOptions _options;
public RazorEmailTemplate(ITemplatePathProvider pathProvider, IOptions<TemplateOptions> options) {
_options = options.Value;
_pathProvider = pathProvider;
}
public Task<EmailBody> CompileAsync(string partialBodyPath, IEnumerable<Placeholder> placeholders) {
string layoutPath = _pathProvider.Resolve(_options.LayoutPath);
string bodyPath = _pathProvider.Resolve(partialBodyPath);
// TODO: compile body given layout above
// TODO: do placeholder replacement
// TODO: build plain text version
throw new NotImplementedException();
}
} WebUI layer using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
public interface ITemplatePathProvider
{
string Resolve(string relativePath);
}
public class WebTemplateProvider : ITemplatePathProvider
{
private readonly string _basePath;
public WebTemplateProvider(IWebHostEnvironment env)
{
_basePath = env.WebRootPath
.Replace('/', Path.DirectorySeparatorChar)
.TrimEnd(Path.DirectorySeparatorChar);
}
public string Resolve(string relativePath)
{
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new ArgumentNullException(nameof(relativePath));
}
relativePath = relativePath.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar);
// it can accept ~/folder of just /folder format
return relativePath.StartsWith('~')
? relativePath.Replace("~", _basePath)
: Path.Combine(_basePath, relativePath);
}
} And finally, you can write email service wrapper as follows (can be in infrastructure/application layer) public class EmailService : IEmailService
{
private readonly IEmailSender _emailSender;
private readonly IEmailTemplate _emailTemplate;
public EmailService(IEmailTemplate emailTemplate, IEmailSender emailSender) {
_emailTemplate = emailTemplate;
_emailSender = emailSender;
}
public async Task SendAsync(EmailMessage message) {
var emailBody = await _emailTemplate.CompileAsync(message.BodyPath, message.Placeholders);
var compiledMessage = new CompiledEmailMessage(message.Subject, emailBody, message.From, message.To,
message.Cc, message.Bcc, message.Attachments);
await _emailSender.SendAsync(compiledMessage);
}
} |
Beta Was this translation helpful? Give feedback.
I'm separating email sending into 3 parts:
Its job is reading email template (currently in HTML format), populating placeholder variable (currently using
{variable_name}
format), then give the rendered HTML format. It also provides conversion from HTML to plain text. It's highly suggested to set plain text alternative when sending HTML email. We can also apply layout template here. So the email wrapper will pass only partial body.Its job is sending email to SMTP or other provider. This can be implemented in builtin .NET lib or using MailKit or any 3rd party email service provider (like sendgrid). It accepts common sending email parameter, i.e. TO, CC…