FluentEmail: Layouts with RazorLight cannot be resolved

Hi, I can’t use layouts in my project. Whatever I do it always ends up with

Can not resolve a content for the template "{0}" as there is no project set.You can only render a template by passing it's content directly via string using coresponding function overload

I keep all my emails in the same project inside of directory Emails (like Emails/AccountCreated.cshtml). My method looks like that:

private async Task SendEmail(EmailMessage message, CancellationToken cancellationToken)
    {
      var templatePath = $"{Directory.GetCurrentDirectory()}/Emails/{message.TemplateName}.cshtml";

      await _email
        .To(message.To)
        .Subject(message.Subject)
        .UsingTemplateFromFile(templatePath, message.Model)
        .SendAsync(cancellationToken);
    }

Example page:

@{
    Layout = "Layout.cshtml";
}

<h1>Hello World</h1>

Layout.cshtml:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title></title>
        <link href="/css/site.css" rel="stylesheet" type="text/css" />
    </head>
    <body>
        @RenderBody()
    </body>
</html>

Help 🤷‍♂️

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 17 (2 by maintainers)

Most upvoted comments

Some hints if you’re gonna encourage this problem:

  1. Allow embedded resources
services.TryAdd(ServiceDescriptor.Singleton<ITemplateRenderer, RazorRenderer>(sp =>
        new RazorRenderer(typeof(Program)))); // Type is required for embedded resources (ROOT)
  1. Allow views discovery
<EmbeddedResource Include="Views\**\*.cshtml" />
  1. Path for layout is really important! (I know I know - internalisation would be better than having different files for the same divs)
@{
  //Layout = "Layout.cshtml";
  // Layout = "./pl/Layout.cshtml"; // NOPE
  // Layout = "Views/pl/Layout.cshtml"; // 😭
  Layout = "Views.pl.Layout.cshtml"; // FINALLY!
  // Layout = "Skelvy.WebAPI.Views.pl.Layout.cshtml"; // NOT TODAY BABY!
}
  1. For production purposes
    <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> // NOPE!
   <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> // YES!
  1. Not sure about some paths?
this.GetType().GetTypeInfo().Assembly.GetManifestResourceNames() <- check this array
  1. More code in case you don’t use this library fully (for demonstration purposes - most important is path). But if you don’t care just use library abstraction:
private async Task<string> GetHtmlBody(EmailMessage message)
    {
     // full path necessary! This template uses Layout.html
      var path = $"Skelvy.WebAPI.Views.{message.Language}.{message.TemplateName}.cshtml"; 
      var template = GetResourceAsString(GetType().GetTypeInfo().Assembly, path);
      return await _templateRenderer.ParseAsync(template, message.Model);
    }

    private static string GetResourceAsString(Assembly assembly, string path)
    {
      string result;

      using (var stream = assembly.GetManifestResourceStream(path))
      using (var reader = new StreamReader(stream ?? throw new InternalServerErrorException("Could not resolve email template")))
      {
        result = reader.ReadToEnd();
      }

      return result;
    }

Thanks guys again. Have a good day!

We have the same issue. Looks like pull request #145 adds support for layouts but it’s not in a release or pre release yet. Would it be possible to push a new release?

I’m happy to help with deployment via azure devops if thats the plan.

It’s arguably abstraction overkill that FluentEmail provides much opinion at all about RazorLight API calling conventions. I’m not sure there is a ton of benefit to hiding RazorLight as a transitive dependency in FluentEmail vs. letting downstream consumers plug in RazorLight.

@rafalschmidt97 , I’m using EmbeddedResources as my email templates and I was adding razor render using DI like this:

services.AddRazorRenderer()

After checking the unit tests of FluentEMail Razor and RazorLight doc I figured out that I should pass typeof(rootType) to

services.AddRazorRenderer(typeof(rootType))

in order that my code works properly, without doing this RazorLight can’t find the TemplateKey.

In your case I think you should pass a RootPath to you RazorRenderer, bellow the code taken from FluentEmail Razor Unit Tests:

public void Should_be_able_to_use_project_layout_with_viewbag()
	    {
		    var projectRoot = Directory.GetCurrentDirectory();
		    Email.DefaultRenderer = new RazorRenderer(projectRoot);

		    string template = @"
@{
	Layout = ""./Shared/_Layout.cshtml"";
}
sup @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }";

			dynamic viewBag = new ExpandoObject();
			viewBag.Title = "Hello!";
		    var email = new Email(fromEmail)
			    .To(toEmail)
			    .Subject(subject)
			    .UsingTemplate(template, new ViewModelWithViewBag{ Name = "LUKE", Numbers = new[] { "1", "2", "3" }, ViewBag = viewBag});

		    Assert.AreEqual($"<h1>Hello!</h1>{Environment.NewLine}<div>{Environment.NewLine}sup LUKE here is a list 123</div>", email.Data.Body);
	    }

@rafalschmidt97 Thank you so much for your reply above. This worked for me. I appreciate the time you spent doing this for us.

I went with an approach that just parses a Header partial and prepends it and Footer partial that is appended to the template string. Works for with the current released version.