linqentity-framework-coreexpression-treesdynamic-linqlinqkit

Conditionally ignore fields with LINQ select


I've been unable to find any examples where you can conditionally exclude fields based off a variable within a select projection in LINQ, see also LINQ: Select an object and change some properties without creating a new object.

Let me give some background on what I'm trying to achieve. I want to limit some fields in the DTO being set from the model based on if a user can edit data (i.e. a comment field). For example, the following select with a delegate named CustomerView.

var qry = _ctx.Customer.Select(CustomerView(User.IsInRole("Editor")));

The Customer model has an Orders navigation property and the following function transforms the data into the CusomerViewModel DTO.

private Expression<Func<Customer, CustomerViewModel>> CustomerView(bool isEditor) {
    return c => new CustomerViewModel
    {
        Id = c.Id,
        Name = c.Name,
        Comment = isEditor ? c.Comment : null,
        OrderCount = c.Orders.Count()
    };
}

This will generate SQL like CASE WHEN @__isEditor_0 = TRUE THEN Comment ELSE NULL which works, but I'd prefer the expression not even be generated, i.e. field left as it's default. That is a simple use case, but if I wanted to do the same with the OrderCount field a SQL subquery would still get included.

Of course I could create a another function for non-editor users that excludes certain fields, but I'd rather not have separate projections to maintain especially when they are more complex.

I see questions where dynamic LINQ is used for where clauses but not that many for select. Is this approach feasible?

Edit: Is there anyway to manually remove fields from an expression tree after a select has been used, maybe through an extension method?


Solution

  • Using LINQKit I was able to achieve the desired result by adding AsExpandable() to select.

    var qry = _ctx.Customer.AsExpandable().Select(CustomerView(User.IsInRole("Editor")));

    Then adding an expression for comment field and calling Invoke() on the field assignment.

    private Expression<Func<Customer, CustomerViewModel>> CustomerView(bool isEditor) {
        Expression<Func<Customer, string>> exprComment;
        if (isEditor)
            exprComment = c => c.Comment;
        else
            exprComment = c => null;
    
        return c => new CustomerViewModel
        {
            Id = c.Id,
            Name = c.Name,
            Comment = exprComment.Invoke(c),
            OrderCount = c.Orders.Count()
        };
    }
    

    It does appear to work, but I would still be interested to hear of any alternative approaches.