Virtual Members Using GetValue

Remember from the last section that when trying to resolve a member expression, Fluid will call GetValue on a FluidType and pass it the member string to get a new FluidValue back.

Here are two things that do not matter inside GetValue:

  • The subclass of FluidValue it returns. So long as it can cast to FluidValue, this can be anything you like, even something totally different than the type in which GetValue is currently executing
  • The logic of how it gets this value

This means that we can implement “virtual members” – members that return calculated values.

There are a few virtual members already in the library.

{{ name.size }}
{{ children.size }}
{{ children.first }}

(Why two examples of size? To show that it works on the length of strings in the case of name, and on the length of enumerables, in the case of children.)

size and first are not “real” members – they’re just member strings that GetValue detects and uses in its logic.

Let’s return to our AwesomeValue from above. Remember that when it’s output, it appends “ is awesome” to everything. We might decide we’d like to provide a way to yell this, rather than just say it. Like this:

{{ awesomePerson.yell }}

Assuming our AwesomeValue wrapped the string “Deane”, this should output:

I, DEANE, AM AWESOME!!!

We can implement AwesomeValue.GetValue like this:

protected override FluidValue GetValue(string name, TemplateContext context)
{
  if(name == "yell")
  {
    return StringValue.Create($"I, {_value}, AM AWESOME!!!");
  }
  return NilValue.Instance;
}

Now, Fluid will detect that awesomePerson is an instance of AwesomeObject. It will then pass the “yell” member string to GetValue which will return a StringValue.

Once Fluid gets the value back, it “forgets” what the original value was. All it knows is that it has a string value now, which means all the GetValue logic in StringValue now applies.

So we can do this:

{{ awesomePerson.yell.size }}

(Of course, this sets up some ambiguity between a virtual member and a filter. Why not just do awesomePerson | yell | size?)

Note that with anything other than “yell”, we return a NilValue. Why? Because “yell” should be the only member that ever comes after an AwesomeValue. We just don’t support anything else, so if someone tries to call anything else, they get nothing back.

For a much more practical example, consider this code for XmlValue which allows you to push an XML document into the context, and search it via a series of virtual members that GetValue uses internally.

The value of the /person/name/first element: {{ person.name.first._text }}.
The value of the "type" attribute on the name:: {{ person.name._attrs.type }}
Children elements can be iterated by {{ for kid in person.kids._children }}{{ endfor}}

Look through that code, and you’ll set that GetValue sometimes returns another XmlValue, but – depending on the member string – might return a StringValue or an DictionaryValue.