Version: 2024.1.1

Templates

Nugets

There are several Nugets for inPoint.Templates in the HSRelease feed. If you are in an inPoint environment, most of them come as reference of inPoint.Common.

NugetDescription
HS.inPoint.Templates.EngineContains the engine implementation itself - needed for rendering.
HS.inPoint.Templates.AdminContains the template admin classes to administer (CRUD) templates.
HS.inPoint.Templates.Renderer.SharpScriptContains the underlying renderer of the engine. Since different renderers are supported, this is a extra nuget. The engine needs a renderer to work properly.
HS.inPoint.Templates.AbstractionsProvides interfaces and models, used by other nugets.
HS.inPoint.Templates.CommonProvides helper classes for the engine.
HS.inPoint.Templates.StorageProvider.FileStorage provider implementation based on files. This is only needed in a standalone environment if you want to store multiple templates and render them by id.

inPoint

Since custom templates can be created via the admin ui, it is possible to render templates in custom code. The engine is available everywhere, where a HierarchyProvider is available (Plugins etc.).

Render location

Templates can be rendered locally or on the server.

Locally

The templates will be rendered with the local engine ( = where the HierarchyProvider is). To use local rendering, the HierarchyProvider provides a LocalTemplateEngine property with a RenderTemplateAsync method.

Server

The templates will be rendered on the server and the result will be returned. If templates should be rendered on the server, the HierarchyProvider exposes the Template_RenderAsync method.

Render a template

Both methods (locally or on the server) take the same parameters and will return the same result.

Parameter

The render call usually takes one parameter of the type ITemplateContext

    public class TemplateContext
{
/// <summary>
/// The id of the template to render.
/// </summary>
public string TemplateId { get; set; }
/// <summary>
/// Inline template data/content to render a template without storage provider.
/// If inPoint.Templates.Abstractions.Models.ITemplateContext.TemplateIds set, this will be ignored!
/// </summary>
public string TemplateData { get; set; }
/// <summary>
/// The culture to render (e.g. en-us).
/// </summary>
public string Culture { get; set; }
/// <summary>
/// Additional values for the variable resolver.
/// </summary>
public IDictionary<string, object> Values { get; set; }
/// <summary>
/// The cancellation token
/// </summary>
public CancellationToken CancellationToken { get; set; }
}
  • TemplateId: If the template was created via the admin ui, you have to supply the id/name (hs.inpoint.reminder...). If this is set, TemplateData will be ignored.
  • TemplateData: If there is no saved template, this could be set to the template content to render it inline (without having it stored anywhere).
  • Culture: The language to be rendered (e.g. en-us).
  • Values: Additional values that could be used as template variables. See [Variable resolving](development-templates.md#Variable resolving)
  • CancellationToken: The CancellationToken, if available.

Result

    public class TemplateRenderResult
{
/// <summary>
/// The rendered content as string.
/// </summary>
public string Content { get; set; }
/// <summary>
/// The culture of the rendered template.
/// This could be different to the requested one if the exact requested on is not available.
/// </summary>
public string Culture { get; set; }
/// <summary>
/// Additional rendered arguments from the template.
/// </summary>
public IDictionary<string, string> Arguments { get; set; }
/// <summary>
/// Defines if the rendered template is a (system) default template.
/// </summary>
public bool IsDefault { get; set; }
}
  • Content: The rendered template content
  • Culture: The culture of the rendered template (could be different from the requested one, if the requested one is not available).
  • Arguments: The rendered template arguments. See Template Arguments.
  • IsDefault: True if the rendered template is a system (H&S) default template.

Variable resolving

There are multiple stages that will be called when resolving variables:

  1. InlineResolver
  2. Registered resolver
  3. Values

Starting with the InlineResolver, every stage will try to resolve the current variable. If found it will be returned and the other stages will be skipped.

Inline resolver

A local function which can be supplied in the TemplateContext to resolve variables.

InlineResolver = (token, context) =>
{
if (token.Name == "myvariable")
return Task.FromResult(TokenResolverResult.Ok("myvalue")));

return Task.FromResult(TokenResolverResult.NotResolved());
}

If the function can resolve that variable, it should return TokenResolverResult.Ok(<resolved value>). If not TokenResolverResult.NotResolved().

This can be used to "lazy" resolve tokens, since the inline resolver will be only called if the variable is really used in the template. This enables the developer to only call long running variable resolve operations if they are really needed.

Registered resolver

For more global approach you can register a resolver with the template engine. This means, a resolver is registered one time, but will be used in every render call in the whole process.

To create a resolver, the ITokenResolver has to be implemented and registered with the engine via the RegisterTokenResolver method.

