如何在层间对象转换Assembler接口中实现Flatten Expression Tree?
实现Assembler接口中的扁平化表达式树(针对Book/Publisher场景)
搞定这个扁平化表达式树的需求其实不难,我刚好做过类似的对象转换Assembler实现,结合你提到的Book/Publisher场景,给你梳理下具体的实现思路和代码示例:
1. 先理清楚核心需求
咱们的核心痛点是:上层的BookAssembler调用下层PublisherAssembler的GetIncludes方法时,没法提前知道它会返回0个、1个还是N个需要加载的表达式。我们要做的就是把这些嵌套在Publisher层级的表达式,"拉平"到Book层级,和BookAssembler自身的includes合并成一个扁平的集合,方便后续ORM框架直接解析执行。
2. 定义统一的Assembler接口
首先得给所有Assembler定个标准接口,确保GetIncludes方法的返回格式一致:
public interface IAssembler<TSource, TDestination> { // 返回需要从源对象加载的字段/关联对象的表达式树集合 IEnumerable<Expression<Func<TSource, object>>> GetIncludes(); // 核心的对象转换方法 TDestination Assemble(TSource source); }
举个PublisherAssembler的实现例子,它可能返回任意数量的includes:
public class PublisherAssembler : IAssembler<Publisher, PublisherDto> { public IEnumerable<Expression<Func<Publisher, object>>> GetIncludes() { // 这里可能返回0个、1个或多个表达式,比如加载Publisher的地址和关联书籍 yield return p => p.Address; yield return p => p.Books; } public PublisherDto Assemble(Publisher source) { return new PublisherDto { Id = source.Id, Name = source.Name, Address = source.Address?.ToString() }; } }
3. 核心的扁平化实现逻辑
接下来是BookAssembler的关键部分——把PublisherAssembler返回的表达式树,从Publisher层级转换到Book层级:
public class BookAssembler : IAssembler<Book, BookDto> { private readonly IAssembler<Publisher, PublisherDto> _publisherAssembler; public BookAssembler(IAssembler<Publisher, PublisherDto> publisherAssembler) { _publisherAssembler = publisherAssembler; } public IEnumerable<Expression<Func<Book, object>>> GetIncludes() { // 先返回Book自身需要加载的内容,比如作者 yield return b => b.Author; // 遍历PublisherAssembler的includes,逐个扁平化处理 foreach (var publisherInclude in _publisherAssembler.GetIncludes()) { // 构建参数:Book类型的变量b var bookParam = Expression.Parameter(typeof(Book), "b"); // 获取Book的Publisher属性 var publisherProp = Expression.Property(bookParam, nameof(Book.Publisher)); // 把publisherInclude的表达式(p => p.X)转换成b => b.Publisher.X var flattenedBody = Expression.Invoke(publisherInclude, publisherProp); var flattenedExpr = Expression.Lambda<Func<Book, object>>(flattenedBody, bookParam); yield return flattenedExpr; } } public BookDto Assemble(Book source) { return new BookDto { Id = source.Id, Title = source.Title, Publisher = _publisherAssembler.Assemble(source.Publisher) }; } }
4. 优化:封装通用扩展方法
为了避免每个Assembler都重复写扁平化的逻辑,咱们可以把这段逻辑封装成一个通用扩展方法,复用性更强:
public static class AssemblerExtensions { public static IEnumerable<Expression<Func<TSource, object>>> FlattenIncludes<TSource, TChildSource>( this IEnumerable<Expression<Func<TChildSource, object>>> childIncludes, Expression<Func<TSource, TChildSource>> childPropertySelector) { var sourceParam = Expression.Parameter(typeof(TSource), "source"); var childProperty = Expression.Invoke(childPropertySelector, sourceParam); foreach (var childInclude in childIncludes) { var invokedBody = Expression.Invoke(childInclude, childProperty); yield return Expression.Lambda<Func<TSource, object>>(invokedBody, sourceParam); } } }
这样BookAssembler里的代码就可以简化成:
public IEnumerable<Expression<Func<Book, object>>> GetIncludes() { yield return b => b.Author; // 直接调用扩展方法完成扁平化 foreach (var flattenedInclude in _publisherAssembler.GetIncludes().FlattenIncludes(b => b.Publisher)) { yield return flattenedInclude; } }
这个方案完美适配你说的0~N个includes的场景:如果PublisherAssembler返回空列表,循环直接跳过,不会影响BookAssembler自身的includes;如果返回多个,全部都会被扁平化成Book层级的表达式。
内容的提问来源于stack exchange,提问作者Thomas Lee
相关产品推荐
相关产品推荐

