Razor Integration

The Razor template is a standard C# class, so you can freely use the functions of C#, such as dependency injection, using LINQ, custom methods, and even using Repository.

Installation

It is suggested to use the ABP CLI to install this package.

Using the ABP CLI

Open a command line window in the folder of the project (.csproj file) and type the following command:

abp add-package Volo.Abp.TextTemplating.Razor

Manual Installation

If you want to manually install;

  1. Add the Volo.Abp.TextTemplating.Razor NuGet package to your project:
Install-Package Volo.Abp.TextTemplating.Razor
  1. Add the AbpTextTemplatingRazorModule to the dependency list of your module:
[DependsOn(
    //...other dependencies
    typeof(AbpTextTemplatingRazorModule) //Add the new module dependency
    )]
public class YourModule : AbpModule
{
}

Add MetadataReference to CSharpCompilerOptions

You need to add the MetadataReference of the type used in the template to CSharpCompilerOptions's References.

public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AbpRazorTemplateCSharpCompilerOptions>(options =>
    {
        options.References.Add(MetadataReference.CreateFromFile(typeof(YourModule).Assembly.Location));
    });
}

Add MetadataReference for a template.

You can add some MetadataReference to the template

public override void ConfigureServices(ServiceConfigurationContext context)
{
    services.Configure<AbpCompiledViewProviderOptions>(options =>
    {
        //Hello is template name.
        options.TemplateReferences.Add("Hello", new List<Assembly>()
            {
                Assembly.Load("Microsoft.Extensions.Logging.Abstractions"),
                Assembly.Load("Microsoft.Extensions.Logging")
            }
            .Select(x => MetadataReference.CreateFromFile(x.Location))
            .ToList());
    });
}

Defining Templates

Before rendering a template, you should define it. 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"
                .WithRazorEngine()
                .WithVirtualFilePath(
                    "/Demos/Hello/Hello.cshtml", //template content path
                    isInlineLocalized: true
                )
        );
    }
}
  • context object is used to add new templates or get the templates defined by depended modules. Used context.Add(...) to define a new template.
  • TemplateDefinition is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template).
  • /Demos/Hello/Hello.cshtml is the path of the template file.
  • isInlineLocalized is used to declare if you are using a single template for all languages (true) or different templates for each language (false). See the Localization section below for more.
  • WithRenderEngine method is used to set the render engine of the template.

The Template Base

Every cshtml template page needs to inherit RazorTemplatePageBase or RazorTemplatePageBase<Model>. There are some useful properties in the base class that can be used in templates. eg: Localizer, ServiceProvider.

The Template Content

WithVirtualFilePath indicates that we are using the Virtual File System to store the template content. Create a Hello.cshtml file inside your project and mark it as "embedded resource" on the properties window:

hello-template-razor

Example Hello.cshtml content is shown below:

@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<HelloModelNamespace.HelloModel>
Hello @Model.Name

The HelloModel class is:

namespace HelloModelNamespace
{
    public class HelloModel
    {
        public string Name { get; set; }
    }
}

The Virtual File System requires to add your files in the ConfigureServices method of your module class:

Configure<AbpVirtualFileSystemOptions>(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.

Rendering the Template

ITemplateRenderer service is used to render a template content.

Example: Rendering a Simple Template

public class HelloDemo : ITransientDependency
{
    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).

The result shown below for this example:

Hello John :)

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 localization system to localize texts inside templates.

Example: Reset Password Link

Assuming you need to send an email to a user to reset her/his password. Here, the model/template content:

namespace ResetMyPasswordModelNamespace
{
    public class ResetMyPasswordModel
    {
        public string Link { get; set; }

        public string Name { get; set; }
    }
}
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<ResetMyPasswordModelNamespace.ResetMyPasswordModel>
<a title="@Localizer["ResetMyPasswordTitle"]" href="@Model.Link">@Localizer["ResetMyPassword", Model.Name]</a>

Localizer service is used to localize the given key based on the current user culture. You need to define the ResetMyPassword key inside your localization file:

"ResetMyPasswordTitle": "Reset my password",
"ResetMyPassword": "Hi {0}, Click here to reset your password"

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
        )
        .WithRazorEngine()
        .WithVirtualFilePath(
            "/Demos/PasswordReset/PasswordReset.cshtml", //template content path
            isInlineLocalized: true
        )
);

That's all. When you render this template like that:

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

You will see 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 no need to declare the 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.cshtml, tr.cshtml... one for each culture you support:

multiple-file-template-razor

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

