Skip to content

Instantly share code, notes, and snippets.

@tystol
Last active June 30, 2020 08:34
Show Gist options
  • Save tystol/9fd4db5e42d5d1943e60 to your computer and use it in GitHub Desktop.
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.
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