我试图用返回类型IQueryable
实现左外连接扩展方法。
我写的功能如下
public static IQueryable<TResult> LeftOuterJoin2<TOuter, TInner, TKey, TResult>(
this IQueryable<TOuter> outer,
IQueryable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector)
{
return
from outerItem in outer
join innerItem in inner on outerKeySelector(outerItem)
equals innerKeySelector(innerItem) into joinedData
from r in joinedData.DefaultIfEmpty()
select resultSelector(outerItem, r);
}
它无法生成查询。原因可能是:我使用Func<>
而不是Expression<>
。我也试过Expression<>
。它给我一个outerKeySelector(outerItem)
线上的错误,这是outerKeySelector
是一个变量,它被用作一种方法
我发现了一些关于SO(如here)和CodeProjects的讨论,但那些适用于IEnumerable
类型的讨论不适用于IQueryable
。
介绍
这个问题非常有趣。问题是Funcs是委托,而表达式是trees,它们是完全不同的结构。当您使用当前的扩展实现时,它使用循环并在每个元素的每个步骤上执行选择器,并且它运行良好。但是当我们谈论实体框架和LINQ时,我们需要树遍历来将其转换为SQL查询。所以它比Funcs更“难”(但我还是喜欢Expressions)并且下面描述了一些问题。
如果你想做左外连接你可以使用这样的东西(取自这里:How to implement left join in JOIN Extension method)
var leftJoin = p.Person.Where(n => n.FirstName.Contains("a"))
.GroupJoin(p.PersonInfo,
n => n.PersonId,
m => m.PersonId,
(n, ms) => new { n, ms = ms.DefaultIfEmpty() })
.SelectMany(z => z.ms.Select(m => new { n = z.n, m ));
这是好的,但它不是我们需要的扩展方法。我想你需要这样的东西:
using (var db = new Database1Entities("..."))
{
var my = db.A.LeftOuterJoin2(db.B, a => a.Id, b => b.IdA,
(a, b) => new { a, b, hello = "Hello World!" });
// other actions ...
}
创建此类扩展有许多难点:
Where
,Select
等方法需要反思脚步
考虑两个简单的表:A
(列:Id,文本)和B
(列Id,IdA,文本)。
外连接可以分3步实现:
// group join as usual + use DefaultIfEmpty
var q1 = Queryable.GroupJoin(db.A, db.B, a => a.Id, b => b.IdA,
(a, b) => new { a, groupB = b.DefaultIfEmpty() });
// regroup data to associated list a -> b, it is usable already, but it's
// impossible to use resultSelector on this stage,
// beacuse of type difference (quite deep problem: some anonymous type != TOuter)
var q2 = Queryable.SelectMany(q1, x => x.groupB, (a, b) => new { a.a, b });
// second regroup to get the right types
var q3 = Queryable.SelectMany(db.A,
a => q2.Where(x => x.a == a).Select(x => x.b),
(a, b) => new {a, b});
码
好吧,我不是一个好的出纳员,这里是我的代码(对不起,我无法更好地格式化它,但它有效!):
public static IQueryable<TResult> LeftOuterJoin2<TOuter, TInner, TKey, TResult>(
this IQueryable<TOuter> outer,
IQueryable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
// generic methods
var selectManies = typeof(Queryable).GetMethods()
.Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3)
.OrderBy(x=>x.ToString().Length)
.ToList();
var selectMany = selectManies.First();
var select = typeof(Queryable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2);
var where = typeof(Queryable).GetMethods().First(x => x.Name == "Where" && x.GetParameters().Length == 2);
var groupJoin = typeof(Queryable).GetMethods().First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5);
var defaultIfEmpty = typeof(Queryable).GetMethods().First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1);
// need anonymous type here or let's use Tuple
// prepares for:
// var q2 = Queryable.GroupJoin(db.A, db.B, a => a.Id, b => b.IdA, (a, b) => new { a, groupB = b.DefaultIfEmpty() });
var tuple = typeof(Tuple<,>).MakeGenericType(
typeof(TOuter),
typeof(IQueryable<>).MakeGenericType(
typeof(TInner)
)
);
var paramOuter = Expression.Parameter(typeof(TOuter));
var paramInner = Expression.Parameter(typeof(IEnumerable<TInner>));
var groupJoinExpression = Expression.Call(
null,
groupJoin.MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), tuple),
new Expression[]
{
Expression.Constant(outer),
Expression.Constant(inner),
outerKeySelector,
innerKeySelector,
Expression.Lambda(
Expression.New(
tuple.GetConstructor(tuple.GetGenericArguments()),
new Expression[]
{
paramOuter,
Expression.Call(
null,
defaultIfEmpty.MakeGenericMethod(typeof (TInner)),
new Expression[]
{
Expression.Convert(paramInner, typeof (IQueryable<TInner>))
}
)
},
tuple.GetProperties()
),
new[] {paramOuter, paramInner}
)
}
);
// prepares for:
// var q3 = Queryable.SelectMany(q2, x => x.groupB, (a, b) => new { a.a, b });
var tuple2 = typeof (Tuple<,>).MakeGenericType(typeof (TOuter), typeof (TInner));
var paramTuple2 = Expression.Parameter(tuple);
var paramInner2 = Expression.Parameter(typeof(TInner));
var paramGroup = Expression.Parameter(tuple);
var selectMany1Result = Expression.Call(
null,
selectMany.MakeGenericMethod(tuple, typeof (TInner), tuple2),
new Expression[]
{
groupJoinExpression,
Expression.Lambda(
Expression.Convert(Expression.MakeMemberAccess(paramGroup, tuple.GetProperty("Item2")),
typeof (IEnumerable<TInner>)),
paramGroup
),
Expression.Lambda(
Expression.New(
tuple2.GetConstructor(tuple2.GetGenericArguments()),
new Expression[]
{
Expression.MakeMemberAccess(paramTuple2, paramTuple2.Type.GetProperty("Item1")),
paramInner2
},
tuple2.GetProperties()
),
new[]
{
paramTuple2,
paramInner2
}
)
}
);
// prepares for final step, combine all expressinos together and invoke:
// var q4 = Queryable.SelectMany(db.A, a => q3.Where(x => x.a == a).Select(x => x.b), (a, b) => new { a, b });
var paramTuple3 = Expression.Parameter(tuple2);
var paramTuple4 = Expression.Parameter(tuple2);
var paramOuter3 = Expression.Parameter(typeof (TOuter));
var selectManyResult2 = selectMany
.MakeGenericMethod(
typeof(TOuter),
typeof(TInner),
typeof(TResult)
)
.Invoke(
null,
new object[]
{
outer,
Expression.Lambda(
Expression.Convert(
Expression.Call(
null,
select.MakeGenericMethod(tuple2, typeof(TInner)),
new Expression[]
{
Expression.Call(
null,
where.MakeGenericMethod(tuple2),
new Expression[]
{
selectMany1Result,
Expression.Lambda(
Expression.Equal(
paramOuter3,
Expression.MakeMemberAccess(paramTuple4, paramTuple4.Type.GetProperty("Item1"))
),
paramTuple4
)
}
),
Expression.Lambda(
Expression.MakeMemberAccess(paramTuple3, paramTuple3.Type.GetProperty("Item2")),
paramTuple3
)
}
),
typeof(IEnumerable<TInner>)
),
paramOuter3
),
resultSelector
}
);
return (IQueryable<TResult>)selectManyResult2;
}
用法
再次使用:
db.A.LeftOuterJoin2(db.B, a => a.Id, b => b.IdA,
(a, b) => new { a, b, hello = "Hello World!" });
看看这个你可以想一下这个sql查询是什么?这可能是巨大的。你猜怎么着?它很小:
SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Extent1].[Text] AS [Text],
[Join1].[Id1] AS [Id1],
[Join1].[IdA] AS [IdA],
[Join1].[Text2] AS [Text2],
N'Hello World!' AS [C2]
FROM [A] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Id] AS [Id2], [Extent2].[Text] AS [Text], [Extent3].[Id] AS [Id1], [Extent3].[IdA] AS [IdA], [Extent3].[Text2] AS [Text2]
FROM [A] AS [Extent2]
LEFT OUTER JOIN [B] AS [Extent3] ON [Extent2].[Id] = [Extent3].[IdA] ) AS [Join1] ON [Extent1].[Id] = [Join1].[Id2]
希望能帮助到你。
接受的答案是解释左外连接背后复杂性的一个很好的开始。
我发现了三个相当严重的问题,特别是在采用这种扩展方法并在更复杂的查询中使用它时(将多个左外连接与正常连接链接,然后汇总/ max / count / ...)在将所选答案复制到您的生产环境,请继续阅读。
考虑链接的SO帖子中的原始示例,它代表LINQ中完成的任何左外连接:
var leftJoin = p.Person.Where(n => n.FirstName.Contains("a"))
.GroupJoin(p.PersonInfo,
n => n.PersonId,
m => m.PersonId,
(n, ms) => new { n, ms = ms })
.SelectMany(z => z.ms.DefaultIfEmpty(), (n, m) => new { n = n, m ));
internal class KeyValuePairHolder<T1, T2>
{
public T1 Item1 { get; set; }
public T2 Item2 { get; set; }
}
internal class ResultSelectorRewriter<TOuter, TInner, TResult> : ExpressionVisitor
{
private Expression<Func<TOuter, TInner, TResult>> resultSelector;
public Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>> CombinedExpression { get; private set; }
private ParameterExpression OldTOuterParamExpression;
private ParameterExpression OldTInnerParamExpression;
private ParameterExpression NewTOuterParamExpression;
private ParameterExpression NewTInnerParamExpression;
public ResultSelectorRewriter(Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
this.resultSelector = resultSelector;
this.OldTOuterParamExpression = resultSelector.Parameters[0];
this.OldTInnerParamExpression = resultSelector.Parameters[1];
this.NewTOuterParamExpression = Expression.Parameter(typeof(KeyValuePairHolder<TOuter, IEnumerable<TInner>>));
this.NewTInnerParamExpression = Expression.Parameter(typeof(TInner));
var newBody = this.Visit(this.resultSelector.Body);
var combinedExpression = Expression.Lambda(newBody, new ParameterExpression[] { this.NewTOuterParamExpression, this.NewTInnerParamExpression });
this.CombinedExpression = (Expression<Func<KeyValuePairHolder<TOuter, IEnumerable<TInner>>, TInner, TResult>>)combinedExpression;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (node == this.OldTInnerParamExpression)
return this.NewTInnerParamExpression;
else if (node == this.OldTOuterParamExpression)
return Expression.PropertyOrField(this.NewTOuterParamExpression, "Item1");
else
throw new InvalidOperationException("What is this sorcery?", new InvalidOperationException("Did not expect a parameter: " + node));
}
}
使用表达式visitor和KeyValuePairHolder来避免使用元组,我在下面选择的答案的更新版本修复了三个问题,更短,并产生更短的SQL:
internal class QueryReflectionMethods
{
internal static System.Reflection.MethodInfo Enumerable_Select = typeof(Enumerable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2);
internal static System.Reflection.MethodInfo Enumerable_DefaultIfEmpty = typeof(Enumerable).GetMethods().First(x => x.Name == "DefaultIfEmpty" && x.GetParameters().Length == 1);
internal static System.Reflection.MethodInfo Queryable_SelectMany = typeof(Queryable).GetMethods().Where(x => x.Name == "SelectMany" && x.GetParameters().Length == 3).OrderBy(x => x.ToString().Length).First();
internal static System.Reflection.MethodInfo Queryable_Where = typeof(Queryable).GetMethods().First(x => x.Name == "Where" && x.GetParameters().Length == 2);
internal static System.Reflection.MethodInfo Queryable_GroupJoin = typeof(Queryable).GetMethods().First(x => x.Name == "GroupJoin" && x.GetParameters().Length == 5);
internal static System.Reflection.MethodInfo Queryable_Join = typeof(Queryable).GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public).First(c => c.Name == "Join");
internal static System.Reflection.MethodInfo Queryable_Select = typeof(Queryable).GetMethods().First(x => x.Name == "Select" && x.GetParameters().Length == 2);
public static IQueryable<TResult> CreateLeftOuterJoin<TOuter, TInner, TKey, TResult>(
IQueryable<TOuter> outer,
IQueryable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
var keyValuePairHolderWithGroup = typeof(KeyValuePairHolder<,>).MakeGenericType(
typeof(TOuter),
typeof(IEnumerable<>).MakeGenericType(
typeof(TInner)
)
);
var paramOuter = Expression.Parameter(typeof(TOuter));
var paramInner = Expression.Parameter(typeof(IEnumerable<TInner>));
var groupJoin =
Queryable_GroupJoin.MakeGenericMethod(typeof(TOuter), typeof(TInner), typeof(TKey), keyValuePairHolderWithGroup)
.Invoke(
"ThisArgumentIsIgnoredForStaticMethods",
new object[]{
outer,
inner,
outerKeySelector,
innerKeySelector,
Expression.Lambda(
Expression.MemberInit(
Expression.New(keyValuePairHolderWithGroup),
Expression.Bind(
keyValuePairHolderWithGroup.GetMember("Item1").Single(),
paramOuter
),
Expression.Bind(
keyValuePairHolderWithGroup.GetMember("Item2").Single(),
paramInner
)
),
paramOuter,
paramInner
)
}
);
var paramGroup = Expression.Parameter(keyValuePairHolderWithGroup);
Expression collectionSelector = Expression.Lambda(
Expression.Call(
null,
Enumerable_DefaultIfEmpty.MakeGenericMethod(typeof(TInner)),
Expression.MakeMemberAccess(paramGroup, keyValuePairHolderWithGroup.GetProperty("Item2")))
,
paramGroup
);
Expression newResultSelector = new ResultSelectorRewriter<TOuter, TInner, TResult>(resultSelector).CombinedExpression;
var selectMany1Result =
Queryable_SelectMany.MakeGenericMethod(keyValuePairHolderWithGroup, typeof(TInner), typeof(TResult))
.Invoke(
"ThisArgumentIsIgnoredForStaticMethods", new object[]{
groupJoin,
collectionSelector,
newResultSelector
}
);
return (IQueryable<TResult>)selectMany1Result;
}
}
如前面的答案中所述,当您希望将IQueryable转换为SQL时,您需要使用Expression而不是Func,因此您必须使用Expression Tree路由。
但是,这里有一种方法可以实现相同的结果,而无需自己构建表达式树。诀窍是,您需要引用LinqKit(可通过NuGet获得)并在查询上调用AsExpandable()。这将负责构建基础表达式树(请参阅here)。
下面的示例使用GroupJoin和SelectMany以及DefaultIfEmpty()方法:
码
public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(
this IQueryable<TOuter> outer,
IQueryable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
return outer
.AsExpandable()// Tell LinqKit to convert everything into an expression tree.
.GroupJoin(
inner,
outerKeySelector,
innerKeySelector,
(outerItem, innerItems) => new { outerItem, innerItems })
.SelectMany(
joinResult => joinResult.innerItems.DefaultIfEmpty(),
(joinResult, innerItem) =>
resultSelector.Invoke(joinResult.outerItem, innerItem));
}
样本数据
假设我们有以下EF实体,用户和地址变量是对底层DbSet的访问:
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class UserAddress
{
public int UserId { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
}
IQueryable<User> users;
IQueryable<UserAddress> addresses;
用法1
让我们按用户ID加入:
var result = users.LeftOuterJoin(
addresses,
user => user.Id,
address => address.UserId,
(user, address) => new { user.Id, address.Street });
这转换为(使用LinqPad):
SELECT
[Extent1].[Id] AS [Id],
[Extent2].[Street] AS [Street]
FROM [dbo].[Users] AS [Extent1]
LEFT OUTER JOIN [dbo].[UserAddresses] AS [Extent2]
ON [Extent1].[Id] = [Extent2].[UserId]
用法2
现在让我们使用匿名类型作为键来连接多个属性:
var result = users.LeftOuterJoin(
addresses,
user => new { user.Id, user.LastName },
address => new { Id = address.UserId, address.LastName },
(user, address) => new { user.Id, address.Street });
请注意,匿名类型属性必须具有相同的名称,否则您将收到语法错误。
这就是为什么我们有Id = address.UserId而不仅仅是address.UserId。
这将被翻译为:
SELECT
[Extent1].[Id] AS [Id],
[Extent2].[Street] AS [Street]
FROM [dbo].[Users] AS [Extent1]
LEFT OUTER JOIN [dbo].[UserAddresses] AS [Extent2]
ON ([Extent1].[Id] = [Extent2].[UserId]) AND ([Extent1].[LastName] = [Extent2].[LastName])
这是我去年想要简化.GroupJoin时创建的.LeftJoin扩展方法。我好运。我包含了XML注释,因此您可以获得完整的智能感知。 IEqualityComparer也有一个重载。希望对你有帮助。
我的全套Join Extensions就在这里:https://github.com/jolsa/Extensions/blob/master/ExtensionLib/JoinExtensions.cs
// JoinExtensions: Created 07/12/2014 - Johnny Olsa
using System.Linq;
namespace System.Collections.Generic
{
/// <summary>
/// Join Extensions that .NET should have provided?
/// </summary>
public static class JoinExtensions
{
/// <summary>
/// Correlates the elements of two sequences based on matching keys. A specified
/// System.Collections.Generic.IEqualityComparer<T> is used to compare keys.
/// </summary>
/// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam>
/// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam>
/// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam>
/// <typeparam name="TResult">The type of the result elements.</typeparam>
/// <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 function to extract the join key from each element of the first sequence.</param>
/// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param>
/// <param name="resultSelector">A function to create a result element from two combined elements.</param>
/// <param name="comparer">A System.Collections.Generic.IEqualityComparer<T> to hash and compare keys.</param>
/// <returns>
/// An System.Collections.Generic.IEnumerable<T> that has elements of type TResult
/// that are obtained by performing an left outer join on two sequences.
/// </returns>
/// <example>
/// Example:
/// <code>
/// class TestClass
/// {
/// static int Main()
/// {
/// var strings1 = new string[] { "1", "2", "3", "4", "a" };
/// var strings2 = new string[] { "1", "2", "3", "16", "A" };
///
/// var lj = strings1.LeftJoin(
/// strings2,
/// a => a,
/// b => b,
/// (a, b) => (a ?? "null") + "-" + (b ?? "null"),
/// StringComparer.OrdinalIgnoreCase)
/// .ToList();
/// }
/// }
/// </code>
/// </example>
public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer)
{
return outer.GroupJoin(
inner,
outerKeySelector,
innerKeySelector,
(o, ei) => ei
.Select(i => resultSelector(o, i))
.DefaultIfEmpty(resultSelector(o, default(TInner))), comparer)
.SelectMany(oi => oi);
}
/// <summary>
/// Correlates the elements of two sequences based on matching keys. The default
/// equality comparer is used to compare keys.
/// </summary>
/// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam>
/// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam>
/// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam>
/// <typeparam name="TResult">The type of the result elements.</typeparam>
/// <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 function to extract the join key from each element of the first sequence.</param>
/// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param>
/// <param name="resultSelector">A function to create a result element from two combined elements.</param>
/// <returns>
/// An System.Collections.Generic.IEnumerable<T> that has elements of type TResult
/// that are obtained by performing an left outer join on two sequences.
/// </returns>
/// <example>
/// Example:
/// <code>
/// class TestClass
/// {
/// static int Main()
/// {
/// var strings1 = new string[] { "1", "2", "3", "4", "a" };
/// var strings2 = new string[] { "1", "2", "3", "16", "A" };
///
/// var lj = strings1.LeftJoin(
/// strings2,
/// a => a,
/// b => b,
/// (a, b) => (a ?? "null") + "-" + (b ?? "null"))
/// .ToList();
/// }
/// }
/// </code>
/// </example>
public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector)
{
return outer.LeftJoin(inner, outerKeySelector, innerKeySelector, resultSelector, default(IEqualityComparer<TKey>));
}
}
}
我之前回答的更新。当我发布它时,我没有注意到问题是转换为SQL。此代码适用于本地项目,因此将首先拉取对象然后连接,而不是在服务器上执行外部联接。但是要使用我之前发布的Join扩展来处理空值,这是一个例子:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class EmailAddress
{
public int Id { get; set; }
public Email Email { get; set; }
}
public class Email
{
public string Name { get; set; }
public string Address { get; set; }
}
public static void Main()
{
var people = new []
{
new Person() { Id = 1, Name = "John" },
new Person() { Id = 2, Name = "Paul" },
new Person() { Id = 3, Name = "George" },
new Person() { Id = 4, Name = "Ringo" }
};
var addresses = new[]
{
new EmailAddress() { Id = 2, Email = new Email() { Name = "Paul", Address = "[email protected]" } },
new EmailAddress() { Id = 3, Email = new Email() { Name = "George", Address = "[email protected]" } },
new EmailAddress() { Id = 4, Email = new Email() { Name = "Ringo", Address = "[email protected]" } }
};
var joinedById = people.LeftJoin(addresses, p => p.Id, a => a.Id, (p, a) => new
{
p.Id,
p.Name,
a?.Email.Address
}).ToList();
Console.WriteLine("\r\nJoined by Id:\r\n");
joinedById.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j.Address ?? "<null>"}"));
var joinedByName = people.LeftJoin(addresses, p => p.Name, a => a?.Email.Name, (p, a) => new
{
p.Id,
p.Name,
a?.Email.Address
}, StringComparer.OrdinalIgnoreCase).ToList();
Console.WriteLine("\r\nJoined by Name:\r\n");
joinedByName.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j.Address ?? "<null>"}"));
}
@Licentia,这是我想出来解决你的问题。我创建了类似于你向我展示的DynamicJoin
和DynamicLeftJoin
扩展方法,但我处理输出的方式不同,因为字符串解析容易受到许多问题的影响。这不会加入匿名类型,但您可以调整它来执行此操作。它也没有IComparable
的重载,但可以很容易地添加。属性名称必须与类型相同。这与我上面的扩展方法一起使用(即没有它们就无法工作)。我希望它有所帮助!
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class EmailAddress
{
public int PersonId { get; set; }
public Email Email { get; set; }
}
public class Email
{
public string Name { get; set; }
public string Address { get; set; }
}
public static void Main()
{
var people = new[]
{
new Person() { Id = 1, Name = "John" },
new Person() { Id = 2, Name = "Paul" },
new Person() { Id = 3, Name = "George" },
new Person() { Id = 4, Name = "Ringo" }
};
var addresses = new[]
{
new EmailAddress() { PersonId = 2, Email = new Email() { Name = "Paul", Address = "[email protected]" } },
new EmailAddress() { PersonId = 3, Email = new Email() { Name = "George", Address = "[email protected]" } },
new EmailAddress() { PersonId = 4, Email = new Email() { Name = "Ringo" } }
};
Console.WriteLine("\r\nInner Join:\r\n");
var innerJoin = people.DynamicJoin(addresses, "Id", "PersonId", "outer.Id", "outer.Name", "inner.Email").ToList();
innerJoin.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j?.Email?.Address ?? "<null>"}"));
Console.WriteLine("\r\nOuter Join:\r\n");
var leftJoin = people.DynamicLeftJoin(addresses, "Id", "PersonId", "outer.Id", "outer.Name", "inner.Email").ToList();
leftJoin.ForEach(j => Console.WriteLine($"{j.Id}-{j.Name}: {j?.Email?.Address ?? "<null>"}"));
}
public static class DynamicJoinExtensions
{
private const string OuterPrefix = "outer.";
private const string InnerPrefix = "inner.";
private class Processor<TOuter, TInner>
{
private readonly Type _typeOuter = typeof(TOuter);
private readonly Type _typeInner = typeof(TInner);
private readonly PropertyInfo _keyOuter;
private readonly PropertyInfo _keyInner;
private readonly List<string> _outputFields;
private readonly Dictionary<string, PropertyInfo> _resultProperties;
public Processor(string outerKey, string innerKey, IEnumerable<string> outputFields)
{
_outputFields = outputFields.ToList();
// Check for properties with the same name
string badProps = string.Join(", ", _outputFields.Select(f => new { property = f, name = GetName(f) })
.GroupBy(f => f.name, StringComparer.OrdinalIgnoreCase)
.Where(g => g.Count() > 1)
.SelectMany(g => g.OrderBy(f => f.name, StringComparer.OrdinalIgnoreCase).Select(f => f.property)));
if (!string.IsNullOrEmpty(badProps))
throw new ArgumentException($"One or more {nameof(outputFields)} are duplicated: {badProps}");
_keyOuter = _typeOuter.GetProperty(outerKey);
_keyInner = _typeInner.GetProperty(innerKey);
// Check for valid keys
if (_keyOuter == null || _keyInner == null)
throw new ArgumentException($"One or both of the specified keys is not a valid property");
// Check type compatibility
if (_keyOuter.PropertyType != _keyInner.PropertyType)
throw new ArgumentException($"Keys must be the same type. ({nameof(outerKey)} type: {_keyOuter.PropertyType.Name}, {nameof(innerKey)} type: {_keyInner.PropertyType.Name})");
Func<string, Type, IEnumerable<KeyValuePair<string, PropertyInfo>>> getResultProperties = (prefix, type) =>
_outputFields.Where(f => f.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(f => new KeyValuePair<string, PropertyInfo>(f, type.GetProperty(f.Substring(prefix.Length))));
// Combine inner/outer outputFields with PropertyInfo into a dictionary
_resultProperties = getResultProperties(OuterPrefix, _typeOuter).Concat(getResultProperties(InnerPrefix, _typeInner))
.ToDictionary(k => k.Key, v => v.Value, StringComparer.OrdinalIgnoreCase);
// Check for properties that aren't found
badProps = string.Join(", ", _resultProperties.Where(kv => kv.Value == null).Select(kv => kv.Key));
if (!string.IsNullOrEmpty(badProps))
throw new ArgumentException($"One or more {nameof(outputFields)} are not valid: {badProps}");
// Check for properties that aren't the right format
badProps = string.Join(", ", _outputFields.Where(f => !_resultProperties.ContainsKey(f)));
if (!string.IsNullOrEmpty(badProps))
throw new ArgumentException($"One or more {nameof(outputFields)} are not valid: {badProps}");
}
// Inner Join
public IEnumerable<dynamic> Join(IEnumerable<TOuter> outer, IEnumerable<TInner> inner) =>
outer.Join(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i));
// Left Outer Join
public IEnumerable<dynamic> LeftJoin(IEnumerable<TOuter> outer, IEnumerable<TInner> inner) =>
outer.LeftJoin(inner, o => GetOuterKeyValue(o), i => GetInnerKeyValue(i), (o, i) => CreateItem(o, i));
private static string GetName(string fieldId) => fieldId.Substring(fieldId.IndexOf('.') + 1);
private object GetOuterKeyValue(TOuter obj) => _keyOuter.GetValue(obj);
private object GetInnerKeyValue(TInner obj) => _keyInner.GetValue(obj);
private object GetResultProperyValue(string key, object obj) => _resultProperties[key].GetValue(obj);
private dynamic CreateItem(TOuter o, TInner i)
{
var obj = new ExpandoObject();
var dict = (IDictionary<string, object>)obj;
_outputFields.ForEach(f =>
{
var source = f.StartsWith(OuterPrefix, StringComparison.OrdinalIgnoreCase) ? (object)o : i;
dict.Add(GetName(f), source == null ? null : GetResultProperyValue(f, source));
});
return obj;
}
}
public static IEnumerable<dynamic> DynamicJoin<TOuter, TInner>(this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner, string outerKey, string innerKey,
params string[] outputFields) =>
new Processor<TOuter, TInner>(outerKey, innerKey, outputFields).Join(outer, inner);
public static IEnumerable<dynamic> DynamicLeftJoin<TOuter, TInner>(this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner, string outerKey, string innerKey,
params string[] outputFields) =>
new Processor<TOuter, TInner>(outerKey, innerKey, outputFields).LeftJoin(outer, inner);
}