public interface ITokenResolver
{
//
// Summary:
// Defines the priority of the resolver. Lower numbers will be called first when
// resolving a token.
int Priority { get; }

//
// Summary:
// Trys to resolve the given variable.
//
// Parameters:
// token:
// The token to resolve.
//
// context:
// Additional context informations.
//
// Returns:
// Resolve result which holds the resolved value and the state.
Task<TokenResolverResult> TryResolveTokenAsync(IToken token, ITemplateContext? context);
}
  • Priority: If there are multiple resolvers, they will be called in order of their priority (lower numbers will be called first)
  • TryResolveTokenAsync: This method will be called when resolving a variable. Like the InlineResolver it should either return TokenResolverResult.Ok(<resolved value>) or TokenResolverResult.NotResolved().

If TokenResolverResult.NotResolved() is returned the next resolver will be called.

This also has the advantage of being only called when the template is rendered and the tokens are used.

Values

If non of the other stages can resolve a variable, the values dictionary will be check if it has a key with the name of the variable in it.

Stand alone

The template engine can also be used in applications without any inPoint relation.

Requirements

  • Net Framework 4.8 or net6
  • Nuget

Engine creation

Standard

var templateEngine = new TemplateEngine(
new TemplateEngineOptions()
.SetSharpScriptRenderer()
);

Dependency injection

_ = services.AddTemplateEngine((options) =>
options
.SetSharpScriptRenderer()
);

Adding registered resolver

Resolver could be either added at creation in the TemplateEngineOptions object, or at runtime on the engine itself.

var templateEngine = new TemplateEngine(
new TemplateEngineOptions()
.SetSharpScriptRenderer()
.AddTokenResolver<MyResolverType>()
);

The AddTokenResolver method could be either used with an type argument (.AddTokenResolver<MyResolverType>()) or with an already created instance (.AddTokenResolver(new MyResolverType())) On how to implement a resolver, see Registered resolvers

Resolver can also be added to the DI service collection. The will be registered automatically when the engine is created.

Storage provider

Storage provider handle the low level access to templates. They provide template data for specific templated ids and implement basic CRUD management functionality for administration. If it is only planned to render templates inline via the TemplateData property, a storage provider is not needed.

Implementation

To create a storage provider, the IStorageProvider interface needs to be implemented:

 public interface IStorageProvider
{
/// <summary>
/// Retrieves a file by id and culture.
/// </summary>
/// <remarks>
/// If the templateId exists, but the requested culture does not, this will throw an <see cref="StorageProviderException"/>
/// when options is set to <see cref="GetOptions.None"/>.
/// If the template id exists, the requested culture does not and options is set to <see cref="GetOptions.OtherCultures"/>
/// It will return a file with an empty culture, no data but a populated <see cref="IStorageFile.AvailableCultures"/> list.
/// </remarks>
/// <param name="id">The file id to retrieve.</param>
/// <param name="culture">The culture to retrieve.</param>
/// <param name="options">Optional options.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>The storage file.</returns>
Task<IStorageFile> GetAsync(string id, TemplateCulture culture, GetOptions options = GetOptions.None, CancellationToken? cancellationToken = default);
/// <summary>
/// Lists all available ids.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of all available file ids.</returns>
Task<IEnumerable<string>> ListAsync(CancellationToken? cancellationToken = default);
/// <summary>
/// Lists all available cultures for a specific id.
/// </summary>
/// <param name="id">The file id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A list of available cultures.</returns>
Task<IEnumerable<string>> ListCulturesAsync(string id, CancellationToken? cancellationToken = default);
/// <summary>
/// Adds a new file to the storage provider.
/// </summary>
/// <param name="file">The file to add.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>True if added, false if the file exists already, exception on error.</returns>
Task<bool> AddAsync(IStorageFile file, CancellationToken? cancellationToken = default);
/// <summary>
/// Updates a file in the storage provider.
/// </summary>
/// <param name="file">The file to update. It will be matched by id and culture.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>True if updated, false if not found, exception on error.</returns>
Task<bool> UpdateAsync(IStorageFile file, CancellationToken? cancellationToken = default);
/// <summary>
/// Deletes a file by id and culture.
/// </summary>
/// <param name="id">The id to delete.</param>
/// <param name="culture">One or more cultures to delete, null to delete everything.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>True if deleted, false if not found, exception on error.</returns>
Task<bool> DeleteAsync(string id, TemplateCulture? culture = null, CancellationToken? cancellationToken = default);
/// <summary>
/// Checks if a file exists.
/// </summary>
/// <param name="id">The id to check.</param>
/// <param name="culture">The culture to check.</param>
/// <param name="cancellationToken">The CancellationToken.</param>
/// <returns>True if the file exists, false if otherwise.</returns>
Task<bool> ExistsAsync(string id, string culture, CancellationToken? cancellationToken = default);
}

Adding it to the engine

Like the resolver, the storage provider can be added via the options object with the SetStorageProvider method or via adding it to the DI service collection.

Rendering

var result = await templateEngine.RenderTemplateAsync(new TemplateContext
{
TemplateId = "my.template.id", // use TemplateId OR TemplateData - if both are set, TemplateId will be used!
TemplateData = "<h1>Hello, {{ val(\"username\") }}!</h1>",
Culture = "en-us",
Values = new Dictionary<string, object>
{
{ "additionalKey", "additionalValue" }
}
});

See Result for details on the result object.