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
.
Nuget | Description |
---|---|
HS.inPoint.Templates.Engine | Contains the engine implementation itself - needed for rendering. |
HS.inPoint.Templates.Admin | Contains the template admin classes to administer (CRUD) templates. |
HS.inPoint.Templates.Renderer.SharpScript | Contains 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.Abstractions | Provides interfaces and models, used by other nugets. |
HS.inPoint.Templates.Common | Provides helper classes for the engine. |
HS.inPoint.Templates.StorageProvider.File | Storage 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:
- InlineResolver
- Registered resolver
- 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 returnTokenResolverResult.Ok(<resolved value>)
orTokenResolverResult.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.