Skip to content

Commit

Permalink
Merge pull request #78 from Maschmi/groupjoin
Browse files Browse the repository at this point in the history
New feature: GroupJoin
  • Loading branch information
StefH committed Apr 30, 2017
2 parents 640bd49 + 3eb203d commit 7330281
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs
Expand Up @@ -431,6 +431,75 @@ static IEnumerable<GroupResult> GroupByManyInternal<TElement>(IEnumerable<TEleme
}
#endregion GroupByMany

#region GroupJoin
/// <summary>
/// Correlates the elements of two sequences based on equality of keys and groups the results. The default equality comparer is used to compare keys.
/// </summary>
/// <param name="outer">The first sequence to join.</param>
/// <param name="inner">The sequence to join to the first sequence.</param>
/// <param name="outerKeySelector">A dynamic function to extract the join key from each element of the first sequence.</param>
/// <param name="innerKeySelector">A dynamic function to extract the join key from each element of the second sequence.</param>
/// <param name="resultSelector">A dynamic function to create a result element from an element from the first sequence and a collection of matching elements from the second sequence.</param>
/// <param name="args">An object array that contains zero or more objects to insert into the predicates as parameters. Similar to the way String.Format formats strings.</param>
/// <returns>An <see cref="IQueryable"/> obtained by performing a grouped join on two sequences.</returns>
public static IQueryable GroupJoin([NotNull] this IQueryable outer, [NotNull] IEnumerable inner, [NotNull] string outerKeySelector, [NotNull] string innerKeySelector, [NotNull] string resultSelector, params object[] args)
{
Check.NotNull(outer, nameof(outer));
Check.NotNull(inner, nameof(inner));
Check.NotEmpty(outerKeySelector, nameof(outerKeySelector));
Check.NotEmpty(innerKeySelector, nameof(innerKeySelector));
Check.NotEmpty(resultSelector, nameof(resultSelector));

Type outerType = outer.ElementType;
Type innerType = inner.AsQueryable().ElementType;

bool createParameterCtor = outer.IsLinqToObjects();
LambdaExpression outerSelectorLambda = DynamicExpressionParser.ParseLambda(createParameterCtor, outerType, null, outerKeySelector, args);
LambdaExpression innerSelectorLambda = DynamicExpressionParser.ParseLambda(createParameterCtor, innerType, null, innerKeySelector, args);

Type outerSelectorReturnType = outerSelectorLambda.Body.Type;
Type innerSelectorReturnType = innerSelectorLambda.Body.Type;

// If types are not the same, try to convert to Nullable and generate new LambdaExpression
if (outerSelectorReturnType != innerSelectorReturnType)
{
if (ExpressionParser.IsNullableType(outerSelectorReturnType) && !ExpressionParser.IsNullableType(innerSelectorReturnType))
{
innerSelectorReturnType = ExpressionParser.ToNullableType(innerSelectorReturnType);
innerSelectorLambda = DynamicExpressionParser.ParseLambda(createParameterCtor, innerType, innerSelectorReturnType, innerKeySelector, args);
}
else if (!ExpressionParser.IsNullableType(outerSelectorReturnType) && ExpressionParser.IsNullableType(innerSelectorReturnType))
{
outerSelectorReturnType = ExpressionParser.ToNullableType(outerSelectorReturnType);
outerSelectorLambda = DynamicExpressionParser.ParseLambda(createParameterCtor, outerType, outerSelectorReturnType, outerKeySelector, args);
}

// If types are still not the same, throw an Exception
if (outerSelectorReturnType != innerSelectorReturnType)
{
throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.IncompatibleTypes, outerType, innerType), -1);
}
}

ParameterExpression[] parameters =
{
Expression.Parameter(outerType, "outer"),
Expression.Parameter(typeof(IEnumerable<>).MakeGenericType(inner.AsQueryable().ElementType), "inner")
};

LambdaExpression resultSelectorLambda = DynamicExpressionParser.ParseLambda(createParameterCtor, parameters, null, resultSelector, args);

