I use razor engine like this:
public class EmailService : IService
{
private readonly ITemplateService templateService;
public EmailService(ITemplateService templateService)
{
if (templateService == null)
{
throw new ArgumentNullException("templateService");
}
this.templateService = templateService;
}
public string GetEmailTemplate(string templateName)
{
if (templateName == null)
{
throw new ArgumentNullException("templateName");
}
Assembly assembly = Assembly.GetAssembly(typeof(EmailTemplate));
Stream stream = assembly.GetManifestResourceStream(typeof(EmailTemplate), "{0}.cshtml".FormatWith(templateName));
string template = stream.ReadFully();
return template;
}
public string GetEmailBody(string templateName, object model = null)
{
if (templateName == null)
{
throw new ArgumentNullException("templateName");
}
string template = GetEmailTemplate(templateName);
string emailBody = templateService.Parse(template, model, null, null);
return emailBody;
}
}
The templating service I use is injected although it's just a default implementation:
internal ITemplateService InstanceDefaultTemplateService()
{
ITemplateServiceConfiguration configuration = new TemplateServiceConfiguration();
ITemplateService service = new TemplateService(configuration);
return service;
}
Since in this case in particular I will be building emails from these templates. I want to be able to use @section
s for the email'a subject, and different sections of the email body, while using a layout where I specify the styles that are common to the whole email structure (which will look like one of MailChimp's probably).
The question is then twofold:
RazorEngine
?Maybe I wasn't clear, but I'm referring to the RazorEngine library.
Turns out after some digging that layouts are supported, we just have to declare them with _Layout
instead of Layout
As for the embedded resource issue, I implemented the following ITemplateResolver
using System;
using System.IO;
using System.Reflection;
using Bruttissimo.Common;
using RazorEngine.Templating;
namespace Website.Extensions.RazorEngine
{
/// <summary>
/// Resolves templates embedded as resources in a target assembly.
/// </summary>
public class EmbeddedTemplateResolver : ITemplateResolver
{
private readonly Assembly assembly;
private readonly Type type;
private readonly string templateNamespace;
/// <summary>
/// Specify an assembly and the template namespace manually.
/// </summary>
/// <param name="assembly">The assembly where the templates are embedded.</param>
/// <param name="templateNamespace"></param>
public EmbeddedTemplateResolver(Assembly assembly, string templateNamespace)
{
if (assembly == null)
{
throw new ArgumentNullException("assembly");
}
if (templateNamespace == null)
{
throw new ArgumentNullException("templateNamespace");
}
this.assembly = assembly;
this.templateNamespace = templateNamespace;
}
/// <summary>
/// Uses a type reference to resolve the assembly and namespace where the template resources are embedded.
/// </summary>
/// <param name="type">The type whose namespace is used to scope the manifest resource name.</param>
public EmbeddedTemplateResolver(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
this.assembly = Assembly.GetAssembly(type);
this.type = type;
}
public string Resolve(string name)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
Stream stream;
if (templateNamespace == null)
{
stream = assembly.GetManifestResourceStream(type, "{0}.cshtml".FormatWith(name));
}
else
{
stream = assembly.GetManifestResourceStream("{0}.{1}.cshtml".FormatWith(templateNamespace, name));
}
if (stream == null)
{
throw new ArgumentException("EmbeddedResourceNotFound");
}
string template = stream.ReadFully();
return template;
}
}
}
Then you just wire it like this:
internal static ITemplateService InstanceTemplateService()
{
TemplateServiceConfiguration configuration = new TemplateServiceConfiguration
{
Resolver = new EmbeddedTemplateResolver(typeof(EmailTemplate))
};
ITemplateService service = new TemplateService(configuration);
return service;
}
The type you pass is just for referencing the assembly and namespace where the resources are embedded.
namespace Website.Domain.Logic.Email.Template
{
/// <summary>
/// The purpose of this class is to expose the namespace of razor engine templates in order to
/// avoid having to hard-code it when retrieving the templates embedded as resources.
/// </summary>
public sealed class EmailTemplate
{
}
}
One last thing, in order to have the templates resolved with our resolver we have to resolve them like this:
ITemplate template = templateService.Resolve(templateName, model);
string body = template.Run();
return body;
.Run
is just a simple extension method since I can't find any use for a ViewBag
.
public static class ITemplateExtensions
{
public static string Run(this ITemplate template)
{
ExecuteContext context = new ExecuteContext();
string result = template.Run(context);
return result;
}
}
Here are the missing extensions
public static string FormatWith(this string text, params object[] args)
{
return string.Format(text, args);
}
public static string ReadFully(this Stream stream)
{
using (StreamReader reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
I needed to supply my own layout as either a string or a file name. Here is how I solved this (based on this blog post)
public static class RazorEngineConfigurator
{
public static void Configure()
{
var templateConfig = new TemplateServiceConfiguration
{
Resolver = new DelegateTemplateResolver(name =>
{
//no caching cause RazorEngine handles that itself
var emailsTemplatesFolder = HttpContext.Current.Server.MapPath(Properties.Settings.Default.EmailTemplatesLocation);
var templatePath = Path.Combine(emailsTemplatesFolder, name);
using (var reader = new StreamReader(templatePath)) // let it throw if doesn't exist
{
return reader.ReadToEnd();
}
})
};
RazorEngine.Razor.SetTemplateService(new TemplateService(templateConfig));
}
}
Then I call RazorEngineConfigurator.Configure() in Global.asax.cs and it's ready.
The path to my templates is in Properties.Settings.Default.EmailTemplatesLocation
In my view I have this:
@{ Layout = "_layout.html";}
_layout.html is in emailsTemplatesFolder
It's a pretty standard HTML with a @RenderBody() call in the middle.
As far as I understand, RazorEngine uses the template name ("_layout.html" in this case) as a key to its cache so the delegate in my configurator is called only once per template.
I think it uses that resolver for every template name it doesn't know (yet).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With