4

I think I'm missing something fundamental when implementing a LinqToHql generator class.

I've successfully registered the SQL Server 2008 contains query using a custom dialect with this registration:

RegisterFunction("contains", new StandardSQLFunction("contains", null));

I have only one class with a full text index to be queried:

public class SearchName
{
  public virtual Guid Id {get; set;}
  public virtual string Name {get; set;} // this is the search field
}

The contains function works properly in HQL:

var names = Session.CreateQuery("from SearchName where contains(Name,:keywords)")
                    .SetString("keywords", "john")
                    .List();

and the generated SQL is perfect:

select searchname0_.Id   as Id4_,
       searchname0_.Name as Name4_
from   Search_Name searchname0_
where  contains(searchname0_.Name, 'john' /* @p0 */)

The next challenge was to implement the Linq to HQL generator:

 public class MyLinqtoHqlGeneratorsRegistry :
    DefaultLinqToHqlGeneratorsRegistry
    {
        public MyLinqtoHqlGeneratorsRegistry()
        {
            this.Merge(new ContainsGenerator());
        }
    }

    public class ContainsGenerator : BaseHqlGeneratorForMethod
    {
        public ContainsGenerator()
        {
            SupportedMethods = new[] {
                ReflectionHelper.GetMethodDefinition<SearchName>(d => d.Name.Contains(String.Empty))
          };
        }

        public override HqlTreeNode BuildHql(MethodInfo method,
          System.Linq.Expressions.Expression targetObject,
          ReadOnlyCollection<System.Linq.Expressions.Expression> arguments,
          HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
        {
            return treeBuilder.MethodCall("contains",
                    visitor.Visit(targetObject).AsExpression(),
                    visitor.Visit(arguments[0]).AsExpression()
                );
        }
    }
}

Calling the method like this:

var namesLinq = Session.Query<SearchName>().Where(x=> x.Name.Contains("john")).ToList();

Unfortunately, this doesn't seem to override the built-in Contains method, and the generated SQL is wrong:

select searchname0_.Id   as Id4_,
       searchname0_.Name as Name4_
from   Search_Name searchname0_
where  searchname0_.Name like ('%' + 'john' /* @p0 */ + '%')

Is it not possible to override the default Contains method, or have I just made a silly mistake?

PS - I'm using NHibernate 3.3.1.4000

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
David Montgomery
  • 1,618
  • 15
  • 28
  • There may be another approach to take, have you seen this blog post http://fabiomaulo.blogspot.co.uk/2010/07/nhibernate-linq-provider-extension.html - shows how to extend the LINQ provider (you may not be able to override `Contains` as a registered function) – Rippo Jan 11 '14 at 08:48
  • @Rippo Yes, I've seen that post. It provided most of the information I used to create my own extension. – David Montgomery Jan 11 '14 at 08:50
  • What I am saying is you may need come up with another method name instead of `Contains` use `FullTextContains` as Fabio is using `IsLike` which translates to `%foo%` – Rippo Jan 11 '14 at 08:55
  • Yes, that was one of my three problems. See the answer for details. – David Montgomery Jan 11 '14 at 09:09

1 Answers1

9

OK, I've finally figured it out!

First, I managed to delete the registration code from my configuration:

