Implemented a simple email templating system.

pull/7950/head
Halil ibrahim Kalkan 7 years ago
parent aaaa97f61d
commit a53d36d7fa

@ -14,6 +14,12 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Volo\Abp\Emailing\Templates\*.html" />
<None Remove="Volo\Abp\Emailing\Templates\*.html" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Localization\Volo.Abp.Localization.csproj" />
<ProjectReference Include="..\Volo.Abp.Settings\Volo.Abp.Settings.csproj" />
<ProjectReference Include="..\Volo.Abp.VirtualFileSystem\Volo.Abp.VirtualFileSystem.csproj" />
</ItemGroup>

@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Emailing.Templates;
using Volo.Abp.Emailing.Templates.Virtual;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Settings;
using Volo.Abp.VirtualFileSystem;
@ -7,7 +10,8 @@ namespace Volo.Abp.Emailing
{
[DependsOn(
typeof(AbpSettingsModule),
typeof(AbpVirtualFileSystemModule)
typeof(AbpVirtualFileSystemModule),
typeof(AbpLocalizationModule)
)]
public class AbpEmailingModule : AbpModule
{
@ -18,6 +22,23 @@ namespace Volo.Abp.Emailing
options.DefinitionProviders.Add<EmailSettingProvider>();
});
context.Services.Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpEmailingModule>();
});
context.Services.Configure<EmailTemplateOptions>(options =>
{
options.Templates
.Add(
new EmailTemplateDefinition(StandardEmailTemplates.DefaultLayout, isLayout: true, layout: null)
.SetVirtualFilePath("/Volo/Abp/Emailing/Templates/DefaultLayout.html")
).Add(
new EmailTemplateDefinition(StandardEmailTemplates.SimpleMessage)
.SetVirtualFilePath("/Volo/Abp/Emailing/Templates/SimpleMessageTemplate.html")
);
});
context.Services.AddAssemblyOf<AbpEmailingModule>();
}
}

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
{{#content}}
</body>
</html>

@ -1,12 +1,42 @@
namespace Volo.Abp.Emailing.Templates
using System.Text;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplate
{
public string Content { get; }
public EmailTemplateDefinition Definition { get; }
public string Content => ContentBuilder.ToString();
protected StringBuilder ContentBuilder { get; set; }
public EmailTemplate(string content, EmailTemplateDefinition definition)
{
ContentBuilder = new StringBuilder(content);
Definition = definition;
}
public virtual void SetLayout(EmailTemplate layoutTemplate)
{
if (!layoutTemplate.Definition.IsLayout)
{
throw new AbpException($"Given template is not a layout template: {layoutTemplate.Definition.Name}");
}
var newStrBuilder = new StringBuilder(layoutTemplate.Content);
newStrBuilder.Replace("{{#content}}", ContentBuilder.ToString());
ContentBuilder = newStrBuilder;
}
public virtual void SetContent(string content)
{
ContentBuilder = new StringBuilder(content);
}
public EmailTemplate(string content)
public virtual void Replace(string name, string value)
{
Content = content;
ContentBuilder.Replace("{{" + name + "}}", value);
}
}
}

@ -1,12 +1,21 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateDefinition
{
public const string DefaultLayoutPlaceHolder = "_";
public string Name { get; }
public bool IsLayout { get; }
public string Layout { get; set; }
public Type LocalizationResource { get; set; }
public Dictionary<string, object> Properties { get; }
/// <summary>
@ -23,10 +32,13 @@ namespace Volo.Abp.Emailing.Templates
set => Properties[name] = value;
}
public EmailTemplateDefinition([NotNull]string name)
public EmailTemplateDefinition([NotNull]string name, Type localizationResource = null, bool isLayout = false, string layout = DefaultLayoutPlaceHolder)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
Properties = new Dictionary<string, object>();
LocalizationResource = localizationResource;
IsLayout = isLayout;
Layout = layout;
}
}
}

