Parsing and evaluating expressions both use IExpressionContext
objects that indicate the environment available to the expression. An expression Salary + Bonus
references two identifiers, which must be available in the context or expression-parsing in strict mode fails.
In most cases, the context will be an IMetaClass while parsing and an IDataObject during evaluation. The properties of the class or object are those against which the parser and evaluator will validate.
Other objects
A product will generally encounter contexts when working with Delegate Expression. There are two types of objects that the product can extract from the context:
Transient objects that are specific to the call.
Singleton services provided by Quino and the product.
Transient objects
The context contains zero or more transient objects, usually an IDataSession
and IDataObject
but also sometimes an ILanguage
that indicates the preferred language, if different from the user's selected language (e.g. in some reporting functions).
Most products will use builder methods that pass type-safe business objects to product code as parameters. See Using generated code for more information.
However, a product can get the session with context.TryGetSession()
and the underlying object with context.TryGetInstance<IDataObject>()
. See Accessing the object in the context for more information.
Singleton Services
An expression function can always directly inject and use singleton services. This is the "proper" way to access services (see the chapter on the IOC for more information).
The following example shows how to inject a service into a metadata builder and then use it from an expression.
The overload of CreateDelegate
used here doesn't even provide the context to the product. Instead, it extracts the Person
from the context and calls the product code only if it's actually present.
internal class GeneratedPersonBuilder : GeneratedProductMetadataBuilderBase { public GeneratedPersonBuilder([NotNull] IProductService productService) { _productService = productService; } protected override void AddProperties() { Metadata.Person.FormattedAddress.SetValueExpression( f => f.CreateDelegate<Person>( p => _productService.GetFormattedAddress(p) ) ); } private readonly IProductService _productService; }
The example above is clean and straightforward, but a bit more verbose, especially when scaled to multiple services and properties.
Another way of getting at the service is to extract it from the context instead of injecting it. As noted above, this may make it more difficult to determine dependencies, but it's up to the product to decide how to use its services.
internal class GeneratedPersonBuilder : GeneratedProductMetadataBuilderBase { protected override void AddProperties() { Metadata.Person.FormattedAddress.SetValueExpression( f => f.CreateDelegate<Person>( (c, p) => c.GetSingle<IProductService>().GetFormattedAddress(p) ) ); } }
The example above calls GetSingle()
, which throws an exception if the IOC is not available to the context.
During initialization, Quino tests some expressions (e.g. for default values) with an empty context in order to determine whether they are "static" (e.g. to determine whether they can be mapped to the database as part of schema-migration).
Therefore, products must be careful not to assume that the context will contain anything.
In this case, it's fine to call GetSingle()
because the product can correctly assume that if the context contains a Person
, then it also has access to the IOC.
However, if the expression were not dependent on anything else, or if the product needed to be more careful, then it can use TryGetSingle
instead.
In either case, if there is an IOC in the context, but the requested object is not registered, then the call crashes, just as any other failed call to GetInstance()
would crash. This is by design, as a product using an unregistered service is a program error, not a situation to be handled.
Because the following implementation depends on the Person
as well, the product can rely on Quino to handle empty-context situations correctly.
internal class GeneratedPersonBuilder : GeneratedProductMetadataBuilderBase { protected override void AddProperties() { Metadata.Person.FormattedAddress.SetValueExpression( f => f.CreateDelegate<Person>( (c, p) => { if (c.TryGetSingle<IProductService>(out var s)) { return s.GetFormattedAddress(p); } return string.Empty; } ) ); } }
In some cases, though, the product has to be more careful about the result. If it always returns a value, then Quino will assume that it always has the same value (i.e. is "static") and may make incorrect assumptions about the code.
In these cases, the product can return not just the result, but also a Boolean that indicates whether the value could be calculated.
Quino provides an overload that supports a tuple result, where the first element is the result and the second element is a Boolean value indicating whether the value could be calculated.
The following example shows how a product can use this overload to return a complete answer.
internal class GeneratedPersonBuilder : GeneratedProductMetadataBuilderBase { protected override void AddProperties() { Metadata.Person.FormattedAddress.SetValueExpression( f => f.CreateDelegate<Person>( (c, p) => { if (c.TryGetSingle<IProductService>(out var s)) { return (s.GetFormattedAddress(p), true); } return (null, false); } ) ); } }
In the example above, the result that includes true
indicates that Quino can use the value. The false
indicates that it cannot, so we just use the null
value because it will not be used.