Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Next »

A product can integrate expressions that call C# code using IExpressionFactory.CreateDelegate() (or one of its several extension methods).

Advantages and Drawbacks

Pure expressions

Standard expressions and functions have the following advantages:

  • They can be mapped to the database, improving ORM speed.

  • They can be serialized in remoting drivers

  • They can be analyzed and used by Quino to optimize other operations

They have the following drawbacks:

  • The logic is limited to a single-statement (with some conditional logic, like that provided by the If function)

  • Evaluation is not easily debugged (there's no source-level debugger for Quino expressions)

Delegate expressions

Delegate expressions (and Expression Eigene Funktionen) have the following advantages:

  • They can contain more complex logic

  • They can be easily debugged

They have the following drawbacks:

  • They cannot be mapped to the database (there is a mechanism for mapping custom functions to databases. See Expression Eigene Funktionen)

  • They cannot be serialized in remoting drivers

  • They cannot be analyzed by Quino

With those pros and cons in mind, the following document explains how to add C# delegates to expressions.

Context

The context passed to a delegate expression when evaluated contains the following objects by default:

  • The IDataSession

  • The IDataObject to which the property belongs

A product can either use the session to get other dependencies or it can retrieve them from the IExpressionContext directly with GetInstance(). A product can also register its own IExpressionContextFactory to control which objects are added.

Examples

Simple expression

Let's start with a calculated property that doesn't use a delegate. Instead, it just returns the value of another property:

personBuilder
  .Add
  .CalculatedProperty("Seniority", MetaType.Integer)
  .Value("YearsActive");

Accessing the object in the context

The next example shows how to get this property with a delegate, but without any dependency on Metadata Generated.

personBuilder
  .Add
  .CalculatedProperty("Seniority", MetaType.Integer)
  .ValueExpression<Person>(c => c.YearsActive + 1);

This example has two problems that we need to address:

  • It accesses the property by name rather than statically

  • It requires that the context contains an object of type IDataObject

Indicating computability

In many cases the product will want to indicate that the expression cannot be evaluated with some contexts.

In these cases, the product should use a different overload that returns a bool and sets an out parameter for the value instead.

person
  .Add
  .CalculatedProperty("Bonus", MetaType.Integer)
  .ValueExpression(TryGetBonus);

// ...

private bool TryGetBonus(IExpressionContext context, out object value)
{
  if (context.TryGetInstance<Person>(out var person))
  {
    if (person.Company != null)
    {
      if (context.TryGetSingle(out ISystemClock clock))
      {
        if (person.HireDate < clock.UtcNow)
        {
          value = person.Company.GetBonus(person.HireDate.Year);

          return true;
        }
      }
    }
  }

  value = null;

  return return false;
}

Comparing parsed and delegate expressions

At the same time, you can see how powerful and useful the Expression Text Formatierung is: the GetSalutation() method has a lot of hand-coding just to get the same behavior as the much shorter text-formatting expression.

Note: The example below assumes that the field _expressionParser is an injected IExpressionParser.

const string conditionText = "Salutation.IsFormal";
const string familiarText = "'<{Salutation.Text} {FirstName}>'";
const string formalText = "'<<{Salutation.Text} {Title}> {LastName}>'";

var condition = _expressionParser.CreateExpression(
  conditionText, true, metaClass
);
var familiar = _expressionParser.CreateExpression(
  familiarText, true, metaClass
);
var formal = _expressionParser.CreateExpression(
  formatText, true, metaClass
);

// Use a pure, parsed expression text
metaClass
  .Add
  .CalculatedProperty("SalutationText",  MetaType.Text)
  .ValueExpression(
    $"If({conditionText}, {formatText}, {familiarText})"
  );

// Use a delegate with a strongly typed parameter
metaClass
  .Add
  .CalculatedProperty("SalutationTextPureDelegate", MetaType.Text)
  .ValueExpression<Address>(
    GetSalutation
  );

private static object GetSalutation(Address address)
{
  var salutation = address.Salutation;

  if (salutation != null)
  {
    var result = salutation.Text;

    if (salutation.IsFormal)
    {
      if (!string.IsNullOrEmpty(address.Title))
      {
        if (string.IsNullOrEmpty(result))
        {
          result = address.Title;
        }
        else
        {
          result += $" {address.Title}";
        }
      }

      if (string.IsNullOrEmpty(address.Name))
      {
        return result;
      }

      return result + $" {address.Name}";
    }
    
    if (!string.IsNullOrEmpty(address.FirstName))
    {
      if (string.IsNullOrEmpty(result))
      {
        return address.FirstName;
      }

      result += $" {address.FirstName}";
    }

    return result;
  }

  return string.Empty;
}

Mapping to SQL

Until the issue QNO-6478 provides more well-integrated support for mapping DelegateExpressions to SQL, a product can build custom mapping with the following pattern.

The example below extends the standard DelegateExpression to implement IMappableExpression and map the simple expression not Active for people.

private class IsPersonActiveExpression : DelegateExpression, IMappableExpression
{
  /// <inheritdoc />
  public IsPersonActiveExpression()
    : base(ExecuteLocally)
  {
  }

  /// <inheritdoc />
  public bool TryMapExpressionToSql(IExpressionContext context, IExpressionMapper mapper, out string sql)
  {
    switch (mapper.Dialect)
    {
      case SqlDialect.PostgreSql:
        sql = "not \"Person\".\"active\"";

        return true;
      case SqlDialect.SqlServer:
        sql = "\"Person\".\"active\" = 0";

        return true;
      default:
        throw new UnexpectedEnumValueException(mapper.Dialect);
    }
  }

  private static bool ExecuteLocally([NotNull] IExpressionContext context, out object value)
  {
    if (context.TryGetInstance<Person>(out var person))
    {
      value = person.Active;

      return true;
    }

    value = null;

    return false;
  }
}

Note the following in the example above:

  • ExecuteLocally only returns true if the context contains a Person

  • When mapping SQL, the context is either empty or contains only a session (but no person), so ExecuteLocally returns false

  • The method TryMapExpressionToSql handles the SQL-mapping case for PostgreSql and SQl Server.

You can use the expression in a query as follows:

var people = Session.GetList<Person>(q => q.Where(new IsPersonActiveExpression()));

You can use the expression in metadata as follows:

Elements.PersonBuilder
  .Add
  .Property("LastActivityTime", MetaType.Date)
  .IsVisible(new IsPersonActiveExpression());

  • No labels