@ -4,6 +4,19 @@ namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateDefinitionDictionary : Dictionary<string, EmailTemplateDefinition>
{
public EmailTemplateDefinitionDictionary Add(EmailTemplateDefinition emailTemplateDefinition)
{
if (ContainsKey(emailTemplateDefinition.Name))
{
throw new AbpException(
"There is already an email template definition with given name: " +
emailTemplateDefinition.Name
);
}
this[emailTemplateDefinition.Name] = emailTemplateDefinition;
return this;
}
}
}

@ -5,18 +5,22 @@ namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateOptions
{
public List<IEmailTemplateProvider> Providers { get; }
public List<IEmailTemplateProviderContributor> Providers { get; }
public EmailTemplateDefinitionDictionary Templates { get; }
public string DefaultLayout { get; set; }
public EmailTemplateOptions()
{
Providers = new List<IEmailTemplateProvider>
Providers = new List<IEmailTemplateProviderContributor>
{
new VirtualFileEmailTemplateProvider()
new VirtualFileEmailTemplateProviderContributor()
};
Templates = new EmailTemplateDefinitionDictionary();
DefaultLayout = StandardEmailTemplates.DefaultLayout;
}
}
}

@ -0,0 +1,91 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateProvider : IEmailTemplateProvider, ITransientDependency
{
protected IServiceProvider ServiceProvider { get; }
protected ITemplateLocalizer TemplateLocalizer { get; }
protected EmailTemplateOptions Options { get; }
public EmailTemplateProvider(
IOptions<EmailTemplateOptions> options,
IServiceProvider serviceProvider,
ITemplateLocalizer templateLocalizer)
{
ServiceProvider = serviceProvider;
TemplateLocalizer = templateLocalizer;
Options = options.Value;
}
public async Task<EmailTemplate> GetAsync(string name)
{
using (var scope = ServiceProvider.CreateScope())
{
return await GetInternalAsync(scope.ServiceProvider, name);
}
}
protected virtual async Task<EmailTemplate> GetInternalAsync(IServiceProvider serviceProvider, string name)
{
var context = new EmailTemplateProviderContributorContext(name, serviceProvider);
foreach (var provider in Options.Providers)
{
await provider.ProvideAsync(context);
}
if (context.Template == null)
{
throw new AbpException($"Could not found the template: {name}");
}
await SetLayoutAsync(serviceProvider, context);
await LocalizeAsync(serviceProvider, context);
return context.Template;
}
protected virtual async Task SetLayoutAsync(IServiceProvider serviceProvider, EmailTemplateProviderContributorContext context)
{
var layout = context.Template.Definition.Layout;
if (layout.IsNullOrEmpty())
{
return;
}
if (layout == EmailTemplateDefinition.DefaultLayoutPlaceHolder)
{
layout = Options.DefaultLayout;
}
var layoutTemplate = await GetInternalAsync(serviceProvider, layout);
context.Template.SetLayout(layoutTemplate);
}
protected virtual Task LocalizeAsync(IServiceProvider serviceProvider, EmailTemplateProviderContributorContext context)
{
if (context.Template.Definition.LocalizationResource == null)
{
return Task.CompletedTask;
}
var localizer = serviceProvider
.GetRequiredService<IStringLocalizerFactory>()
.Create(context.Template.Definition.LocalizationResource);
context.Template.SetContent(
TemplateLocalizer.Localize(localizer, context.Template.Content)
);
return Task.CompletedTask;
}
}
}