...
.ExposeConfiguration(cfg =>
     {
        cfg.LinqToHqlGeneratorsRegistry<MyLinqtoHqlGeneratorsRegistry>();
        ...
     }

Second, don't try to override the existing Linq behaviors. I moved my Contains extension method to the full-text class.

Third, build the Hql tree correctly.

For others trying to implement a SQL 2008 Free-text contains search, here's the complete implementation:

public static class DialectExtensions
    {
        public static bool Contains(this SearchName sn, string searchString)
        {
            // this is just a placeholder for the method info.  
            // It does not otherwise matter.
            return false;
        }
    }

    public class MyLinqtoHqlGeneratorsRegistry : DefaultLinqToHqlGeneratorsRegistry
    {
        public MyLinqtoHqlGeneratorsRegistry()
            : base()
        {
            RegisterGenerator(ReflectionHelper.GetMethod(() =>
                 DialectExtensions.Contains(null, null)),
                 new ContainsGenerator());
        }
    }

    public class ContainsGenerator : BaseHqlGeneratorForMethod
    {
        string fullTextFieldName = "Name";

        public ContainsGenerator()
            : base()
        {
            SupportedMethods = new[] {
                ReflectionHelper.GetMethodDefinition(() =>
                DialectExtensions.Contains(null, null))
          };
        }

        public override HqlTreeNode BuildHql(MethodInfo method,
          System.Linq.Expressions.Expression targetObject,
          ReadOnlyCollection<System.Linq.Expressions.Expression> arguments,
          HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
        {
            // cannot figure out how to interrogate the model class to get an 
            // arbitrary field name...
            // perhaps the RegisterGenerator() call above could be used to pass a
            // property name to the ContainsGenerator constructor?
            // in our case, we only have one full text searchable class, and its
            // full-text searchable field is "Name"
            HqlExpression[] args = new HqlExpression[2] {
                 treeBuilder.Ident(fullTextFieldName).AsExpression(),
                 visitor.Visit(arguments[1]).AsExpression() 
                 };
            return treeBuilder.BooleanMethodCall("contains", args);
        }
    }

For the above to work, you must have declared and used your custom dialect:

public class CustomMsSql2008Dialect : NHibernate.Dialect.MsSql2008Dialect
{
    public CustomMsSql2008Dialect()
    {
        RegisterFunction(
            "contains",
            new StandardSQLFunction("contains", null)
            );
    }
}

Then you can use your new contains search this way:

var namesLinq = Session.Query<SearchName>().Where(x => x.Contains("john")).ToList();

... and the resulting SQL is perfect! (at least if you only have one table you're performing full-text searches on)

EDIT: UPDATED IMPLEMENTATION TO SUPPORT MORE THAN ONE FULLTEXT 'Contains' SEARCH PER QUERY.

Here's the revised version:

public static class DialectExtensions
    {
        public static bool FullTextContains(this string source, string pattern)
        {
            return false;
        }
    }

    public class MyLinqtoHqlGeneratorsRegistry : DefaultLinqToHqlGeneratorsRegistry
    {
        public MyLinqtoHqlGeneratorsRegistry()
            : base()
        {
            RegisterGenerator(ReflectionHelper.GetMethod(() => DialectExtensions.FullTextContains(null, null)),
                          new FullTextContainsGenerator());
        }
    }

    public class FullTextContainsGenerator : BaseHqlGeneratorForMethod
    {
        public FullTextContainsGenerator()
        {
            SupportedMethods = new[] { ReflectionHelper.GetMethod(() => DialectExtensions.FullTextContains(null, null)) };
        }

        public override HqlTreeNode BuildHql(MethodInfo method,
          System.Linq.Expressions.Expression targetObject,
          ReadOnlyCollection<System.Linq.Expressions.Expression> arguments,
          HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
        {
            HqlExpression[] args = new HqlExpression[2] { 
                visitor.Visit(arguments[0]).AsExpression(),
                visitor.Visit(arguments[1]).AsExpression() 
            };
            return treeBuilder.BooleanMethodCall("contains", args);
        }
    }

To use the revised version, the syntax is slightly different:

var namesLinq = Session.Query<SearchName>().Where(x => x.Name.FullTextContains("john")).ToList();
David Montgomery
  • 1,618
  • 15
  • 28
  • This is pretty good if you never what to use the default linq for contains `%foo%`... Good job – Rippo Jan 11 '14 at 09:08
  • Well, searching through a few million records using LIKE to query a varchar(300) field was not making me happy! Also, while NHibernate.Search is available (and is a superior choice to using Fulltext within the database instance), not everyone has the ability to add a new Lucene environment to their production systems on the fly. – David Montgomery Jan 11 '14 at 09:13
  • Have to agree with Rippo. Nice, even great, to see how Configurable and Extensible NHibernate is. good! +1 – Radim Köhler Jan 11 '14 at 09:28
  • I think you have misunderstood me. I was not saying use %foo%. But register a `FreeTextContains` function same way Fabio is using islike. I like to stick to the principle of least surprise. Like what you done though. – Rippo Jan 11 '14 at 09:57
  • @DavidMontgomery, for readability of your answer, may you remove or at least put at the end the obsolete code you had first written? (Side note : `MethodCall` may be more appropriate than `BooleanMethodCall` since [`freetext`](https://msdn.microsoft.com/en-us/library/ms176078.aspx) and [`contains`](https://msdn.microsoft.com/en-us/library/ms187787.aspx) are predicate function who do not return anything: it would not work to try comparing their 'result' to anything.) – Frédéric Feb 29 '16 at 09:43