Value Resolution

When Fluid encounters an identifier, it needs to find the value for it. It does two things to accomplish this:

  1. By searching the scopes for the value
  2. By successively calling GetValue on each additional “member,” if more than one exists

If the identifier is “simple”, like this –

My name is {{ name }}.

– then Fluid just does #1 above. It retrieves a FluidValue from a Scope or the Model (see Passing Data into Templates). It probably gets a StringValue in this case, then calls WriteTo on it.

However, if the token contains dots – and therefore multiple members – Fluid has to “resolve” the true value. It has to successively modify the base FluidValue by calling GetValue and passing in each successive member string (Note: the “.” separator is hard-coded in FluidParser.)

Consider:

My name has {{ person.Name.size }} letters.

In this case, we clearly don’t want a value under the string key person.Name.size. Rather, we want person, progressively “modified” by Name and size.

What we’re effectively saying is:

  1. Get the value of person
  2. On that value, get the value of whatever Name means…
  3. On that value, get the value of whatever size means…
  4. Tell that value to write itself out.

The entire token person.Name.size is parsed by Fluid into an MemberExpression which is made up of three IdentiferSegment objects: person, Name, and size.

Fluid iterates over each member in an attempt to resolve the final value. Each member will result in a FluidValue until WriteTo is called on the final value.

I’ll number the descriptions below to match the four-step logical flow from above:

#1

If the value is null (so, on the first iteration – person in this case), Fluid will search the Scopes for a FluidValue that maps to the person identifier.

Let’s assume it finds one – an ObjectValue which wraps a Person object.

(If there were no dots in the token – so no further members – we’d be done here, and Fluid would just call WriteTo on that ObjectValue instance.)

#2

On the second iteration (Name in this case), the initial value is already set, so Fluid instead calls GetValue on the FluidValue it has (which, remember, everything coming out of a Scope has to extend from FluidValue) and passes the member as a string: “Name”.

What GetValue does internally is up to the underlying class. It has to return another FluidValue, but can return any subclass of that using whatever logic it likes.

(Also remember that GetValue isn’t WriteTo. At this point, Fluid isn’t writing anything out, it’s just resolving a value. Once it has the final value, it will then call WriteTo on whatever it ends up with.)

So, on our second iteration, Fluid calls GetValue on the ObjectValue wrapper around a Person object, passing it “Name”. In the case of an ObjectValue, that reflects the underlying Person object and returns the value of the Name property, which is a StringValue.

(See Member Access and Controlling Template Data for how to safeguard this.)

Note that person was an ObjectValue, but its GetValue method returned a StringValue. This is fine, because they’re both FluidValue objects. Each member iteration can return different subclasses of FluidValue (you might even end up back at the same type you started with…).

#3

On the third iteration (size), Fluid now calls GetValue on StringValue. That method looks at the member string that’s passed in – if it’s the literal string “size” it returns a NumberValue which represents the length of the wrapped string. In all other cases, it returns a NilValue.

(Why return a NilValue for anything other than “size”? Well, what should it return? The only valid member that Fluid supports on StringValue is size.

If we did this: person.Name.foo so that GetValue took in a member string of “foo”…what would that even mean? In no case other than “size” does it make sense to add a dot-something after a StringValue. In any other case, the StringValue should be the final segment.)

We’ve now reached the end of the resolution process, and we have a NumberValue.

If any of the above iterations returned a NilValue, Fluid would abandon any further iterations and just use the NilValue. A NilValue writes nothing out, so this is why looking for something invalid like “foo” on a StringValue doesn’t throw an error, it just appears to do…nothing. A NilValue is actually doing something, but that something is just to write nothing out.

#4

At the end of this resolution process, we have a NumberValue. Fluid calls WriteTo on that, which writes out a numeric value.