Reusing Lambda Select Fragments in LINQ

c# entity-framework-core lambda linq

Question

I would like to be able to reuse fragments of my select lambda expressions in my Entity Framework Core 2.0 queries.

For example:

var result = await ctx.Customers
  .Select(cust => new CustomerDto {
    CustomerId = cust.Id,
    CustomerName = cust.Name,
    CurrentValue = cust.Orders
      .Where(order => order.OrderDate >= DateTime.Now.AddDays(-30)
      .Sum(order => order.TotalValue)
    })
    .ToListAsync();

Since I might want to calculate the CurrentValue property in other queries (in practice the sub-query is more complex than this), I would ideally like to refactor the above code to something like:

var result = await ctx.Customers
  .Select(cust => new CustomerDto {
    CustomerId = cust.Id,
    CustomerName = cust.Name,
    CurrentValue = CalculateCustomerCurrentValueExpr(cust)
  })
  .ToListAsync();

I have created Linq predicates using a Linq.Expression, but I have been unable to find a way to use an Expression as an element of the select statement.

Any help would be much appreciated.

Update - Performance with .AsExpandable()/.Invoke()

For anyone interested, I ran some test code ten times which produced the following result:

Standard Inline Code: 17ms (58,609 ticks) With .AsExpandable() and inline code 16ms (58,029 ticks) With .AsExpandable() and .Invoke() 16ms (58,224 ticks)

I suspect that if more test cycles had been run, the average processing time for all three scenarios would have been the same - at least with the level of accuracy I could measure at (simple StopWatch()).

Thanks to all contributors, particularly SergeyA for the solution and Ivan Stoev for the simple explanation of .AsExpandable()

1
3
2/2/2018 8:59:28 AM

Accepted Answer

You can reuse expressions with AsExpandable extension from LinqKit liblary (http://www.albahari.com/nutshell/linqkit.aspx).

Example:

Expression<Func<Customer,long>> func = c => c.Orders
  .Where(order => order.OrderDate >= DateTime.Now.AddDays(-30)
  .Sum(order => order.TotalValue);

var result = await ctx.Customers
  .AsExpandable() // this allow to unwrap injected expression
  .Select(cust => new CustomerDto {
    CustomerId = cust.Id,
    CustomerName = cust.Name,
    CurrentValue = func.Invoke(cust) // this inject predefined expression
  })
  .ToListAsync(); 
4
2/1/2018 5:27:37 PM

Popular Answer

I store my expressions in a static file and reuse the expressions where I need them to ensure to include all related data. Maybe this can work for you as well

In GetStore() I reuse an expression called ClientAccess and passes that to the expression at ShopExpressions.

GetPage() uses a simple straight forward implementation.

ShopExpressions.cs:

public static IQueryable<IStore> StoreLite(IQueryable<IStore> dbSet)
{
    var result = dbSet
        .Include(str => str.VATs)
            .ThenInclude(vat => vat.VAT)
                .ThenInclude(vat => vat.Culture)
                    .ThenInclude(cult => cult.Items)
                        .ThenInclude(itm => itm.Culture)
        .Include(str => str.Options)
            .ThenInclude(opt => opt.Items)
                .ThenInclude(itm => itm.Option)
        .Include(str => str.Cultures)
            .ThenInclude(cult => cult.Items)
                .ThenInclude(itm => itm.Culture)
                    .ThenInclude(cult => cult.Items)
                        .ThenInclude(itm => itm.Culture)
        .Include(str => str.Pages)
            .ThenInclude(page => page.Sections)
                .ThenInclude(section => section.Elements);

    return result;
}

public static IQueryable<IStore> Store(IQueryable<IStore> dbSet)
{
    var result = StoreLite(dbSet)
        .Include(str => str.Categorys)
            .ThenInclude(cat => cat.Products)
                .ThenInclude(prd => prd.InfoItems)
                    .ThenInclude(itm => itm.Culture)
                        .ThenInclude(cult => cult.Items)
                            .ThenInclude(itm => itm.Culture);

    return result;
}

public static IQueryable<IPage> Page(IQueryable<IPage> dbSet)
{
    var result = dbSet
        .Include(page => page.Sections)
            .ThenInclude(sec => sec.Elements)
        .Include(page => page.CSS)
        .Include(page => page.Script)
        .Include(page => page.Meta);

    return result;
}

Controller.cs:

[HttpGet]
public async Task<IStore> GetStore(int id)
{
    IStore result = await ShopExpressions.Store(GenericExpressions.ClientAccess(this.Worker.GetRepo<Store>().DbSet))
        .SingleAsync(str => str.Id.Equals(id));

    this.Worker.ValidateClientAccess(result);

    return result;
}

[HttpGet]
public async Task<IStore> GetStoreLite(int id)
{
    IStore result = await ShopExpressions.StoreLite(GenericExpressions.ClientAccess(this.Worker.GetRepo<Store>().DbSet))
        .SingleAsync(str => str.Id.Equals(id));

    this.Worker.ValidateClientAccess(result);

    return result;
}

[HttpGet]
public async Task<IPage> GetPage(int id)
{
    IPage result = await ShopExpressions.Page(this.Worker.GetRepo<Page>().DbSet)
        .SingleAsync(page => page.Id.Equals(id));

    return result;
}


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow