如何在不执行 Queryable 的情况下以通用方式替换 Queryable<T> 中的列

Posted

技术标签:

【中文标题】如何在不执行 Queryable 的情况下以通用方式替换 Queryable<T> 中的列【英文标题】:How to replace columns in a Queryable<T> in a generic fashion without executing the Queryable 【发布时间】:2020-07-01 19:15:28 【问题描述】:

例如,我有一个 Product 实体、一个 ProductViewModel 和一个 Label 实体。我的两个产品属性对应于标签代码而不是实际值。因此,例如,产品名称是“code1234”,它对应于具有“code1234”作为代码和“牛奶”作为值的标签。标签不作为外键加入。我们正在使用 AutoMapper 进行投影。

public class Product
    public int ProductId get; set;
    public string Name get; set;
    public string Description get; set;


public class ProductViewModel
    public int ProductId get; set;
    public string Name get; set;
    public string Description get; set;


public class Label
    public int LabelId get; set;
    public string Code get; set;
    public string Value get; set;
    public int LanguageId get; set

我正在寻找替代方法

var currentLanguageId = 1; //As an example
IQueryable<Product> queryFromDb;

var Products = queryFromDb
    .ProjectTo<ProductViewModel>().AsEnumerable();

foreach(var Product in Products) 
    Product.Name = db.Labels.Where(x => x.Code == Product.Name && x.LanguageId == currentLanguageId).Single().Value;
    Product.Description = db.Labels.Where(x => x.Code == Product.Description && x.LanguageId == currentLanguageId).Single().Value;

使用将延迟查询的代码,因为过滤和排序是在与产品名称和描述相对应的标签值上完成的,而不是在名称和描述本身上进行的,它们是没有意义的代码。

然后我还需要一种方法来使整个事情通用,因为我们数据库中的许多实体都有标签代码的属性。

到目前为止我所拥有的:

var result = scope.GetRepository<Product>().GetAll() //returns Queryable<Product>
                .ProjectTo<ProductViewModel>(_mapper.ConfigurationProvider) //returns Queryable<ProductViewModel>
                .WithLabels(_mapper, scope, x => x.Name, x => x.Description) //returns Queryable with columns replaced with a query
                .ToDataResult(request); //sorts filters takes skips, etc.
public static IQueryable<T> WithLabels<T>(this IQueryable<T> instance,
    IMapper mapper,
    IBaseReadContextScope scope,
    params Expression<Func<T, string>>[] expressions) where T : class

    var currentLanguage = 1; //as an example
    var labels = scope.GetRepository<Label>().GetAll(x => x.Language == currentLanguageId);
    foreach (var expression in expressions)
    
        var query = instance
                .GroupJoin(
                    labels,
                    expression,
                    label => label.Code,
                    (x, y) => new  Obj = x, Label = y )
                .SelectMany(
                    xy => xy.Label.DefaultIfEmpty(),
                    (x, y) => new  Obj = x.Obj, Label = y )
                .Select(s => new ObjectWithLabel<T>()
                
                    Object = s.Obj,
                    Label = s.Label
                );
        instance = mapper.ProjectTo<T>(query, new  propertyName = ExpressionUtilities.PropertyName(expression) );
    

    return instance;

CreateMap<ObjectWithLabel<ProductViewModel>, ProductViewModel>()
    .ForMember(x => x.Name, m =>
    
        m.Condition(x => propertyName == nameof(ProductViewModel.Name));
        m.MapFrom(x => x.Label.Value);
    )
    .ForMember(x => x.Name, m =>
    
        m.Condition(x => propertyName != nameof(ProductViewModel.Name));
        m.MapFrom(x => x.Object.Name);
    )
    .ForMember(x => x.Description, m =>
    
        m.Condition(x => propertyName == nameof(ProductViewModel.Description));
        m.MapFrom(x => x.Label.Value);
    )
    .ForMember(x => x.Description, m =>
    
        m.Condition(x => propertyName != nameof(ProductViewModel.Description));
        m.MapFrom(x => x.Object.Description);
    );

这实际上适用于单个属性,但不适用于多个属性。最重要的是,必须从 ProductViewModel 和 ObjectWithLabel 来回投影并不是很好。我使用 .Condition 而不是简单的三元运算符的原因是 ProjectTo 不支持表达式,我无法让 UseAsDataSource() 工作(这将为我们翻译该表达式)

目前的想法:

    使用查询拦截器。使用 AutoMapper,我们将构建一个类似于 INTERCEPTME_ColumnName_Code_Language 的字符串,并用查询替换该字符串(用 LINQ 编写或以 SQL 函数的形式)。这似乎可行,但缺点是不适用于 NOSQL 的单元测试(可能),需要检查每个传入的查询(除非有办法将查询标记为“可拦截”。

    使用类似于我当前的 WithLabels 方法的方法,但构建一个包含多个列的单个联接,然后从 ObjectWithLabels 仅投影一次到 ProductViewModel。 (不知道在未知数量的列上的通用连接会是什么样子)

    通过构建查询并将它们作为参数发送到 AutoMapper,找到一种无需使用中间对象/投影即可直接替换列的方法,快速说明基本思想:

Dictionary<string, IQueryable<string>> dictionary = null;

CreateMap<Product, ProductViewModel>()
    .ForMember(x => x.Name, m => m.MapFrom(x => 
        dictionary["Name"]))
    .ForMember(x => x.Description, m => m.MapFrom(x => 
        dictionary["Description"])));

