简体   繁体   English

将计算属性与动态 LINQ 一起使用的正确方法是什么?

[英]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 Idstring 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM