MultiQuery/ToFuture broken with Contains (in)

Description

There is an error in NamedParameterSpecification handling when using multiqueries or linq .ToFuture() with a query having an "in" clause.

Here's a failing unit test, put it in LinqFutureFixture.cs:

[Test]
public void CanUseToFutureWithContains()
{
using (var s = sessions.OpenSession())
{
var ids = new[] { 1, 2, 3 };
var persons10 = s.Query<Person>()
.Where(p => ids.Contains(p.Id))
.FetchMany(p => p.Children)
.Skip(5)
.Take(10)
.ToFuture().ToList();

Assert.Pass();
}
}

Stack trace:

System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
at System.ThrowHelper.ThrowKeyNotFoundException()
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at NHibernate.Param.NamedParameterSpecification.SetEffectiveType(QueryParameters queryParameters) in NamedParameterSpecification.cs: line 70
at NHibernate.Param.ParametersBackTrackExtensions.ResetEffectiveExpectedType(IEnumerable`1 parameterSpecs, QueryParameters queryParameters) in ParametersBackTrackExtensions.cs: line 48
at NHibernate.Hql.Ast.ANTLR.Loader.QueryLoader.ResetEffectiveExpectedType(IEnumerable`1 parameterSpecs, QueryParameters queryParameters) in QueryLoader.cs: line 428
at NHibernate.Loader.Loader.CreateSqlCommand(QueryParameters queryParameters, ISessionImplementor session) in Loader.cs: line 1649
at NHibernate.Impl.MultiQueryImpl.AggregateQueriesInformation() in MultiQueryImpl.cs: line 641
at NHibernate.Impl.MultiQueryImpl.get_Parameters() in MultiQueryImpl.cs: line 774
at NHibernate.Impl.MultiQueryImpl.CreateCombinedQueryParameters() in MultiQueryImpl.cs: line 754
at NHibernate.Impl.MultiQueryImpl.List() in MultiQueryImpl.cs: line 400
at NHibernate.Impl.FutureQueryBatch.GetResultsFrom(IMultiQuery multiApproach) in FutureQueryBatch.cs: line 24
at NHibernate.Impl.FutureBatch`2.GetResults() in FutureBatch.cs: line 73
at NHibernate.Impl.FutureBatch`2.get_Results() in FutureBatch.cs: line 29
at NHibernate.Impl.FutureBatch`2.GetCurrentResult(Int32 currentIndex) in FutureBatch.cs: line 79
at NHibernate.Impl.FutureBatch`2.<>c_DisplayClass4`1.<GetEnumerator>b_3() in FutureBatch.cs: line 63
at NHibernate.Impl.DelayedEnumerator`1.<get_Enumerable>d__0.MoveNext() in DelayedEnumerator.cs: line 26
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList(IEnumerable`1 source)
at NHibernate.Test.NHSpecificTest.Futures.LinqFutureFixture.CanUseToFutureWithContains

Environment

None

Activity

Show:

Alex Zaytsev February 20, 2013 at 8:43 PM

It seems not

Daniel Laberge February 20, 2013 at 3:19 PM

Will this be included in 3.3.1 GA?

Alex Zaytsev June 8, 2012 at 5:26 PM

Ok, I've found shorter way to fix this issue.

Fix commited to master 92ba8462db29e5b1d547d3b8701dddede558f927

ChristopherB May 26, 2012 at 9:39 PM

Here is a work around I created in case it helps any one:

using System;
using System.Linq;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Linq;
using System.Collections.Generic;

public static class NHExtensions
{

public static IQueryable<TSource> AppendContains<TSource, TResult>(this IQueryable<TSource> source,
Expression<Func<TSource, TResult>> selector,
IEnumerable<TResult> options)
{
if (!options.Any())
throw new ArgumentException("At least one options must be specified.", "options");

Expression orExpression = null;

Expression selectorExpression = (MemberExpression)selector.Body;

foreach (var item in options)
{
Expression equalExpression = Expression.Equal(selectorExpression, Expression.Constant(item));

orExpression = orExpression == null ? equalExpression :
Expression.OrElse(orExpression, equalExpression);
}

var funcType = Expression.GetFuncType(typeof(TSource), typeof(bool));
var sourceParam = Expression.Parameter(typeof(TSource), "source");
var whereClause = (Expression<Func<TSource, bool>>)Expression.Lambda(funcType, orExpression, sourceParam);

return source.Where(whereClause);
}
}

Here is an example usage:

var users = (from u in session.Query<Users>()
where u.UserType == "Admin"
select u).AppendContains(u => user.UserId, new[]{ 1, 2, 3 });

This will generate SQL similar to SELECT ... FROM User WHERE UserType = 'Admin' AND (UserId = 1 OR UserId = 3 or UserId = 3)

I've only tested this in very limited circumstances, so use at your own risk. The downsides are that this isn't as fluent as using .Contains(), and that if you have F future queries that depend on P options, then you end up with F*P total parameters to the query (not including any other parameters that are necessary) so the number of parameters can increase quickly if you aren't careful.

Hope it helps a few people until a real fix is in place.

  • Chris

Daniel Laberge March 14, 2012 at 3:04 PM

@Alexander I. Zaytsev: Thanks! I will continue watching this issue as I imagine it will be marked as resolved when 3.4 ships?

Fixed

Details

Assignee

Reporter

Components

Fix versions

Affects versions

Priority

Who's Looking?

Open Who's Looking?
Created September 23, 2011 at 3:46 PM
Updated September 21, 2014 at 12:40 PM
Resolved June 8, 2012 at 5:26 PM
Who's Looking?