Last active
June 30, 2020 08:34
-
-
Save tystol/9fd4db5e42d5d1943e60 to your computer and use it in GitHub Desktop.
Postal Emails with layouts outside a http request context. There's a few application specific interfaces in here, but should be fairly easy to strip out and adapt.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class PostalEmail<T> : Email where T : IEmail | |
{ | |
public PostalEmail(string viewName, T model) | |
: base(viewName) | |
{ | |
To = model.To; | |
Email = model; | |
} | |
public string To { get; private set; } | |
public T Email { get; private set; } | |
} | |
public class PostalEmailService : Application.IEmailService | |
{ | |
private readonly Postal.IEmailService emailService; | |
public PostalEmailService(string emailTemplateRoot) | |
{ | |
emailService = CreateService(emailTemplateRoot); | |
} | |
public static Postal.IEmailService CreateService(string emailTemplateRoot) | |
{ | |
if ( !Directory.Exists(emailTemplateRoot) ) | |
throw new ArgumentException("The specified email template directory was not found", "emailTemplateRoot"); | |
var viewEngines = new HttpContextSafeViewEngineCollection | |
{ | |
new FileSystemWithLayoutsRazorViewEngine(emailTemplateRoot) | |
}; | |
return new EmailService(viewEngines); | |
} | |
public void Send<T>(string emailTemplate, T model) where T : IEmail | |
{ | |
var email = new PostalEmail<T>(emailTemplate, model); | |
emailService.Send(email); | |
} | |
private class HttpContextSafeViewEngineCollection : ViewEngineCollection | |
{ | |
public HttpContextSafeViewEngineCollection() | |
{ | |
// Horrible reflection based hack to pass in custom dependency resolver. | |
// This is needed otherwise ViewEngineCollection uses global Autofac MVC resolver, which requires a HttpContext.Current, | |
// which doesn't work from background thread. | |
// http://discuss.hangfire.io/t/hangfire-job-throws-autofac-exception-even-though-job-class-is-not-managed-by-autofac/488/5 | |
var resolverField = typeof (ViewEngineCollection).GetField("_dependencyResolver", | |
BindingFlags.NonPublic | BindingFlags.Instance); | |
var resolver = new DefaultResolver(); | |
resolverField.SetValue(this, resolver); | |
} | |
private class DefaultResolver : IDependencyResolver | |
{ | |
public object GetService(Type serviceType) | |
{ | |
return null; | |
} | |
public IEnumerable<object> GetServices(Type serviceType) | |
{ | |
return Enumerable.Empty<object>(); | |
} | |
} | |
} | |
// Temporary custom implementation until this PR is integrated: | |
// https://github.com/andrewdavey/postal/pull/117 | |
private class FileSystemWithLayoutsRazorViewEngine : IViewEngine | |
{ | |
readonly string viewPathRoot; | |
readonly ITemplateService razorService; | |
public FileSystemWithLayoutsRazorViewEngine(string viewPathRoot) | |
{ | |
this.viewPathRoot = viewPathRoot; | |
var razorConfig = new TemplateServiceConfiguration(); | |
var webConfigPath = Path.Combine(viewPathRoot, "Web.config"); | |
if (File.Exists(webConfigPath)) | |
{ | |
var xml = XDocument.Parse(File.ReadAllText(webConfigPath)); | |
var namespaces = xml.Root.Descendants("namespaces").SelectMany(n => n.Elements("add")) | |
.Select(e => e.Attribute("namespace").Value); | |
foreach (var ns in namespaces) | |
{ | |
razorConfig.Namespaces.Add(ns); | |
} | |
} | |
razorConfig.Resolver = new DelegateTemplateResolver(ResolveTemplate); | |
razorService = new TemplateService(razorConfig); | |
} | |
string GetViewFullPath(string path) | |
{ | |
return Path.Combine(viewPathRoot, path); | |
} | |
private string ResolveTemplate(string viewName) | |
{ | |
var path = ResolveTemplatePath(viewName); | |
if (path == null) return null; | |
var templateContents = File.ReadAllText(path); | |
return templateContents; | |
} | |
private string ResolveTemplatePath(string viewName) | |
{ | |
IEnumerable<string> searchedPaths; | |
var existingPath = ResolveTemplatePath(viewName, out searchedPaths); | |
return existingPath; | |
} | |
private string ResolveTemplatePath(string viewName, out IEnumerable<string> searchedPaths) | |
{ | |
var possibleFilenames = new List<string>(); | |
if (!viewName.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase) | |
&& !viewName.EndsWith(".vbhtml", StringComparison.OrdinalIgnoreCase)) | |
{ | |
possibleFilenames.Add(viewName + ".cshtml"); | |
possibleFilenames.Add(viewName + ".vbhtml"); | |
} | |
else | |
{ | |
possibleFilenames.Add(viewName); | |
} | |
var possibleFullPaths = possibleFilenames.Select(GetViewFullPath).ToArray(); | |
var existingPath = possibleFullPaths.FirstOrDefault(File.Exists); | |
searchedPaths = possibleFullPaths; | |
return existingPath; | |
} | |
/// <summary> | |
/// Tries to find a razor view (.cshtml or .vbhtml files). | |
/// </summary> | |
public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) | |
{ | |
IEnumerable<string> searchedPaths; | |
var existingPath = ResolveTemplatePath(partialViewName, out searchedPaths); | |
if (existingPath != null) | |
return new ViewEngineResult(new FileSystemWithLayoutsRazorView(razorService, existingPath), this); | |
return new ViewEngineResult(searchedPaths); | |
} | |
/// <summary> | |
/// Tries to find a razor view (.cshtml or .vbhtml files). | |
/// </summary> | |
public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) | |
{ | |
return FindPartialView(controllerContext, viewName, useCache); | |
} | |
/// <summary> | |
/// Does nothing. | |
/// </summary> | |
public void ReleaseView(ControllerContext controllerContext, IView view) | |
{ | |
// Nothing to do here - FileSystemRazorView does not need disposing. | |
} | |
} | |
// Temporary custom implementation until this PR is integrated: | |
// https://github.com/andrewdavey/postal/pull/117 | |
private class FileSystemWithLayoutsRazorView : IView | |
{ | |
static readonly ITemplateService DefaultRazorService = new TemplateService(); | |
readonly ITemplateService razorService; | |
readonly string template; | |
readonly string cacheName; | |
/// <summary> | |
/// Creates a new <see cref="FileSystemRazorView"/> using the given view filename. | |
/// </summary> | |
/// <param name="filename">The filename of the view.</param> | |
public FileSystemWithLayoutsRazorView(string filename) | |
: this(DefaultRazorService, filename) | |
{ | |
} | |
/// <summary> | |
/// Creates a new <see cref="FileSystemRazorView"/> using the given view filename. | |
/// </summary> | |
/// <param name="razorService">The RazorEngine ITemplateService to use to render the view</param> | |
/// <param name="filename">The filename of the view.</param> | |
public FileSystemWithLayoutsRazorView(ITemplateService razorService, string filename) | |
{ | |
this.razorService = razorService; | |
template = File.ReadAllText(filename); | |
cacheName = filename; | |
} | |
/// <summary> | |
/// Renders the view into the given <see cref="TextWriter"/>. | |
/// </summary> | |
/// <param name="viewContext">The <see cref="ViewContext"/> that contains the view data model.</param> | |
/// <param name="writer">The <see cref="TextWriter"/> used to write the rendered output.</param> | |
public void Render(ViewContext viewContext, TextWriter writer) | |
{ | |
var content = razorService.Parse(template, viewContext.ViewData.Model, null, cacheName); | |
writer.Write(content); | |
writer.Flush(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment