NHibernate QueryOver Coalesce a property to another property

╄→гoц情女王★ 提交于 2019-12-01 04:52:25

Let start with this:

// This doesn't compile:
//.Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate) <= end)
//.And(() => taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start)

and modify it to:

.Where(() => taskAlias.StartDateOverride.Coalesce(wiAlias.StartDate) <= end)
.And(() => taskAlias.EndDateOverride.Coalesce(wiAlias.EndDate) >= start)

now it will compile. But at runtime it generates the same NullReferenceException. No good.

It turns out that NHibernate indeed tries to evaluate the Coalesce argument. This can easily be seen by looking at ProjectionExtensions class implementation. The following method handles the Coalesce translation:

internal static IProjection ProcessCoalesce(MethodCallExpression methodCallExpression)
{
  IProjection projection = ExpressionProcessor.FindMemberProjection(methodCallExpression.Arguments[0]).AsProjection();
  object obj = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
  return Projections.SqlFunction("coalesce", (IType) NHibernateUtil.Object, projection, Projections.Constant(obj));
}

Notice the different handling of the first argument (FindMemberExpresion) vs second argument (FindValue). Well, FindValue simply tries to evaluate the expression.

Now we know what is causing the issue. I have no idea why it is implemented that way, so will concentrate on finding a solution.

Fortunately, the ExpressionProcessor class is public and also allows you to register a custom methods via RegisterCustomMethodCall / RegisterCustomProjection methods. Which leads us to the solution:

  • Create a custom extensions methods similar to Coalesce (let call them IfNull for instance)
  • Register a custom processor
  • Use them instead of Coalesce

Here is the implementation:

public static class CustomProjections
{
    static CustomProjections()
    {
        ExpressionProcessor.RegisterCustomProjection(() => IfNull(null, ""), ProcessIfNull);
        ExpressionProcessor.RegisterCustomProjection(() => IfNull(null, 0), ProcessIfNull);
    }

    public static void Register() { }

    public static T IfNull<T>(this T objectProperty, T replaceValueIfIsNull)
    {
        throw new Exception("Not to be used directly - use inside QueryOver expression");
    }

    public static T? IfNull<T>(this T? objectProperty, T replaceValueIfIsNull) where T : struct
    {
        throw new Exception("Not to be used directly - use inside QueryOver expression");
    }

    private static IProjection ProcessIfNull(MethodCallExpression mce)
    {
        var arg0 = ExpressionProcessor.FindMemberProjection(mce.Arguments[0]).AsProjection();
        var arg1 = ExpressionProcessor.FindMemberProjection(mce.Arguments[1]).AsProjection();
        return Projections.SqlFunction("coalesce", NHibernateUtil.Object, arg0, arg1);
    }
}

Since these methods are never called, you need to ensure the custom processor is registered by calling Register method. It's an empty method just to make sure the static constructor of the class is invoked, where the actual registration happens.

So in your example, include at the beginning:

CustomProjections.Register();

then use inside the query:

.Where(() => taskAlias.StartDateOverride.IfNull(wiAlias.StartDate) <= end)
.And(() => taskAlias.EndDateOverride.IfNull(wiAlias.EndDate) >= start)

and it will work as expected.

P.S. The above implementation works for both constant and expression arguments, so it's really a safe replacement of the Coalesce.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!