Member Access and Controlling Template Data

One of the goals of a templating language is to enable safe template development, which means template developers should have limited access to data. Part of this is controlled the members they can access on any particular object. If you push an model into the context, template developers won’t automatically have access to every bit of data it contains.

First, let’s get this out of the way – if you trust your template developers or you know that you will never give them access to anything dangerous, then just do this:

TemplateOptions.Default.MemberAccessStrategy = new UnsafeMemberAccessStrategy();

That will eliminate any restrictions on member access, and none of the rest of this chapter matters.

But, if you need to control access, read on.

Consider this:

var deane = new Person() { Name = "Deane" };

var context = new TemplateContext();
context.SetValue("person", deane);

var template = parser.Parse("{{ person.Name }}");

With the default TemplateOptions in place, that won’t display anything because neither the Person class nor the Name property on it have been registered with Fluid as being “safe.”

It doesn’t matter if the object was pushed into the context via SetValue or set on the model. This would have the same problem:

var context = new TemplateContext(new { person = deane });

When you pass in a model to Fluid, all members on the model are automatically marked as safe, but not the members of any properties exposed on that model. In this case, if we did this –

{{ person }}

We would actually get some output – the ToString() method of the Person class – which can be a little confusing. If you want to prevent the members of the model from being automatically marked as safe, you can pass in a boolean to the constructor:

var context = new TemplateContext(new { person = deane }, false);

Now, nothing is safe and nothing will output.

Any time the Fluid engine attempts to reflect a property on an object, it does so through a MemberAccessStrategy. There are two supplied with the default library:

  • DefaultMemberAccessStrategy requires classes and properties to be registered
  • UnsafeMemberAccessStrategy just let’s anything through, as mentioned

They are set on TemplateOptions as noted above.

If you use the DefaultMemberAccessStrategy you can register classes like this:

TemplateOpions.Default.MemberAccessStrategy.Register<Person>()

That will register all members of the Person class as safe and therefore accessible.

(Almost hilariously, this all the UnsafeMemberAccessStrategy does behind-the-scenes. It extends from DefaultMemberAccessStrategy and just performs the extra step of auto-registering any type from which data is requested.)

If you want to pick and choose what members you want to allow, you can pass in the names of the members which are safe:

TemplateOptions.Default.MemberAccessStrategy.Register<Person>("Name");

That will allow the Name property of Person, but nothing else.

If you want something in the middle – you don’t want to register every class and member, and you don’t want to allow everything through – you have some options.

The key is to remember that your template developers can only access what you give them. So instead of giving them a raw person object, give them…less. If you don’t like the idea of giving them “full” objects with a bunch of stuff they shouldn’t output, then don’t give them those objects. Give them safer objects built from those objects.

Perhaps create a “model class” to wrap it, which omits what you don’t want to let through.

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
}

public class PersonModel
{
  private Person person;
  public PersonModel(Person _person) { person = _person; }

  public string Name => person.Name;
}

We can use the unsafe member access now without worry, because we’re controlling what’s passed into the model. There’s no way for template developers to get at the private person property of PersonModel. We can control what data you expose from Person via PersonModel.

Also, we can take advantage of automatic value convertors. We’ll talk more about that later, but it’s possible to create custom FluidValue objects automatically from objects.

For example see Fluid Values and Virtual Members Using GetValue for more on the below.

public class PersonValue : FluidValue
{
  private Person person;
  public PersonValue(Person _person)
  {
    person = person;
  }

  public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) => writer.Write(ToStringValue());

  protected override FluidValue GetValue(string name, TemplateContext context)
  {
    if(name == "Name")
    {
      return StringValue.Create(person.Name);
    }
    return NilValue.Instance;
  }

  public override string ToStringValue() => person.Name;

  // We don't really care about any of this, but FluidValue is
  // abstract, so we have to override it all
  public override FluidValues Type => FluidValues.Object;
  public override bool Equals(FluidValue other) => false;
  public override bool ToBooleanValue() => false;
  public override decimal ToNumberValue() => 0;
  public override object ToObjectValue() => ToStringValue();
}

And then our convertor.

TemplateOptions.Default.ValueConverters.Add((v) =>
{
  if (v is Person person)
  {
     return new PersonValue(person);

  }
  return null;
});

Now, no one can ever add a “raw” Person object to a context. It will always be translated into a PersonValue, through which we can carefully control and extend its behavior.