return outer.Provider.CreateQuery(Expression.Call(
typeof(Queryable),
"GroupJoin", new[] { outer.ElementType, innerType, outerSelectorLambda.Body.Type, resultSelectorLambda.Body.Type },
outer.Expression,
Expression.Constant(inner),
Expression.Quote(outerSelectorLambda),
Expression.Quote(innerSelectorLambda),
Expression.Quote(resultSelectorLambda)));
}
#endregion

#region Join
/// <summary>
/// Correlates the elements of two sequences based on matching keys. The default equality comparer is used to compare keys.
Expand Down
Expand Up @@ -245,6 +245,9 @@
<Compile Include="..\System.Linq.Dynamic.Core.Tests\QueryableTests.GroupByMany.cs">
<Link>QueryableTests.GroupByMany.cs</Link>
</Compile>
<Compile Include="..\System.Linq.Dynamic.Core.Tests\QueryableTests.GroupJoin.cs">
<Link>QueryableTests.GroupJoin.cs</Link>
</Compile>
<Compile Include="..\System.Linq.Dynamic.Core.Tests\QueryableTests.Join.cs">
<Link>QueryableTests.Join.cs</Link>
</Compile>
Expand Down
174 changes: 174 additions & 0 deletions test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupJoin.cs
@@ -0,0 +1,174 @@
using System.Collections.Generic;
using System.Linq.Dynamic.Core.Exceptions;
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
using NFluent;
using Xunit;

