ObjectLiquefier 0.4.0

.NET Standard 2.0
dotnet add package ObjectLiquefier --version 0.4.0
NuGet\Install-Package ObjectLiquefier -Version 0.4.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ObjectLiquefier" Version="0.4.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ObjectLiquefier --version 0.4.0
#r "nuget: ObjectLiquefier, 0.4.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install ObjectLiquefier as a Cake Addin
#addin nuget:?package=ObjectLiquefier&version=0.4.0

// Install ObjectLiquefier as a Cake Tool
#tool nuget:?package=ObjectLiquefier&version=0.4.0


A (very) tiny library to help pretty-printing dotnet objects (for logging, auditing, visualization, etc. purposes). It uses Liquid templates powered by the incredible Fluid library.


The fastest and simplest way to start using the library is from it's nuget package.

NuGet\Install-Package ObjectLiquefier

It targets .NET Standard 2.0 which makes it compatible with all modern (and some legacy) dotnet projects.

"Strongly"-typed Templates

The library uses a convention-based template resolution mechanism which automatically resolves templates on disk based on an object's type name and inheritance hierarchy. When a suitable template is found it is read from disk, compiled and the compiled template is cached using the object's type name as the cache key.

The resolution algorithm uses the Type.FullName to search for a template on disk. It start's with the most qualified name and descends down to the unqualified name. For example, take a Person class declared in the following namespace:

