How to pass multiple properties to Expression<Func<TEntity, IEnumerable<TProperty>>> - c#

I have an EF core that I am trying to "Include" some relations by key, and found an answer I can do it like this:
//Get entity by key first, then load relations for it like this:
await context.Entry(entity).Collection(expressions).LoadAsync();
This collection method takes:
Expression<Func<TEntity, IEnumerable<TProperty>>> expressions
All examples I have seen, is a way to pass a single property to it.
Here is the full code of a wrapper that I want:
public static async Task<TEntity> FindInContextAsync<TEntity, TProperty>(object keyValue, Expression<Func<TEntity, IEnumerable<TProperty>>> expressions)
where TEntity : class where TProperty : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetService<AccountsDbContext>();
var dbSet = context.Set<TEntity>();
var Entit = await dbSet.FindAsync(keyValue);
if (Entit == null)
{
throw new NotFoundException(nameof(TEntity), keyValue);
}
await context.Entry(Entit).Collection(expressions).LoadAsync();
return Entit;
}
I would like to call it as cleanly and as little code as possible, something like this:
await FindInAccountsContextAsync<MyEntity, dynamic>(id, x => x.RelationClass1, x.RelationClass2...);
Seems like above would be more of a params case:
params Expression<Func<TEntity, TProperty>[] expressions
But then this would not get accepted by Collection()
In the end, I want to be able to retrieve any object from DbContext by key, with eager loading specified
Any solution is welcomed, as I can't get anything to work!

DbEntityEntry.Collection takes:
Expression<Func<TEntity, ICollection<TElement>>> navigationProperty
This is to select a single property of type ICollection<TElement>, not multiple properties.
Because ICollection is invariant, and TElement is likely going to be different for each navigation property, you aren't going to be able to pass a collection of these to FindInContextAsync.
The simplest workaround would be to remove the delegate and load the navigations after calling FindInContextAsync, but if you must pass delegates you could do this:
public static async Task<TEntity> FindInContextAsync<TEntity>(
object keyValue,
params Func<TEntity, Task>[] afterFind)
where TEntity : class
{
// ...
var entity = await dbSet.FindAsync(keyValue);
// ...
await Task.WhenAll(afterFind.Select(func => func(entity)));
return entity;
}
And call like this:
var entity = await FindInAccountsContextAsync(
id,
(MyEntity x) => context.Entry(x).Collection(y => y.RelationClass1).LoadAsync(),
(MyEntity x) => context.Entry(x).Collection(y => y.RelationClass2).LoadAsync());
The above could be simplified slightly by adding a helper function:
Task LoadCollectionAsync<TEntity, TProperty>(
TEntity entity,
Expression<Func<TEntity, ICollection<TElement>>> navigationProperty)
{
return context.Entry(entity).Collection(navigationProperty).LoadAsync();
}
Which would allow you to do this:
var entity = await FindInAccountsContextAsync(
id,
(MyEntity x) => LoadCollectionAsync(x, y => y.RelationClass1),
(MyEntity x) => LoadCollectionAsync(x, y => y.RelationClass2));

