Ef Core generic repository include nested navigation properties - c#

I am using unit of work with repository pattern and I use this method to get direct navigation properties
public async Task<IEnumerable<T>> GetAllIncluding(params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = dbSet;
return await includes.Aggregate(query, (current, include) => current.Include(include)).ToListAsync();
}
and I use it like this
public async Task<IEnumerable<SomeEntity>> GetAllDetailsAsync()
{
var data = await unitOfWork.Service.GetAllIncluding(
x => x.DirectNavigation1,
x => x.DirectNavigation2,
x => x.DirectNavigation3) ;
}
I try to use
x => x.DirectNavigation3.Select(o => o.NestedNavigation1)
But then I got a error.
In order to do this, I had to change my params to
GetAllIncluding(params string[] includes)
with the same implementation no change at all
The code works just fine but I don't like work with string 😒 due to its difficulty in maintenance
Please share any alternative ways of including nested navigation properties without using string.
Thank you in advance.

The real question seems to be how to load all related properties. This has nothing to do with repositories. A DbContext already is a multi-entity repository and Unit-of-Work anyway.
EF Core 6 added the ability to automatically load navigations by using the AutoInclude() call in the model :
modelBuilder.Entity<Theme>().Navigation(e => e.ColorScheme).AutoInclude();
This can be turned off in queries with IgnoreAutoIncludes(), eg
var themes = context.Themes.IgnoreAutoIncludes().ToList();
It's not possible to chain Include calls from Expression<Func<T, object>> expressions. In LINQ the expressions are ... expressions that specify properties or code that eventually gets translated to something else. They aren't functions that get called. Besides, specifying all those expressions would take as many lines as chaining Include methods and look uglier.
In .Include(theme=>theme.ColorScheme) the expression isn't returning the value of ColorScheme. The Expression<Func<TEntity,TProperty>> is used to find the correct property to use using reflection :
public static IIncludableQueryable<TEntity, TProperty> Include<TEntity, TProperty>(
this IQueryable<TEntity> source,
Expression<Func<TEntity, TProperty>> navigationPropertyPath)
where TEntity : class
{
Check.NotNull(navigationPropertyPath, nameof(navigationPropertyPath));
return new IncludableQueryable<TEntity, TProperty>(
source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TEntity>(
Expression.Call(
instance: null,
method: IncludeMethodInfo.MakeGenericMethod(typeof(TEntity), typeof(TProperty)),
arguments: new[] { source.Expression, Expression.Quote(navigationPropertyPath) }))
: source);
}

Related

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

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

Is using EF Core include method inside a foreach can be a performance issue

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

.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];

Why is the Lambda Expression not valid in this Include()?

I'm working with EFCore for the first time, after moving from EF6, and for some reason I keep getting this error:
System.InvalidOperationException: 'Lambda expression used inside Include is not valid.'
Here is my Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
using (var IJ = IJContext.CreateNew(false))
{
var ChargeList = iJ.Charges.Where(charge => charge.CourtCase.CaseNumber == "13457894561")
.Include(charge => charge.ChargeDate)
.ToList();
return View(ChargeList);
}
}
}
Am I missing something important here? Does EFCore handle Lambdas totally differently or something?
It seems that the ChargeDate is not a related entity. Check Retaled entities documentation to see the purpose of include
Seems like you either have not a relationship between the types and that you want to use something like the select property to get new objects based on the data already retrieved from the query.
If you look at the signature of the Include extension method it looks like as follows:
public static IIncludableQueryable<TEntity, TProperty> Include<TEntity, TProperty>(
this IQueryable<TEntity> source,
Expression<Func<TEntity, TProperty>> navigationPropertyPath)
where TEntity : class
{
// Method body
}
The paramter navigationPropertyPath expecting a lambda expression representing the navigation property to eager load the data for that navigation property. But you are passing the entity property ChangeDate which is not navigation property.
For more details: EF Core Eager loading

Moq - mocking a complex repository method - list object not being returned

I am using Entity Framework and have a generic repository method that allows me to query a DbSet and also include navigation properties. I am trying to write a unit test for some code that uses this chunk of code and I need to mock it for a unit test. I am using Moq.
Here is the repository method - it is a method that allows me to query using an expression and also including the relevant navigation properties that I want. I saw this pattern in Julie Lerman's EF in the Enterprise course on Pluralsight.
public IEnumerable<TEntity> FindByInclude(Expression<Func<TEntity, bool>> predicate,
params Expression<Func<TEntity, object>>[] includeProperties)
{
var query = GetAllIncluding(includeProperties);
IEnumerable<TEntity> results = query.Where(predicate).ToList();
return results;
}
private IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] includeProperties)
{
IQueryable<TEntity> queryable = DbSet.AsNoTracking();
return includeProperties.Aggregate
(queryable, (current, includeProperty) => current.Include(includeProperty));
}
Here is an example of how I am calling using this method in my code ( I am just showing the relevant part of the method):
public ApiResult DeleteLocation(int id)
{
var location = _locationRepository
.FindByInclude(l => l.Id == id, l => l.LocationRegions, l => l.Pools)
.Single();
So this query would bring back a Single Location entity by the id I have passed in and the related LocationRooms and Staff collections.
How do I set up Moq for the FindByInclude method? Here is what I have for my unit test mock setup:
var mockLocationRepository = new Mock<ILocationRepository>();
var location = new Location {Id = 1,Name = "LocationName", LocationRooms = new List<LocationRoom>(), Staff = new List<Staff>()};
mockLocationRepository.Setup(r => r.FindByInclude(l => l.Id == It.IsAny<int>(), l => l.LocationRooms, l => l.Staff))
.Returns(() => new List<Location> { location });
From the Moq setup code shown here - I think I should be getting back a list of 1 location - the location object I specified with Id of 1. However when I run my unit test and hit this code - the setup method for FindByInclude returns an empty list. And therefore when the code in the DeleteLocation method gets hit and the Single() method is called I am getting an error that the "element contains no sequence".
I think the problem is I have something wrong with the syntax with the Moq Setup for the FindByInclude method but not sure what is wrong.
As an alternative to #Nkosi's answer, how about you don't use Moq but implement yourself a stub implementation of ILocationRepository? The idea behind that is that if mocking becomes hard to do, maybe you shouldn't do it?
public class StubLocationRepository : ILocationRepository
{
private readonly IEnumerable<Location> _findByInclude;
public StubLocationRepository(IEnumerable<Location> findByInclude)
{
_findByInclude = findByInclude;
}
public IEnumerable<Location> FindByInclude(
Expression<Func<Location, bool>> predicate,
params Expression<Func<Location, object>>[] includeProperties)
{
return _findByInclude;
}
}
That's simplistic because it assumes you only have one method. If you have a lot and don't want to pass constant values for each of them, you could have the ctor of the stub take optional parameters so you only stub the desired methods.
Also, since ILocationRepository most likely inherits from a generic interface, you could have a generic stub implementation that you subclass to build specific stubs - i.e to implement the methods that ILocationRepository defines.
Too long for a comment so adding as an answer
it is the expressions. Try a more general expression setup first and see if it works.
var location = new Location {
Id = 1,
Name = "LocationName",
LocationRooms = new List<LocationRoom>(),
Staff = new List<Staff>()
};
mockLocationRepository
.Setup(m => m.FindByInclude(It.IsAny<Expression<Func<TEntity, bool>>>(), It.IsAny<Expression<Func<TEntity, object>>[]>())
.Returns(() => new List<Location> { location });

Categories