@ -3,7 +3,7 @@ using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateProviderContext : IServiceProviderAccessor
public class EmailTemplateProviderContributorContext : IServiceProviderAccessor
{
public string Name { get; }
@ -11,7 +11,7 @@ namespace Volo.Abp.Emailing.Templates
public EmailTemplate Template { get; set; }
public EmailTemplateProviderContext(string name, IServiceProvider serviceProvider)
public EmailTemplateProviderContributorContext(string name, IServiceProvider serviceProvider)
{
Name = name;
ServiceProvider = serviceProvider;

@ -1,40 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateStore : IEmailTemplateStore, ITransientDependency
{
protected IServiceProvider ServiceProvider { get; }
protected EmailTemplateOptions Options { get; }
public EmailTemplateStore(IOptions<EmailTemplateOptions> options, IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Options = options.Value;
}
public async Task<EmailTemplate> GetAsync(string name)
{
using (var scope = ServiceProvider.CreateScope())
{
var context = new EmailTemplateProviderContext(name, scope.ServiceProvider);
foreach (var provider in Options.Providers)
{
await provider.ProvideAsync(context);
}
if (context.Template == null)
{
//TODO: Return a default email template!
throw new NotImplementedException();
}
return context.Template;
}
}
}
}

@ -4,6 +4,6 @@ namespace Volo.Abp.Emailing.Templates
{
public interface IEmailTemplateProvider
{
Task ProvideAsync(EmailTemplateProviderContext context);
Task<EmailTemplate> GetAsync(string name);
}
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Volo.Abp.Emailing.Templates
{
public interface IEmailTemplateProviderContributor
{
Task ProvideAsync(EmailTemplateProviderContributorContext contributorContext);
}
}

@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace Volo.Abp.Emailing.Templates
{
public interface IEmailTemplateStore
{
Task<EmailTemplate> GetAsync(string name);
}
}

@ -0,0 +1,8 @@
namespace Volo.Abp.Emailing.Templates
{
public static class StandardEmailTemplates
{
public const string DefaultLayout = "Abp.DefaultLayout";
public const string SimpleMessage = "Abp.SimpleMessage";
}
}

@ -4,13 +4,13 @@
{
public static EmailTemplateDefinition SetVirtualFilePath(this EmailTemplateDefinition emailTemplateDefinition, string path)
{
emailTemplateDefinition[VirtualFileEmailTemplateProvider.VirtualFilePathKey] = path;
emailTemplateDefinition[VirtualFileEmailTemplateProviderContributor.VirtualFilePathKey] = path;
return emailTemplateDefinition;
}
public static string GetVirtualFilePathOrNull(this EmailTemplateDefinition emailTemplateDefinition)
{
return emailTemplateDefinition[VirtualFileEmailTemplateProvider.VirtualFilePathKey] as string;
return emailTemplateDefinition[VirtualFileEmailTemplateProviderContributor.VirtualFilePathKey] as string;
}
}
}

@ -7,39 +7,39 @@ using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Emailing.Templates.Virtual
{
public class VirtualFileEmailTemplateProvider : IEmailTemplateProvider
public class VirtualFileEmailTemplateProviderContributor : IEmailTemplateProviderContributor
{
public const string VirtualFilePathKey = "VirtualFilePath";
public Task ProvideAsync(EmailTemplateProviderContext context)
public Task ProvideAsync(EmailTemplateProviderContributorContext contributorContext)
{
var templateDefinition = FindTemplateDefinition(context);
var templateDefinition = FindTemplateDefinition(contributorContext);
if (templateDefinition == null)
{
return Task.CompletedTask;
}
var fileInfo = FindVirtualFileInfo(context, templateDefinition);
var fileInfo = FindVirtualFileInfo(contributorContext, templateDefinition);
if (fileInfo == null)
{
return Task.CompletedTask;
}
context.Template = new EmailTemplate(fileInfo.ReadAsString());
contributorContext.Template = new EmailTemplate(fileInfo.ReadAsString(), templateDefinition);
return Task.CompletedTask;
}
protected virtual EmailTemplateDefinition FindTemplateDefinition(EmailTemplateProviderContext context)
protected virtual EmailTemplateDefinition FindTemplateDefinition(EmailTemplateProviderContributorContext contributorContext)
{
return context
return contributorContext
.ServiceProvider
.GetRequiredService<IOptions<EmailTemplateOptions>>()
.Value
.Templates
.GetOrDefault(context.Name);
.GetOrDefault(contributorContext.Name);
}
protected virtual IFileInfo FindVirtualFileInfo(EmailTemplateProviderContext context, EmailTemplateDefinition templateDefinition)
protected virtual IFileInfo FindVirtualFileInfo(EmailTemplateProviderContributorContext contributorContext, EmailTemplateDefinition templateDefinition)
{
var virtualFilePath = templateDefinition?.GetVirtualFilePathOrNull();
if (virtualFilePath == null)
@ -47,7 +47,7 @@ namespace Volo.Abp.Emailing.Templates.Virtual
return null;
}
var virtualFileProvider = context.ServiceProvider.GetRequiredService<IVirtualFileProvider>();
var virtualFileProvider = contributorContext.ServiceProvider.GetRequiredService<IVirtualFileProvider>();
var fileInfo = virtualFileProvider.GetFileInfo(virtualFilePath);
if (fileInfo?.Exists != true)

@ -0,0 +1,9 @@
using Microsoft.Extensions.Localization;
namespace Volo.Abp.Localization
{
public interface ITemplateLocalizer
{
string Localize(IStringLocalizer localizer, string text);
}
}

@ -0,0 +1,18 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Localization;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Localization
{
public class TemplateLocalizer : ITemplateLocalizer, ITransientDependency
{
public string Localize(IStringLocalizer localizer, string text)
{
return new Regex("\\{\\{#L:.+?\\}\\}")
.Replace(
text,
match => localizer[match.Value.Substring(5, match.Length - 7)]
);
}
}
}

@ -7,17 +7,17 @@ namespace Volo.Abp.Emailing
{
public class EmailTemplateStore_Tests : AbpIntegratedTest<AbpEmailingTestModule>
{
private readonly IEmailTemplateStore _emailTemplateStore;
private readonly IEmailTemplateProvider _emailTemplateProvider;
public EmailTemplateStore_Tests()
{
_emailTemplateStore = GetRequiredService<IEmailTemplateStore>();
_emailTemplateProvider = GetRequiredService<IEmailTemplateProvider>();
}
[Fact]
public async Task Should_Get_Registered_Template()
{
var template = await _emailTemplateStore.GetAsync("template1");
var template = await _emailTemplateProvider.GetAsync("template1");
template.Content.ShouldContain("This is a test template!");
}
}

@ -0,0 +1,62 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Shouldly;
using Volo.Abp.Localization.TestResources.Source;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
using Xunit;
namespace Volo.Abp.Localization
{
public class TemplateLocalizer_Tests : AbpIntegratedTest<TemplateLocalizer_Tests.TestModule>
{
private readonly ITemplateLocalizer _templateLocalizer;
private readonly IStringLocalizer<LocalizationTestResource> _testResource;
public TemplateLocalizer_Tests()
{
_testResource = GetRequiredService<IStringLocalizer<LocalizationTestResource>>();
_templateLocalizer = GetRequiredService<ITemplateLocalizer>();
}
[Fact]
public void Should_Localize()
{
using (AbpCultureHelper.Use("en"))
{
_templateLocalizer.Localize(_testResource, "<p>{{#L:CarPlural}} <b>{{#L:Universe}}</b></p>")
.ShouldBe("<p>Cars <b>Universe</b></p>");
}
}
[Fact]
public void Should_Work_Even_If_No_Text_To_Localize()
{
using (AbpCultureHelper.Use("en"))
{
_templateLocalizer.Localize(_testResource, "<p>test</p>")
.ShouldBe("<p>test</p>");
}
}
[DependsOn(typeof(AbpTestBaseModule))]
[DependsOn(typeof(AbpLocalizationModule))]
public class TestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpLocalization_Tests.TestModule>();
});
context.Services.Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<LocalizationTestResource>("en")
.AddVirtualJson("/Volo/Abp/Localization/TestResources/Source");
});
}
}
}
}

@ -4,6 +4,7 @@
"Hello <b>{0}</b>.": "Hello <b>{0}</b>.",
"Car": "Car",
"CarPlural": "Cars",
"MaxLenghtErrorMessage": "This field's length can be maximum of '{0}' chars"
"MaxLenghtErrorMessage": "This field's length can be maximum of '{0}' chars",
"Universe": "Universe"
}
}

@ -3,6 +3,7 @@
"texts": {
"Hello <b>{0}</b>.": "Merhaba <b>{0}</b>.",
"Car": "Araba",
"CarPlural": "Araba"
"CarPlural": "Araba",
"Universe": "Evren"
}
}
Loading…
Cancel
Save