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.
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()
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();
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;
}