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.
Code Block |
---|
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.
Code Block |
---|
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.
Code Block |
---|
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.
Code Block |
---|
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);
}
)
);
}
}
|
...
Identifiers
Any text not contained in quotes and adhering to the following rules is an identifier that refers to data in the context in which the expression is evaluated.
An identifier:
Includes one or more letters (a-z or A-Z), digits (0-9) or underscores
Starts with at least one letter
For the programmers, the following snippets are from the official grammar:
Code Block |
---|
ID : LETTER (LETTER|DIGIT|'_')*;
fragment DIGIT : '0'..'9';
fragment LETTER : ('a'..'z'|'A'..'Z');
|
Chaining
Identifiers can be chained together using dots (".") to reference functions or identifiers in nested namespaces.
For example:
Trim(LastName)
Trim(Company.Address.City)
CreateGuid(Company.Address.City)
Technischer Hintergrund
Um zu verstehen welche Entitäten in der jeweiligen Expression angesprochen werden können, sehen Sie die Technischen Hintergründe in:
DevOps -> Quino 10 -> Developer Documentation -> Expressions (Nur für Entwickler zugänglich)