Project can not find template with key _Layout.cshtml

1.4k views Asked by At

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("[email protected]")
 .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!

3

There are 3 answers

2
Ash K On BEST ANSWER

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("[email protected]")
        // 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 = "[email protected];[email protected]";
            // '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>

1
Matt Turnbull On

In your template instead of:

Layout = "_Layout.cshtml"

try:

Layout = ""./Shared/_Layout.cshtml""
0
diffie On

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