namespace Some.Test
    public class Person {
        public int Id { get; set; } 
        public string Name { get; set; }

The TemplateResolver will search for a file named some.test.person.liquid, then for test.person.liquid and, finally, simply for person.liquid. Files are searched in the configurable TemplateSettings.TemplateFolder (which defaults to "liquefier").

Note: the .liquid extension is hard-coded and cannot be changed. Templates MUST have this extension.

Template inheritance hierarchy

Template inheritance allows you to provide a single "base template" for a class hierarchy, and avoid the need to create one template for each class in the inheritance tree.

If no suitable template is found for a given Type then the object hierarchy will be traversed backwards, up to the immediate System.Object descendant, checking for a suitable template match along the way. Given the following class hierarchy:

namespace Game {
    public class Vehicle { }
    public class Car : Vehicle { }
    public class Truck : Car { }

The template resolution algorithm will search the following templates in this exact precedence order for the Truck class :

game.truck.liquid >> truck.liquid >> game.car.liquid >> car.liquid >> game.vehicle.liquid >> vehicle.liquid

Type name collisions

Templates are not actually strongly-typed since the engine does not care about assembly version, strong names, etc. Only the Type's Full Name is used for template resolution. Since the resolution mechanics do search for fully, partially and unqualified names, two classes with the same name can share the same template. For example:

namespace Test.One {
    public record Person(string Name);
namespace Test.Two {
    public class Person {
    	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string Name => $"{LastName}, {FirstName}";

Both classes would share a template named person.liquid. If you need different templates for them you should qualify the template name further. For instance, a template named two.person.liquid would only satisfy the second Person class above.

NOTE: while the class and record above could end up using the same template, the compiled template will be cached under different keys regardless as it uses the Type's full name as the cache key. This will result in a separate template compilation for each Type even though the template file is the same.


The easiest way to liquefy an object is to use Liquefier.LiquefyObject static method which uses the default settings and a singleton Liquefier instance for template compilation, caching, resolution, etc.:

var felipe = new Person { Name = "Felipe Machado", };
var liquefied = Liquefier.LiquefyObject(Felipe);

This will liquefy the instance of the Person class above using a suitable liquid template (if none is available it returns an empty string). It will cache the compiled template, so subsequent calls with instances of the same class will get an already compiled liquid template from the cache.

Liquid templates should be put inside a subfolder, under the current dir, named liquefier (can be configured). Considering the template bellow in the file .\liquefier\person.liquid:

	<h1>{{ Name }}</h1>
	<p><em>Birth:</em> {{ Birth | date: "%d/%m/%Y" }}
	<p>{{ Bio }}</p>

You can liquefy classes named Person, as shown in the follwing code:

public class Person {
    public string Name { get; set; } = "";
    public DateTime Birth { get; set; }
    public string? Bio { get; set; }
    public int Age => (int)((DateTime.Today - Birth).TotalDays / 365.2425);

var liquefied = Liquefier.LiquefyObject(new Person {
    Name = "Felipe Machado",
    Birth = new DateTime(1976, 03, 31),
    Bio = "Felipe was born in Volta Redonda, Rio de Janeiro, Brazil."

Which will output this to the Console:

	<h1>Felipe Machado</h1>
	<p><em>Birth:</em> 31/03/1976
	<p>Felipe was born in Volta Redonda, Rio de Janeiro, Brazil.</p>

Default Settings

You can change the library's default settings by assigning a new Func<LiquefierSettings> to Liquefier.DefaultSettings:

Liquefier.DefaultSettings = () => new LiquefierSettings {
    TemplateFolder = "another\\folder",
    // new liquefier instances will use "./another/folder" as the liquid source template directory

New Liquefier instances will automatically use the new default settings. The default liquefier instance used by Liquefier.LiquefyObject() will also use the new defaults if it was NEVER used prior to changing the settings (e.g.: you didn't call Liquefier.LiquefyObject() nor read the Liquefier.Instance property, as both would have instantiated the default Liquefier before the change).

Instance Settings

If you want to use a liquefier with custom settings without changing the defaults, you can configure the settings by passing an Action<LiquefierSettings> in it's constructor:

var liquefier = new Liquefier(cfg => {
    cfg.TemplateFolder = "data\\templates";
    cfg.ParserOptions = new Fluid.FluidParserOptions { AllowFunctions = false };
    cfg.TemplateOptions.MaxSteps = 567;
    cfg.TemplateOptions.MaxRecursion = 2;

The LiquefierSettings passed onto the configuration Action will reflect the current default settings, so you can change only what is needed.

Ad-hoc Templates

Given a simple class (or record) you can pretty-print it using an ad-hoc liquid template with minimal code:

const string template = "Hello {{What}}";
var helloWorld = Liquefier.LiquefyObject(new { What = "World" }, template);

Which yields Hello World in the Console output.

The following unit test asserts the correctness of a more complex liquid template.

const string personTemplate =
    Name: {{ Name }}
    Birth: {{ Birth | date: "%d/%m/%Y" }}

const string felipeLiquefied =
    Name: Felipe
    Birth: 31/03/1976

public void LiquefyCanAcceptAdHocTemplate() {
    var liquefier = new Liquefier();
    var liquefied = liquefier.Liquefy(new { Name = "Felipe", Birth = new DateTime(1976, 03, 31) }, personTemplate);
    Assert.Equal(felipeLiquefied, liquefied);

Ad-hoc template caching

Like strongly-typed templates, ad-hoc templates are compiled at first use and the compiled template is cached by a 128-bit cache key.

To generate the 128-bit cache key the library uses Jon Hanna's donet implementation (Spookily Sharp) of Bob Jenkins’ SpookyHash version 2. While collisions are possible they are very improbable so the library does nothing at all to prevent it as 1 in 16 quintillion chances is low enough for me.

Pretty-printing properties inside templates

The library ships with a {% liquefy [expression] %} expression tag which allows you to pretty-print objects from within a liquid template. It will run under the same FluidParser and TemplateContext instances that the current liquid template is being executed on, and will use all the same caching and template resolution rules for whichever object the expression resolves to (if no template is found, it will output the value for the expression using the internals of the Fluid library).

For example, given the following class structure:

public class Person {
    public string Name { get; set; } = "";
    public DateTime Birth { get; set; }

public class Parent : Person {
    public Child FirstBorn { get; set; } = new();

public class Child : Person { }

And the following templates:

  • {template_folder}\person.liquid:
    Name: {{ Name }}
    Birth: {{ Birth | date: "%d/%m/%Y" }}
  • {template_folder}\parent.liquid:
    Name: {{ Name }}
    Birth: {{ Birth | date: "%d/%m/%Y" }}
    {% liquefy FirstBorn %}

Given the following code:

var liquefier = new Liquefier();
var parent = new Parent {
    Name = "Felipe",
    Birth = new DateTime(1976, 3, 31),
    FirstBorn = new Child {
        Name = "Bernardo",
        Birth = new DateTime(2014, 10, 10)
var liquefied = liquefier.Liquefy(parent);

We'll get this output in the console:

Name: Felipe
Birth: 31/03/1976
Name: Bernardo
Birth: 10/10/2014

The engine will use the template at parent.liquid for the parent object since it's an object of type Parent. Whithin the template, the liquefy tag is executed and it's expression resolves to parent.FirstBorn, which is of type Child. The template resolution will then use the person.liquid template, since there's no child.liquid template to use and Person is an ancestor of Child (see Template inheritance hierarchy).

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.4.0 93 4/10/2023
0.3.1 139 3/9/2023
0.3.0 133 3/8/2023
0.2.0 140 3/7/2023
0.1.2 142 3/7/2023
0.0.1 145 3/4/2023