7

I'm struggling to make email work while working with embedded resource for _Layout.cshtml in FluentEmail.

This is the exception error I'm getting:

Project can not find template with key _Layout.cshtml

This is my setup so far:

Inside the ConfigureServices in Program.cs, I've added the RazorRenderer as:

//Set email service using FluentEmail
 services.AddFluentEmail("appname@domain.com")
 .AddRazorRenderer(typeof(Program))
 .AddSmtpSender("smtp.somesmtp.com", 25)
 .AddSmtpSender(new System.Net.Mail.SmtpClient() { });

Inside my NotificationService.cs, I always fall into that exception block:

private async Task SendEmailAsync<TModel>(string subject, TModel model)
{
    try
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            var email = await scope.ServiceProvider.GetRequiredService<IFluentEmail>()
                    .To(string.Join(";", _emailRecipients))
                    .Subject(subject)
                    .UsingTemplateFromEmbedded("AppName.Views.Emails.SomeReport.cshtml", model, GetType().Assembly)
                    .SendAsync();
        }
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to send email. Check exception for more information.");
    }
}

SomeReport.cshtml is inside Views\Emails\SomeReport.cshtml which looks like this:

@using System;
@using RazorLight;
@using System.Collections.Generic;
@using AppName.Models;

@inherits TemplatePage<IEnumerable<SomeReport>>
@{
    @* For working with Embedded Resource Views *@
    Layout = "_Layout.cshtml";
}

@* Work with the Model here... *@

_Layout.cshtml is inside Views\Shared\_Layout.cshtml which looks like this:

@using RazorLight;
@* Some common layout styles here *@
@RenderBody()

Both the SomeReport.cshtml and _Layout.cshtml are Embedded resources:

My references has RazorLight through FluentEmail.Razor package.

If it helps, this is a .NET 5 Worker Service project and I've also added PreserveCompilationContext and PreserveCompilationReferences in the .csproj file:

<PropertyGroup>
  <TargetFramework>net5.0</TargetFramework>
  <PreserveCompilationContext>true</PreserveCompilationContext>
  <PreserveCompilationReferences>true</PreserveCompilationReferences>
</PropertyGroup>

I've looked at everywhere and still haven't found a solution to this. Something this simple has been such a struggle to make work. Please help.

Thanks!

Ash K
  • 1,802
  • 17
  • 44

3 Answers3

3

TL;DR

Point to Note 1:

Specifying the path to _Layout.cshtml in your Views must be relative to the embedded resource root you enter while adding FluentEmail. For eg:

.AddRazorRenderer(typeof(Program))

Since my Program.cs and Views folder are in the same level, to locate _Layout.cshtml by SomeReport.cshtml, my path needs to be:

@{
    Layout = "Views.Shared._Layout.cshtml";
}

Point to Note 2:

Specifying the path to your View needs to be the full path starting from your Assembly name. For eg:

var razorTemplatePath = "AppName.Views.Emails.SomeReport.cshtml";

It works like a charm now!


My full setup

Inside the ConfigureServices method in Program.cs -->

// Set email service using FluentEmail
services.AddFluentEmail("appname@domain.com")
        // For Fluent Email to find _Layout.cshtml, just mention here where your views are. This took me hours to figure out. - AshishK
        //.AddRazorRenderer(@$"{Directory.GetCurrentDirectory()}/Views/")
        .AddRazorRenderer(typeof(Program)) //If you want to use views as embedded resource, use this
        .AddSmtpSender("smtp.somesmtp.com", 25)
        .AddSmtpSender(new System.Net.Mail.SmtpClient() { });

Inside my FluentEmailService.cs:

public class FluentEmailService
{
    private readonly IFluentEmailFactory _fluentEmailFactory;
    private readonly ILogger<FluentEmailService> _logger;

    // Keep in mind that only the Singleton services can be injected through the constructor - AshishK.
    public FluentEmailService(ILogger<FluentEmailService> logger, IFluentEmailFactory fluentEmailFactory)
    {
        _logger = logger;
        _fluentEmailFactory = fluentEmailFactory;
    }

    public async Task<SendResponse> SendEmailAsync<TModel>(string subject, string razorTemplatePath, TModel model, string semicolonSeparatedEmailRecipients)
    {
        try
        {
            // var razorTemplatePathExample = "AppName.Views.Emails.SomeReport.cshtml";
            // var semicolonSeparatedEmailRecipientsExample = "CoolGuy@company.com;ChillGuy@company.com";
            // 'model' is whatever you send to the View. For eg: @model IEnumerable<SomeReport> that we put at the top of SomeReport.cshtml view (see below).

            var email = await _fluentEmailFactory
                            .Create()
                            .To(semicolonSeparatedEmailRecipients)
                            .Subject(subject)
                            .UsingTemplateFromEmbedded(razorTemplatePath, model, GetType().Assembly) //If you want to use embedded resource Views.
                            .SendAsync();
            return email;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send email. Check exception for more information.");
            return new SendResponse() { ErrorMessages = new string[] { ex.Message } };
        }
    }
}

The properties of my Views (SomeReport.cshtml, _Layout.cshtml) look like this:

(You don't need to do this for _ViewImports.cshtml and _ViewStart.cshtml.)

My View SomeReport.cshtml is inside Views\Emails\SomeReport.cshtml and looks like this:

@model IEnumerable<SomeReport>

@{
    Layout = "Views.Shared._Layout.cshtml";
}

@* Work with the Model here... *@

_Layout.cshtml is inside Views\Shared\_Layout.cshtml which looks like this:

<!DOCTYPE html>
<html>
...
</html>

My references has RazorLight through FluentEmail.Razor package.

The settings for Razorlight looks like this in my .csproj file:

<PropertyGroup>
  <!-- This group contains project properties for RazorLight on .NET Core -->
  <PreserveCompilationContext>true</PreserveCompilationContext>
  <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
  <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
</PropertyGroup>

Ash K
  • 1,802
  • 17
  • 44
  • Hey thanks, I also got it working somehow after experimenting on a sample project. My case was `Views` are in Web Project and `Mailer` code is in Domain Project and Web consumes Domain Project as reference. – Jawand Singh Aug 26 '22 at 06:08
  • 1
    Thank you for saving my hairs being torn out :D This isn't even written down in the documentation, these descrptions should be copied to docs as well. Clearly explained, in and out. :) – Zsombi Jul 31 '23 at 17:35
1

Tell Razorlight that you are using embedded resources like this:

Email.DefaultRenderer = new RazorRenderer(typeof(Assembly.SomeClassInsideAssembly));

OR in Startup:

.AddRazorRenderer(typeof(Assembly.SomeClassInsideAssembly));

Then inside your view specify your layout page relative to "SomeClassInsideAssembly":

@{
    Layout = "Views._Layout.cshtml";
}
diffie
  • 11
  • 1
0

In your template instead of:

Layout = "_Layout.cshtml"

try:

Layout = ""./Shared/_Layout.cshtml""