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?
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.