0

One of my Join key-selectors looks like this:

x => x.A + "-" + x.B

NHibernate makes "-" an extra parameter. This parameter gets the SQL type nvarchar and so the whole statement gets converted on the SQL Server from varchar to nvarchar.

The problem with this is, that SQL Server has a huge problem if the queried column is of type varchar instead of nvarchar. This is because the column is of another type than the parameter and so the index can't be used.

I cannot change the type of the column so I need to define somehow that NHibernate should use varchar for string literals when converting lambdas.

Any way to do this?

UPDATE

With help from Oskar Berggren I setup this classes:

public static class VarcharFix
    {
        /// This method returns its argument and is a no-op in C#.
        /// It's presence in a Linq expression sends a message to the NHibernate Linq Provider.
        public static string AsVarchar(string s)
        {
            return s;
        }
    }

public class MyHqlIdent : HqlExpression
    {
        internal MyHqlIdent(IASTFactory factory, string ident)
            : base(HqlSqlWalker.IDENT, ident, factory)
        {
        }

        internal MyHqlIdent(IASTFactory factory, System.Type type)
            : base(HqlSqlWalker.IDENT, "", factory)
        {
            if (IsNullableType(type))
            {
                type = ExtractUnderlyingTypeFromNullable(type);
            }

            switch (System.Type.GetTypeCode(type))
            {
                case TypeCode.Boolean:
                    SetText("bool");
                    break;
                case TypeCode.Int16:
                    SetText("short");
                    break;
                case TypeCode.Int32:
                    SetText("integer");
                    break;
                case TypeCode.Int64:
                    SetText("long");
                    break;
                case TypeCode.Decimal:
                    SetText("decimal");
                    break;
                case TypeCode.Single:
                    SetText("single");
                    break;
                case TypeCode.DateTime:
                    SetText("datetime");
                    break;
                case TypeCode.String:
                    SetText("string");
                    break;
                case TypeCode.Double:
                    SetText("double");
                    break;
                default:
                    if (type == typeof(Guid))
                    {
                        SetText("guid");
                        break;
                    }
                    if (type == typeof(DateTimeOffset))
                    {
                        SetText("datetimeoffset");
                        break;
                    }
                    throw new NotSupportedException(string.Format("Don't currently support idents of type {0}", type.Name));
            }
        }

        private static System.Type ExtractUnderlyingTypeFromNullable(System.Type type)
        {
            return type.GetGenericArguments()[0];
        }

        // TODO - code duplicated in LinqExtensionMethods
        private static bool IsNullableType(System.Type type)
        {
            return (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>));
        }
    }

    public class MyHqlCast : HqlExpression
    {
        public MyHqlCast(IASTFactory factory, IEnumerable<HqlTreeNode> children)
            : base(HqlSqlWalker.METHOD_CALL, "method", factory, children)
        {

        }

        public static MyHqlCast Create(IASTFactory factory, HqlExpression expression, string targetType)
        {
            return new MyHqlCast(factory,
                                new HqlTreeNode[]
                                {
                                    new MyHqlIdent(factory, "cast"),
                                    new HqlExpressionList(factory, expression,
                                    new MyHqlIdent(factory, targetType))
                                });
        }
    }

    public class MyBaseHqlGeneratorForMethod : BaseHqlGeneratorForMethod
    {
        public MyBaseHqlGeneratorForMethod()
            : base()
        {
            SupportedMethods = new MethodInfo[] { typeof(VarcharFix).GetMethod("AsVarchar") };
        }

        public override HqlTreeNode BuildHql(MethodInfo method, System.Linq.Expressions.Expression targetObject, System.Collections.ObjectModel.ReadOnlyCollection<System.Linq.Expressions.Expression> arguments, HqlTreeBuilder treeBuilder, global::NHibernate.Linq.Visitors.IHqlExpressionVisitor visitor)
        {
            return MyHqlCast.Create(new ASTFactory(new ASTTreeAdaptor()),
                        visitor.Visit(targetObject).AsExpression(),
                        "varchar");
        }
    }

public class ExtendedLinqtoHqlGeneratorsRegistry : DefaultLinqToHqlGeneratorsRegistry
    {
        public ExtendedLinqtoHqlGeneratorsRegistry()
        {
            this.Merge(new MyBaseHqlGeneratorForMethod());
        }
    }

For now it's still not working but I see light ;)

UPDATE 2: The Query

var query = aQueryable
            .Join(bQueryable,
        x => x.AB, x => x.A + VarcharFix.AsVarchar("-") + x.B,
                                (head, middle) => new ...)

UPDATE 3:

As "-".AsVarchar() gets optimized to "-" we need a dummy parameter, which cannot be optimized like "-".AsVarchar(x.A) - that way the Linq-extension kicks in!

var query = aQueryable
            .Join(bQueryable,
        x => x.AB, x => x.A + "-".AsVarchar(x.A) + x.B,
                                (head, middle) => new ...)
David Rettenbacher
  • 5,088
  • 2
  • 36
  • 45

1 Answers1

1

There may be multiple ways to do this but here is one:

Invent your own method such as:

/// This method returns its argument and is a no-op in C#.
/// It's presence in a Linq expression sends a message to the NHibernate Linq Provider.
public static string AsVarchar(string s)
{
    return s;
}

Also create a class to represent the HQL expression fragment:

public class MyHqlCast : HqlExpression
{
    private MyHqlCast(IASTFactory factory, IEnumerable<HqlTreeNode> children)
        : base(HqlSqlWalker.METHOD_CALL, "method", factory, children)
    {
    }

    public static MyHqlCast Create(IASTFactory factory, HqlExpression expression,
                                   string targetType)
    {
        return new MyHqlCast(factory,
                             new [] {
                                 new HqlIdent(factory, "cast")),
                                 new HqlExpressionList(factory, expression,
                                         new HqlIdent(factory, targetType)),
                             });
    }
}

Then derive a class from BaseHqlGeneratorForMethod. In its constructor, set the SupportedMethods property to the AsVarchar() method. Override the BuildHql() method. It should output the HQL cast constructs equivalent to cast(@param as varchar). Normally you would use the Cast() method on the treeBuilder parameter, but unfortunately this accepts just a System.Type, which isn't good enough for this case. Instead create and return an instance of your MyHqlCast:

return MyHqlCast.Create(new ASTFactory(new ASTTreeAdaptor()),
                        visitor.Visit(arguments[0]).AsExpression(),
                        "varchar");

Your implementation of BaseHqlGeneratorForMethod then needs to be registered by deriving from DefaultLinqToHqlGeneratorsRegistry. Call this.Merge(new MyGenerator()); in the constructor. Then register your registry type by

nhibernateConfiguration.LinqToHqlGeneratorsRegistry<MyRegistry>();
Oskar Berggren
  • 5,583
  • 1
  • 19
  • 36
  • First, thank you for your detailed answer! In the 2nd code block you call `AddChild` but as I saw in the NHibernate source it's internal! – David Rettenbacher Jan 21 '13 at 15:57
  • OK, got around the interal thing via another constructor-overload and custom `HqlIdent`. My `MyBaseHqlGeneratorForMethod` constructor gets called via the `LinqToHqlGeneratorsRegistry` and inside I assign `new MethodInfo[] { typeof(VarcharFix).GetMethod("AsVarchar") }` to SupportedMethods ... but `BuildHql` doesn't get called... – David Rettenbacher Jan 21 '13 at 16:38
  • I've updated my code example. Though I missed that the HqlIdent ctor was also internal. – Oskar Berggren Jan 21 '13 at 16:46
  • Just to make sure, you didn't forget calling Merge()? – Oskar Berggren Jan 21 '13 at 16:48
  • Yes, I do. I've updated the question with my current implementations. – David Rettenbacher Jan 21 '13 at 17:01
  • Do you mean that you've verified with a debugger and breakpoints that the MyBaseHqlGeneratorForMethod ctor is in fact called during configuration, but the BuildHql() is not? Does the MethodInfo array contain non-null values? – Oskar Berggren Jan 21 '13 at 17:11
  • Yes: breakpoint in ctor and BuildHql. Ctor is hit, BuildHql not. In NHProf it says that the parameter is String(4000). – David Rettenbacher Jan 21 '13 at 17:19
  • And MethodInfo-Array count is 1. I even used the built-in `ReflectionHelper.GetMethodDefinition(() => VarcharFix.AsVarchar(""))` helper method. – David Rettenbacher Jan 21 '13 at 17:26
  • In the "did you put the plug in the socket"-section, did you add the call to AsVarchar() in your query? – Oskar Berggren Jan 21 '13 at 17:31
  • Yes ;) - see update 2 in the question. Except the names it's copied from my original query. – David Rettenbacher Jan 21 '13 at 17:59
  • The expression is even evaluated by `PartialEvaluatingExpressionTreeVisitor.EvaluateSubtree()` - I can confirm that `AsVarchar()` function is called by remotion. To me it seems that extensions are **not meant to extend key-selectors** but ***just*** `Where` expressions because I have another extension flawlessly working – David Rettenbacher Jan 22 '13 at 11:07
  • Interesting... try putting the entire string concatenation inside the call to AsVarchar(). It might be that it evaluates the expression since it doesn't use anything that comes from the database. – Oskar Berggren Jan 22 '13 at 11:49
  • @Warappa Did you try my latest suggestion of putting the entire concatenation expression inside the call to AsVarchar()? – Oskar Berggren Jan 30 '13 at 10:44
  • Yes, I did, but the last time I tried it didn't call BuildHql - *now it does*! (Don't know, I must have something wrong - silly me). `targetObject` is now `null` - I'm just playing around with `arguments` expression list but until now that causes other exceptions... – David Rettenbacher Jan 30 '13 at 12:22
  • Yes, silly of me, targetObject will be null since the method is static. – Oskar Berggren Jan 30 '13 at 12:40
  • OK, if entire lambda is surrounded it gets called - if only string gets surrounded then not. This is because Relinq seems to "reduce" the whole ast so after call of `EvaluateIndependentSubtrees` in `NHibernate.Linq.Visitors.NhPartialEvaluatingExpressionTreeVisitor` the expression `... + "-".AsVarchar() + ...` is reduced to `... + "-" + ...` It seems like AsVarchar() is not allowed to be a noop... – David Rettenbacher Jan 30 '13 at 15:10
  • After adding a "dummy" parameter with cannot be "optimized" (like `x.A`) the varchar-cast works. So the key selector reads `x.A + "-".AsVarchar(x.A) + x.B` - update you answer according to my next update and I accept it. Thanks for keeping on helping! – David Rettenbacher Jan 30 '13 at 16:24
  • All the constructors for HqlIdent are internal. So how can this be done? – Sam Jan 14 '14 at 04:39