context.Add(
    new TemplateDefinition(
            name: "WelcomeEmail",
            defaultCultureName: "en"
        )
        .WithRazorEngine()
        .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.
  • 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 modal 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 Core MVC / 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:

@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
</head>
<body>
    @Body
</body>
</html>
  • A layout template must have a Body part as a place holder for the rendered child content.

The register your template in the template definition provider:

context.Add(
    new TemplateDefinition(
        "EmailLayout",
        isLayout: true //SET isLayout!
    )
    .WithRazorEngine()
    .WithVirtualFilePath(
        "/Demos/EmailLayout/EmailLayout.cshtml",
        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
        )
        .WithRazorEngine()
        .WithVirtualFilePath(
            "/Demos/WelcomeEmail/Templates",
            isInlineLocalized: false
        )
);

Global Context

ABP passes the model that can be used to access to the model inside the template. You can pass more global variables if you need.

An example template content:

@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase
A global object value: @GlobalContext["myGlobalObject"]

This template assumes that that is a myGlobalObject object in the template rendering context. You can provide it like shown below:

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

The rendering result will be:

A global object value: TEST VALUE

Replacing the Existing Templates

It is possible to replace a template defined by a module that used in your application. In this way, you can customize the templates based on your requirements without changing the module code.

Option-1: Using the Virtual File System

The Virtual File System allows you to override any file by placing the same file into the same path in your project.

Example: Replace the Standard Email Layout Template

ABP Framework provides an email sending system that internally uses the text templating to render the email content. It defines a standard email layout template in the /Volo/Abp/Emailing/Templates/Layout.cshtml path. The unique name of the template is Abp.StandardEmailTemplates.Layout and this string is defined as a constant on the Volo.Abp.Emailing.Templates.StandardEmailTemplates static class.

Do the following steps to replace the template file with your own;

1) Add a new file into the same location (/Volo/Abp/Emailing/Templates/Layout.cshtml) in your project:

replace-email-layout-razor

2) Prepare your email layout template:

@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
</head>
<body>
    <h1>This my header</h1>

    @Body

    <footer>
      This is my footer...
    </footer>
</body>
</html>

This example simply adds a header and footer to the template and renders the content between them (see the Layout Templates section above to understand it).

3) Configure the embedded resources in the .csproj file

  • Add Microsoft.Extensions.FileProviders.Embedded NuGet package to the project.
  • Add <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest> into the <PropertyGroup>...</PropertyGroup> section of your .csproj file.
  • Add the following code into your .csproj file:
<ItemGroup>
  <None Remove="Volo\Abp\Emailing\Templates\*.cshtml" />
  <EmbeddedResource Include="Volo\Abp\Emailing\Templates\*.cshtml" />
</ItemGroup>

This makes the template files "embedded resource".

4) Configure the virtual file system

Configure the AbpVirtualFileSystemOptions in the ConfigureServices method of your module to add the embedded files into the virtual file system:

Configure<AbpVirtualFileSystemOptions>(options =>
{
    options.FileSets.AddEmbedded<BookStoreDomainModule>();
});

BookStoreDomainModule should be your module name, in this example code.

Be sure that your module (directly or indirectly) depends on the AbpEmailingModule. Because the VFS can override files based on the dependency order.

Now, your template will be used when you want to render the email layout template.

Option-2: Using the Template Definition Provider

You can create a template definition provider class that gets the email layout template and changes the virtual file path for the template.

Example: Use the /MyTemplates/EmailLayout.cshtml file instead of the standard template

using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing.Templates;
using Volo.Abp.TextTemplating;

namespace MyProject
{
    public class MyTemplateDefinitionProvider
        : TemplateDefinitionProvider, ITransientDependency
    {
        public override void Define(ITemplateDefinitionContext context)
        {
            var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout);

            emailLayoutTemplate
                .WithVirtualFilePath(
                    "/MyTemplates/EmailLayout.cshtml",
                    isInlineLocalized: true
                );
        }
    }
}

You should still add the file /MyTemplates/EmailLayout.cshtml to the virtual file system as explained before. This approach allows you to locate templates in any folder instead of the folder defined by the depended module.

Beside the template content, you can manipulate the template definition properties, like DisplayName, Layout or LocalizationSource.

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
            .GetContentOrNullAsync("Hello");

        Console.WriteLine(result);
    }
}

The result will be the raw template content:

@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<HelloModelNamespace.HelloModel>
Hello @Model.Name
  • GetContentOrNullAsync 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).

See Also

Contributors


Last updated: May 03, 2021 Edit this page on GitHub

Was this page helpful?

Please make a selection.

To help us improve, please share your reason for the negative feedback in the field below.

Please enter a note.

Thank you for your valuable feedback!

Please note that although we cannot respond to feedback, our team will use your comments to improve the experience.

In this document
Community Talks

What’s New with .NET 9 & ABP 9?

21 Nov, 17:00
Online
Watch the Event
Mastering ABP Framework Book
Mastering ABP Framework

This book will help you gain a complete understanding of the framework and modern web application development techniques.

Learn More