Search code examples
sqlxmlnhibernatehqlcustom-linq-providers

Custom linq provider to search into an XML field for an xml attribute with a certain value


Some of my database tables, which I interact with through NHibernate, contain an XML field with the following structure:

<L xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
   <I>
     <C>
       <N>Attribute1</N>
       <V>a_value</V>
     </C>
     <C>
       <N>Attribute2</N>
       <V>123</V>
     </C>
  </I>
</L>

Basically, each "C" tag contains an attribute, where it's name is contained in tag "N" and it's value in tag "V".

What I want to achieve is being able to write this kind of LINQ syntax in my queries:

..
.Where(m=>m.XMLField(attribute_name, attribute_value))
..

so that I'm able to get the entities of a specific table whose XML field contains the attribute named "attribute_name" with the string value specified by "attribute_value".

It's as simple as that, the XML structure is always like that and I only need to query for a single attribute with a specific value.

Doing my searches I've found that there's a specific technique to implement a custom LINQ provider:

  1. http://www.primordialcode.com/blog/post/nhibernate-3-extending-linq-provider-fix-notsupportedexception
  2. http://fabiomaulo.blogspot.it/2010/07/nhibernate-linq-provider-extension.html
  3. How would I alter the SQL that Linq-to-Nhibernate generates for specific columns?

Unfortunately, I wasn't able to find some structured documentation on how to use the treebuilder, so, at the moment this is what I have:

I have figured out the correct HQL to perform such a task:

where [some other statements] and XML_COLUMN_NAME.exist('/L/I/C[N=\"{0}\" and V=\"{1}\"]') = 1","attribute_name", "attribute_value");

the method which I'm going to call inside the LINQ query:

public static bool AttributeExists(this string xmlColumnName, string attributeName, string attributeValue)
{
   throw new NotSupportedException();
}

the integration part with HQL:

public class XMLAttributeGenerator : BaseHqlGeneratorForMethod
{
   public XMLAttributeGenerator()
   {
       SupportedMethods = new[] { ReflectionHelper.GetMethodDefinition(() => TestClass.AttributeExists(null, null, null)) };
   }

    public override HqlTreeNode BuildHql(MethodInfo method, Expression targetObject,
        ReadOnlyCollection<Expression> arguments, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
    {
        return treeBuilder.Exists(???);
    }
}

As you can see, I still haven't figure out how to properly use the treebuilder with the visitor object to replicate the HQL syntax expressed above. May somebody help me out with this or at least point me to some basic documentation about the usage of the treebuilder? Thanks


Solution

  • This is how I achieved the desired result:

    MOCK METHOD

    public static class MockLINQMethods
        {
            public static bool XMLContains(this MyCustomNHType input, string element, string value)
            {
                throw new NotImplementedException();
            }
    }
    

    CUSTOM GENERATOR

    public class CustomLinqToHqlGeneratorsRegistry : DefaultLinqToHqlGeneratorsRegistry
        {
            public CustomLinqToHqlGeneratorsRegistry()
                : base()
            {
                RegisterGenerator(ReflectionHelper.GetMethod(() => MockLINQMethods.XMLContains((MyCustomNHType) null, null, null)),
                                  new LINQtoHQLGenerators.MyCustomNHTypeXMLContainsGenerator());
            }
    }
    
    public class MyCustomNHTypeXMLContainsGenerator : BaseHqlGeneratorForMethod
            {
                public MyCustomNHTypeXMLContainsGenerator()
                {
                    SupportedMethods = new[] { ReflectionHelper.GetMethod(() => MockLINQMethods.XMLContains((MyCustomNHType) null, null, null)) };
                }
    
                public override HqlTreeNode BuildHql(MethodInfo method, Expression targetObject,
                    ReadOnlyCollection<Expression> arguments, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
                {
                    var column_name = visitor.Visit(arguments[0]).AsExpression();
                    var element_name = visitor.Visit(arguments[1]).AsExpression();
                    var value = visitor.Visit(arguments[2]).AsExpression();
    
                    return treeBuilder.BooleanMethodCall("_ExistInMyCustomNHType", new [] { column_name, element_name, value});
                }
            }
    

    CUSTOM FUNCTION

    public class CustomLinqToHqlMsSql2008Dialect : MsSql2008Dialect
        {
            public CustomLinqToHqlMsSql2008Dialect()
            {
                RegisterFunction("_ExistInMyCustomNHType", 
                    new SQLFunctionTemplate(NHibernateUtil.Boolean, 
                        "?1.exist('/L/I/C[N=sql:variable(\"?2\") and V=sql:variable(\"?3\")]') = 1"));
            }
        }
    

    FINALLY, LINK THE CUSTOM GENERATOR AND THE CUSTOM FUNCTION IN THE SESSION HELPER

        factory = Fluently.Configure()
    .Database(MsSqlConfiguration.MsSql2008
        .ConnectionString(connectionString)
            .Dialect<CustomLinqToHqlMsSql2008Dialect>())
            ..
            .ExposeConfiguration(c =>
                {    
                ..         
                c.SetProperty("linqtohql.generatorsregistry", "APP.MyNAMESPACE.CustomLinqToHqlGeneratorsRegistry, APP.MyNAMESPACE");
                ..                        
                })                    
            .BuildSessionFactory();