RazorEngine Error trying to send email
Asked Answered
C

2

9

I have an MVC 4 application that sends out multiple emails. For example, I have an email template for submitting an order, a template for cancelling an order, etc...

I have an Email Service with multiple methods. My controller calls the Send method which looks like this:

public virtual void Send(List<string> recipients, string subject, string template, object data)
{
    ...
    string html = GetContent(template, data);
    ...
}

The Send method calls GetContent, which is the method causing the problem:

private string GetContent(string template, object data)
{
    string path = Path.Combine(BaseTemplatePath, string.Format("{0}{1}", template, ".html.cshtml"));
    string content = File.ReadAllText(path);
    return Engine.Razor.RunCompile(content, "htmlTemplate", null, data);
}

I am receiving the error:

The same key was already used for another template!

In my GetContent method should I add a new parameter for the TemplateKey and use that variable instead of always using htmlTemplate? Then the new order email template could have newOrderKey and CancelOrderKey for the email template being used to cancel an order?

Champac answered 22/4, 2015 at 16:24 Comment(0)
O
17

Explanation

This happens because you use the same template key ("htmlTemplate") for multiple different templates. Note that the way you currently have implemented GetContent you will run into multiple problems:

  • Even if you use a unique key, for example the template variable, you will trigger the exception when the templates are edited on disk.

  • Performance: You are reading the template file every time even when the template is already cached.

Solution:

Implement the ITemplateManager interface to manage your templates:

public class MyTemplateManager : ITemplateManager
{
    private readonly string baseTemplatePath;
    public MyTemplateManager(string basePath) {
      baseTemplatePath = basePath;
    }

    public ITemplateSource Resolve(ITemplateKey key)
    {
        string template = key.Name;
        string path = Path.Combine(baseTemplatePath, string.Format("{0}{1}", template, ".html.cshtml"));
        string content = File.ReadAllText(path);
        return new LoadedTemplateSource(content, path);
    }

    public ITemplateKey GetKey(string name, ResolveType resolveType, ITemplateKey context)
    {
        return new NameOnlyTemplateKey(name, resolveType, context);
    }

    public void AddDynamic(ITemplateKey key, ITemplateSource source)
    {
        throw new NotImplementedException("dynamic templates are not supported!");
    }
}

Setup on startup:

var config = new TemplateServiceConfiguration();
config.Debug = true;
config.TemplateManager = new MyTemplateManager(BaseTemplatePath); 
Engine.Razor = RazorEngineService.Create(config);

And use it:

// You don't really need this method anymore.
private string GetContent(string template, object data)
{
    return Engine.Razor.RunCompile(template, null, data);
}

RazorEngine will now fix all the problems mentioned above internally. Notice how it is perfectly fine to use the name of the template as key, if in your scenario the name is all you need to identify a template (otherwise you cannot use NameOnlyTemplateKey and need to provide your own implementation).

Hope this helps. (Disclaimer: Contributor of RazorEngine)

Octavla answered 22/4, 2015 at 18:51 Comment(8)
Thank you so much, this helps a lot! I implemented the ITemplateManager interface and I removed the GetContent method like you said. Does the setup on startup section you mentioned above go in Send()?Champac
It should go on application setup where it is executed once and before using the Engine.Razor property. However the concrete location depends on your application: For example use a static constructor, or add it to the start of Main in a console applicationOctavla
Ahh okay, that's kind of what I thought after I left the comment. I'll put it in the Application_Start() method in my MVC project. I still keep the Engine.Razor.RunCompile line in GetContent(), correct?Champac
Yes, that is correct! With this kind of setup you can pre-compile all templates in Application_Start() as well (if you want to; this will move the initial compile time delay when sending the first email to the startup time). When doing this you use Compile on startup and Run instead of RunCompile in GetContentOctavla
@Octavla What should i do if template coming from database?Pavid
@Mvcdev depends if your "Name" is unique as well: If yes you can basically do the same just read from the database, instead of reading from a file.Octavla
This answer was a great help. Just to add information, we must include namespace RazorEngine.Templating in the code file to be able to use the overload of function Engine.Razor.RunCompile as used in the answer.Rehabilitate
This was a big help. Thanks. Personally I think this API is overly complicated and it should be as simple as string content = razorEngine.Run(templateFile, model);Wellgrounded
G
0

Just as an answer for someone who ends up here, you can also use the Engine.Razor.IsTemplateCached method to choose between .Run and .RunCompile

 private static string GetEmailBody(string template, EmailClass model)
  {
      string emailBody = Task.Run(() =>
      {
          if (Engine.Razor.IsTemplateCached("T" + model.Type.ToString(), model.GetType()))
          {
              return Engine.Razor.Run("T" + model.Type.ToString(), typeof(EmailClass ), model);
          }
          else
          {
              return Engine.Razor.RunCompile(template, "T" + model.Type.ToString(), typeof(EmailClass ), model);

          }            

      }).Result;
      
     
      return emailBody;
  }
Grateful answered 18/9 at 5:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.