Skip to content
JPVenson edited this page May 10, 2022 · 44 revisions

Morestachio can be used with Formatters.

A Formatter is custom .net code that can be invoked from within the Template to format a value for display. Formatters are managed by the MorestachioFormatterService. This class holds and executes all Formatters that are called via the Path.To.Data.FormatAs() syntax. The last part of the Path is seen as the name of the formatter that should be invoked, its just like a normal function you call from c# code.

An Formatter call is build like this: flowdiagram

'(' (Optional)
   : path   
   | string

string
   : dot (Optional)
   | ')' (Optional)

path
   | dot
   | ')'

dot
   | path
   | formatter_name

'('
   : [argument_name] (Optional)
   : "string"|'string'
   : expression
   : , (Seperator)

')' (Optional)
   : path

Formatter Framework

The Formatter Framework is build for mapping C# functions to be used within Morestachio. You can declare them by annotating an method or function with one of the 3 Attributes.
There are 3 different kinds of formatters you can declare:

  • Standard Formatter:
    • An formatter with a name that is called directly on an object (SourceObject), that as no, one or many arguments and can return another object. Reminiscent of an c# Extension method but without the this keyword. Declared by [MorestachioFormatter]
  • Global Formatter:
    • An formatter with a name that can be called anywhere and does not check for an specific type, that has none, one or many other arguments and can return another object. Declared by [MorestachioGlobalFormatter]
  • Operator Formatter:

Binding static methods

You can add static methods from an external class by calling ParserOptionsBuilder.WithFormatters<T> or ParserOptionsBuilder.WithFormatters(Type) on the type. The ParserOptionsBuilder.WithFormatters method relies on the presence of an attribute (see list above)

Example

public static class StringFormatter
{
        /// <summary>
	///	Example Formatter: "string".reverse()
	///     For MorestachioFormatter the first argument represents the calling argument so this method can only be called on a value of type string
	/// </summary>
	/// <returns></returns>
	[MorestachioFormatter("reverse", "XXX")]
	public static string Reverse(string originalObject)
	{
		return originalObject.Reverse().Select(e => e.ToString()).Aggregate((e, f) => e + f);
	}

        /// <summary>
	///	Example Global Formatter: Random()
	/// </summary>
	/// <returns></returns>
	[MorestachioGlobalFormatter("Random", "Gets a non-negative random number")]
	public static int Random()
	{
		return _random.Next();
	}

	/// <summary>
	///	Example Binary Operator: "test" + "test"
	/// </summary>
	/// <param name="source"></param>
	/// <param name="target"></param>
	/// <returns></returns>
	[MorestachioOperator(OperatorTypes.Add, "Concatenates two strings")]
	public static string Append(string source, string target)
	{
		return source + target;
	}
}

You can add the whole class's formatters by calling the WithFormatters Extension method from your ParserOptionsBuilder like this:

ParserOptionsBuilder.New()
    .WithTemplate(...)
    .WithFormatters(typeof(StringFormatter));

this function does only support static functions from that type. If you want to add an delegate you can call ParserOptionsBuilder.WithFormatter(Delegate delegate, String name = null) method or one of its overloads. take not that this is WithFormatter Singular instead of WithFormatters plural.

Binding instance methods

To allow the calling of an instance method you must also annotate the method on your class with an attribute but on instance methods only the MorestachioFormatterAttribute is supported. Instance methods cannot be global or operators. Then proceed by invoking ParserOptionsBuilder.WithFormatters with your instance type. Example

public class MyDataType
{
        /// <summary>
	///	Example Formatter: (MyDataType).GetTextFromMe()
	///     This is an instance formatter and can only be called on an object of type MyDataType.
	/// </summary>
	/// <returns></returns>
	[MorestachioFormatter("[MethodName]", "XXX")]
	public string GetTextFromMe()
	{
		return "Hello World";
	}
}

Binding