将使用返回与所需代码和语言对应的 Label.Value 的查询构建字典。

我将不胜感激有关基本问题的任何输入,即在不执行该查询的情况下替换查询中的对象列,以及我提到的任何潜在解决方案。

谢谢

【问题讨论】:

太多了!无论您在使用 FK-s 时遇到什么问题,这肯定会更糟,对吧? :) 您是否考虑过构建自定义LINQ Expressions?我觉得适合 【参考方案1】:

我不确定我的问题陈述是否正确,而且我认为我在 cmets 中的 LINQ 方向完全错误。

您似乎正在尝试从数据库中连接两个表(并在其中一个上应用 LanguageId 过滤器)。 我相信使用 EF.Core 3(我假设您使用最新版本)您不需要定义 FK 来连接表:

var Products = db.Products.Join(
                    db.Labels.Where(l => l.LanguageId == 1), product => product.Name,
                    label => label.Code,
                    (p, l) => new p, l)
                .Join(db.Labels.Where(l => l.LanguageId == 1), p => p.p.Description, l => l.Code,
                    (pp, l) => new ProductViewModel  Name = pp.l.Value, ProductId = pp.p.ProductId, Description = l.Value);

产生以下 SQL:

SELECT [t].[Value] AS [Name], [p].[ProductId], [t0].[Value] AS [Description]
FROM [Products] AS [p]
INNER JOIN (
    SELECT [l].[LabelId], [l].[Code], [l].[LanguageId], [l].[Value]
    FROM [Labels] AS [l]
    WHERE [l].[LanguageId] = 1
) AS [t] ON [p].[Name] = [t].[Code]
INNER JOIN (
    SELECT [l0].[LabelId], [l0].[Code], [l0].[LanguageId], [l0].[Value]
    FROM [Labels] AS [l0]
    WHERE [l0].[LanguageId] = 1
) AS [t0] ON [p].[Description] = [t0].[Code]

如您所见,这样在 select 语句中创建 ProductViewModel 可能会更容易。

【讨论】:

这种方法可以让我绑定多列吗?因为 Name 和 Description 都对应于标签。 它变得有点复杂,但肯定是可行的。查看更新的答案【参考方案2】:

解决方案是在调用映射器时检索标签存储库并将其作为参数传递,并直接在自动映射器配置文件中构建标签查询。

用法:

var labels = scope.GetRepository<Label>().GetAll().Where(x => x.LanguageId == 1)
var result = scope.GetRepository<Product>().GetAll() //returns Queryable<Product>
                .ProjectTo<ProductViewModel>(_mapper.ConfigurationProvider, new labels)
                .Orderby(x => x.Description)
                .Take(10);

映射配置文件:

IQueryable<Label> labels = null;
CreateMap<Product, ProductViewModel>()
    .ForMember(x => x.Name, m =>
    
        m.MapFrom(x => (from label in labels
                where label.Code == x.Name
                select label.Value).First());
    )
    .ForMember(x => x.Description, m =>
    
        m.MapFrom(x => (from label in labels
                where label.Code == x.Description
                select label.Value).First());
    );

MapFrom 可以放入 MapFromLabel 方法中,这样可以避免重复代码。

【讨论】:

【参考方案3】:

正如我所见,您通过 ProjectTo 运算符直接在数据访问层中使用 AutoMapper。

直接返回所需模型的另一种通用方法是,

 var labelQuery = db.Labels.Where(x => x.LanguageId == 1);// 1 is passed parameter.
            var Products = db.Products.Select(x => new ProductViewModel()
            
                ProductId = x.ProductId,
                Name = labelQuery.FirstOrDefault(l => l.Code == x.Name).Name,
                Description = labelQuery.FirstOrDefault(l => l.Code== x.Description).Value
          ).ToList();

希望它能解决问题

【讨论】:

以上是关于如何在不执行 Queryable 的情况下以通用方式替换 Queryable<T> 中的列的主要内容,如果未能解决你的问题,请参考以下文章

如何在不使用 UINavigationController 的情况下以编程方式进入 rootViewController

如何在不使用 presentViewController 的情况下以模态方式呈现自定义视图?

如何在不单击“下一步”按钮的情况下以重力形式进入下一页

如何在不编码窗口状态的情况下以编程方式终止 NSApp?

如何在不“连接”它们的情况下以编程方式访问 NIB 中的 UI 元素?

如何在不使用外部主机的情况下以编程方式查找设备的外部 IP 地址?