From 081e42e16a4b6f132ce8ee934a0684ca5d002e4a Mon Sep 17 00:00:00 2001 From: Florian van Dillen Date: Tue, 9 Feb 2021 13:56:26 +0100 Subject: [PATCH 01/16] Add support for SendGrid dynamic templates. https://sendgrid.com/docs/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates/ --- .../IFluentEmailExtensions.cs | 18 ++++++ .../FluentEmail.SendGrid/ISendGridSender.cs | 27 +++++++++ .../FluentEmail.SendGrid/SendGridSender.cs | 57 +++++++++++++------ .../SendGridSenderTests.cs | 21 +++++++ 4 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 src/Senders/FluentEmail.SendGrid/IFluentEmailExtensions.cs create mode 100644 src/Senders/FluentEmail.SendGrid/ISendGridSender.cs diff --git a/src/Senders/FluentEmail.SendGrid/IFluentEmailExtensions.cs b/src/Senders/FluentEmail.SendGrid/IFluentEmailExtensions.cs new file mode 100644 index 00000000..459acaa0 --- /dev/null +++ b/src/Senders/FluentEmail.SendGrid/IFluentEmailExtensions.cs @@ -0,0 +1,18 @@ +using FluentEmail.Core; +using FluentEmail.Core.Models; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace FluentEmail.SendGrid +{ + public static class IFluentEmailExtensions + { + public static async Task SendWithTemplateAsync(this IFluentEmail email, string templateId, object templateData) + { + var sendGridSender = email.Sender as ISendGridSender; + return await sendGridSender.SendWithTemplateAsync(email, templateId, templateData); + } + } +} diff --git a/src/Senders/FluentEmail.SendGrid/ISendGridSender.cs b/src/Senders/FluentEmail.SendGrid/ISendGridSender.cs new file mode 100644 index 00000000..3dd989f4 --- /dev/null +++ b/src/Senders/FluentEmail.SendGrid/ISendGridSender.cs @@ -0,0 +1,27 @@ +using FluentEmail.Core; +using FluentEmail.Core.Interfaces; +using FluentEmail.Core.Models; +using SendGrid; +using SendGrid.Helpers.Mail; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentEmail.SendGrid +{ + public interface ISendGridSender : ISender + { + /// + /// SendGrid specific extension method that allows you to use a template instead of a message body. + /// For more information, see: https://sendgrid.com/docs/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates/. + /// + /// Fluent email. + /// SendGrid template ID. + /// SendGrid template data. + /// Optional cancellation token. + /// A SendResponse object. + Task SendWithTemplateAsync(IFluentEmail email, string templateId, object templateData, CancellationToken? token = null); + } +} diff --git a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs index 889bee25..7be7cdce 100644 --- a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs +++ b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs @@ -13,7 +13,7 @@ namespace FluentEmail.SendGrid { - public class SendGridSender : ISender + public class SendGridSender : ISendGridSender { private readonly string _apiKey; private readonly bool _sandBoxMode; @@ -30,8 +30,41 @@ public SendResponse Send(IFluentEmail email, CancellationToken? token = null) public async Task SendAsync(IFluentEmail email, CancellationToken? token = null) { - var sendGridClient = new SendGridClient(_apiKey); + var mailMessage = await BuildSendGridMessage(email); + + if (email.Data.IsHtml) + { + mailMessage.HtmlContent = email.Data.Body; + } + else + { + mailMessage.PlainTextContent = email.Data.Body; + } + + if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) + { + mailMessage.PlainTextContent = email.Data.PlaintextAlternativeBody; + } + + var sendResponse = await SendViaSendGrid(mailMessage, token); + + return sendResponse; + } + + public async Task SendWithTemplateAsync(IFluentEmail email, string templateId, object templateData, CancellationToken? token = null) + { + var mailMessage = await BuildSendGridMessage(email); + + mailMessage.SetTemplateId(templateId); + mailMessage.SetTemplateData(templateData); + + var sendResponse = await SendViaSendGrid(mailMessage, token); + return sendResponse; + } + + private async Task BuildSendGridMessage(IFluentEmail email) + { var mailMessage = new SendGridMessage(); mailMessage.SetSandBoxMode(_sandBoxMode); @@ -57,15 +90,6 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? mailMessage.AddHeaders(email.Data.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); } - if (email.Data.IsHtml) - { - mailMessage.HtmlContent = email.Data.Body; - } - else - { - mailMessage.PlainTextContent = email.Data.Body; - } - switch (email.Data.Priority) { case Priority.High: @@ -92,11 +116,6 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? break; } - if (!string.IsNullOrEmpty(email.Data.PlaintextAlternativeBody)) - { - mailMessage.PlainTextContent = email.Data.PlaintextAlternativeBody; - } - if (email.Data.Attachments.Any()) { foreach (var attachment in email.Data.Attachments) @@ -107,6 +126,12 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? } } + return mailMessage; + } + + private async Task SendViaSendGrid(SendGridMessage mailMessage, CancellationToken? token = null) + { + var sendGridClient = new SendGridClient(_apiKey); var sendGridResponse = await sendGridClient.SendEmailAsync(mailMessage, token.GetValueOrDefault()); var sendResponse = new SendResponse(); diff --git a/test/FluentEmail.Core.Tests/SendGridSenderTests.cs b/test/FluentEmail.Core.Tests/SendGridSenderTests.cs index 18e5ddd6..6ce9dd56 100644 --- a/test/FluentEmail.Core.Tests/SendGridSenderTests.cs +++ b/test/FluentEmail.Core.Tests/SendGridSenderTests.cs @@ -42,6 +42,27 @@ public async Task CanSendEmail() Assert.IsTrue(response.Successful); } + [Test, Ignore("No sendgrid credentials")] + public async Task CanSendTemplateEmail() + { + const string subject = "SendMail Test"; + const string templateId = "123456-insert-your-own-id-here"; + object templateData = new + { + Name = toName, + ArbitraryValue = "The quick brown fox jumps over the lazy dog." + }; + + var email = Email + .From(fromEmail, fromName) + .To(toEmail, toName) + .Subject(subject); + + var response = await email.SendWithTemplateAsync(templateId, templateData); + + Assert.IsTrue(response.Successful); + } + [Test, Ignore("No sendgrid credentials")] public async Task CanSendEmailWithReplyTo() { From e519482e7897046746bec179e37149d15a26d140 Mon Sep 17 00:00:00 2001 From: Atakan Ozceviz Date: Thu, 18 Mar 2021 17:46:22 +0100 Subject: [PATCH 02/16] fix SmtpSender service lifetime --- .../FluentEmail.Smtp/FluentEmailSmtpBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.Smtp/FluentEmailSmtpBuilderExtensions.cs b/src/Senders/FluentEmail.Smtp/FluentEmailSmtpBuilderExtensions.cs index ddc7a601..0fabbca8 100644 --- a/src/Senders/FluentEmail.Smtp/FluentEmailSmtpBuilderExtensions.cs +++ b/src/Senders/FluentEmail.Smtp/FluentEmailSmtpBuilderExtensions.cs @@ -22,7 +22,7 @@ public static FluentEmailServicesBuilder AddSmtpSender(this FluentEmailServicesB public static FluentEmailServicesBuilder AddSmtpSender(this FluentEmailServicesBuilder builder, Func clientFactory) { - builder.Services.TryAdd(ServiceDescriptor.Scoped(_ => new SmtpSender(clientFactory))); + builder.Services.TryAdd(ServiceDescriptor.Singleton(_ => new SmtpSender(clientFactory))); return builder; } } From d3cddcbd8622865bbfc996fc7e6e1c24642416c4 Mon Sep 17 00:00:00 2001 From: Kevin Petit Date: Tue, 17 Aug 2021 17:40:11 +0200 Subject: [PATCH 03/16] Update Liquid renderer to Fluid latest version --- .../FluentEmail.Liquid.csproj | 2 +- .../FluentEmail.Liquid/FluidViewTemplate.cs | 17 ---- .../FluentEmail.Liquid/LiquidParser.cs | 84 +++++++++++++++++++ .../FluentEmail.Liquid/LiquidRenderer.cs | 17 ++-- .../FluentEmail.Liquid/Tags/LayoutTag.cs | 28 ------- .../Tags/RegisterSectionBlock.cs | 26 ------ .../FluentEmail.Liquid/Tags/RenderBodyTag.cs | 31 ------- .../Tags/RenderSectionTag.cs | 30 ------- test/FluentEmail.Liquid.Tests/LiquidTests.cs | 10 +-- 9 files changed, 97 insertions(+), 148 deletions(-) delete mode 100644 src/Renderers/FluentEmail.Liquid/FluidViewTemplate.cs create mode 100644 src/Renderers/FluentEmail.Liquid/LiquidParser.cs delete mode 100644 src/Renderers/FluentEmail.Liquid/Tags/LayoutTag.cs delete mode 100644 src/Renderers/FluentEmail.Liquid/Tags/RegisterSectionBlock.cs delete mode 100644 src/Renderers/FluentEmail.Liquid/Tags/RenderBodyTag.cs delete mode 100644 src/Renderers/FluentEmail.Liquid/Tags/RenderSectionTag.cs diff --git a/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj b/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj index b628429d..dceefbab 100644 --- a/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj +++ b/src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Renderers/FluentEmail.Liquid/FluidViewTemplate.cs b/src/Renderers/FluentEmail.Liquid/FluidViewTemplate.cs deleted file mode 100644 index 21477075..00000000 --- a/src/Renderers/FluentEmail.Liquid/FluidViewTemplate.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentEmail.Liquid.Tags; - -using Fluid; - -namespace FluentEmail.Liquid -{ - public class FluidViewTemplate : BaseFluidTemplate - { - static FluidViewTemplate() - { - Factory.RegisterTag("layout"); - Factory.RegisterTag("renderbody"); - Factory.RegisterBlock("section"); - Factory.RegisterTag("rendersection"); - } - } -} diff --git a/src/Renderers/FluentEmail.Liquid/LiquidParser.cs b/src/Renderers/FluentEmail.Liquid/LiquidParser.cs new file mode 100644 index 00000000..00351086 --- /dev/null +++ b/src/Renderers/FluentEmail.Liquid/LiquidParser.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Fluid; +using Fluid.Ast; + +public class LiquidParser : FluidParser +{ + public LiquidParser() + { + RegisterExpressionTag("layout", OnRegisterLayoutTag); + RegisterEmptyTag("renderbody", OnRegisterRenderBodyTag); + RegisterIdentifierBlock("section", OnRegisterSectionBlock); + RegisterIdentifierTag("rendersection", OnRegisterSectionTag); + } + + private async ValueTask OnRegisterLayoutTag(Expression expression, TextWriter writer, TextEncoder encoder, TemplateContext context) + { + const string viewExtension = ".liquid"; + + var relativeLayoutPath = (await expression.EvaluateAsync(context)).ToStringValue(); + + if (!relativeLayoutPath.EndsWith(viewExtension, StringComparison.OrdinalIgnoreCase)) + { + relativeLayoutPath += viewExtension; + } + + context.AmbientValues["Layout"] = relativeLayoutPath; + + return Completion.Normal; + } + + private async ValueTask OnRegisterRenderBodyTag(TextWriter writer, TextEncoder encoder, TemplateContext context) + { + static void ThrowParseException() + { + throw new ParseException("Could not render body, Layouts can't be evaluated directly."); + } + + if (context.AmbientValues.TryGetValue("Body", out var body)) + { + await writer.WriteAsync((string)body); + } + else + { + ThrowParseException(); + } + + return Completion.Normal; + } + + private ValueTask OnRegisterSectionBlock(string sectionName, IReadOnlyList statements, TextWriter writer, TextEncoder encoder, TemplateContext context) + { + if (context.AmbientValues.TryGetValue("Sections", out var sections)) + { + var dictionary = (Dictionary>) sections; + + dictionary[sectionName] = statements.ToList(); + } + + return new ValueTask(Completion.Normal); + } + + private async ValueTask OnRegisterSectionTag(string sectionName, TextWriter writer, TextEncoder encoder, TemplateContext context) + { + if (context.AmbientValues.TryGetValue("Sections", out var sections)) + { + var dictionary = (Dictionary>) sections; + + if (dictionary.TryGetValue(sectionName, out var section)) + { + foreach(var statement in section) + { + await statement.WriteToAsync(writer, encoder, context); + } + } + } + + return Completion.Normal; + } +} \ No newline at end of file diff --git a/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs b/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs index fb916603..ab92cbe0 100644 --- a/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs +++ b/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs @@ -12,13 +12,13 @@ namespace FluentEmail.Liquid { public class LiquidRenderer : ITemplateRenderer { - private static readonly Func FluidTemplateFactory = () => new FluidViewTemplate(); - private readonly IOptions _options; + private readonly LiquidParser _parser; public LiquidRenderer(IOptions options) { _options = options; + _parser = new LiquidParser(); } public string Parse(string template, T model, bool isHtml = true) @@ -42,9 +42,10 @@ public async Task ParseAsync(string template, T model, bool isHtml = ["FileProvider"] = fileProvider, ["Sections"] = new Dictionary>() }, - ParserFactory = FluidViewTemplate.Factory, - TemplateFactory = FluidTemplateFactory, - FileProvider = fileProvider + Options = + { + FileProvider = fileProvider + } }; rendererOptions.ConfigureTemplateContext?.Invoke(context, model!); @@ -63,7 +64,7 @@ public async Task ParseAsync(string template, T model, bool isHtml = return body; } - private static FluidViewTemplate ParseLiquidFile( + private IFluidTemplate ParseLiquidFile( string path, IFileProvider? fileProvider) { @@ -85,9 +86,9 @@ static void ThrowMissingFileProviderException() return ParseTemplate(sr.ReadToEnd()); } - private static FluidViewTemplate ParseTemplate(string content) + private IFluidTemplate ParseTemplate(string content) { - if (!FluidViewTemplate.TryParse(content, out var template, out var errors)) + if (!_parser.TryParse(content, out var template, out var errors)) { throw new Exception(string.Join(Environment.NewLine, errors)); } diff --git a/src/Renderers/FluentEmail.Liquid/Tags/LayoutTag.cs b/src/Renderers/FluentEmail.Liquid/Tags/LayoutTag.cs deleted file mode 100644 index 2b2b5cdf..00000000 --- a/src/Renderers/FluentEmail.Liquid/Tags/LayoutTag.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; - -using Fluid; -using Fluid.Ast; -using Fluid.Tags; - -namespace FluentEmail.Liquid.Tags -{ - internal sealed class LayoutTag : ExpressionTag - { - private const string ViewExtension = ".liquid"; - - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, Expression expression) - { - var relativeLayoutPath = (await expression.EvaluateAsync(context)).ToStringValue(); - if (!relativeLayoutPath.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)) - { - relativeLayoutPath += ViewExtension; - } - - context.AmbientValues["Layout"] = relativeLayoutPath; - return Completion.Normal; - } - } -} diff --git a/src/Renderers/FluentEmail.Liquid/Tags/RegisterSectionBlock.cs b/src/Renderers/FluentEmail.Liquid/Tags/RegisterSectionBlock.cs deleted file mode 100644 index a5151466..00000000 --- a/src/Renderers/FluentEmail.Liquid/Tags/RegisterSectionBlock.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Fluid; -using Fluid.Ast; -using Fluid.Tags; - -namespace FluentEmail.Liquid.Tags -{ - internal sealed class RegisterSectionBlock : IdentifierBlock - { - private static readonly ValueTask Normal = new ValueTask(Completion.Normal); - - public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, string sectionName, List statements) - { - if (context.AmbientValues.TryGetValue("Sections", out var sections)) - { - var dictionary = (Dictionary>) sections; - dictionary[sectionName] = statements; - } - - return Normal; - } - } -} diff --git a/src/Renderers/FluentEmail.Liquid/Tags/RenderBodyTag.cs b/src/Renderers/FluentEmail.Liquid/Tags/RenderBodyTag.cs deleted file mode 100644 index 7a65af2b..00000000 --- a/src/Renderers/FluentEmail.Liquid/Tags/RenderBodyTag.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Fluid; -using Fluid.Ast; -using Fluid.Tags; - -namespace FluentEmail.Liquid.Tags -{ - internal sealed class RenderBodyTag : SimpleTag - { - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) - { - static void ThrowParseException() - { - throw new ParseException("Could not render body, Layouts can't be evaluated directly."); - } - - if (context.AmbientValues.TryGetValue("Body", out var body)) - { - await writer.WriteAsync((string)body); - } - else - { - ThrowParseException(); - } - - return Completion.Normal; - } - } -} diff --git a/src/Renderers/FluentEmail.Liquid/Tags/RenderSectionTag.cs b/src/Renderers/FluentEmail.Liquid/Tags/RenderSectionTag.cs deleted file mode 100644 index a5a15cc0..00000000 --- a/src/Renderers/FluentEmail.Liquid/Tags/RenderSectionTag.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Fluid; -using Fluid.Ast; -using Fluid.Tags; - -namespace FluentEmail.Liquid.Tags -{ - internal sealed class RenderSectionTag : IdentifierTag - { - public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, string sectionName) - { - if (context.AmbientValues.TryGetValue("Sections", out var sections)) - { - var dictionary = (Dictionary>) sections; - if (dictionary.TryGetValue(sectionName, out var section)) - { - foreach(var statement in section) - { - await statement.WriteToAsync(writer, encoder, context); - } - } - } - - return Completion.Normal; - } - } -} diff --git a/test/FluentEmail.Liquid.Tests/LiquidTests.cs b/test/FluentEmail.Liquid.Tests/LiquidTests.cs index 368e20e0..2d53e3b9 100644 --- a/test/FluentEmail.Liquid.Tests/LiquidTests.cs +++ b/test/FluentEmail.Liquid.Tests/LiquidTests.cs @@ -22,7 +22,7 @@ public class LiquidTests public void SetUp() { // default to have no file provider, only required when layout files are in use - SetupRenderer(null); + SetupRenderer(); } private static void SetupRenderer( @@ -51,7 +51,6 @@ public void Model_With_List_Template_Matches() Assert.AreEqual("sup LUKE here is a list 123", email.Data.Body); } - [Test] public void Custom_Context_Values() { @@ -155,8 +154,7 @@ public void Should_be_able_to_use_project_layout() { SetupRenderer(new PhysicalFileProvider(Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory!.FullName, "EmailTemplates"))); - const string template = @" -{% layout '_layout.liquid' %} + const string template = @"{% layout '_layout.liquid' %} sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; var email = new Email(FromEmail) @@ -167,14 +165,12 @@ public void Should_be_able_to_use_project_layout() Assert.AreEqual($"