My mistake for thinking I need to use Collection(). I was able to achieve below using Reference() instead, which takes Expression<Func<TEntity, TProperty>> instead of IEnumerable which is what was causing me issues. Thanks to #NetMage, end result:
var entity = await FindInAccountsContextAsync<AccountCreditDebit>(
id, x => x.CreditDebitType, x => x.CreditDebitSource;
This allows me to pass any DbSet in my Account DB Context (in this case AccountCreditDebit DbSet) and return the entity from DbSet with relations specified (CreditDebitType, CreditDebitSource).
Full Code:
public static async Task<TEntity> FindInAccountsContextAsync<TEntity>(object keyValue, params Expression<Func<TEntity, object>>[] expressions)
where TEntity : class where TProperty : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetService<AccountsDbContext>();
var dbSet = context.Set<TEntity>();
var entity = await dbSet.FindAsync(keyValue);
if (entity == null)
{
throw new NotFoundException(nameof(TEntity), keyValue);
}
foreach (var expression in expressions)
{
await context.Entry(entity).Reference(expression).LoadAsync();
}
return entity;
}
The limitation I can think of, is I cannot pass a function if ICollection in the mix with single property such as:
var entity = await FindInAccountsContextAsync<AccountCreditDebit>(
id,
x => x.CreditDebitType, /*<= Single property */
x => x.ManyCreditDebitSources; //<= A Collection
Old Comment answered in comments:
The only thing I would like to improve, is to remove this dynamic if possible
await FindInAccountsContextAsync<AccountCreditDebit, dynamic>(...)

Related

LinqKit applying nested filtering through extension-method not working

I'm facing a strange behavior using LinqKit with EF Core.
This is my query:
var divisionFilter = new DivisionFilter
{
VisibilityFilter = (DivisionVisibilityFilter)true
};
await DbContext.Events.Get(new EventFilter
{
IsVisibleFilter = (EventIsVisibleFilter)true,
IdIncludeExcludeFilter = (EventIdIncludeExcludeFilter)new IncludeExcludeFilter<Guid> { Includes = new[] { eventId } }
}).Select(e => new
{
Divisions = e.Divisions
.Get(divisionFilter) // throws excepetion
.Where(d2 => divisionFilter.FilterExpression.Expand().Invoke(d2)) // works fine
.Select(division => new
{
division.DivisionId,
division.DivisionName,
division.DivisionGender
})
}).SingleAsync()
Currently I use two extensions to apply the filter's FilterExpression which is of type Expression<Func<TEntity, bool>>
public static IQueryable<TEntity> Get<TEntity, TEntityFilter>(this DbSet<TEntity> dbSet, TEntityFilter filter)
where TEntity : BaseEntity<TEntity>, new()
where TEntityFilter : EntityFilter<TEntity>
{
return dbSet.AsExpandable().Where(entity => filter.FilterExpression.Invoke(entity));
}
public static IEnumerable<TEntity> Get<TEntity, TEntityFilter>(this IEnumerable<TEntity> entities, TEntityFilter filter)
where TEntity : BaseEntity<TEntity>, new()
where TEntityFilter : EntityFilter<TEntity>
{
return entities.Where(e => filter.FilterExpression.Expand().Invoke(e));
}
whereas the first one applies the AsExpandable to the top-table and the second one is used for filtering nested navigation-properties.
However, if using the Get on the nested property it throws the exception
The LINQ expression 'division' could not be translated
which is coming from the Select-clause.
If I directly use the Where-clause and adding Expand().Invoke(d2) it work as expected but not when using the extension.
In my eyes, both calls are identical but obviously, they are not and I don't see what the issue is.
You have to mark your Get method as expandable:
[Expandable(name(GetEnumerable))]
public static IEnumerable<TEntity> Get<TEntity, TEntityFilter>(this IEnumerable<TEntity> entities, TEntityFilter filter)
where TEntity : BaseEntity<TEntity>, new()
where TEntityFilter : EntityFilter<TEntity>
{
return entities.Where(e => filter.FilterExpression.Expand().Invoke(e));
}
private static Expression<Func<IEnumerable<TEntity>, TEntityFilter, IEnumerable<TEntity>>> GetEnumerablet<TEntity, TEntityFilter>()
{
return (entities, filter) =>
entities.Where(e => filter.FilterExpression.Expand().Invoke(e));
}

.Net 5 Entity Framework Generic Any Query

As I am a lazy dog I would like to implement a generic UniqueValidator for .net FluentValidation. The goal is simple: have a validator to which I pass the model, the Expression to get the Property / Field that has to be unique and run a EF Any query. That would avoid to write a dumb class every single time one have to verify the unicity of a value in DB.
I came up after few tweaks to what seems to me a fair solution to avoid passing and invoking a precompiled Lambda to EF query translator which of course would result in a expression could not be translated exception.
Here what I have implemented:
public class UniqueValidator : IUniqueValidator
{
private ApplicationContext _dbContext;
public UniqueValidator(ApplicationContext dbContext)
{
_dbContext = dbContext;
}
public async Task<bool> IsUnique<T>(T model, Expression<Func<T, string>> expression, CancellationToken cancellationToken) where T : class
{
// Getting the value to check for unicity:
Func<T, string> method = expression.Compile(true);
var value = method(model);
// For GetDbSet<T>() test purpose, run perfectly:
bool test = await GetDbSet<T>().OfType<BL.Driver>().AnyAsync(d => d.Email == "any.valid#email.com");
// Building Linq expression
var binaryExpression = Expression.Equal(expression.Body, Expression.Constant(value));
var pe = new ParameterExpression[] { Expression.Parameter(typeof(T)) };
var anyExpression = Expression.Lambda<Func<T, bool>>(binaryExpression, pe);
return !(await GetDbSet<T>().AnyAsync(anyExpression));
}
private DbSet<T> GetDbSet<T>() where T : class
{
return (DbSet<T>)typeof(ApplicationContext)
.GetProperties()
.FirstOrDefault(p => p.PropertyType == typeof(DbSet<T>))
.GetValue(_dbContext);
}
}
Here is how the validator is used:
RuleFor(d => d)
.MustAsync((driver, cancellationToken) => {
return uv.IsUnique(driver, d => d.Email, cancellationToken);
});
Unfortunately this throw a very cumbersome and not helpful exception:
System.InvalidOperationException: The LINQ expression 'DbSet<Driver>() .Any(d => d.Email == "any.valid#email.com")' could not be translated...
Note: in the UniqueValidator implementation I added a line to test this very same query that is described in the exception and it runs perfectly, just to discard any doubt on the query validity.
I imagine the problem is with the translation of expression.Body but can't see any reason why nor how to workaround it.
Any help would be appreciated.
You must reuse the original parameter of the expression, or you must use an expression replacer:
var pe = new ParameterExpression[] { Expression.Parameter(typeof(T)) };
change to
var pe = expression.Parameters[0];

Entity Framework Core Explicitly loading related data according to Type of Class

I have Repository class with includes. the code below works for one to one relationships. However for Collections I need to change DbContext.Entry(model.Result).Reference(include).Load(); to DbContext.Entry(model.Result).Collection(include).Load();
public virtual async Task<TEntity> GetByIdAsync(object[] keyValues,
List<Expression<Func<TEntity, object>>> includes,
CancellationToken cancellationToken = default(CancellationToken))
{
var model = DbSet.FindAsync(keyValues, cancellationToken);
if (includes == null) return await model;
foreach (var include in includes)
//if (include.Body.Type.IsAssignableFrom(typeof(ICollection<>)))
DbContext.Entry(model.Result).Reference(include).Load();
return await model;
//return await DbSet.FindAsync(keyValues, cancellationToken);
}
What kind of condition may I use here in order to seperate References and Collections from each other?
thank you.
Edit:
example object is
System.Collections.Generic.IList`1[[WestCore.Domain.Entities.WestLife.MhpProduct]]
generally, the collection can be ICollection or IList.
Instead of separate Reference / Collection methods, you can use the more general Navigation method. Unfortunately it has no overload with lambda expressions, so you need to extract the property name manually. Since your method design does not support nested properties, you can get that information from lambda expression body, cast to MemberExpression:
foreach (var include in includes)
{
var propertyName = ((MemberExpression)include.Body).Member.Name;
DbContext.Entry(model.Result).Navigation(propertyName).Load();
}
I'd change it so rather than expressly mimicking FindByIdAsync I'd have a slightly more flexible method:
public async Task<T> GetItemAsync<T>(Expression<Func<T, bool>> filter,
List<Expression<Func<T,object>>> includes) where T: class
{
var model = Set<T>();
foreach (var include in includes)
{
model.Include(include);
}
return await model.FirstOrDefaultAsync(filter);
}
which you'd then call with:
var result = GetItemAsync<MyEntity>(x => x.Id == 3,
new List<Expression<Func<MyEntity, object>>>
{
x => s.SomeProperty,
x => s.SomeOtherProperty
});
which is very similar to yours, but a bit more flexible, and will only hit the DB once rather than lazily-loading everything separately like I believe your existing code would. This may be your intention but we don't have that context.
Also - Calling model.Result and using the non-asyc Load() method means your code is less async-friendly than it could be...
EDIT:
I figured out that I am still having problems. Be
public virtual async Task<TEntity> GetByIdAsync(object[] keyValues,
List<Expression<Func<TEntity, object>>> includes,
CancellationToken cancellationToken = default(CancellationToken))
{
Task<TEntity> model = null;
foreach (var include in includes)
{
if (include.Body.Type.GetInterface(nameof(IEnumerable<TEntity>)) != null)
{ //this part generates SQL Below
await DbSet.Include(include).LoadAsync(cancellationToken);
model = DbSet.FindAsync(keyValues, cancellationToken);
}
else //this part is working
{
var propertyName = ((MemberExpression)include.Body).Member.Name;
model = DbSet.FindAsync(keyValues, cancellationToken);
await DbContext.Entry(model.Result).Navigation(propertyName).LoadAsync(cancellationToken);
}
}
if (model == null)
model = DbSet.FindAsync(keyValues, cancellationToken);
return await model;
}
IEnumerable part generates theSQL below:
SELECT "product.MhpProducts".MP_MHP_ID, "product.MhpProducts".MP_PRODUCT_ID, "product.MhpProducts".MP_G_ORDER, "product.MhpProducts".G_END_DATE, "product.MhpProducts".G_INSERT_BY, "product.MhpProducts".G_INSERT_DATE, "product.MhpProducts".G_IS_DELETED, "product.MhpProducts".G_START_DATE, "product.MhpProducts"."MP_WEST_CORE._DOMAIN._ENTITIES._WEST_LIFE._MHP_PRODUCT"
FROM MHP_PRODUCT "product.MhpProducts"
INNER JOIN (
SELECT "product0".TP_ID
FROM TREE_PRODUCT "product0"
) "t" ON "product.MhpProducts".MP_PRODUCT_ID = "t".TP_ID
ORDER BY "t".TP_ID

Repository generic method GetById using eager loading

I am using Entity Framework and would like to create generic GetById method in Repository class with eager loading:
Here is my method which uses lazy-loading:
public virtual TEntity GetById(object id)
{
return DbSet.Find(id);
}
I know method Find does not support eager loading, but how it is possible to modify this method to use eager loading, so that I use this method as follows(for an example):
_unitOfWork.MyRepository.GetById(includeProperties: "Users");
One possible way is to use FirstOrDefault with predicate over the DbSet with Includes. It's not hard to build manually a predicate using Expression.Equal method, but the main challenge is how to get the key property name. Luckily we can use some ObjectContext methods to do that, so the implementation could be like this (assuming we have access to the concrete DbContext instance):
public virtual TEntity GetById(object id, params string[] includeProperties)
{
var propertyName = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)DbContext).ObjectContext
.CreateObjectSet<TEntity>().EntitySet.ElementType.KeyMembers.Single().Name;
var parameter = Expression.Parameter(typeof(TEntity), "e");
var predicate = Expression.Lambda<Func<TEntity, bool>>(
Expression.Equal(
Expression.PropertyOrField(parameter, propertyName),
Expression.Constant(id)),
parameter);
var query = DbSet.AsQueryable();
if (includeProperties != null && includeProperties.Length > 0)
query = includeProperties.Aggregate(query, System.Data.Entity.QueryableExtensions.Include);
return query.FirstOrDefault(predicate);
}
This is the update for Entity Framework Core 2.0. Also this method using the new metadata properties with EF to automatically get the first level navigation properties.
public virtual T Get(object id)
{
var propertyName = "AddressId";
//get all navigation properties defined for entity
var navigationProps = _context.Model.FindEntityType(typeof(T)).GetNavigations();
//turn those navigation properties into a string array
var includeProperties = navigationProps.Select(p => p.PropertyInfo.Name).ToArray();
//make parameter of type T
var parameter = Expression.Parameter(typeof(T), "e");
//create the lambda expression
var predicateLeft = Expression.PropertyOrField(parameter, propertyName);
var predicateRight = Expression.Constant(id);
var predicate = Expression.Lambda<Func<T, bool>>(Expression.Equal(predicateLeft, predicateRight), parameter);
//get queryable
var query = _context.Set<T>().AsQueryable();
//apply Include method to the query multiple times
query = includeProperties.Aggregate(query, Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Include);
//return first item in query
return query.FirstOrDefault(predicate);
}
Use This In Repository
public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression)
{
return this.RepositoryContext.Set<T>().Where(expression).AsNoTracking();
}
and This In Action Method
var User = _IRepositoryWrapper.User.FindByCondition(x=>x.Id == id).Include(a=>a.Photos).FirstOrDefault();

Generic Repository & Entity Framework Logic to Retrieve Entity with Includes

I have two Entity Framework 5 Get() methods that perform (i) a single entity get by ID, and (ii) a single entity get via a filter with any eager loading bolted on. See below for the code:
internal readonly FallenNovaContext Context;
private readonly DbSet<TEntity> _dbSet;
internal GenericRepository(FallenNovaContext context)
{
Context = context;
_dbSet = context.Set<TEntity>();
}
// (i) Get by ID.
public TEntity GetById(int id)
{
return _dbSet.Find(id);
}
// (ii) Get by filter and optional eager loading includes.
public TEntity Get(
Expression<Func<TEntity, bool>> filter = null,
IEnumerable<string> includePaths = null)
{
IQueryable<TEntity> query = _dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (includePaths != null)
{
query = includePaths.Aggregate(query, (current, includePath) => current.Include(includePath));
}
return query.SingleOrDefault();
}
All of which works fine now what I'm finding as my application grows is I'm writing a lot of non-generic methods that need a mix of both - more specifically I want a generic get by ID and also be able to eager load related entities.
So the method signature would look something like this:
public TEntity GetById(
int id,
IEnumerable<string> includePaths)
{
// ???
}
Which I could call like this:
User user = UnitOfWork.UserRepository.GetById(117, new List<string>() { "UserRole", "UserStatus" });
Or like this:
Car car = UnitOfWork.CarRepository.GetById(51, new List<string>() { "Make", "Model", "Tyres" });
Any help on the suggestions of how I use Entity Framework 5 to code the logic for the TEntity GetById(int id, IEnumerable includePaths) method would be appreciated.
First, write a base class for entities, which defines the primary key field. Something like the following may work:
public abstract class BaseEntity
{
public int Id {get;set;}
}
Then, write a base class for your repositories; define all generic methods in this base repository. Let this repository have a generic parameter of entity type:
public class RepositoryBase<TEntity> where TEntity : BaseEntity
{
public TEntity GetById(
int id,
params Expression<Func<TEntity, object>>[] includeList)
{
TEntity entity = null;
ObjectQuery<TEntity> itemWithIncludes = context.Set<TEntity>() as ObjectQuery<TEntity>;
foreach (Expression<Func<TEntity, object>> path in includeList)
{
itemWithIncludes = ((IQueryable)itemWithIncludes.Include(path)) as ObjectQuery<T>;
}
IQueryable<TEntity> items = itemWithIncludes.AsQueryable<TEntity>();
entity = items.Where(p => p.Id == id).SingleOrDefault();
return entity;
}
}
Update: #Bern asked whether there is any other way to find primary key than declaring a base class. The following questions refer to this problem.
Entity Framework 4: How to find the primary key?
Entity Framework code first. Find primary key
On the otherhand I do not know if there is any other way in EF 5.

Categories