Search code examples
c#embedded-resourcerazorengine

RazorEngine string layouts and sections?


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 @sections 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:

  • How can I specify layouts in RazorEngine?
  • How can I specify these layouts from strings (or a stream)? since as you can see, I use embedded resources to store the razor email templates.

Update

Maybe I wasn't clear, but I'm referring to the RazorEngine library.


Solution

  • 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;
        }
    }
    

    UPDATE

    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();
            }
        }