Filters

Templates are sandboxed, they can't call methods on objects nor do they have any access to any static functions built into the .NET Framework, so just as Arguments define all data and objects available to templates, filters define all functionality available to templates.

The only filters registered by default are the Default Filters containing a comprehensive suite of filters useful within View Engine or Template environments and HTML Filters. There's nothing special about these filters other than they're pre-registered by default, your filters have access to the same APIs and functionality and can do anything that built-in filters can do.

Shadowing Filters

You can easily override default filters with the same name and arguments by inserting them at the start of the TemplateFilters list:

context.TemplateFilters.Insert(0, new MyTemplateFilters());
Removing Default Filters

Or if you want to start from a clean slate, the default filters can be removed by clearing the collection:

context.TemplateFilters.Clear();

What are Filters?

Filters are just C# public instance methods from a class that inherits from TemplateFilter, e.g:

class MyFilter : TemplateFilter
{
    public string echo(string text) => $"{text} {text}";
    public double squared(double value) => value * value;
    public string greetArg(string key) => $"Hello {Context.Args[key]}";
            
    public ICacheClient Cache { get; set; }
    public string fromCache(string key) => Cache.Get(key);
}

Registering Filters

The examples below show the number of different ways filters can be registered:

Add them to the TemplateContext.TemplateFilters

Filters can be registered by adding them to the context.TemplateFilters collection directly:

var context = new TemplateContext
{
    Args =
    {
        ["contextArg"] = "foo"
    },
    TemplateFilters = { new MyFilter() }
}.Init();

That can now be called with:

var output = context.EvaluateTemplate("<p>{{ 'contextArg' | greetArg }}</p>");

This is also shows that Filters are initialized and have access to the TemplateContext through the Context property.

Add them to PageResult.TemplateFilters

If you only want to use a custom filter in a single Template, it can be registered on the PageResult that renders it instead:

var output = new PageResult(context.OneTimePage("<p>{{ 'hello' | echo }}</p>"))
{
    TemplateFilters = { new MyFilter() }
}.Result;
Autowired using TemplateContext IOC

Autowired instances of filters can also be created using TemplateContext's configured IOC where they're also injected with any registered IOC dependencies. To utilize this you need to specify the Type of the filter that should be Autowired by either adding it to the ScanTypes collection:

var context = new TemplateContext
{
    ScanTypes = { typeof(MyFilter) }
};
context.Container.AddSingleton(() => new MemoryCacheClient());
context.Container.Resolve().Set("key", "foo");
context.Init();

var output = context.EvaluateTemplate("<p>{{ 'key' | fromCache }}</p>");

When the TemplateContext is initialized it will go through each Type and create an autowired instance of each Type and register them in the TemplateFilters collection. An alternative to registering a single Type is to register an entire Assembly, e.g:

var context = new TemplateContext
{
    ScanAssemblies = { typeof(MyFilter).GetAssembly() }
};

Where it will search each Type in the Assembly for Template Filters and automatically register them.

Filter Resolution

Templates will use the first matching filter with the same name and argument count it can find by searching through all filters registered in the TemplateFilters collection, so you could override default filters with the same name by inserting your filters as the first item in the collection, e.g:

context.TemplateFilters.Insert(0, new MyTemplateFilters());

Auto coercion into Filter argument Types

A unique feature of Filters is that each of their arguments are automatically coerced into the filter argument Type using the powerful conversion facilities built into ServiceStack's Auto Mapping Utils and Text Serializers which can deserialize most of .NET's primitive Types like DateTime, TimeSpan, Enums, etc in/out of strings as well being able to convert a Collection into other Collection Types and any Numeric Type into any other Numeric Type which is how, despite only accepting doubles:

double squared(double value) => value * value;

squared can also be used with any other .NET Numeric Type, e.g: byte, int, long, decimal, etc. The consequence to this is that there's no method overloading in filters, filters are matched based on their name and their number of arguments and each argument is automatically converted into its filter method Param Type before it's called.

Context Filters

Filters can also get access to the current scope by defining a TemplateScopeContext as it's first parameter which can be used to access arguments in the current scope or add new ones as done by the assignTo filter:

public object assignTo(TemplateScopeContext scope, object value, string argName) //from filter
{
    scope.ScopedParams[argName] = value;
    return IgnoreResult.Value;
}

Block Filters

Filters can also write directly into the OutputStream instead of being forced to return buffered output. A Block Filter is declared by its Task return Type where instead of returning a value it instead writes directly to the TemplateScopeContext OutputStream as seem with the implementation of the includeFile protected filter:

public async Task includeFile(TemplateScopeContext scope, string virtualPath)
{
    var file = scope.Context.VirtualFiles.GetFile(virtualPath);
    if (file == null)
        throw new FileNotFoundException($"includeFile '{virtualPath}' was not found");

    using (var reader = file.OpenRead())
    {
        await reader.CopyToAsync(scope.OutputStream);
    }
}
For maximum performance all default filters which perform any I/O use Block filters to write directly to the OutputStream and avoid any blocking I/O or buffering.

Block Filters ends the filter chain

Block filters effectively end the Filter chain expression since they don't return any value that can be injected into a normal filter. The only thing that can come after a Block Filter are other Block Filters or Filter Transformers. If any are defined, the output of the Block Filter is buffered into a MemoryStream and passed into the next Block Filter or Filter Transformer in the chain, its output is then passed into the next one in the chain if any, otherwise the last output is written to the OutputStream.

An example of using a Block filter with a Filter Transformer is when you want include a markdown document and then convert it to HTML using the markdown Filter Transformer before writing its HTML output to the OutputStream:

{{ 'doc.md' | includeFile | markdown }}

Capture Block Filter Output

You can also capture the output of a Block Filter and assign it to a normal argument by using the assignTo Block Filter:

{{ 'doc.md' | includeFile | assignTo: contents }}

made with by ServiceStack