FluentEmail
FluentEmail copied to clipboard
RazorRender .Net core 3.0
When calling RazzoRender on .Net Core 3.0 I get the following error message Could not load type 'Microsoft.AspNetCore.Mvc.Razor.Extensions.NamespaceDirective' from assembly 'Microsoft.AspNetCore.Mvc.Razor.Extensions, Version=3.0.0.0,
Any possible solutions?
I have the same issue, there is no solution to this yet. It's because the namespace in .net core 3 changed. You need to write your own library for this.
https://github.com/Gago993/EmailClientLibrary/tree/EmailClientRazorLight/EmailClient
@kevinkrs Can you elaborate? This is fixed in RazorLight-2.0.0-beta2. The issue is FluentEmail is using beta1
@jzabroski I overcame this by installing razorlight and using that to generate the template
`Email.DefaultSender = new SmtpSender(client);
//RazorRenderer razorRenderer = new RazorRenderer();
//Email.DefaultRenderer = razorRenderer;
var engine = new RazorLightEngineBuilder()
.UseFileSystemProject($"{Directory.GetCurrentDirectory()}/wwwroot/Emails/")
.UseMemoryCachingProvider() .Build();
string generatedTemplate= await engine.CompileRenderAsync($"{Directory.GetCurrentDirectory()}/wwwroot/Emails/ThankYou.cshtml",
new
{
UserName = firstName,
BrandLogo = BrandLogo,
imageLink = imageLink,
Header = tenantRoomName,
Message = message
}).ConfigureAwait(false);
var newEmail = new Email().To(email).To("[email protected]").SetFrom("[email protected]")
.Subject(subject);
newEmail.Body(generatedTemplate, true);
newEmail.Send();`
@DominicQuickpic Just to confirm, are you using the official RazorLight nuget package or one of the forks? Trying to get everyone on the official version now that I'm maintaining it and fixing the bugs, so that I get less backflow of bogus bugs due to people using forked versions. It's a bit of herding cats, but I feel in 3-6 months time will pay off.
gave up on this package and implemented a simple own 'service' with .net SmtpClient to send emails. Then I convert a razor page to a string which the the SmtpClient can read as a big html string.
public class SmtpEmailService : IEmailService
{
private IConfiguration _configuration;
private SmtpClient _smtpClient;
public SmtpEmailService(IConfiguration configuration)
{
_configuration = configuration;
_smtpClient = new SmtpClient()
{
Host = configuration.GetValue<string>("Email:Smtp:Host"),
Port = _configuration.GetValue<int>("Email:Smtp:Port"),
Credentials = new NetworkCredential()
{
UserName = _configuration.GetValue<string>("Email:Smtp:Username"),
Password = _configuration.GetValue<string>("Email:Smtp:Password")
}
};
}
public async Task SendAsync(string to, string name, string subject, string body, List<string> attachments = null)
{
var message = new MailMessage
{
Body = body,
Subject = subject,
From = new MailAddress(_configuration.GetValue<string>("Email:Smtp:From"), name),
IsBodyHtml = true,
};
message.To.Add(to);
if (attachments != null)
{
foreach (var attachment in attachments)
{
Attachment data = new Attachment(attachment);
message.Attachments.Add(data);
}
}
await _smtpClient.SendMailAsync(message);
}
}
In my scenario I needed to send an invoice as attachment. No good for to use PDF libraries were there, so I used a package which use a browser to visit the page and convert it to PDF.
https://github.com/kblok/puppeteer-sharp
It renders a html page like your browser will see it and convert and saves it to pdf format. The end result is just perfect without hacky code.
public class PdfService : IPdfService
{
public async Task GeneratePdfAsync(string url, string outputName)
{
await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
});
var page = await browser.NewPageAsync();
await page.GoToAsync(url);
await page.PdfAsync(outputName + ".pdf");
}
}
@kevinkrs Sorry - did you give up on FluentEmail, or RazorLight? Thanks.
The best PDF library is Aspose. It's fantastic, but requires a paid license. The whole Aspose library is straight up awesome and saves me tons of time. Not everything can be free.
As far as converting a page to PDF with a browser, that's one general trick I mention in StackOverflow: https://stackoverflow.com/a/20155287/1040437 - but it can ALSO be used as an intermediary step for Aspose if you send an SVG file back to your server.
I gave up on FluentEmail as well on RazorLight, both gave me errors which gives me not a lot of hope on future .NET core updates. I also don't wanna have a lot of dependencies on my project for that matter.
I did not try the beta version tho, but I'm trying to void beta in production applications.
And about the PDF, I know there are a few amazing libraries, for example IronPDF is just great, within 2 minutes I was already done.
I also get that not everything is free, and I don't mind to pay, BUT..... IronPDF is 3.000 euro a year, and Aspose is also 3.000 euro. Those are not normal licenses fees anymore, not completely worth it imo for just PDF invoice generation. Alltho I will look in the future for better alternatives.
Would love to try to recruit you back to RazorLight. I use it and became a PR access person with Nuget package upload privileges because I would rather not write my own. Sometimes its better to adopt an existing religion than create your own :)
As far as FluentEmail goes, helping out here is probably next on my list. These problems all seem easy to fix. Just time required.
Time is correct. Hoping to get to most of these problems over the christmas break.
Would love to try to recruit you back to RazorLight. I use it and became a PR access person with Nuget package upload privileges because I would rather not write my own. Sometimes its better to adopt an existing religion than create your own :)
As far as FluentEmail goes, helping out here is probably next on my list. These problems all seem easy to fix. Just time required.
The only thing I have to do is transfer a razor view into a html string. For all other things I use the microsoft razor package.
public class RazorPartialToStringRenderer : IRazorPartialToStringRenderer
{
private IRazorViewEngine _viewEngine;
private ITempDataProvider _tempDataProvider;
private IServiceProvider _serviceProvider;
public RazorPartialToStringRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model)
{
var actionContext = GetActionContext();
var partial = FindView(actionContext, partialName);
using (var output = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
partial,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions()
);
await partial.RenderAsync(viewContext);
return output.ToString();
}
}
private IView FindView(ActionContext actionContext, string partialName)
{
var getPartialResult = _viewEngine.GetView(null, partialName, false);
if (getPartialResult.Success)
{
return getPartialResult.View;
}
var findPartialResult = _viewEngine.FindView(actionContext, partialName, false);
if (findPartialResult.Success)
{
return findPartialResult.View;
}
var searchedLocations = getPartialResult.SearchedLocations.Concat(findPartialResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find partial '{partialName}'. The following locations were searched:" }.Concat(searchedLocations)); ;
throw new InvalidOperationException(errorMessage);
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
@bjcull Cool. I just released beta4, and it looks like it might be a bad beta due to the insanity that is the .NET Core Linker - If you upgrade over the Christmas break just reach out to me on KeyBase and we can coordinate any fixes to unblock you. (I feel dumb every time I try to target things in the new .NET Core Multiverse of target frameworks - but I'm getting better at understanding Microsoft's odd approach to dependency management.)
@kevinkrs I mean, the point to use RazorLight (and FluentEmail) is to not run your email engine inside an HttpContext. At least, that is my rational for using such projects. We use something similar to .NET BullsEye library to create a TaskRunner that runs an ITask
implementation - there is no DefaultHttpContext
because it's just a command line program that executes jobs/tasks. I suppose you can always have your ITask
hit a kestrel web server, but for my taste that is a lot more complicated to debug than just saying, "run this task".
Couldn't actually replicate this one with .net core 3.1 and razorlight beta-1. Also confused by this issue which says it still exists: https://github.com/toddams/RazorLight/issues/273
I'll keep an eye on this but assumed it's fixed under the latest beta.
Fixed by #186
this only occurs when using services.AddFluentEmail(mailOptions.FromAddress, mailOptions.FromName) .AddRazorRenderer(); <-- need to add a type here
You used to be able to write:
var razorEngine = new RazorLightEngineBuilder() .UseMemoryCachingProvider() .Build(); ... but this now throws an exception, saying, "_razorLightProject cannot be null".
✔️
var razorEngine = new RazorLightEngineBuilder() .UseEmbeddedResourcesProject(typeof(AnyTypeInYourSolution)) // exception without this (or another project type) .UseMemoryCachingProvider() .Build(); Affects: RazorLight-2.0.0-beta1 and later.
I see #186 was never merged?
@heinecorp Is your comment related to PR #186 or without PR #186?
@kevinkrs Can you elaborate? This is fixed in RazorLight-2.0.0-beta2. The issue is FluentEmail is using beta1
If you're maintaining it, why is the latest NuGet version (2.7.0) still targeting RazorLight beta1? Can't you release a new version? I'm getting this error with my .NET Core 3.1 project, too.
Is this something that's going to get fixed to work with .NET Core 3.1, or should I find another way of using templates to send emails?
@steveketchum I'd recommend dropping FluentEmail and just using SmtpClient.SendMailAsync()
with the latest RazorLight beta, passing in compiled templates for the body. That worked for me.
I have similar problem and solved it like this.
Remove FluentEmail.Razor form project.
Add custom renderer EmbeddedRazorRenderer.cs:
using FluentEmail.Core.Interfaces;
using FluentEmail.Razor;
using RazorLight;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Common.Utils.AspNetCore.Infrastructure.Mailers
{
/// <summary>
/// Based on: https://github.com/lukencode/FluentEmail/blob/master/src/Renderers/FluentEmail.Razor/RazorRenderer.cs
/// + doc https://github.com/toddams/RazorLight#embeddedresource-source
///
/// After install Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
/// FluentEmail.Razor stop working (not support core 3.0)
/// So we remove reference to FluentEmail.Razor and use RazorLight >= 2.0.0-beta4 directly
/// https://github.com/lukencode/FluentEmail/issues/184
/// https://github.com/lukencode/FluentEmail/pull/186
/// </summary>
public class EmbeddedRazorRenderer : ITemplateRenderer
{
private readonly RazorLightEngine _engine;
public EmbeddedRazorRenderer(Type embeddedResRootType)
{
_engine = new RazorLightEngineBuilder()
.UseEmbeddedResourcesProject(embeddedResRootType)
.UseMemoryCachingProvider()
.Build();
}
public async Task<string> ParseAsync<T>(string path, T model, bool isHtml = true)
{
dynamic viewBag = (model as IViewBagModel)?.ViewBag;
return await _engine.CompileRenderAsync<T>(path, model, viewBag);
}
string ITemplateRenderer.Parse<T>(string path, T model, bool isHtml)
{
return ParseAsync(path, model, isHtml).GetAwaiter().GetResult();
}
}
}
Also need to add interface for IViewBagModel
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Text;
/// <summary>
/// We have to remove reference to FluentEmail.Razor (not support core 3.0)
/// For now this is only one thing we need from this lib
/// </summary>
namespace FluentEmail.Razor
{
public interface IViewBagModel
{
ExpandoObject ViewBag { get; }
}
}
Custom factory which assigns custom renderer:
using Common.Utils.AspNetCore.Infrastructure.Mailers;
using FluentEmail.Core;
using FluentEmail.Core.Interfaces;
using Identity.Service.Infrastructure.Mailers.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Identity.Service.Infrastructure.Mailers
{
public interface IIdentityServiceFluentEmailFactory : IFluentEmailFactory
{
}
public class IdentityServiceFluentEmailFactory : IIdentityServiceFluentEmailFactory
{
private readonly IFluentEmailFactory _fluentEmailFactory;
private readonly ITemplateRenderer _templateRednerer;
public IdentityServiceFluentEmailFactory(IFluentEmailFactory fluentEmailFactory)
{
_fluentEmailFactory = fluentEmailFactory;
_templateRednerer = new EmbeddedRazorRenderer(typeof(DummyViewsRootType));
}
public IFluentEmail Create()
{
var email = _fluentEmailFactory.Create();
email.Renderer = _templateRednerer;
return email;
}
}
}
Plus bonus. Custom extension to work with EmbeddedResources With this you don't have to pass assembly. It is also works with partials, layouts etc.
using Common.Models.Email;
using Common.Utils.AspNetCore.Infrastructure.Mailers;
using FluentEmail.Core;
using FluentEmail.Core.Models;
using FluentEmail.Razor;
using System;
using System.Collections.Generic;
using System.Text;
namespace Common.Utils.AspNetCore.Infrastructure.Mailers
{
public static class IFluentEmailExtensions
{
/// <summary>
/// Based on: https://github.com/lukencode/FluentEmail/blob/master/src/FluentEmail.Core/Email.cs#L305
/// For RazorLight based on embedded resources there is no need to read assembly ourself like in original project
/// </summary>
public static IFluentEmail UsingTemplateFromEmbedded<T>(this IFluentEmail email, string path, T model, bool isHtml = true)
{
if (email.Renderer is EmbeddedRazorRenderer)
{
var result = email.Renderer.Parse(path, model, isHtml);
email.Data.IsHtml = isHtml;
email.Data.Body = result;
return email;
}
throw new InvalidOperationException($"Only {nameof(EmbeddedRazorRenderer)} renderer is supported");
}
}
}
Example of using all together:
var email = _fluentEmailFactory.Create()
.To(model.To)
.Subject("Reset password")
.UsingTemplateFromEmbedded("Account.ResetPassword.cshtml", viewModel, true);
I hope it will helps you a bit ;)
Regards, Mateusz
I solved this problem with "Reo.Core.FluentEmail.RazorEngine" nuget package. In Startup.cs file (ConfigureServices) you could use the following configuration:
services.AddFluentEmail("DefaultSenderAddress", "DefaultSenderTitle") .AddRazorRenderer() .AddSmtpSender(smtpConfig);
Email.DefaultRenderer = new Reo.Core.FluentEmail.RazorEngine.RazorRenderer();
Email.DefaultSender = new SmtpSender(smtpConfig);