In difference to the single type single formatter usage of Morestachio lib, this syntax also supports Generic types and extended mapping of types to formatters. You must annotate the method with the MorestachioFormatter and define a name for it.

The Formatter Framework understands Generics for any parameter and for all kinds of formatters. Generic constraints are not supported nor are they checked.

Limitations

As morestachio is a Weak typed language some of Csharps features that would require compile time type binding are not available or simply way to costly to implement. That includes Generic return types only. If you declare a Formatter that has an generic argument that is only represented in the return type, morestachio will always replace the generic type with typeof(object) as it cannot determine the real type. Example

public class MyDataType
{
	[MorestachioFormatter("[MethodName]", "XXX")]
	public Ta A<Ta>() //Ta will always be object
	{
		return "Hello World";
	}
	[MorestachioFormatter("[MethodName]", "XXX")]
	public Tb B<Tb>(Tb value) //Tb will always be of the correct given type
	{
		return value;
	}
}

In the example, the generic Argument Ta will always be object. The other generic argument Tb can be determinated and will be properly replaced.

Warning: MemoryLeaks ahead

If you are using the ParserOptionsBuilder.WithFormatter(Delegate, String) method to add an delegate that will access any other objects properties morestachio will hold a reference to that object as long as the parent MorestachioFormatterService exists. Be aware that this might causes event leaks (https://stackoverflow.com/questions/4526829/why-and-how-to-avoid-event-handler-memory-leaks)
There is nothing we can do as this is expected behavior from .net!

Async/Await in formatter

All formatters support async & await. When using NetStandard Version >= 2.1 you could also use ValueTask

Formatter service injection

Please see https://github.com/JPVenson/morestachio/wiki/Services

Formatter arguments

You can ether give an string or an expression as an argument to the Formatter.

Formatter string

{{Data.Path.To.Value.NameOfFormatter("i am a string value")}}

Escaping formatter values

In general all formatter arguments are considered strings or references. You must escape every string with ether " or ' if you write something without escaping it, it will be considered an expression to a property. To escape the string keyword write " or '.

Example

{{this.is.a.valid.formatter("a string, with a comma, and other {[]}{§$%& stuff. also a escaped \" and \\" and so on")}}

When using a Formatter with an tilde operator (~) for formatting an root object take note that you cannot access the current scope. For example:

{{#Data.Data}} <-- Scopes to Data.Data
{{ValueInDataData}} <-- Access values inside Data.Data
{{~Data.Foo(ValueInDataData)}} <--not possible as the whole expression is in the scope of the root object
{{/Data.Data}}

this is by design, as when using the ~ operator you scope the whole expression that follows to the Root object.

Expressions

To include another value of the Model in a formatter just use Path in the place of the parameter. Can be combined with named parameters [Name]Path.To.Value.

You can also call Formatter within the expression and give them individual arguments that can also be ether a string or an expression.

Value Converter

Morstachio understands the basic .net conversion like converting an string value from the template for an int argument used in the arguments of an formatter. You can extend this list of formatters globally and per function argument.

To extend the global list of formatters just add your implementation of IFormatterValueConverter to the used MorestachioFormatterService. If you want to specify a formatter that should be always used for a certain argument, you can annotate the parameter with the FormatterValueConverterAttribute

For Example, if you want to convert any object to an string for an certain parameter on a certain formatter, you will have to first, create a IFormatterValueConverter to make this conversion like this:

Example Encoding Converter

/// <summary>
///		Parses an string to an encoding object
/// </summary>
public class EncodingConverter : IFormatterValueConverter
{
	/// <inheritdoc />
	public bool CanConvert(Type sourceType, Type requestedType)
	{
		return (sourceType == typeof(string) || sourceType == typeof(Encoding))
		       && requestedType == typeof(Encoding);
	}
	
	/// <inheritdoc />
	public object Convert(object value, Type requestedType)
	{
		if (value is Encoding)
		{
			return value;
		}

		return Encoding.GetEncoding(value.ToString());
	}
}

This formatter can ether pass an encoding if both sourceType (the type of value from the Expression) and requestedType (the type of value present as the argument) are Encoding or if the sourceType is an string.

To use this Converter you can ether add it to the list of global Converters to allow a general conversion of (Encoding, string) => Encoding like this:

ParserOptionsBuilder.New()
    .WithTemplate(...)
    .WithValueConverter(new EncodingConverter());

Or you can annotate a single parameter with the FormatterValueConverterAttribute to only declare a conversion for this single parameter. Example:

[MorestachioFormatter("FormatterName", "XXX")]
public static int BlaBliBlub([FormatterValueConverter(typeof(EncodingConverter))]Encoding value)
{
	return value.CodePage;
}

Special Parameter types

There are a few types you can declare your formatter with that have special support

  • IMorestachioExpression gets you the raw expression of your parameter instead of the value
  • MorestachioTemplateExpression gets you a lambda expression if declared in themplate
  • params object[] or [RestParameter]object[] like in c#, gets you all additional arguments of that function call after all other declared arguments are matched
  • FormatterParameterList gets you a list of all unmatched parameters with there declared parameter names like [Name]"string"
  • first parameter or [SourceObject] declares the source type for this formatter
  • [ExternalData] will be matched from IMorestachioFormatterService.Services instead from the template

Expression as Parameter

Morestachio allows you to get the expression instead of the evaluated value by simply declaring your C# formatter argument of type IMorestachioExpression

[Test]
public async Task TestParserCanBindExpression()
{
	var template = @"Data: {{this.RenderExpression(123, data.value.test)}}";
	var data = new
	{
		data = 777
	};

	var result = await ParserFixture.CreateAndParseWithOptions(template, data, _options, builder =>
	{
		return builder.WithFormatters<BoundExpressionFormatter>();
	});
	Assert.That(result, Is.EqualTo(@"Data: 123|data.value.test"));
}

private class BoundExpressionFormatter
{
	[MorestachioFormatter("[MethodName]", "")]
	public static string RenderExpression(object source, int constValue, IMorestachioExpression expression)
	{
		var visitor = new ToParsableStringExpressionVisitor();
		expression.Accept(visitor);
		return constValue + "|" + visitor.StringBuilder.ToString();
	}
}

As another special type, you can also use the MorestachioTemplateExpression type as an argument and allow your template to declare an inline function like this:

{{list.Fod(e => e.Value == true).Key}}
[MorestachioFormatter("Fod", "Selected the first item that matches the condition")]
public static T FirstOrDefault<T>(IEnumerable<T> items, MorestachioTemplateExpression expression)
{
	return items.FirstOrDefault(expression.AsFunc<T, bool>());
}

Beware that when using TemplateExpressions, type evaluation and delegate creation is done during Runtime and can result in exceptions thrown if the types do not match.

Generics in Formatter

A word on the use of Generics in formatters. Internally morestachio does not care about types in any way as property access is always evaluated on the specific present type in line with C# inheritance rules. But if you want to preserve your types for chained use of formatters the use of generics is a must have. For example the IEnumerable<T> interface, that relies on a generic parameter to be present.

Generics in C# have one specific feature morestachio cannot comply with. That is return only generics. In your normal C# code, the compiler is able to evaluate the type of a method based on ether your explicit declaration like Enum.TryParse<EnumType>("A") => EnumType.A or through evaluation of a parameter like Enumerable.FirstOrDefault(listOfInt) => IEnumerable<int>. Morestachio does not support any kind of generic declaration nor is it able to reliably evaluate the type of a parameter before that parameter is evaluated. The first case is simply not supported and most likly will never be and the other case is a hen & egg problem where to invoke the method morestacho needs the value and to get the value morestachio needs needs to know how the value is supposed to be.

In that case whenever a return only generic is detected and cannot be resolved, object is substituted in that generic. If logging is enabled a warning is printed.