diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 4323a4b8..4bd0ab78 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -431,6 +431,75 @@ static IEnumerable GroupByManyInternal(IEnumerable + /// Correlates the elements of two sequences based on equality of keys and groups the results. The default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A dynamic function to extract the join key from each element of the first sequence. + /// A dynamic function to extract the join key from each element of the second sequence. + /// 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. + /// An object array that contains zero or more objects to insert into the predicates as parameters. Similar to the way String.Format formats strings. + /// An obtained by performing a grouped join on two sequences. + 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 /// /// Correlates the elements of two sequences based on matching keys. The default equality comparer is used to compare keys. diff --git a/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj b/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj index a4018e89..378df829 100644 --- a/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj +++ b/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj @@ -245,6 +245,9 @@ QueryableTests.GroupByMany.cs + + QueryableTests.GroupJoin.cs + QueryableTests.Join.cs diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupJoin.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupJoin.cs new file mode 100644 index 00000000..2ef5de43 --- /dev/null +++ b/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 { magnus, terry, charlotte }; + var petsList = new List { 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(); + + Assert.Equal(realResult.Length, dynamicResult.Length); + for (int i = 0; i < realResult.Length; i++) + { + Assert.Equal(realResult[i].OwnerName, dynamicResult[i].GetDynamicPropertyValue("OwnerName")); + for (int j = 0; j < realResult[i].Pets.Count(); j++) + { + Assert.Equal(realResult[i].Pets.ElementAt(j).Name, dynamicResult[i].GetDynamicPropertyValue>("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)((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 { magnus, terry, charlotte }; + var petsList = new List { 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(); + + Assert.Equal(realResult.Length, dynamicResult.Length); + for (int i = 0; i < realResult.Length; i++) + { + Assert.Equal(realResult[i].OwnerName, dynamicResult[i].GetDynamicPropertyValue("OwnerName")); + for (int j = 0; j < realResult[i].Pets.Count(); j++) + { + Assert.Equal(realResult[i].Pets.ElementAt(j).Name, dynamicResult[i].GetDynamicPropertyValue>("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 { magnus, terry, charlotte }; + var petsList = new List { 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(); + + Assert.Equal(realResult.Length, dynamicResult.Length); + for (int i = 0; i < realResult.Length; i++) + { + Assert.Equal(realResult[i].OwnerName, dynamicResult[i].GetDynamicPropertyValue("OwnerName")); + for (int j = 0; j < realResult[i].Pets.Count(); j++) + { + Assert.Equal(realResult[i].Pets.ElementAt(j).Name, dynamicResult[i].GetDynamicPropertyValue>("Pets").ElementAt(j).Name); + } + } + } + + [Fact] + public void GroupJoinOnNullableType_NotSameTypesThrowsException() + { + var person = new Person { Id = 1, Name = "Hedlund, Magnus" }; + var people = new List { person }; + var pets = new List { 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(); + } + } +} \ No newline at end of file