Skip to content

Instantly share code, notes, and snippets.

@lasododo
Last active October 13, 2024 13:02
Show Gist options
  • Save lasododo/a2c039171da0ff10e4008a144e636f4f to your computer and use it in GitHub Desktop.
Save lasododo/a2c039171da0ff10e4008a144e636f4f to your computer and use it in GitHub Desktop.
Example usage of Expression Trees with EF Core Query Object
using DAL;
using DAL.Models;
using Infrastructure.EFCore.ExpressionHelpers;
using Infrastructure.EFCore.UnitOfWork;
using Infrastructure.Query;
using Infrastructure.UnitOfWork;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Infrastructure.EFCore.Query
{
public class EntityFrameworkQuery<TEntity> : Query<TEntity> where TEntity : class, IEntity, new()
{
protected SchoolDBContext Dbcontext { get; set; }
private EFUnitOfWork _unitOfWork;
protected EFUnitOfWork UnitOfWork
{
get
{
if (_unitOfWork != null)
{
_unitOfWork = new(Dbcontext);
}
return _unitOfWork;
}
}
public EntityFrameworkQuery(SchoolDBContext dbcontext)
{
Dbcontext = dbcontext;
}
public override IEnumerable<TEntity> Execute()
{
IQueryable<TEntity> query = Dbcontext.Set<TEntity>();
if (WherePredicate.Capacity != 0)
{
query = ApplyWhere(query);
}
if (OrderByContainer != null)
{
query = OrderBy(query);
}
if (PaginationContainer.HasValue)
{
query = Pagination(query);
}
return query.ToList();
}
private IQueryable<TEntity> ApplyWhere(IQueryable<TEntity> query)
{
foreach (var expr in WherePredicate)
{
// parameter for new lambda
// note: if we wanted to combine expressions, we would need to use the EXACT SAME PARAMETER for all of them.
var p = Expression.Parameter(typeof(TEntity), "p");
// get the property name from the etity. For instance, its the same as calling `nameof(Subject.Name)`
var columnNameFromObject = typeof(TEntity)
.GetProperty(expr.columnName)
?.Name;
// basically creates the property call, i.e -> p.Name
var exprProp = Expression.Property(p, columnNameFromObject);// nameof(Customer.CustomerID));
// replace parameter in original expression
var expression = expr.expression;
// gets the Expression Parameters
var parameters = (IReadOnlyCollection<ParameterExpression>)expression
.GetType()
.GetProperty("Parameters")
?.GetValue(expression);
// gets the expression body
var body = (Expression)expression
.GetType()
.GetProperty("Body")
?.GetValue(expression);
// (both) replaces the old lambda parameter with the new one
// Example:
// a => a > 10
// -> p => p.Price > 10
var visitor = new ReplaceParamVisitor(parameters.First(), exprProp);
var exprNewBody = visitor.Visit(body);
// creates the new lambda expression
var lambda = Expression.Lambda<Func<TEntity, bool>>(exprNewBody, p);
query = query.Where(lambda);
}
return query;
}
private IQueryable<TEntity> OrderBy(IQueryable<TEntity> query)
{
var orderByColumn = OrderByContainer.Value.tableName;
var isAscending = OrderByContainer.Value.isAscending;
var argumentType = OrderByContainer.Value.argumentType;
var p = Expression.Parameter(typeof(TEntity), "p");
var columnNameFromObject = typeof(TEntity)
.GetProperty(orderByColumn)
?.Name;
var exprProp = Expression.Property(p, columnNameFromObject);
var lambda = Expression.Lambda(exprProp, p);
var orderByMethod = typeof(Queryable)
.GetMethods()
.First(a => a.Name == (isAscending ? "OrderBy" : "OrderByDescending") && a.GetParameters().Length == 2);
var orderByClosedMethod = orderByMethod.MakeGenericMethod(typeof(TEntity), argumentType);
return (IQueryable<TEntity>)orderByClosedMethod.Invoke(null, new object[] { query, lambda })!;
}
private IQueryable<TEntity> Pagination(IQueryable<TEntity> query)
{
var page = PaginationContainer.Value.PageToFetch;
var pageSize = PaginationContainer.Value.PageSize;
return query
.Skip((page - 1) * pageSize)
.Take(pageSize);
}
}
}
using DAL.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Infrastructure.Query
{
public abstract class Query<TEntity> : IQuery<TEntity> where TEntity : class, IEntity, new()
{
public List<(Expression expression, Type argumentType, string columnName)> WherePredicate { get; set; } = new();
public (string tableName, bool isAscending, Type argumentType)? OrderByContainer { get; set; }
public (int PageToFetch, int PageSize)? PaginationContainer { get; set; }
public IQuery<TEntity> Page(int pageToFetch, int pageSize = 10)
{
PaginationContainer = (pageToFetch, pageSize);
return this;
}
public IQuery<TEntity> OrderBy<T>(string columnName, bool ascendingOrder = true) where T : IComparable<T>
{
OrderByContainer = (columnName, ascendingOrder, typeof(T));
return this;
}
// 1st argument -> table name
public IQuery<TEntity> Where<T>(Expression<Func<T, bool>> predicate, string columnName) where T : IComparable<T>
{
WherePredicate.Add((predicate, typeof(T), columnName));
return this;
}
public abstract IEnumerable<TEntity> Execute();
}
}
@lasododo
Copy link
Author

It is not optimal, but it is in my opinion "good enough" to show to student that have an understanding of how does ASP .NET Core works to see how they can start playing around with Expression Trees.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment