Skip to content
Norbert Bietsch edited this page Mar 9, 2021 · 4 revisions
Logo

Axuno.TextTemplating

Introduction

Axuno.TextTemplating is a simple, yet efficient text template system. Text templating is used to dynamically render content based on a template and a data object as a model:

Template + Model => renderer => Rendered Content

This the same basic concept as with ASP.NET Razor Views or Razor Pages. The template rendering engine is very powerful:

  • It is based on the Scriban library, so it supports conditional logics, loops and much more.
  • Template content can be localized.
  • You can define layout templates to be used as the layout while rendering other templates.
  • You can pass arbitrary (global) objects to the template context (beside the model) for advanced scenarios.
  • Scriban is one of the fasted engines, if not the fastest.

You can use the rendered output for any purpose, like sending emails or preparing some reports.

Example

  1. Define the model
public class HelloModel
{
    public string Name { get; set; }
}
  1. Define the template
Hello {{model.name}} :)

Note, that by default, the engine will use "snake_case". See below for more details.

If you render the template with a HelloModel with its Name property set to John, the rendered output is will be:

Hello John :)

Defining Templates

In order to render templates, there are a few steps to implement.

  1. Create a class inheriting from the TemplateDefinitionProvider base class:
public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider
{
    public override void Define(ITemplateDefinitionContext context)
    {
        context.Add(
            new TemplateDefinition("Hello") //template name: "Hello"
                .WithVirtualFilePath(
                    "/Demos/Hello/Hello.tpl", //template content path
                    isInlineLocalized: true
                )
        );
    }
}
  • context is used to add new templates or get the templates defined by depended modules. Use context.Add(...) to define a new template.
  • TemplateDefinition is the class representing a template. Each template must have a unique name. This name will be used when you want to renderg the template.
  • /Demos/Hello/Hello.tpl is the virtual path of the template file.
  • isInlineLocalized: If true, it means you are using a single template for all languages. If it is false, you'll have to provide different complete templates for each language. See the Localization section for more.

The Template Content

WithVirtualFilePath indicates that we are using the Axuno.VirtualFileSystem to store the template content. Create a Hello.tpl file inside your project and mark it as "embedded resource" on the properties window of Visual Studio.

The file for the example is Hello.tpl and it has the following content:

Hello {{model.name}} :)

The Axuno.VirtualFileSystem requires to add your files in the ConfigureServices part of your app:

Configure<VirtualFileSystemOptions>(options =>
{
    options.FileSets.AddEmbedded<TextTemplateDemoModule>("TextTemplateDemo");
});
  • TextTemplateDemoModule is the module class that you define your template in.
  • TextTemplateDemo is the root namespace of your project.

If you prefer to use phyiscal files, or if you would like to override the content of embedded templates, the change the FileSets options like this:

Configure<VirtualFileSystemOptions>(options =>
{
    options.FileSets.AddPhysical(Path.Combine(Directory.GetCurrentDirectory(), "Templates"));
});

Rendering the Template

ITemplateRenderer service is used to render a template content.

Example: Rendering a Simple Template

public class HelloDemo
{
    private readonly ITemplateRenderer _templateRenderer;

    public HelloDemo(ITemplateRenderer templateRenderer)
    {
        _templateRenderer = templateRenderer;
    }

    public async Task RunAsync()
    {
        var result = await _templateRenderer.RenderAsync(
            "Hello", //the template name
            new HelloModel
            {
                Name = "John"
            }
        );

        Console.WriteLine(result);
    }
}
  • HelloDemo is a simple class that injects the ITemplateRenderer in its constructor and uses it inside the RunAsync method.
  • RenderAsync gets two fundamental parameters:
    • templateName: The name of the template to be rendered (Hello in this example).
    • model: An object that is used as the model inside the template (a HelloModel object in this example).

In this simple example we will just write the rendered output to the console:

Hello John :)

Anonymous Model

While it is suggested to create model classes for the templates, it sometimes it ispractical (and possible) to use anonymous objects for simple cases:

var result = await _templateRenderer.RenderAsync(
    "Hello",
    new { Name = "John" }
);

We haven't created a model class, but created an anonymous object as the model.

PascalCase vs snake_case

By default, PascalCase property names (like UserName) are used as snake_case (like user_name) in the templates.

Localization

It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections.

Inline localization

Inline localization uses the .NET resource files to localize texts inside templates.

Example: Reset Password Link

Assuming you need to send an email to a user to reset her/his password. Let's assume the following content:

<a title="{{L "ResetMyPasswordTitle"}}" href="{{model.link}}">{{L "ResetMyPassword" model.name}}</a>

L stands for the function that is used to localize the given word based on the current user culture. You need to define the ResetMyPassword in a .NET resource file.

You also need to declare the localization resource to be used with this template, inside your template definition provider class:

context.Add(
    new TemplateDefinition(
            "PasswordReset", //Template name
            typeof(DemoResource) //LOCALIZATION RESOURCE
        ).WithVirtualFilePath(
            "/Demos/PasswordReset/PasswordReset.tpl", //template content path
            isInlineLocalized: true
        )
);

That's all. Render the template with this code...

var result = await _templateRenderer.RenderAsync(
    "PasswordReset", //the template name
    new PasswordResetModel
    {
        Name = "john",
        Link = "https://axuno.net/example-link?userId=123&token=ABC"
    }
);

... and get the localized result:

<a title="Reset my password" href="https://abp.io/example-link?userId=123&token=ABC">Hi john, Click here to reset your password</a>

If you define the default localization resource for your application, then there is no need to declare a separate resource type for the template definition.

Multiple Contents Localization

Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations.

Example: Welcome Email Template

Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture.

First, create a folder and put your templates inside it, like en.tpl, tr.tpl... one for each culture you support:

TextTemplateDemo
   +-- Demos
   +-- WelcomeEmail
          +-- Templates
                 +-- en.tpl
                 +-- de.tpl

Then add your template definition in the template definition provider class:

context.Add(
    new TemplateDefinition(
            name: "WelcomeEmail",
            defaultCultureName: "en"
        )
        .WithVirtualFilePath(
            "/Demos/WelcomeEmail/Templates", //template content folder
            isInlineLocalized: false
        )
);
  • Set default culture name, so it fallbacks to the default culture if there is no template for the desired culture.
  • Specify the template folder rather than a single template file.
  • Important: Set isInlineLocalized to false for this case.

That's all, you can render the template for the current culture:

var result = await _templateRenderer.RenderAsync("WelcomeEmail");

Skipped the model for this example to keep it simple, but you can use models as just explained before.

Specify the Culture

ITemplateRenderer service uses the current culture (CultureInfo.CurrentUICulture) if not specified. If you need, you can specify the culture as the cultureName parameter:

var result = await _templateRenderer.RenderAsync(
    "WelcomeEmail",
    cultureName: "en"
);

Layout Templates

Layout templates are used to create shared layouts among other templates. It is similar to the layout system in the ASP.NET MVC or Razor Pages.

Example: Email HTML Layout Template

For example, you may want to create a single layout for all of your email templates.

First, create a template file just like before:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
</head>
<body>
    {{ content }}
</body>
</html>
  • A layout template must have a {{ content }} placeholder for the rendered child content.

Then register your template in the template definition provider:

context.Add(
    new TemplateDefinition(
        "EmailLayout",
        isLayout: true //SET isLayout!
    ).WithVirtualFilePath(
        "/Demos/EmailLayout/EmailLayout.tpl",
        isInlineLocalized: true
    )
);

Now, you can use this template as the layout of any other template:

context.Add(
    new TemplateDefinition(
            name: "WelcomeEmail",
            defaultCultureName: "en",
            layout: "EmailLayout" //Set the LAYOUT
        ).WithVirtualFilePath(
            "/Demos/WelcomeEmail/Templates",
            isInlineLocalized: false
        )
);

Global Context

You can pass global variables to the global context.

An example template content:

A global object value: {{ myGlobalObject }}

This template assumes, that myGlobalObject object is part of the global context. You can provide it like shown below:

var result = await _templateRenderer.RenderAsync(
    "GlobalContextUsage",
    globalContext: new Dictionary<string, object>
    {
        {"myGlobalObject", "TEST VALUE"}
    }
);

The rendered result will be:

A global object value: TEST VALUE

Advanced Features

This section covers some internals and more advanced usages of the text templating system.

Template Content Provider

ITemplateRenderer is used to render the template, which is what you want for most of the cases. However, you can use the ITemplateContentProvider to get the raw (not rendered) template contents.

ITemplateContentProvider is internally used by the ITemplateRenderer to get the raw template contents.

Example:

public class TemplateContentDemo : ITransientDependency
{
    private readonly ITemplateContentProvider _templateContentProvider;

    public TemplateContentDemo(ITemplateContentProvider templateContentProvider)
    {
        _templateContentProvider = templateContentProvider;
    }

    public async Task RunAsync()
    {
        var result = await _templateContentProvider
            .GetContentAsync("Hello");

        Console.WriteLine(result);
    }
}

The result will be the raw template content:

Hello {{model.name}} :)
  • GetContentAsync returns null if no content defined for the requested template.
  • It can get a cultureName parameter that is used if template has different files for different cultures (see Multiple Contents Localization section above).

Template Content Contributor

ITemplateContentProvider service uses ITemplateContentContributor implementations to find template contents. There is a single pre-implemented content contributor, VirtualFileTemplateContentContributor, which gets template contents from the virtual file system as described above.

You can implement the ITemplateContentContributor to read raw template contents from another source.

Example:

public class MyTemplateContentProvider
    : ITemplateContentContributor, ITransientDependency
{
    public async Task<string> GetOrNullAsync(TemplateContentContributorContext context)
    {
        var templateName = context.TemplateDefinition.Name;

        //TODO: Try to find content from another source
        return null;
    }
}

Return null if your source can not find the content, so ITemplateContentProvider fallbacks to the next contributor.

Template Definition Manager

ITemplateDefinitionManager service can be used to get the template definitions (created by the template definition providers).