首页 > 解决方案 > 如何替换 Queryable 中的列以通用方式不执行 Queryable

问题描述

例如,我有一个 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() 工作(这将为我们翻译该表达式)

到目前为止的想法:

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

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

  3. 通过构建查询并将它们作为参数发送到 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 的查询构建字典。

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

谢谢

标签: c#sqlentity-frameworklinqasp.net-core

解决方案


我不确定我的问题陈述是否正确,而且我认为我在评论中的 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]

如您所见,这种方式可能更容易直接ProductViewModel在 select 语句中创建。


推荐阅读