[英]What is the correct way to use computed properties with Dynamic LINQ?
I have these models.我有这些模型。
public class Ticket
{
public int Id {get;set;}
public List<History> History {get;set;}
}
public class History
{
public Employee Employee {get;set;}
public string Comments {get;set;}
public DateTime Timestamp {get;set;}
}
Employee
is just a class with an int Id
and string Name
. Employee
只是一个带有int Id
和string Name
的 class 。
What I had in mind is that I needed to allow the users to query Tickets
by a computed property in Tickets
, such as我的想法是,我需要允许用户通过Tickets
中的计算属性查询Tickets
,例如
public Employee LatestEmployee
{
get => History.MaxBy(x=>x.Timestamp).Employee;
}
From the user, I get the name of the queried property/column, the search term and whether the user wants it to contain , start with or be equal to , and I'm using dynamic LINQ, so I can concatenate multiple queries as the user applies further filters.从用户那里,我得到了查询属性/列的名称、搜索词以及用户是否希望它包含、开头或等于用户应用进一步的过滤器。 Basically I get a JSON like this:基本上我得到一个像这样的 JSON :
{
"column":"Employee",
"query":"Batman",
"type":"equals"
}
Which is converted to a Dynamic LINQ "Employee == Batman"
.将其转换为动态 LINQ "Employee == Batman"
。
But on the database itself, it still creates an EmployeeId column with a foreign key, so I need to fill it with a random (existing, because of the FK) value.但是在数据库本身上,它仍然创建一个带有外键的 EmployeeId 列,所以我需要用一个随机(存在,因为 FK)值来填充它。
I, then, query it as然后,我将其查询为
var filteredListQuery =
_context.Tickets.Include(x=>x.History)
.ThenInclude(y=>y.Employee)
.Where("Employee.Name == \"Batman\"");
Following that, I must return a JSON with information on the tickets to display.之后,我必须返回一个 JSON,其中包含要显示的门票信息。 So I try所以我尝试
var filteredListQueryDTO = filteredListQuery
.Select(x=>new TicketDTO()
{
Id = x.Id.ToString(),
Employee = x.Employee.Name,
Timestamp = x.Timestamp.ToString("g")
}).ToListAsync();
But then it reads the employee saved on the unnecesary column, rather than using the computed property.但随后它会读取保存在不必要列中的员工,而不是使用计算属性。 And if I try Employee = x.History.MaxBy(x=>x.Timestamp).Employee.Name
I get a LINQ expression could not be translated
.如果我尝试Employee = x.History.MaxBy(x=>x.Timestamp).Employee.Name
我得到一个LINQ expression could not be translated
。
It does work, though, if I, as per the linked KB article on the exception, first call ToList()
on the filteredQueryList()
:但是,如果我按照有关异常的链接 KB 文章,它确实有效,首先在filteredQueryList()
的QueryList() 上调用ToList()
) :
var filteredListQueryDTO = (await filteredListQuery.ToListAsync())
.Select(x=>new TicketDTO()
{
Id = x.Id.ToString(),
Employee = x.Employee.Name,
Timestamp = x.Timestamp.ToString("g")
}).ToList();
Or if I iterate through filteredListQuery with a foreach loop (which I think is pretty much the same).或者,如果我使用 foreach 循环(我认为几乎相同)遍历过滤列表查询。
Is there an easier (less convoluted and more professional) way to do this (and without the extra unused column on the database)?有没有更简单(不那么复杂和更专业)的方法来做到这一点(并且没有数据库上额外未使用的列)?
Basically, instead of "dynamic", you will use a parameters object, and optionally "if" the parameters are populated, you will add in optional parts of your IQueryable
.基本上,您将使用参数对象而不是“动态”,并且可以选择“如果”填充参数,您将添加IQueryable
的可选部分。
public class OrganizationSearchArgs
{
public string OrganizationNameNotLike { get; set; } = null;
public ICollection<int> OrgStatuses { get; set; } = new List<int>();
public ICollection<string> OrganizationNamesIn { get; set; } = new List<string>();
public ICollection<string> OrganizationNamesNotIn { get; set; } = new List<string>();
}
(below would be a class that exposes functionality on your dbcontext) (下面将是一个在您的 dbcontext 上公开功能的类)
public async Task<int> GetListByArgsCount(OrganizationSearchArgs args, CancellationToken token)
{
IQueryable<OrganizationEntity> qry = this.GetGetListByArgsBaseIQueryable(args);
int returnValue = await qry.CountAsync(token);
return returnValue;
}
public async Task<IEnumerable<OrganizationEntity>> GetListByArgs(
OrganizationSearchArgs args,
CancellationToken token)
{
IQueryable<OrganizationEntity> qry = this.GetGetListByArgsBaseIQueryable(args);
List<OrganizationEntity> entities = await qry.AsNoTracking().ToListAsync(token);
return entities;
}
private IQueryable<OrganizationEntity> GetGetListByArgsBaseIQueryable(OrganizationSearchArgs args)
{
IQueryable<OrganizationEntity> qry = this.entityDbContext.Organizations
.Include(org => org.SomeNavigationProperty).AsNoTracking();
if (!string.IsNullOrEmpty(args.OrganizationNameNotLike))
{
qry = qry.Where(o => !o.OrganizationName.Contains(args.OrganizationNameNotLike));
}
if (null != args.OrgStatuses && args.OrgStatuses.Any())
{
qry = qry.Where(org => args.OrgStatuses.Contains(org.OrgStatus));
}
if (null != args.OrganizationNamesIn && args.OrganizationNamesIn.Any())
{
qry = qry.Where(org => args.OrganizationNamesIn.Contains(org.OrganizationName));
}
if (null != args.OrganizationNamesNotIn && args.OrganizationNamesNotIn.Any())
{
qry = qry.Where(org => !args.OrganizationNamesNotIn.Contains(org.OrganizationName));
}
return qry;
}
}
I've found it best to make my actual Entity models reflect the database structure as closely as possible, and largely use them for CRUD operations.我发现最好让我的实际实体模型尽可能地反映数据库结构,并在很大程度上将它们用于 CRUD 操作。 You can create a separate class to represent other queryable facets of your entity, but instead of using computed properties, use mapping logic to create a projection for those properties.您可以创建一个单独的类来表示实体的其他可查询方面,但不要使用计算属性,而是使用映射逻辑为这些属性创建投影。
public class TicketSummary
{
public int TicketId {get;set;}
public Employee LatestEmployee {get;set;}
}
public IQueryable<TicketSummary> BuildSummaryQuery()
{
return _context.Tickets.Select(t => new TicketSummary
{
TicketId = t.Id,
LatestEmployee = t.History.MaxBy(x=>x.Timestamp).Employee
});
}
Then you can apply your query criteria as appropriate.然后,您可以根据需要应用您的查询条件。 Using your dynamic LINQ example above, for example:使用上面的动态 LINQ 示例,例如:
var filteredListQuery = BuildSummaryQuery()
.Where("LatestEmployee.Name == \"Batman\"");
Without Dynamic LINQ Library:不带动态 LINQ 库:
public static class ExpressionBuilder
{
public static Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, string comparison, object value)
{
if (propertyName == null) return t => true;
var parameter = Expression.Parameter(typeof(T));
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.PropertyOrField);
var body = MakeComparison(left, comparison, value);
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
static Expression MakeComparison(Expression left, string comparison, object value)
{
var constant = Expression.Constant(value, left.Type);
switch (comparison)
{
case "==":
return Expression.MakeBinary(ExpressionType.Equal, left, constant);
case "!=":
return Expression.MakeBinary(ExpressionType.NotEqual, left, constant);
case ">":
return Expression.MakeBinary(ExpressionType.GreaterThan, left, constant);
case ">=":
return Expression.MakeBinary(ExpressionType.GreaterThanOrEqual, left, constant);
case "<":
return Expression.MakeBinary(ExpressionType.LessThan, left, constant);
case "<=":
return Expression.MakeBinary(ExpressionType.LessThanOrEqual, left, constant);
case "Contains":
case "StartsWith":
case "EndsWith":
if (value is string)
{
return Expression.Call(left, comparison, Type.EmptyTypes, constant);
}
throw new NotSupportedException($"Comparison operator '{comparison}' only supported on string.");
default:
throw new NotSupportedException($"Invalid comparison operator '{comparison}'.");
}
}
}
usage:用法:
var data = querable.Where(ExpressionBuilder.BuildPredicate("Name", "==", "Batman"));
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.