Expressions can refer to any functions that are defined and registered with the product.
Product-specific functions
This section illustrates how to create and register a product-specific namespace with an operation.
Define Namespace
In order to include an operation, you have to define a namespace first. When using the operation in an expression, you must always declare this namespace name.
public class CustomNamespace : SubNamespace { public CustomNamespace(IGlobalNamespace globalNamespace) : base(globalNamespace, "Custom") { } }
Register with Product
The product includes the namespace in the expression evaluation-engine with the following code.
void ConfigureStuff() { application .UseExpressionNamespace<CustomNamespace>(); }
Register Constants
A product can use this mechanism to register all the values of an enum
with the expression engine.
Given the following enum,
public enum ProductStatus { Available, Offline, Archived, Deleted }
A product can register ProductStatus
as follows,
internal void ConfigureStuff() { application .UseEnumExpressionNamespace<ProductStatus>(); }
The constants will now be available in expressions. Instead of using the raw value,
If(Status in [2, 3], "Down", "Up")
A product can use the constants,
If(Status in [ProductStatus.Archived, ProductStatus.Deleted], "Down", "Up")
Note: Quino automatically registers any
enum
used withAddEnumeratedClass
.
Register Indexers
A function can return an indexable type to access data in as a list, array, or dictionary, with an arbitrary number of dimensions.
For example, given that there is a constant named Settings
that returns a dictionary of boolean values. A product can use this to access a list of constants, as shown below
Settings["LogEnabled"]
The standard indexing function supports nested dictionaries/lists/arrays, which is a convenient way of making many constants available with a single registration.
Settings["Logging", "Enabled"]
Add Operation
The following example comes from the System
namespace. It defines a function that creates a Guid
from a string. As with other code examples, documentation and error-handling have been removed to make it more concise.
The code does the following:
Extracts and validates its parameters
Creates the
Guid
and sets the valueReturns
true
only if it could actually evaluateReturns
false
in all other casesThrow an exception if the parameter cannot be converted to a
Guid
private bool CreateGuid( IOperation operation, IEnumerable<IExpression> parameters, IExpressionContext context, out Guid value) { var parameterList = parameters as IList<IExpression> ?? parameters.ToList(); var count = parameterList.Count; if (count != 1) { throw new InvalidOperationException("[parameterList.Count] must be equal to 1."); } if (parameterList[0].TryGetValue(context, out var guidText)) { value = new Guid(guidText.ToString()); return true; } value = Guid.Empty; return false; }
Finally, add the operation in the constructor, binding the function defined above to its name and specifying the parameter validator. In this case the function requires a single parameter. The OpTools
define some functions to validate common cases.
public class CustomNamespace : Namespace { public CustomNamespace(IGlobalNamespace globalNamespace) : base(globalNamespace, "Custom") { Operations.Add(new Operation<Guid>(this, "CreateGuid", CreateGuid, OpTools.MatchOne)); } }
A product can add an operation to the Global
namespace instead of the namespace itself by using the globalNamespace
passed in the constructor. For example:
public class CustomNamespace : Namespace { public CustomNamespace(IGlobalNamespace globalNamespace) : base(globalNamespace, "Custom") { globalNamespace.Operations.Add(new Operation<Guid>(globalNamespace, "CreateGuid", CreateGuid, OpTools.MatchOne)); } }
Map to SQL
Note: The issue QNO-5694: Improve separation of SQL, expression and value mapping will affect how the following works.
An Operation
has an optional final parameter that is a callback to map it to SQL. The following example comes from the DateOperationsNamespace
, for the Now
function.
Operations.Add(new Operation<DateTime>(this, "Now", Now, OpTools.MatchZero, MapNow));
The MapNow
function is defined as follows:
private bool MapNow(IExpression expression, IEnumerable<IExpression> parameters, IExpressionContext context, IExpressionMapper mapper, out string value) { switch (mapper.Dialect) { case SqlDialect.SqlServer: value = "GetDate()"; return true; case SqlDialect.PostgreSql: value = "now()"; return true; } value = null; return false; }
In this case, the mapper calls a standard database function without any parameters. A product is free to map to call a stored procedure using the values obtained from parameters. For example,
private bool MapCustom(IExpression expression, IEnumerable<IExpression> parameters, IExpressionContext context, IExpressionMapper mapper, out string value) { switch (mapper.Dialect) { case SqlDialect.SqlServer: if (parameterList[0].TryGetValue(context, out var parameterOne)) { value = $"spCreateGuid({parameterOne})"; return true; } } value = null; return false; }
Change Resolution Order
A product can adjust the order in which namespaces are searched by adjusting the order of items in the INamespaceProvider.
The following code shows how to ensure that the ProductOperationsNamespace
is registered before the DateTime
namespace.
application.Configure<INamespaceProvider>(p => p .RegisterBefore<INamespace, ProductOperationsNamespace>(DateOperationsNamespace.DefaultName, "String") );
If product declares an EndOfTime
operation, then referencing EndOfTime
in an expression would resolve to the product-specific version. The product can still use the original version in Quino by including the namespace, DateTime.EndOfTime
.
Override Operations
Rather than adding a new operation, a product can also override an existing operation by inheriting from the namespace and replacing the operation.
The following example illustrates how a product could replace the DateTime.EndOfTime
operation with its own implementation. Nullability attributes and null-checking code has been left off for clarity.
internal class ProductDateOperationsNamespace : DateOperationsNamespace { public ProductDateOperationsNamespace( IGlobalNamespace globalNamespace, ISystemClock systemClock, IValueTools valueTools) : base(globalNamespace, systemClock, valueTools) { Operations.Replace(new Operation<DateTime>( this, "EndOfTime", EndOfTime, OpTools.MatchZero )); } private bool EndOfTime( IOperation operation, IEnumerable<IExpression> parameters, IExpressionContext context, out DateTime value) { value = // TODO Calculate value; return true; } }
A careful reader will note that the implementation above does not include an SQL mapping (after the MatchZero
parameter), but it could.
Finally, a product registers the namespace in the application configuration.
application .UseExpressionNamespace<ProductDateOperationsNamespace>(DateOperationsNamespace.DefaultName)