0

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?

Mark G
  • 2,848
  • 1
  • 24
  • 32
  • 1
    I don't think this is feasible, you most likely need to use third party tools like AutoMapper to have conditional mapping. – hallie Mar 11 '20 at 00:25
  • @hallie Do you know of any examples? A similar question was asked at [How to conditionally ignore mapping for a member](https://github.com/AutoMapper/AutoMapper/issues/3258) w/o an answer. – Mark G Mar 11 '20 at 00:40
  • http://docs.automapper.org/en/latest/Queryable-Extensions.html#explicit-expansion – Lucian Bargaoanu Mar 11 '20 at 04:30
  • @LucianBargaoanu Thanks for the docs on explicit expansion for AutoMapper. That seems similar to doing another projection like `_ctx.Customer.Select(CustomerView(User.IsInRole("Editor"))) .Select(c => new { c.Id, c.Name, c.OrderCount });` for this example. – Mark G Mar 11 '20 at 16:49
  • It's more flexible, because you can decide at runtime what to fetch. But yes, similar :) – Lucian Bargaoanu Mar 11 '20 at 17:16
  • You might be able to use the `.Concat` LINQ method, which translates to a `UNION ALL`; in the first sequence include the fields, while in the second sequence don't include the fields. – Zev Spitz Mar 11 '20 at 17:34

1 Answers1

0

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.

Mark G
  • 2,848
  • 1
  • 24
  • 32