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();
Related
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>(...)
These days I am working on a project, and this project depends on another project that will play the repository, my repository project based on Generic Repository pattern.
In my repository project I created a method that will get all data from a received entity and load data from selected entities in a received expression.
My method:
public List<TEntity> GetAll<TEntity>(params Expression<Func<TEntity, object>>[] expressions) where TEntity : class
{
var result = new List<TEntity>();
foreach (var expression in expressions)
{
result = Context.Set<TEntity>().Include(expression).ToList();
}
return result;
}
The question:
As you will notice in the above method I used Include method inside an iteration (Foreach), so my question is : is that using of Include inside foreach have a performance issue or any other problem ?
Probably you need this:
public List<TEntity> GetAll<TEntity>(params Expression<Func<TEntity, object>>[] expressions) where TEntity : class
{
var query = Context.Set<TEntity>().AsQueryable();
foreach (var expression in expressions)
{
query = query.Include(expression);
}
return query.ToList();
}
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];
I am using a Repository Pattern to access data using Entity Framework. I have implemented everything correctly (I think) using an onion architecture, but when I run tests I get the error :
Instance Property 'IDDocument' is not defined for type 'System.Int64'
(roughly translated from french)
The method I test is the following:
public T Get(long id)
{
ObjectContext objContext = ((IObjectContextAdapter)context).ObjectContext;
ObjectSet<T> set = objContext.CreateObjectSet<T>();
IEnumerable<string> keyNames = set.EntitySet.ElementType
.KeyMembers
.Select(k => k.Name);
if (keyNames.Count() > 1)
return null;//Que faire quand il y a plusieurs clés?
else
{
string idName = keyNames.ElementAt(0); // For Document would be IDDocument
var parameter = Expression.Parameter(id.GetType());
var property = Expression.Property(parameter, idName);
var idValue = Expression.Constant(id, id.GetType());
var equal = Expression.Equal(property, idValue);
var predicate = Expression.Lambda<Func<T, bool>>(equal, parameter);
return entities.SingleOrDefault(predicate);
}
}
My tables have different ID names for reasons I won't explain, but this is why I used Expression builder to add parameters and then use a predicate to retrieve my result. You can see this method in this post: Does a Generic Repository need a Base Entity class to be applied everywhere?
IDDocument is declared in the following way in my EF entities' POCO:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long IDDocument { get; set; }
And when I call it in my test:
[TestMethod]
public void IdExistsGetTest()
{
long valueToCheck = 1L;
repo = new Repository<Web_Documents>(context);
var test = repo.Get(valueToCheck);
test.Should().BeOfType(typeof(Web_Documents));
}
context defines my dbcontext (instantiated before).
Now when I run the tests, I always get the ArgumentException stated above, any idea what I am missing? I think the problem resides in the Get(long id) method, because if I change the code without the Expression, it works fine (but not like i want it to!). Thanks
I think:
var parameter = Expression.Parameter(id.GetType());
Should be:
var parameter = Expression.Parameter(typeof(T));
I'm experiencing my first try on implementing Generic Repository Pattern and Unit of framework. I'm not using MVC on the project in hand.
Please take a look at this method included in Generic Repository class:
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
it must be a powerful method and accomplishes the goal of DRY well.
My problem is that, I cannot order the result as descending? Can anyone write some lines of code to help me on this? Thanks,
Have a look at this: http://prodinner.codeplex.com/ and this http://efmvc.codeplex.com/.
These projects are good examples of simple architecture and you can see how generic repository is implemented and how it is used.
To filter by product category, try this:
var repo = new GenericRepository<Product>();
var results = repo.Get(
p => p.Category.Name == "Foo");
Here we declare an instance of the Generic Repository, with an entity type of Product, then we pass a lamda expression that performs the filtering on each Product's Category whose name is "Foo".