lukencode / FluentEmail

All in one email sender for .NET. Supports popular senders (SendGrid, MailGun, etc) and Razor templates.
https://lukelowrey.com/dotnet-email-guide-2021/
MIT License
2.99k stars 431 forks source link

RazorRender .Net core 3.0 #184

Open DominicQuickpic opened 4 years ago

DominicQuickpic commented 4 years ago

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?

kroos010 commented 4 years ago

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

jzabroski commented 4 years ago

@kevinkrs Can you elaborate? This is fixed in RazorLight-2.0.0-beta2. The issue is FluentEmail is using beta1

DominicQuickpic commented 4 years ago

@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("info@gmail.co.za").SetFrom("info@gmail.co.za")
                                     .Subject(subject);

            newEmail.Body(generatedTemplate, true);
            newEmail.Send();`
jzabroski commented 4 years ago

@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.

kroos010 commented 4 years ago

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");
        }
    }
jzabroski commented 4 years ago

@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.

kroos010 commented 4 years ago

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.

jzabroski commented 4 years ago

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.

bjcull commented 4 years ago

Time is correct. Hoping to get to most of these problems over the christmas break.

kroos010 commented 4 years ago

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());
        }
    }
jzabroski commented 4 years ago

@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".

bjcull commented 4 years ago

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.

bjcull commented 4 years ago

Fixed by #186

heinecorp commented 4 years ago

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.

jzabroski commented 4 years ago

I see #186 was never merged?

@heinecorp Is your comment related to PR #186 or without PR #186?

jez9999 commented 4 years ago

@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.

steveketchum commented 4 years ago

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?

jez9999 commented 4 years ago

@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.

Karql commented 4 years ago

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

MertKaygusuz commented 3 years ago

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);