mirror of https://github.com/abpframework/abp.git
83 changed files with 1401 additions and 119 deletions
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,16 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.Localization.Abstractions\Volo.Abp.Localization.Abstractions.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.VirtualFileSystem\Volo.Abp.VirtualFileSystem.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,46 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpVirtualFileSystemModule), |
||||
|
typeof(AbpLocalizationAbstractionsModule) |
||||
|
)] |
||||
|
public class AbpTextTemplatingAbstractionsModule : AbpModule |
||||
|
{ |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
AutoAddProvidersAndContributors(context.Services); |
||||
|
} |
||||
|
|
||||
|
private static void AutoAddProvidersAndContributors(IServiceCollection services) |
||||
|
{ |
||||
|
var definitionProviders = new List<Type>(); |
||||
|
var contentContributors = new List<Type>(); |
||||
|
|
||||
|
services.OnRegistred(context => |
||||
|
{ |
||||
|
if (typeof(ITemplateDefinitionProvider).IsAssignableFrom(context.ImplementationType)) |
||||
|
{ |
||||
|
definitionProviders.Add(context.ImplementationType); |
||||
|
} |
||||
|
|
||||
|
if (typeof(ITemplateContentContributor).IsAssignableFrom(context.ImplementationType)) |
||||
|
{ |
||||
|
contentContributors.Add(context.ImplementationType); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
services.Configure<AbpTextTemplatingOptions>(options => |
||||
|
{ |
||||
|
options.DefinitionProviders.AddIfNotContains(definitionProviders); |
||||
|
options.ContentContributors.AddIfNotContains(contentContributors); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,21 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.TextTemplating.Abstractions\Volo.Abp.TextTemplating.Abstractions.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" /> |
||||
|
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(MicrosoftPackageVersion)" /> |
||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public static class AbpRazorTemplateConsts |
||||
|
{ |
||||
|
public const string DefaultNameSpace = "Abp.Razor"; |
||||
|
public const string DefaultClassName = "Template"; |
||||
|
public const string TypeName = DefaultNameSpace + "." + DefaultClassName; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpTextTemplatingAbstractionsModule) |
||||
|
)] |
||||
|
public class AbpTextTemplatingRazorModule : AbpModule |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,107 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Text; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class CSharpCompiler : ISingletonDependency |
||||
|
{ |
||||
|
protected CSharpCompilerOptions Options { get; } |
||||
|
|
||||
|
public CSharpCompiler(IOptions<CSharpCompilerOptions> options) |
||||
|
{ |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
private static IEnumerable<PortableExecutableReference> DefaultReferences => new List<Assembly>() |
||||
|
{ |
||||
|
Assembly.Load("netstandard"), |
||||
|
Assembly.Load("System.Private.CoreLib"), |
||||
|
Assembly.Load("System.Runtime"), |
||||
|
Assembly.Load("System.Collections"), |
||||
|
Assembly.Load("System.ComponentModel"), |
||||
|
Assembly.Load("System.Linq"), |
||||
|
Assembly.Load("System.Linq.Expressions"), |
||||
|
Assembly.Load("Microsoft.Extensions.DependencyInjection"), |
||||
|
Assembly.Load("Microsoft.Extensions.DependencyInjection.Abstractions"), |
||||
|
Assembly.Load("Microsoft.Extensions.Localization"), |
||||
|
Assembly.Load("Microsoft.Extensions.Localization.Abstractions"), |
||||
|
|
||||
|
typeof(CSharpCompiler).Assembly |
||||
|
} |
||||
|
.Select(x => MetadataReference.CreateFromFile(x.Location)) |
||||
|
.ToImmutableList(); |
||||
|
|
||||
|
public virtual Stream CreateAssembly(string code, string assemblyName, List<MetadataReference> references = null, CSharpCompilationOptions options = null) |
||||
|
{ |
||||
|
var defaultReferences = DefaultReferences.Concat(Options.References); |
||||
|
try |
||||
|
{ |
||||
|
var compilation = CSharpCompilation.Create( |
||||
|
assemblyName, |
||||
|
syntaxTrees: new[] { CreateSyntaxTree(code) }, |
||||
|
references: references != null ? defaultReferences.Concat(references) : defaultReferences, |
||||
|
options ?? GetCompilationOptions()); |
||||
|
|
||||
|
using (var memoryStream = new MemoryStream()) |
||||
|
{ |
||||
|
var result = compilation.Emit(memoryStream); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
var error = new StringBuilder(); |
||||
|
error.AppendLine("Build failed"); |
||||
|
foreach (var diagnostic in result.Diagnostics) |
||||
|
{ |
||||
|
error.AppendLine(diagnostic.ToString()); |
||||
|
} |
||||
|
|
||||
|
throw new Exception(error.ToString()); |
||||
|
} |
||||
|
|
||||
|
memoryStream.Seek(0, SeekOrigin.Begin); |
||||
|
var assemblyStream = new MemoryStream(); |
||||
|
memoryStream.CopyTo(assemblyStream); |
||||
|
|
||||
|
return assemblyStream; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
var error = new StringBuilder(); |
||||
|
error.AppendLine("CreateAssembly failed"); |
||||
|
error.AppendLine(e.Message); |
||||
|
throw new Exception(error.ToString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual SyntaxTree CreateSyntaxTree(string code) |
||||
|
{ |
||||
|
return CSharpSyntaxTree.ParseText(code); |
||||
|
} |
||||
|
|
||||
|
protected virtual CSharpCompilationOptions GetCompilationOptions() |
||||
|
{ |
||||
|
var csharpCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); |
||||
|
|
||||
|
// Disable 1702 until roslyn turns this off by default
|
||||
|
csharpCompilationOptions = csharpCompilationOptions.WithSpecificDiagnosticOptions( |
||||
|
new Dictionary<string, ReportDiagnostic> |
||||
|
{ |
||||
|
{"CS1701", ReportDiagnostic.Suppress}, // Binding redirects
|
||||
|
{"CS1702", ReportDiagnostic.Suppress}, |
||||
|
{"CS1705", ReportDiagnostic.Suppress} |
||||
|
}); |
||||
|
|
||||
|
return csharpCompilationOptions.WithOptimizationLevel(OptimizationLevel.Release); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class CSharpCompilerOptions |
||||
|
{ |
||||
|
public List<PortableExecutableReference> References { get; } |
||||
|
|
||||
|
public CSharpCompilerOptions() |
||||
|
{ |
||||
|
References = new List<PortableExecutableReference>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Razor.Language; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class DefaultCompiledViewProvider : ICompiledViewProvider, ITransientDependency |
||||
|
{ |
||||
|
private static readonly ConcurrentDictionary<string, Assembly> CachedAssembles = new ConcurrentDictionary<string, Assembly>(); |
||||
|
|
||||
|
private readonly CSharpCompiler _cSharpCompiler; |
||||
|
private readonly IRazorProjectEngineFactory _razorProjectEngineFactory; |
||||
|
private readonly ITemplateContentProvider _templateContentProvider; |
||||
|
|
||||
|
public DefaultCompiledViewProvider( |
||||
|
IRazorProjectEngineFactory razorProjectEngineFactory, |
||||
|
CSharpCompiler cSharpCompiler, |
||||
|
ITemplateContentProvider templateContentProvider) |
||||
|
{ |
||||
|
_razorProjectEngineFactory = razorProjectEngineFactory; |
||||
|
_cSharpCompiler = cSharpCompiler; |
||||
|
_templateContentProvider = templateContentProvider; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<Assembly> GetAssemblyAsync(TemplateDefinition templateDefinition) |
||||
|
{ |
||||
|
async Task<Assembly> CreateAssembly(string content) |
||||
|
{ |
||||
|
using (var assemblyStream = await GetAssemblyStreamAsync(templateDefinition, content)) |
||||
|
{ |
||||
|
return Assembly.Load(await assemblyStream.GetAllBytesAsync()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var templateContent = await _templateContentProvider.GetContentOrNullAsync(templateDefinition); |
||||
|
return CachedAssembles.GetOrAdd((templateDefinition.Name + templateContent).ToMd5(), await CreateAssembly(templateContent)); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<Stream> GetAssemblyStreamAsync(TemplateDefinition templateDefinition, string templateContent) |
||||
|
{ |
||||
|
var razorProjectEngine = await _razorProjectEngineFactory.CreateAsync(builder => |
||||
|
{ |
||||
|
builder.SetNamespace(AbpRazorTemplateConsts.DefaultNameSpace); |
||||
|
builder.ConfigureClass((document, node) => |
||||
|
{ |
||||
|
node.ClassName = AbpRazorTemplateConsts.DefaultClassName; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
var codeDocument = razorProjectEngine.Process( |
||||
|
RazorSourceDocument.Create(templateContent, templateDefinition.Name), null, |
||||
|
new List<RazorSourceDocument>(), new List<TagHelperDescriptor>()); |
||||
|
|
||||
|
var cSharpDocument = codeDocument.GetCSharpDocument(); |
||||
|
|
||||
|
return _cSharpCompiler.CreateAssembly(cSharpDocument.GeneratedCode, templateDefinition.Name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Razor.Language; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class DefaultRazorProjectEngineFactory : IRazorProjectEngineFactory, ITransientDependency |
||||
|
{ |
||||
|
public virtual async Task<RazorProjectEngine> CreateAsync(Action<RazorProjectEngineBuilder> configure = null) |
||||
|
{ |
||||
|
return RazorProjectEngine.Create(await CreateRazorConfigurationAsync(), EmptyProjectFileSystem.Empty, configure); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<RazorConfiguration> CreateRazorConfigurationAsync() |
||||
|
{ |
||||
|
return Task.FromResult(RazorConfiguration.Default); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Microsoft.AspNetCore.Razor.Language; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
internal class EmptyProjectFileSystem : RazorProjectFileSystem |
||||
|
{ |
||||
|
internal static readonly RazorProjectFileSystem Empty = new EmptyProjectFileSystem(); |
||||
|
|
||||
|
public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath) |
||||
|
{ |
||||
|
NormalizeAndEnsureValidPath(basePath); |
||||
|
return Enumerable.Empty<RazorProjectItem>(); |
||||
|
} |
||||
|
|
||||
|
[Obsolete("Use GetItem(string path, string fileKind) instead.")] |
||||
|
public override RazorProjectItem GetItem(string path) |
||||
|
{ |
||||
|
return GetItem(path, fileKind: null); |
||||
|
} |
||||
|
|
||||
|
public override RazorProjectItem GetItem(string path, string fileKind) |
||||
|
{ |
||||
|
NormalizeAndEnsureValidPath(path); |
||||
|
return new NotFoundProjectItem(string.Empty, path, fileKind); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public interface ICompiledViewProvider |
||||
|
{ |
||||
|
Task<Assembly> GetAssemblyAsync(TemplateDefinition templateDefinition); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Razor.Language; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public interface IRazorProjectEngineFactory |
||||
|
{ |
||||
|
Task<RazorProjectEngine> CreateAsync(Action<RazorProjectEngineBuilder> configure = null); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public interface IRazorTemplatePage<TModel> : IRazorTemplatePage |
||||
|
{ |
||||
|
TModel Model { get; set; } |
||||
|
} |
||||
|
|
||||
|
public interface IRazorTemplatePage |
||||
|
{ |
||||
|
IServiceProvider ServiceProvider { get; set; } |
||||
|
|
||||
|
IStringLocalizer Localizer { get; set; } |
||||
|
|
||||
|
HtmlEncoder HtmlEncoder { get; set; } |
||||
|
|
||||
|
JavaScriptEncoder JavaScriptEncoder { get; set; } |
||||
|
|
||||
|
UrlEncoder UrlEncoder { get; set; } |
||||
|
|
||||
|
Dictionary<string, object> GlobalContext { get; set; } |
||||
|
|
||||
|
string Body { get; set; } |
||||
|
|
||||
|
Task ExecuteAsync(); |
||||
|
|
||||
|
Task<string> GetOutputAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using Microsoft.AspNetCore.Razor.Language; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
internal class NotFoundProjectItem : RazorProjectItem |
||||
|
{ |
||||
|
public NotFoundProjectItem(string basePath, string path, string fileKind) |
||||
|
{ |
||||
|
BasePath = basePath; |
||||
|
FilePath = path; |
||||
|
FileKind = fileKind ?? FileKinds.GetFileKindFromFilePath(path); |
||||
|
} |
||||
|
|
||||
|
public override string BasePath { get; } |
||||
|
|
||||
|
public override string FilePath { get; } |
||||
|
|
||||
|
public override string FileKind { get; } |
||||
|
|
||||
|
public override bool Exists => false; |
||||
|
|
||||
|
public override string PhysicalPath => throw new NotSupportedException(); |
||||
|
|
||||
|
public override Stream Read() => throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,177 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public abstract class RazorTemplatePageBase<TModel> : RazorTemplatePageBase, IRazorTemplatePage<TModel> |
||||
|
{ |
||||
|
public TModel Model { get; set; } |
||||
|
} |
||||
|
|
||||
|
public abstract class RazorTemplatePageBase : IRazorTemplatePage |
||||
|
{ |
||||
|
public Dictionary<string, object> GlobalContext { get; set; } |
||||
|
|
||||
|
public IServiceProvider ServiceProvider { get; set; } |
||||
|
|
||||
|
public IStringLocalizer Localizer { get; set; } |
||||
|
|
||||
|
public HtmlEncoder HtmlEncoder { get; set; } |
||||
|
|
||||
|
public JavaScriptEncoder JavaScriptEncoder { get; set; } |
||||
|
|
||||
|
public UrlEncoder UrlEncoder { get; set; } |
||||
|
|
||||
|
public string Body { get; set; } |
||||
|
|
||||
|
private readonly StringBuilder _stringBuilder = new StringBuilder(); |
||||
|
|
||||
|
private AttributeInfo _attributeInfo; |
||||
|
|
||||
|
public virtual void WriteLiteral(string literal = null) |
||||
|
{ |
||||
|
_stringBuilder.Append(literal); |
||||
|
} |
||||
|
|
||||
|
public virtual void Write(object obj = null) |
||||
|
{ |
||||
|
_stringBuilder.Append(obj); |
||||
|
} |
||||
|
|
||||
|
public virtual void BeginWriteAttribute(string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount) |
||||
|
{ |
||||
|
_attributeInfo = new AttributeInfo(name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount); |
||||
|
WriteLiteral(prefix); |
||||
|
} |
||||
|
|
||||
|
public virtual void WriteAttributeValue(string prefix, int prefixOffset, object value, int valueOffset, int valueLength, bool isLiteral) |
||||
|
{ |
||||
|
if (_attributeInfo.AttributeValuesCount == 1) |
||||
|
{ |
||||
|
if (IsBoolFalseOrNullValue(prefix, value)) |
||||
|
{ |
||||
|
// Value is either null or the bool 'false' with no prefix; don't render the attribute.
|
||||
|
_attributeInfo.Suppressed = true; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// We are not omitting the attribute. Write the prefix.
|
||||
|
WriteLiteral(_attributeInfo.Prefix); |
||||
|
|
||||
|
if (IsBoolTrueWithEmptyPrefixValue(prefix, value)) |
||||
|
{ |
||||
|
// The value is just the bool 'true', write the attribute name instead of the string 'True'.
|
||||
|
value = _attributeInfo.Name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// This block handles two cases.
|
||||
|
// 1. Single value with prefix.
|
||||
|
// 2. Multiple values with or without prefix.
|
||||
|
if (value != null) |
||||
|
{ |
||||
|
if (!string.IsNullOrEmpty(prefix)) |
||||
|
{ |
||||
|
WriteLiteral(prefix); |
||||
|
} |
||||
|
|
||||
|
WriteUnprefixedAttributeValue(value, isLiteral); |
||||
|
} |
||||
|
|
||||
|
_stringBuilder.Append(prefix); |
||||
|
_stringBuilder.Append(value); |
||||
|
} |
||||
|
|
||||
|
public virtual void EndWriteAttribute() |
||||
|
{ |
||||
|
if (!_attributeInfo.Suppressed) |
||||
|
{ |
||||
|
WriteLiteral(_attributeInfo.Suffix); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public virtual Task ExecuteAsync() |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public virtual Task<string> GetOutputAsync() |
||||
|
{ |
||||
|
return Task.FromResult(_stringBuilder.ToString()); |
||||
|
} |
||||
|
|
||||
|
private void WriteUnprefixedAttributeValue(object value, bool isLiteral) |
||||
|
{ |
||||
|
var stringValue = value as string; |
||||
|
|
||||
|
// The extra branching here is to ensure that we call the Write*To(string) overload where possible.
|
||||
|
if (isLiteral && stringValue != null) |
||||
|
{ |
||||
|
WriteLiteral(stringValue); |
||||
|
} |
||||
|
else if (isLiteral) |
||||
|
{ |
||||
|
//WriteLiteral(value);
|
||||
|
_stringBuilder.Append(value); |
||||
|
} |
||||
|
else if (stringValue != null) |
||||
|
{ |
||||
|
Write(stringValue); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Write(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool IsBoolFalseOrNullValue(string prefix, object value) |
||||
|
{ |
||||
|
return string.IsNullOrEmpty(prefix) && (value == null || (value is bool b && !b)); |
||||
|
} |
||||
|
|
||||
|
private bool IsBoolTrueWithEmptyPrefixValue(string prefix, object value) |
||||
|
{ |
||||
|
// If the value is just the bool 'true', use the attribute name as the value.
|
||||
|
return string.IsNullOrEmpty(prefix) && (value is bool b && b); |
||||
|
} |
||||
|
|
||||
|
private struct AttributeInfo |
||||
|
{ |
||||
|
public AttributeInfo( |
||||
|
string name, |
||||
|
string prefix, |
||||
|
int prefixOffset, |
||||
|
string suffix, |
||||
|
int suffixOffset, |
||||
|
int attributeValuesCount) |
||||
|
{ |
||||
|
Name = name; |
||||
|
Prefix = prefix; |
||||
|
PrefixOffset = prefixOffset; |
||||
|
Suffix = suffix; |
||||
|
SuffixOffset = suffixOffset; |
||||
|
AttributeValuesCount = attributeValuesCount; |
||||
|
|
||||
|
Suppressed = false; |
||||
|
} |
||||
|
|
||||
|
public int AttributeValuesCount { get; } |
||||
|
|
||||
|
public string Name { get; } |
||||
|
|
||||
|
public string Prefix { get; } |
||||
|
|
||||
|
public int PrefixOffset { get; } |
||||
|
|
||||
|
public string Suffix { get; } |
||||
|
|
||||
|
public int SuffixOffset { get; } |
||||
|
|
||||
|
public bool Suppressed { get; set; } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,168 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Localization; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
[Dependency(ReplaceServices = true)] |
||||
|
public class RazorTemplateRenderer : ITemplateRenderer, ITransientDependency |
||||
|
{ |
||||
|
private readonly IServiceScopeFactory _serviceScopeFactory; |
||||
|
private readonly ICompiledViewProvider _compiledViewProvider; |
||||
|
private readonly ITemplateDefinitionManager _templateDefinitionManager; |
||||
|
private readonly IStringLocalizerFactory _stringLocalizerFactory; |
||||
|
|
||||
|
public RazorTemplateRenderer( |
||||
|
IServiceScopeFactory serviceScopeFactory, |
||||
|
ICompiledViewProvider compiledViewProvider, |
||||
|
ITemplateDefinitionManager templateDefinitionManager, |
||||
|
IStringLocalizerFactory stringLocalizerFactory) |
||||
|
{ |
||||
|
_serviceScopeFactory = serviceScopeFactory; |
||||
|
_templateDefinitionManager = templateDefinitionManager; |
||||
|
_stringLocalizerFactory = stringLocalizerFactory; |
||||
|
_compiledViewProvider = compiledViewProvider; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<string> RenderAsync( |
||||
|
[NotNull] string templateName, |
||||
|
[CanBeNull] object model = null, |
||||
|
[CanBeNull] string cultureName = null, |
||||
|
[CanBeNull] Dictionary<string, object> globalContext = null) |
||||
|
{ |
||||
|
Check.NotNullOrWhiteSpace(templateName, nameof(templateName)); |
||||
|
|
||||
|
if (globalContext == null) |
||||
|
{ |
||||
|
globalContext = new Dictionary<string, object>(); |
||||
|
} |
||||
|
|
||||
|
if (cultureName == null) |
||||
|
{ |
||||
|
return await RenderInternalAsync( |
||||
|
templateName, |
||||
|
null, |
||||
|
globalContext, |
||||
|
model |
||||
|
); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
using (CultureHelper.Use(cultureName)) |
||||
|
{ |
||||
|
return await RenderInternalAsync( |
||||
|
templateName, |
||||
|
null, |
||||
|
globalContext, |
||||
|
model |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> RenderInternalAsync( |
||||
|
string templateName, |
||||
|
string body, |
||||
|
Dictionary<string, object> globalContext, |
||||
|
object model = null) |
||||
|
{ |
||||
|
var templateDefinition = _templateDefinitionManager.Get(templateName); |
||||
|
|
||||
|
var renderedContent = await RenderSingleTemplateAsync( |
||||
|
templateDefinition, |
||||
|
body, |
||||
|
globalContext, |
||||
|
model |
||||
|
); |
||||
|
|
||||
|
if (templateDefinition.Layout != null) |
||||
|
{ |
||||
|
renderedContent = await RenderInternalAsync( |
||||
|
templateDefinition.Layout, |
||||
|
renderedContent, |
||||
|
globalContext |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return renderedContent; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> RenderSingleTemplateAsync( |
||||
|
TemplateDefinition templateDefinition, |
||||
|
string body, |
||||
|
Dictionary<string, object> globalContext, |
||||
|
object model = null) |
||||
|
{ |
||||
|
return await RenderTemplateContentWithRazorAsync( |
||||
|
templateDefinition, |
||||
|
body, |
||||
|
globalContext, |
||||
|
model |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> RenderTemplateContentWithRazorAsync( |
||||
|
TemplateDefinition templateDefinition, |
||||
|
string body, |
||||
|
Dictionary<string, object> globalContext, |
||||
|
object model = null) |
||||
|
{ |
||||
|
var assembly = await _compiledViewProvider.GetAssemblyAsync(templateDefinition); |
||||
|
var templateType = assembly.GetType(AbpRazorTemplateConsts.TypeName); |
||||
|
var template = (IRazorTemplatePage) Activator.CreateInstance(templateType); |
||||
|
|
||||
|
using (var scope = _serviceScopeFactory.CreateScope()) |
||||
|
{ |
||||
|
var modelType = templateType |
||||
|
.GetInterfaces() |
||||
|
.Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IRazorTemplatePage<>)) |
||||
|
.Select(x => x.GenericTypeArguments.FirstOrDefault()) |
||||
|
.FirstOrDefault(); |
||||
|
|
||||
|
if (modelType != null) |
||||
|
{ |
||||
|
GetType().GetMethod(nameof(SetModel), BindingFlags.Instance | BindingFlags.NonPublic) |
||||
|
?.MakeGenericMethod(modelType).Invoke(this, new[] {template, model}); |
||||
|
} |
||||
|
|
||||
|
template.ServiceProvider = scope.ServiceProvider; |
||||
|
template.Localizer = GetLocalizerOrNull(templateDefinition); |
||||
|
template.HtmlEncoder = scope.ServiceProvider.GetService<HtmlEncoder>(); |
||||
|
template.JavaScriptEncoder = scope.ServiceProvider.GetService<JavaScriptEncoder>(); |
||||
|
template.UrlEncoder = scope.ServiceProvider.GetService<UrlEncoder>(); |
||||
|
template.Body = body; |
||||
|
template.GlobalContext = globalContext; |
||||
|
|
||||
|
await template.ExecuteAsync(); |
||||
|
|
||||
|
return await template.GetOutputAsync(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SetModel<TModel>(IRazorTemplatePage razorTemplatePage, object model = null) |
||||
|
{ |
||||
|
if (razorTemplatePage is IRazorTemplatePage<TModel> razorTemplatePageWithModel) |
||||
|
{ |
||||
|
razorTemplatePageWithModel.Model = (TModel)model; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private IStringLocalizer GetLocalizerOrNull(TemplateDefinition templateDefinition) |
||||
|
{ |
||||
|
if (templateDefinition.LocalizationResource != null) |
||||
|
{ |
||||
|
return _stringLocalizerFactory.Create(templateDefinition.LocalizationResource); |
||||
|
} |
||||
|
|
||||
|
return _stringLocalizerFactory.CreateDefaultOrNull(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<ConfigureAwait ContinueOnCapturedContext="false" /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
||||
|
<xs:complexType> |
||||
|
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,19 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\configureawait.props" /> |
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.TextTemplating.Abstractions\Volo.Abp.TextTemplating.Abstractions.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Scriban" Version="3.6.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,12 @@ |
|||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Scriban |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpTextTemplatingAbstractionsModule) |
||||
|
)] |
||||
|
public class AbpTextTemplatingScribanModule : AbpModule |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -1,46 +1,9 @@ |
|||||
using System; |
using Volo.Abp.Modularity; |
||||
using System.Collections.Generic; |
|
||||
using Microsoft.Extensions.DependencyInjection; |
|
||||
using Volo.Abp.Localization; |
|
||||
using Volo.Abp.Modularity; |
|
||||
using Volo.Abp.VirtualFileSystem; |
|
||||
|
|
||||
namespace Volo.Abp.TextTemplating |
namespace Volo.Abp.TextTemplating |
||||
{ |
{ |
||||
[DependsOn( |
|
||||
typeof(AbpVirtualFileSystemModule), |
|
||||
typeof(AbpLocalizationAbstractionsModule) |
|
||||
)] |
|
||||
public class AbpTextTemplatingModule : AbpModule |
public class AbpTextTemplatingModule : AbpModule |
||||
{ |
{ |
||||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|
||||
{ |
|
||||
AutoAddProvidersAndContributors(context.Services); |
|
||||
} |
|
||||
|
|
||||
private static void AutoAddProvidersAndContributors(IServiceCollection services) |
|
||||
{ |
|
||||
var definitionProviders = new List<Type>(); |
|
||||
var contentContributors = new List<Type>(); |
|
||||
|
|
||||
services.OnRegistred(context => |
|
||||
{ |
|
||||
if (typeof(ITemplateDefinitionProvider).IsAssignableFrom(context.ImplementationType)) |
|
||||
{ |
|
||||
definitionProviders.Add(context.ImplementationType); |
|
||||
} |
|
||||
|
|
||||
if (typeof(ITemplateContentContributor).IsAssignableFrom(context.ImplementationType)) |
|
||||
{ |
|
||||
contentContributors.Add(context.ImplementationType); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
services.Configure<AbpTextTemplatingOptions>(options => |
|
||||
{ |
|
||||
options.DefinitionProviders.AddIfNotContains(definitionProviders); |
|
||||
options.ContentContributors.AddIfNotContains(contentContributors); |
|
||||
}); |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,23 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.test.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net5.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Volo\Abp\TextTemplating\Razor\SampleTemplates\**\*.cshtml"> |
||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory> |
||||
|
</EmbeddedResource> |
||||
|
<None Remove="Volo\Abp\TextTemplating\Razor\SampleTemplates\**\*.cshtml" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.TextTemplating.Razor\Volo.Abp.TextTemplating.Razor.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj" /> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using Microsoft.Extensions.FileProviders; |
||||
|
using Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class RazorLocalizedTemplateContentReaderFactory_Tests : LocalizedTemplateContentReaderFactory_Tests<RazorTextTemplatingTestModule> |
||||
|
{ |
||||
|
public RazorLocalizedTemplateContentReaderFactory_Tests() |
||||
|
{ |
||||
|
LocalizedTemplateContentReaderFactory = new LocalizedTemplateContentReaderFactory( |
||||
|
new PhysicalFileVirtualFileProvider( |
||||
|
new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), |
||||
|
"Volo", "Abp", "TextTemplating", "Razor")))); |
||||
|
|
||||
|
WelcomeEmailEnglishContent = "@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.WelcomeEmailModel>" + |
||||
|
Environment.NewLine + |
||||
|
"Welcome @Model.Name to the abp.io!"; |
||||
|
|
||||
|
WelcomeEmailTurkishContent = "@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.WelcomeEmailModel>" + |
||||
|
Environment.NewLine + |
||||
|
"Merhaba @Model.Name, abp.io'ya hoşgeldiniz!"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class RazorTemplateDefinitionTests : TemplateDefinitionTests<RazorTextTemplatingTestModule> |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,145 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class RazorTemplateRenderer_Tests : AbpTextTemplatingTestBase<RazorTextTemplatingTestModule> |
||||
|
{ |
||||
|
private readonly ITemplateRenderer _templateRenderer; |
||||
|
|
||||
|
public RazorTemplateRenderer_Tests() |
||||
|
{ |
||||
|
_templateRenderer = GetRequiredService<ITemplateRenderer>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_Rendered_Localized_Template_Content_With_Different_Cultures() |
||||
|
{ |
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.WelcomeEmail, |
||||
|
model: new WelcomeEmailModel() |
||||
|
{ |
||||
|
Name = "John" |
||||
|
}, |
||||
|
cultureName: "en" |
||||
|
)).ShouldBe("Welcome John to the abp.io!"); |
||||
|
|
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.WelcomeEmail, |
||||
|
model: new WelcomeEmailModel() |
||||
|
{ |
||||
|
Name = "John" |
||||
|
}, |
||||
|
cultureName: "tr" |
||||
|
)).ShouldBe("Merhaba John, abp.io'ya hoşgeldiniz!"); |
||||
|
|
||||
|
//"en-US" fallbacks to "en" since "en-US" doesn't exists and "en" is the fallback culture
|
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.WelcomeEmail, |
||||
|
model: new WelcomeEmailModel() |
||||
|
{ |
||||
|
Name = "John" |
||||
|
}, |
||||
|
cultureName: "en-US" |
||||
|
)).ShouldBe("Welcome John to the abp.io!"); |
||||
|
|
||||
|
//"fr" fallbacks to "en" since "fr" doesn't exists and "en" is the default culture
|
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.WelcomeEmail, |
||||
|
model: new WelcomeEmailModel() |
||||
|
{ |
||||
|
Name = "John" |
||||
|
}, |
||||
|
cultureName: "fr" |
||||
|
)).ShouldBe("Welcome John to the abp.io!"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_Rendered_Localized_Template_Content_With_Stronly_Typed_Model() |
||||
|
{ |
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.WelcomeEmail, |
||||
|
model: new WelcomeEmailModel("John"), |
||||
|
cultureName: "en" |
||||
|
)).ShouldBe("Welcome John to the abp.io!"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_Rendered_Inline_Localized_Template() |
||||
|
{ |
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.ForgotPasswordEmail, |
||||
|
new ForgotPasswordEmailModel("John"), |
||||
|
cultureName: "en" |
||||
|
)).ShouldBe("*BEGIN*Hello John, how are you?. Please click to the following link to get an email to reset your password!*END*"); |
||||
|
|
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.ForgotPasswordEmail, |
||||
|
new ForgotPasswordEmailModel("John"), |
||||
|
cultureName: "tr" |
||||
|
)).ShouldBe("*BEGIN*Merhaba John, nasılsın?. Please click to the following link to get an email to reset your password!*END*"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_Localized_Numbers() |
||||
|
{ |
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.ShowDecimalNumber, |
||||
|
new ShowDecimalNumberModel(123.45M), |
||||
|
cultureName: "en" |
||||
|
)).ShouldBe("*BEGIN*123.45*END*"); |
||||
|
|
||||
|
(await _templateRenderer.RenderAsync( |
||||
|
TestTemplates.ShowDecimalNumber, |
||||
|
new ShowDecimalNumberModel(123.45M), |
||||
|
cultureName: "de" |
||||
|
)).ShouldBe("*BEGIN*123,45*END*"); |
||||
|
} |
||||
|
|
||||
|
public class WelcomeEmailModel |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public WelcomeEmailModel() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public WelcomeEmailModel(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class ForgotPasswordEmailModel |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public ForgotPasswordEmailModel() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public ForgotPasswordEmailModel(string name) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class ShowDecimalNumberModel |
||||
|
{ |
||||
|
public decimal Amount { get; set; } |
||||
|
|
||||
|
public ShowDecimalNumberModel() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public ShowDecimalNumberModel(decimal amount) |
||||
|
{ |
||||
|
Amount = amount; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class RazorTestTemplateDefinitionProvider : TemplateDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(ITemplateDefinitionContext context) |
||||
|
{ |
||||
|
context.GetOrNull(TestTemplates.WelcomeEmail)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/WelcomeEmail", false); |
||||
|
|
||||
|
context.GetOrNull(TestTemplates.ForgotPasswordEmail)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail.cshtml", true); |
||||
|
|
||||
|
context.GetOrNull(TestTemplates.TestTemplateLayout1)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/TestTemplateLayout1.cshtml", true); |
||||
|
|
||||
|
context.GetOrNull(TestTemplates.ShowDecimalNumber)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/ShowDecimalNumber.cshtml", true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpTextTemplatingRazorModule), |
||||
|
typeof(AbpTextTemplatingTestModule) |
||||
|
)] |
||||
|
public class RazorTextTemplatingTestModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<RazorTextTemplatingTestModule>("Volo.Abp.TextTemplating.Razor"); |
||||
|
}); |
||||
|
|
||||
|
Configure<CSharpCompilerOptions>(options => |
||||
|
{ |
||||
|
options.References.Add(MetadataReference.CreateFromFile(typeof(RazorTextTemplatingTestModule).Assembly.Location)); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Razor |
||||
|
{ |
||||
|
public class RazorVirtualFileTemplateContributor_Tests : VirtualFileTemplateContributor_Tests<RazorTextTemplatingTestModule> |
||||
|
{ |
||||
|
public RazorVirtualFileTemplateContributor_Tests() |
||||
|
{ |
||||
|
WelcomeEmailEnglishContent = "@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.WelcomeEmailModel>" + |
||||
|
Environment.NewLine + |
||||
|
"Welcome @Model.Name to the abp.io!"; |
||||
|
|
||||
|
WelcomeEmailTurkishContent = "@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.WelcomeEmailModel>" + |
||||
|
Environment.NewLine + |
||||
|
"Merhaba @Model.Name, abp.io'ya hoşgeldiniz!"; |
||||
|
|
||||
|
ForgotPasswordEmailEnglishContent = "@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.ForgotPasswordEmailModel>" + |
||||
|
Environment.NewLine + |
||||
|
"@Localizer[\"HelloText\", Model.Name], @Localizer[\"HowAreYou\"]. Please click to the following link to get an email to reset your password!"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.ForgotPasswordEmailModel> |
||||
|
@Localizer["HelloText", Model.Name], @Localizer["HowAreYou"]. Please click to the following link to get an email to reset your password! |
||||
@ -0,0 +1,2 @@ |
|||||
|
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.ShowDecimalNumberModel> |
||||
|
@Model.Amount |
||||
@ -0,0 +1,2 @@ |
|||||
|
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase |
||||
|
*BEGIN*@Body*END* |
||||
@ -0,0 +1,2 @@ |
|||||
|
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.WelcomeEmailModel> |
||||
|
Welcome @Model.Name to the abp.io! |
||||
@ -0,0 +1,2 @@ |
|||||
|
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<Volo.Abp.TextTemplating.Razor.RazorTemplateRenderer_Tests.WelcomeEmailModel> |
||||
|
Merhaba @Model.Name, abp.io'ya hoşgeldiniz! |
||||
@ -0,0 +1,23 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.test.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net5.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<EmbeddedResource Include="Volo\Abp\TextTemplating\Scriban\SampleTemplates\**\*.tpl"> |
||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory> |
||||
|
</EmbeddedResource> |
||||
|
<None Remove="Volo\Abp\TextTemplating\Scriban\SampleTemplates\**\*.tpl" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.TextTemplating.Scriban\Volo.Abp.TextTemplating.Scriban.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj" /> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System.IO; |
||||
|
using Microsoft.Extensions.FileProviders; |
||||
|
using Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Scriban |
||||
|
{ |
||||
|
public class ScribanLocalizedTemplateContentReaderFactory_Tests : LocalizedTemplateContentReaderFactory_Tests<ScribanTextTemplatingTestModule> |
||||
|
{ |
||||
|
public ScribanLocalizedTemplateContentReaderFactory_Tests() |
||||
|
{ |
||||
|
LocalizedTemplateContentReaderFactory = new LocalizedTemplateContentReaderFactory( |
||||
|
new PhysicalFileVirtualFileProvider( |
||||
|
new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), |
||||
|
"Volo", "Abp", "TextTemplating", "Scriban")))); |
||||
|
|
||||
|
WelcomeEmailEnglishContent = "Welcome {{model.name}} to the abp.io!"; |
||||
|
WelcomeEmailTurkishContent = "Merhaba {{model.name}}, abp.io'ya hoşgeldiniz!"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Volo.Abp.TextTemplating.Scriban |
||||
|
{ |
||||
|
public class ScribanTemplateDefinitionTests : TemplateDefinitionTests<ScribanTextTemplatingTestModule> |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
namespace Volo.Abp.TextTemplating.Scriban |
||||
|
{ |
||||
|
public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(ITemplateDefinitionContext context) |
||||
|
{ |
||||
|
context.GetOrNull(TestTemplates.WelcomeEmail)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/WelcomeEmail", false); |
||||
|
|
||||
|
context.GetOrNull(TestTemplates.ForgotPasswordEmail)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail.tpl", true); |
||||
|
|
||||
|
context.GetOrNull(TestTemplates.TestTemplateLayout1)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/TestTemplateLayout1.tpl", true); |
||||
|
|
||||
|
context.GetOrNull(TestTemplates.ShowDecimalNumber)? |
||||
|
.WithVirtualFilePath("/SampleTemplates/ShowDecimalNumber.tpl", true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.VirtualFileSystem; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Scriban |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpTextTemplatingScribanModule), |
||||
|
typeof(AbpTextTemplatingTestModule) |
||||
|
)] |
||||
|
public class ScribanTextTemplatingTestModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpVirtualFileSystemOptions>(options => |
||||
|
{ |
||||
|
options.FileSets.AddEmbedded<ScribanTextTemplatingTestModule>("Volo.Abp.TextTemplating.Scriban"); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using Volo.Abp.TextTemplating.VirtualFiles; |
||||
|
|
||||
|
namespace Volo.Abp.TextTemplating.Scriban |
||||
|
{ |
||||
|
public class ScribanVirtualFileTemplateContributor_Tests : VirtualFileTemplateContributor_Tests<ScribanTextTemplatingTestModule> |
||||
|
{ |
||||
|
public ScribanVirtualFileTemplateContributor_Tests() |
||||
|
{ |
||||
|
WelcomeEmailEnglishContent = "Welcome {{model.name}} to the abp.io!"; |
||||
|
WelcomeEmailTurkishContent = "Merhaba {{model.name}}, abp.io'ya hoşgeldiniz!"; |
||||
|
ForgotPasswordEmailEnglishContent = "{{L \"HelloText\" model.name}}, {{L \"HowAreYou\" }}. Please click to the following link to get an email to reset your password!"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,45 +1,50 @@ |
|||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Shouldly; |
using Shouldly; |
||||
|
using Volo.Abp.Modularity; |
||||
using Xunit; |
using Xunit; |
||||
|
|
||||
namespace Volo.Abp.TextTemplating.VirtualFiles |
namespace Volo.Abp.TextTemplating.VirtualFiles |
||||
{ |
{ |
||||
public class VirtualFileTemplateContributor_Tests : AbpTextTemplatingTestBase |
public abstract class VirtualFileTemplateContributor_Tests<TStartupModule> : AbpTextTemplatingTestBase<TStartupModule> |
||||
|
where TStartupModule : IAbpModule |
||||
{ |
{ |
||||
private readonly ITemplateDefinitionManager _templateDefinitionManager; |
protected readonly ITemplateDefinitionManager TemplateDefinitionManager; |
||||
private readonly VirtualFileTemplateContentContributor _virtualFileTemplateContentContributor; |
protected readonly VirtualFileTemplateContentContributor VirtualFileTemplateContentContributor; |
||||
|
protected string WelcomeEmailEnglishContent; |
||||
|
protected string WelcomeEmailTurkishContent; |
||||
|
protected string ForgotPasswordEmailEnglishContent; |
||||
|
|
||||
public VirtualFileTemplateContributor_Tests() |
protected VirtualFileTemplateContributor_Tests() |
||||
{ |
{ |
||||
_templateDefinitionManager = GetRequiredService<ITemplateDefinitionManager>(); |
TemplateDefinitionManager = GetRequiredService<ITemplateDefinitionManager>(); |
||||
_virtualFileTemplateContentContributor = GetRequiredService<VirtualFileTemplateContentContributor>(); |
VirtualFileTemplateContentContributor = GetRequiredService<VirtualFileTemplateContentContributor>(); |
||||
} |
} |
||||
|
|
||||
[Fact] |
[Fact] |
||||
public async Task Should_Get_Localized_Content_By_Culture() |
public async Task Should_Get_Localized_Content_By_Culture() |
||||
{ |
{ |
||||
(await _virtualFileTemplateContentContributor.GetOrNullAsync( |
(await VirtualFileTemplateContentContributor.GetOrNullAsync( |
||||
new TemplateContentContributorContext(_templateDefinitionManager.Get(TestTemplates.WelcomeEmail), |
new TemplateContentContributorContext(TemplateDefinitionManager.Get(TestTemplates.WelcomeEmail), |
||||
ServiceProvider, |
ServiceProvider, |
||||
"en"))) |
"en"))) |
||||
.ShouldBe("Welcome {{model.name}} to the abp.io!"); |
.ShouldBe(WelcomeEmailEnglishContent); |
||||
|
|
||||
(await _virtualFileTemplateContentContributor.GetOrNullAsync( |
(await VirtualFileTemplateContentContributor.GetOrNullAsync( |
||||
new TemplateContentContributorContext(_templateDefinitionManager.Get(TestTemplates.WelcomeEmail), |
new TemplateContentContributorContext(TemplateDefinitionManager.Get(TestTemplates.WelcomeEmail), |
||||
ServiceProvider, |
ServiceProvider, |
||||
"tr"))) |
"tr"))) |
||||
.ShouldBe("Merhaba {{model.name}}, abp.io'ya hoşgeldiniz!"); |
.ShouldBe(WelcomeEmailTurkishContent); |
||||
} |
} |
||||
|
|
||||
[Fact] |
[Fact] |
||||
public async Task Should_Get_Non_Localized_Template_Content() |
public async Task Should_Get_Non_Localized_Template_Content() |
||||
{ |
{ |
||||
(await _virtualFileTemplateContentContributor.GetOrNullAsync( |
(await VirtualFileTemplateContentContributor.GetOrNullAsync( |
||||
new TemplateContentContributorContext( |
new TemplateContentContributorContext( |
||||
_templateDefinitionManager.Get(TestTemplates.ForgotPasswordEmail), |
TemplateDefinitionManager.Get(TestTemplates.ForgotPasswordEmail), |
||||
ServiceProvider, |
ServiceProvider, |
||||
null))) |
null))) |
||||
.ShouldBe("{{L \"HelloText\" model.name}}, {{L \"HowAreYou\" }}. Please click to the following link to get an email to reset your password!"); |
.ShouldBe(ForgotPasswordEmailEnglishContent); |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue