Querying with linq a collection mapped as a map (I

2019-08-31 17:24发布

问题:

Using NHibernate, I have a collection of entities mapped as a dictionary. By example, class A has a collection of B named Children, mapped as a IDictionary<int, B>. B has a property Name.

Querying class A base on some condition on B children unrelated to their dictionary indexation is quite straightforward with HQL:

from A where A.Children.Name = 'aName'

Runs flawlessly.

But for achieving the same with LINQ, this is quite less straightforward:

IQueryable<A> query = ...;
query.Where(a => a.Children.Values.Any(b => b.Name == "aName"));

Fails with message could not resolve property: Values of: B

So yes, we can trick it through

IQueryable<A> query = ...;
query.Where(a => ((ICollection<B>)a.Children).Any(b => b.Name == "aName"));

That does works and yields expected results.

But this looks to me a bit ugly, I would rather not have to do that 'invalid' cast (at least 'invalid' outside of linq2NH context).

Is there any better way for querying a children collection mapped as a IDictionary with Linq and NHibernate?

回答1:

As an exercise, I have decided to extend linq-to-nhibernate for supporting Values. This give a solution to the question.

There is many ways for extending linq2NH, see this list. Here, I need to add a new 'generator', as in my answer to another question.

First you need a bunch of using:

using System.Reflection;
using System.Linq.Expressions;
using System.Collections;
using System.Collections.Generic;
using NHibernate.Hql.Ast;
using NHibernate.Linq.Visitors;
using NHibernate.Linq.Functions;

Then, implement HQL translation for Values.

public class DictionaryValuesGenerator : BaseHqlGeneratorForProperty
{
    public override HqlTreeNode BuildHql(
        MemberInfo member, Expression expression,
        HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
    {
        // Just have to skip Values, HQL does not need it.
        return visitor.Visit(expression).AsExpression();
    }
}

Extend the default linq2NH registry with your generator:

public class ExtendedLinqToHqlGeneratorsRegistry : 
    DefaultLinqToHqlGeneratorsRegistry
{
    public override bool TryGetGenerator(MemberInfo property,
        out IHqlGeneratorForProperty generator)
    {
        if (base.TryGetGenerator(property, out generator))
            return true;

        return TryGetDictionaryValuesGenerator(property, out generator);
    }

    private DictionaryValuesGenerator _dictionaryValuesGenerator = 
       new DictionaryValuesGenerator();
    protected bool TryGetDictionaryValuesGenerator(MemberInfo property,
        out IHqlGeneratorForProperty generator)
    {
        generator = null;
        if (property == null || property.Name != "Values")
            return false;

        var declaringType = property.DeclaringType;
        if (declaringType.IsGenericType)
        {
            var genericType = declaringType.GetGenericTypeDefinition();
            if (genericType != typeof(IDictionary<,>))
                return false;
            generator = _dictionaryValuesGenerator;
            return true;
        }

        if (declaringType != typeof(IDictionary))
            return false;
        generator = _dictionaryValuesGenerator;
        return true;
    }
}

I had quite a hard time figuring out how to register a generic class property generator. There is built-in support for many cases including generic dictionaries methods through derived class of GenericDictionaryRuntimeMethodHqlGeneratorBase, but apparently no support for generic dictionaries properties. So I have ended up 'hard coding' it in the TryGetGenerator method for properties.

Now configure NH to use your new registry. With hibernate.cfg.xml, add following property node under session-factory node:

<property name="linqtohql.generatorsregistry">YourNameSpace.ExtendedLinqToHqlGeneratorsRegistry, YourAssemblyName</property>

Now this does work:

IQueryable<A> query = ...;
query.Where(a => a.Children.Values.Any(b => b.Name == "aName"));

Disclaimer: done only as an exercise, I have not even committed that in my actual code. I am currently no more using any map in my mappings. I have added some just for testing, then I have undone all.