如何在不执行 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 的情况下以模态方式呈现自定义视图?