namespace System.Linq.Dynamic.Core.Tests
{
public partial class QueryableTests
{
[Fact]
public void GroupJoin()
{
//Arrange
Person magnus = new Person { Name = "Hedlund, Magnus" };
Person terry = new Person { Name = "Adams, Terry" };
Person charlotte = new Person { Name = "Weiss, Charlotte" };

Pet barley = new Pet { Name = "Barley", Owner = terry };
Pet boots = new Pet { Name = "Boots", Owner = terry };
Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte };
Pet daisy = new Pet { Name = "Daisy", Owner = magnus };

var people = new List<Person> { magnus, terry, charlotte };
var petsList = new List<Pet> { barley, boots, whiskers, daisy };

//Act
var realQuery = people.AsQueryable().GroupJoin(
petsList,
person => person,
pet => pet.Owner,
(person, pets) => new { OwnerName = person.Name, Pets = pets });

var dynamicQuery = people.AsQueryable().GroupJoin(
petsList,
"it",
"Owner",
"new(outer.Name as OwnerName, inner as Pets)");

//Assert
var realResult = realQuery.ToArray();

#if NETSTANDARD
var dynamicResult = dynamicQuery.ToDynamicArray<DynamicClass>();

Assert.Equal(realResult.Length, dynamicResult.Length);
for (int i = 0; i < realResult.Length; i++)
{
Assert.Equal(realResult[i].OwnerName, dynamicResult[i].GetDynamicPropertyValue<string>("OwnerName"));
for (int j = 0; j < realResult[i].Pets.Count(); j++)
{
Assert.Equal(realResult[i].Pets.ElementAt(j).Name, dynamicResult[i].GetDynamicPropertyValue<IEnumerable<Pet>>("Pets").ElementAt(j).Name);
}
}
#else
var dynamicResult = dynamicQuery.ToDynamicArray();

Assert.Equal(realResult.Length, dynamicResult.Length);
for (int i = 0; i < realResult.Length; i++)
{
Assert.Equal(realResult[i].OwnerName, ((dynamic) dynamicResult[i]).OwnerName);
for (int j = 0; j < realResult[i].Pets.Count(); j++)
{
Assert.Equal(realResult[i].Pets.ElementAt(j).Name, (((IEnumerable<Pet>)((dynamic)dynamicResult[i]).Pets)).ElementAt(j).Name);
}
}
#endif
}

[Fact]
public void GroupJoinOnNullableType_RightNullable()
{
//Arrange
Person magnus = new Person { Id = 1, Name = "Hedlund, Magnus" };
Person terry = new Person { Id = 2, Name = "Adams, Terry" };
Person charlotte = new Person { Id = 3, Name = "Weiss, Charlotte" };

Pet barley = new Pet { Name = "Barley", NullableOwnerId = terry.Id };
Pet boots = new Pet { Name = "Boots", NullableOwnerId = terry.Id };
Pet whiskers = new Pet { Name = "Whiskers", NullableOwnerId = charlotte.Id };
Pet daisy = new Pet { Name = "Daisy", NullableOwnerId = magnus.Id };

var people = new List<Person> { magnus, terry, charlotte };
var petsList = new List<Pet> { barley, boots, whiskers, daisy };

//Act
var realQuery = people.AsQueryable().GroupJoin(
petsList,
person => person.Id,
pet => pet.NullableOwnerId,
(person, pets) => new { OwnerName = person.Name, Pets = pets });

var dynamicQuery = people.AsQueryable().GroupJoin(
petsList,
"it.Id",
"NullableOwnerId",
"new(outer.Name as OwnerName, inner as Pets)");

//Assert
var realResult = realQuery.ToArray();
var dynamicResult = dynamicQuery.ToDynamicArray<DynamicClass>();

Assert.Equal(realResult.Length, dynamicResult.Length);
for (int i = 0; i < realResult.Length; i++)
{
Assert.Equal(realResult[i].OwnerName, dynamicResult[i].GetDynamicPropertyValue<string>("OwnerName"));
for (int j = 0; j < realResult[i].Pets.Count(); j++)
{
Assert.Equal(realResult[i].Pets.ElementAt(j).Name, dynamicResult[i].GetDynamicPropertyValue<IEnumerable<Pet>>("Pets").ElementAt(j).Name);
}
}
}

[Fact]
public void GroupJoinOnNullableType_LeftNullable()
{
//Arrange
Person magnus = new Person { NullableId = 1, Name = "Hedlund, Magnus" };
Person terry = new Person { NullableId = 2, Name = "Adams, Terry" };
Person charlotte = new Person { NullableId = 3, Name = "Weiss, Charlotte" };

Pet barley = new Pet { Name = "Barley", OwnerId = terry.Id };
Pet boots = new Pet { Name = "Boots", OwnerId = terry.Id };
Pet whiskers = new Pet { Name = "Whiskers", OwnerId = charlotte.Id };
Pet daisy = new Pet { Name = "Daisy", OwnerId = magnus.Id };

var people = new List<Person> { magnus, terry, charlotte };
var petsList = new List<Pet> { barley, boots, whiskers, daisy };

//Act
var realQuery = people.AsQueryable().GroupJoin(
petsList,
person => person.NullableId,
pet => pet.OwnerId,
(person, pets) => new { OwnerName = person.Name, Pets = pets });

var dynamicQuery = people.AsQueryable().GroupJoin(
petsList,
"it.NullableId",
"OwnerId",
"new(outer.Name as OwnerName, inner as Pets)");

//Assert
var realResult = realQuery.ToArray();
var dynamicResult = dynamicQuery.ToDynamicArray<DynamicClass>();

Assert.Equal(realResult.Length, dynamicResult.Length);
for (int i = 0; i < realResult.Length; i++)
{
Assert.Equal(realResult[i].OwnerName, dynamicResult[i].GetDynamicPropertyValue<string>("OwnerName"));
for (int j = 0; j < realResult[i].Pets.Count(); j++)
{
Assert.Equal(realResult[i].Pets.ElementAt(j).Name, dynamicResult[i].GetDynamicPropertyValue<IEnumerable<Pet>>("Pets").ElementAt(j).Name);
}
}
}

[Fact]
public void GroupJoinOnNullableType_NotSameTypesThrowsException()
{
var person = new Person { Id = 1, Name = "Hedlund, Magnus" };
var people = new List<Person> { person };
var pets = new List<Pet> { new Pet { Name = "Daisy", OwnerId = person.Id } };

Check.ThatCode(() =>
people.AsQueryable()
.GroupJoin(
pets,
"it.Id",
"Name", // This is wrong
"new(outer.Name as OwnerName, inner as Pets)")).Throws<ParseException>();
}
}
}

0 comments on commit 7330281

Please sign in to comment.