Hello!

{Environment.NewLine}
{Environment.NewLine}sup LUKE here is a list 123
", email.Data.Body); } - [Test] public void Should_be_able_to_use_embedded_layout() { SetupRenderer(new EmbeddedFileProvider(typeof(LiquidTests).Assembly, "FluentEmail.Liquid.Tests.EmailTemplates")); - const string template = @" -{% layout '_embedded.liquid' %} + const string template = @"{% layout '_embedded.liquid' %} sup {{ Name }} here is a list {% for i in Numbers %}{{ i }}{% endfor %}"; var email = new Email(FromEmail) From e10a74b31f04b7e6123c11f66ae23d6ed363bea5 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Sun, 19 Sep 2021 21:47:18 +1000 Subject: [PATCH 04/16] feat (Liquid): Add TemplateOptions as a configurable property. Added test for complex object. --- .../FluentEmail.Liquid/LiquidRenderer.cs | 4 +- .../LiquidRendererOptions.cs | 5 ++ .../ComplexModel/ComplexModelRenderTests.cs | 71 +++++++++++++++++++ .../ComplexModel/ParentModel.cs | 17 +++++ .../ComplexModel/TestData.cs | 9 +++ .../FluentEmail.Liquid.Tests.csproj | 1 + 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 test/FluentEmail.Liquid.Tests/ComplexModel/ComplexModelRenderTests.cs create mode 100644 test/FluentEmail.Liquid.Tests/ComplexModel/ParentModel.cs create mode 100644 test/FluentEmail.Liquid.Tests/ComplexModel/TestData.cs diff --git a/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs b/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs index ab92cbe0..fe53645a 100644 --- a/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs +++ b/src/Renderers/FluentEmail.Liquid/LiquidRenderer.cs @@ -34,7 +34,7 @@ public async Task ParseAsync(string template, T model, bool isHtml = var fileProvider = rendererOptions.FileProvider; var viewTemplate = ParseTemplate(template); - var context = new TemplateContext(model) + var context = new TemplateContext(model, rendererOptions.TemplateOptions) { // provide some services to all statements AmbientValues = @@ -56,7 +56,7 @@ public async Task ParseAsync(string template, T model, bool isHtml = if (context.AmbientValues.TryGetValue("Layout", out var layoutPath)) { context.AmbientValues["Body"] = body; - var layoutTemplate = ParseLiquidFile((string) layoutPath, fileProvider!); + var layoutTemplate = ParseLiquidFile((string)layoutPath, fileProvider!); return await layoutTemplate.RenderAsync(context, rendererOptions.TextEncoder); } diff --git a/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs b/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs index feed0c4a..8f8df34b 100644 --- a/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs +++ b/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs @@ -25,5 +25,10 @@ public class LiquidRendererOptions /// File provider to use, used when resolving references in templates, like master layout. /// public IFileProvider? FileProvider { get; set; } + + /// + /// Set custom Template Options for Fluid + /// + public TemplateOptions TemplateOptions { get; set; } = new(); } } \ No newline at end of file diff --git a/test/FluentEmail.Liquid.Tests/ComplexModel/ComplexModelRenderTests.cs b/test/FluentEmail.Liquid.Tests/ComplexModel/ComplexModelRenderTests.cs new file mode 100644 index 00000000..04e642ee --- /dev/null +++ b/test/FluentEmail.Liquid.Tests/ComplexModel/ComplexModelRenderTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using FluentEmail.Core; +using Fluid; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using NUnit.Framework; + +namespace FluentEmail.Liquid.Tests.ComplexModel +{ + public class ComplexModelRenderTests + { + public ComplexModelRenderTests() + { + SetupRenderer(); + } + + [Test] + public void Can_Render_Complex_Model_Properties() + { + var model = new ParentModel + { + ParentName = new NameDetails { Firstname = "Luke", Surname = "Dinosaur" }, + ChildrenNames = new List + { + new NameDetails { Firstname = "ChildFirstA", Surname = "ChildLastA" }, + new NameDetails { Firstname = "ChildFirstB", Surname = "ChildLastB" } + } + }; + + var expected = @" +Parent: Luke +Children: + +* ChildFirstA ChildLastA +* ChildFirstB ChildLastB +"; + + var email = Email + .From(TestData.FromEmail) + .To(TestData.ToEmail) + .Subject(TestData.Subject) + .UsingTemplate(Template(), model); + email.Data.Body.Should().Be(expected); + } + + private string Template() + { + return @" +Parent: {{ ParentName.Firstname }} +Children: +{% for Child in ChildrenNames %} +* {{ Child.Firstname }} {{ Child.Surname }}{% endfor %} +"; + } + + private static void SetupRenderer( + IFileProvider fileProvider = null, + Action configureTemplateContext = null) + { + var options = new LiquidRendererOptions + { + FileProvider = fileProvider, + ConfigureTemplateContext = configureTemplateContext, + TemplateOptions = new TemplateOptions { MemberAccessStrategy = new UnsafeMemberAccessStrategy() } + }; + Email.DefaultRenderer = new LiquidRenderer(Options.Create(options)); + } + } +} \ No newline at end of file diff --git a/test/FluentEmail.Liquid.Tests/ComplexModel/ParentModel.cs b/test/FluentEmail.Liquid.Tests/ComplexModel/ParentModel.cs new file mode 100644 index 00000000..029f3449 --- /dev/null +++ b/test/FluentEmail.Liquid.Tests/ComplexModel/ParentModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace FluentEmail.Liquid.Tests +{ + public class ParentModel + { + public string Id { get; set; } + public NameDetails ParentName { get; set; } + public List ChildrenNames { get; set; } = new List(); + } + + public class NameDetails + { + public string Firstname { get; set; } + public string Surname { get; set; } + } +} \ No newline at end of file diff --git a/test/FluentEmail.Liquid.Tests/ComplexModel/TestData.cs b/test/FluentEmail.Liquid.Tests/ComplexModel/TestData.cs new file mode 100644 index 00000000..e1dd598c --- /dev/null +++ b/test/FluentEmail.Liquid.Tests/ComplexModel/TestData.cs @@ -0,0 +1,9 @@ +namespace FluentEmail.Liquid.Tests +{ + public static class TestData + { + public const string ToEmail = "bob@test.com"; + public const string FromEmail = "johno@test.com"; + public const string Subject = "sup dawg"; + } +} \ No newline at end of file diff --git a/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj b/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj index e2363f10..e2c5dcfc 100644 --- a/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj +++ b/test/FluentEmail.Liquid.Tests/FluentEmail.Liquid.Tests.csproj @@ -21,6 +21,7 @@ + From f68bd530757489f029495a38da108d13805dce47 Mon Sep 17 00:00:00 2001 From: Jason Finch Date: Wed, 29 Sep 2021 09:31:02 +1000 Subject: [PATCH 05/16] Update readme - Add Example to IFluentEmail usage. --- README.markdown | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.markdown b/README.markdown index 3db6ddcb..7717ba80 100644 --- a/README.markdown +++ b/README.markdown @@ -50,6 +50,26 @@ public void ConfigureServices(IServiceCollection services) .AddSmtpSender("localhost", 25); } ``` +Example to take a dependency on IFluentEmail: + +```c# +public class EmailService { + + private IFluentEmail _fluentEmail; + + public EmailService(IFluentEmail fluentEmail) { + _fluentEmail = fluentEmail; + } + + public async Task Send() { + await _fluentEmail.To("hellO@gmail.com") + .Body("The body").SendAsync(); + } +} + +``` + + ## Using a Razor template From 433923eebd7dd447a690884b5f7322090773424e Mon Sep 17 00:00:00 2001 From: Damian Karzon Date: Thu, 14 Oct 2021 14:25:34 +1000 Subject: [PATCH 06/16] Fix the publish packages workflow by setting VERSION_FILE_PATH: src/Directory.Build.props --- .github/workflows/publish-packages.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 1dfc582d..485bbe27 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -23,6 +23,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/FluentEmail.Core/FluentEmail.Core.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true @@ -30,6 +31,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/Senders/FluentEmail.Smtp/FluentEmail.Smtp.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true @@ -37,6 +39,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true @@ -50,6 +53,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/Senders/FluentEmail.MailKit/FluentEmail.MailKit.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true @@ -57,6 +61,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/Senders/FluentEmail.Mailgun/FluentEmail.Mailgun.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true @@ -64,6 +69,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/Renderers/FluentEmail.Razor/FluentEmail.Razor.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true @@ -71,6 +77,7 @@ jobs: uses: brandedoutcast/publish-nuget@v2.5.2 with: PROJECT_FILE_PATH: src/Renderers/FluentEmail.Liquid/FluentEmail.Liquid.csproj + VERSION_FILE_PATH: src/Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_SOURCE: https://api.nuget.org INCLUDE_SYMBOLS: true From 2a56304ef5a5930c0a0a15c5c55870c202015a91 Mon Sep 17 00:00:00 2001 From: Christopher Haws Date: Tue, 4 Jan 2022 14:41:14 -0800 Subject: [PATCH 07/16] Ensure SmtpClient gets disposed in MailtrapSender --- src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs b/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs index 6342df27..c3a18a71 100644 --- a/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs +++ b/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs @@ -14,7 +14,7 @@ namespace FluentEmail.Mailtrap /// /// Send emails to a Mailtrap.io inbox /// - public class MailtrapSender : ISender + public class MailtrapSender : ISender, IDisposable { private readonly SmtpClient _smtpClient; private static readonly int[] ValidPorts = {25, 465, 2525}; @@ -44,6 +44,8 @@ public MailtrapSender(string userName, string password, string host = "smtp.mail }; } + public void Dispose() => this.smtpClient.Dispose(); + public SendResponse Send(IFluentEmail email, CancellationToken? token = null) { var smtpSender = new SmtpSender(_smtpClient); @@ -56,4 +58,4 @@ public Task SendAsync(IFluentEmail email, CancellationToken? token return smtpSender.SendAsync(email, token); } } -} \ No newline at end of file +} From 6fd3c77364c5cf550be4577d8864abe18fd5291b Mon Sep 17 00:00:00 2001 From: pawelhevy Date: Wed, 23 Feb 2022 13:30:08 +0100 Subject: [PATCH 08/16] fix SmtSender not passing CancellationToken to SmtpClient --- src/Senders/FluentEmail.Smtp/SmtpSender.cs | 9 +++-- .../FluentEmail.Core.Tests/SmtpSenderTests.cs | 40 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/Senders/FluentEmail.Smtp/SmtpSender.cs b/src/Senders/FluentEmail.Smtp/SmtpSender.cs index ad511e0a..4b13d288 100644 --- a/src/Senders/FluentEmail.Smtp/SmtpSender.cs +++ b/src/Senders/FluentEmail.Smtp/SmtpSender.cs @@ -62,12 +62,12 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? { using (var client = _clientFactory()) { - await client.SendMailExAsync(message); + await client.SendMailExAsync(message, token ?? default); } } else { - await _smtpClient.SendMailExAsync(message); + await _smtpClient.SendMailExAsync(message, token ?? default); } return response; @@ -202,7 +202,10 @@ private static async Task SendMailExImplAsync( try { client.SendAsync(message, tcs); - using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false)) + using (token.Register(() => + { + client.SendAsyncCancel(); + }, useSynchronizationContext: false)) { await tcs.Task; } diff --git a/test/FluentEmail.Core.Tests/SmtpSenderTests.cs b/test/FluentEmail.Core.Tests/SmtpSenderTests.cs index af13b149..8fca3c39 100644 --- a/test/FluentEmail.Core.Tests/SmtpSenderTests.cs +++ b/test/FluentEmail.Core.Tests/SmtpSenderTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Net.Mail; +using System.Threading; using System.Threading.Tasks; using FluentEmail.Core; using NUnit.Framework; @@ -17,6 +18,12 @@ public class SmtpSenderTests const string subject = "sup dawg"; const string body = "what be the hipitity hap?"; + private static IFluentEmail TestEmail => Email + .From(fromEmail) + .To(toEmail) + .Subject(subject) + .Body(body); + private readonly string tempDirectory; public SmtpSenderTests() @@ -47,9 +54,7 @@ public void TearDown() [Test] public void CanSendEmail() { - var email = Email - .From(fromEmail) - .To(toEmail) + var email = TestEmail .Body("

Test

", true); var response = email.Send(); @@ -75,26 +80,20 @@ public async Task CanSendEmailWithAttachments() Filename = "mailgunTest.txt" }; - var email = Email - .From(fromEmail) - .To(toEmail) - .Subject(subject) - .Body(body) + var email = TestEmail .Attach(attachment); var response = await email.SendAsync(); - var files = Directory.EnumerateFiles(tempDirectory, "*.eml"); Assert.IsTrue(response.Successful); + var files = Directory.EnumerateFiles(tempDirectory, "*.eml"); Assert.IsNotEmpty(files); } [Test] public async Task CanSendAsyncHtmlAndPlaintextTogether() { - var email = Email - .From(fromEmail) - .To(toEmail) + var email = TestEmail .Body("

Test

some body text

", true) .PlaintextAlternativeBody("Test - Some body text"); @@ -106,9 +105,7 @@ public async Task CanSendAsyncHtmlAndPlaintextTogether() [Test] public void CanSendHtmlAndPlaintextTogether() { - var email = Email - .From(fromEmail) - .To(toEmail) + var email = TestEmail .Body("

Test

some body text

", true) .PlaintextAlternativeBody("Test - Some body text"); @@ -116,5 +113,18 @@ public void CanSendHtmlAndPlaintextTogether() Assert.IsTrue(response.Successful); } + + [Test] + public void CancelSendIfCancelationRequested() + { + var email = TestEmail; + + var tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + + var response = email.Send(tokenSource.Token); + + Assert.IsFalse(response.Successful); + } } } From d8ff31ecf97b806c68ea923a4481d6d2b694f23f Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Thu, 3 Mar 2022 13:26:23 +1300 Subject: [PATCH 09/16] Fix #229 add support for sendgrid categories using tags are those are similar concept --- src/FluentEmail.Core/Email.cs | 2 +- .../FluentEmail.SendGrid/SendGridSender.cs | 2 ++ .../SendGridSenderTests.cs | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/FluentEmail.Core/Email.cs b/src/FluentEmail.Core/Email.cs index 6a43caf4..57142656 100644 --- a/src/FluentEmail.Core/Email.cs +++ b/src/FluentEmail.Core/Email.cs @@ -473,7 +473,7 @@ public IFluentEmail AttachFromFilename(string filename, string contentType = nu } /// - /// Adds tag to the Email. This is currently only supported by the Mailgun provider. + /// Adds tag to the Email. This is currently only supported by the Mailgun and SendGrid providers. and /// /// Tag name, max 128 characters, ASCII only /// Instance of the Email class diff --git a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs index 889bee25..67abb4cb 100644 --- a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs +++ b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs @@ -57,6 +57,8 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? mailMessage.AddHeaders(email.Data.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); } + mailMessage.Categories.AddRange(email.Data.Tags); + if (email.Data.IsHtml) { mailMessage.HtmlContent = email.Data.Body; diff --git a/test/FluentEmail.Core.Tests/SendGridSenderTests.cs b/test/FluentEmail.Core.Tests/SendGridSenderTests.cs index 18e5ddd6..803f21bc 100644 --- a/test/FluentEmail.Core.Tests/SendGridSenderTests.cs +++ b/test/FluentEmail.Core.Tests/SendGridSenderTests.cs @@ -60,6 +60,25 @@ public async Task CanSendEmailWithReplyTo() Assert.IsTrue(response.Successful); } + [Test, Ignore("No sendgrid credentials")] + public async Task CanSendEmailWithCategory() + { + const string subject = "SendMail Test"; + const string body = "This email is testing send mail with Categories functionality of SendGrid Sender."; + + var email = Email + .From(fromEmail, fromName) + .To(toEmail, toName) + .ReplyTo(toEmail, toName) + .Subject(subject) + .Tag("TestCategory") + .Body(body); + + var response = await email.SendAsync(); + + Assert.IsTrue(response.Successful); + } + [Test, Ignore("No sendgrid credentials")] public async Task CanSendEmailWithAttachments() { From a5e1cc8e892dbe45c48c4370e75cb65e6f77e658 Mon Sep 17 00:00:00 2001 From: James Fernandes Date: Mon, 14 Mar 2022 15:54:50 -0700 Subject: [PATCH 10/16] Update to lastest SendGrid lib --- src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj b/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj index d750342f..9420fba1 100644 --- a/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj +++ b/src/Senders/FluentEmail.SendGrid/FluentEmail.SendGrid.csproj @@ -13,7 +13,7 @@ - + From 15a0714d616072f585765072d9bb9123f0d3a9db Mon Sep 17 00:00:00 2001 From: James Fernandes Date: Mon, 14 Mar 2022 15:55:08 -0700 Subject: [PATCH 11/16] Fix breaking change in v9.26 --- src/Senders/FluentEmail.SendGrid/SendGridSender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs index 889bee25..ea372f92 100644 --- a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs +++ b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs @@ -121,7 +121,7 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? if (IsHttpSuccess((int)sendGridResponse.StatusCode)) return sendResponse; sendResponse.ErrorMessages.Add($"{sendGridResponse.StatusCode}"); - var messageBodyDictionary = await sendGridResponse.DeserializeResponseBodyAsync(sendGridResponse.Body); + var messageBodyDictionary = await sendGridResponse.DeserializeResponseBodyAsync(); if (messageBodyDictionary.ContainsKey("errors")) { From 16182359a051990b4220eaddfc4d7b625844aeee Mon Sep 17 00:00:00 2001 From: Luke Lowrey Date: Thu, 17 Mar 2022 09:48:32 +1000 Subject: [PATCH 12/16] Bump version --- src/Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 37c709ae..2fb8ff1b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/lukencode/FluentEmail https://github.com/lukencode/FluentEmail MIT - 3.0.0 + 3.0.1 true @@ -39,4 +39,4 @@ - \ No newline at end of file + From 3cb42829e68dc0c34efc413f9904bcb7095330e9 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Thu, 17 Mar 2022 20:14:05 +1300 Subject: [PATCH 13/16] Fix NRE in category --- src/Senders/FluentEmail.SendGrid/SendGridSender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs index 67abb4cb..1f791140 100644 --- a/src/Senders/FluentEmail.SendGrid/SendGridSender.cs +++ b/src/Senders/FluentEmail.SendGrid/SendGridSender.cs @@ -57,7 +57,7 @@ public async Task SendAsync(IFluentEmail email, CancellationToken? mailMessage.AddHeaders(email.Data.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); } - mailMessage.Categories.AddRange(email.Data.Tags); + mailMessage.Categories = email.Data.Tags.ToList(); if (email.Data.IsHtml) { From ae45bb27853b718e7dda7fc82cbed42690bab1fb Mon Sep 17 00:00:00 2001 From: Luke Lowrey Date: Fri, 18 Mar 2022 08:34:43 +1000 Subject: [PATCH 14/16] Bump version --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2fb8ff1b..e2b23164 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,7 +9,7 @@ https://github.com/lukencode/FluentEmail https://github.com/lukencode/FluentEmail MIT - 3.0.1 + 3.0.2 true From 539e01799d4f823b1597788344cd8a1a64d8eaae Mon Sep 17 00:00:00 2001 From: Luke Lowrey Date: Fri, 18 Mar 2022 08:46:13 +1000 Subject: [PATCH 15/16] Fix merge issue on property name --- src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs b/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs index c3a18a71..f765666a 100644 --- a/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs +++ b/src/Senders/FluentEmail.Mailtrap/MailtrapSender.cs @@ -44,7 +44,7 @@ public MailtrapSender(string userName, string password, string host = "smtp.mail }; } - public void Dispose() => this.smtpClient.Dispose(); + public void Dispose() => _smtpClient?.Dispose(); public SendResponse Send(IFluentEmail email, CancellationToken? token = null) { From e1379c4d81b7b02cf3e48cf9bd718c307215e4bb Mon Sep 17 00:00:00 2001 From: Luke Lowrey Date: Tue, 22 Mar 2022 07:33:55 +1000 Subject: [PATCH 16/16] Fix constructor --- src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs b/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs index 8f8df34b..f1debd78 100644 --- a/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs +++ b/src/Renderers/FluentEmail.Liquid/LiquidRendererOptions.cs @@ -29,6 +29,6 @@ public class LiquidRendererOptions /// /// Set custom Template Options for Fluid /// - public TemplateOptions TemplateOptions { get; set; } = new(); + public TemplateOptions TemplateOptions { get; set; } = new TemplateOptions(); } -} \ No newline at end of file +}