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];
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>(...)
I created a repository for my entity Master. In the repository, I have a Get method to get my entity by Id using Entity Core.
The method receives:
public TEntity Get(object id, params Expression<Func<TEntity, object>>[] includedRelatedEntities)
{
return GetById(IncludeEntities(DBContext.Set<TEntity>().AsQueryable(), includedRelatedEntities), id);
}
Then, when I use it in my code, I just pass to the method the id of the entity I´m looking for and and expression tree of the related entities that I need to include in the query (Expression<Func<TEntity, object>>)
An example of use is the following one:
var master = MasterRepository.Get(1, x => x.BranchOffice.Location);
In that case I´m looking for the Master with Id = 1 and I want it to include the BranchOffice related entity and the Location related to that BranchOffice.
From one to many relationships, it works fine, but for related lists, I dont know how to resolve it using an expression.
For example, if I want to include the Product entity of the list of Detail named Details related to my Master, I dont know how to express it in the expression tree.
var master = MasterRepository.Get(1, x => x.Details.Product);
Details is a list, so I cant access product as it is in the example above.
How can I express that in a Expression<Func<TEntity, object>>?
EDIT:
I´ve already tried:
var master = MasterRepository.Get(1, x => x.Details.Select(y=> y.Product));
But I´m getting the following exception:
The property expression 'x => {from Detail y in [x].Details select
[y].Product}' is not valid. The expression should represent a property
access: 't => t.MyProperty'. For more information on including related
data, see go.microsoft.com/fwlink/?LinkID=746393.'
I don't know can you change or replace IncludeEntities implementations, so maybe answer would not be helpful for you. Well, x => x.Details.Product will looks like this DbContext.Set<SomeType>().Include(x => x.Details).ThenInclude(o => o.Product) in the EF.Core.
So if you want to include multiple levels I can suggest you to build a query at runtime that will contains Include and ThenInclude. So, this query will be built from input expression looks like this x => x.Details.Select(y => y.Product). It's method that build this query:
/// <summary>
/// Takes include looks like 'x => x.Collections.Select(o => o.List.Select(p => p.Date))'
/// </summary>
public static IQueryable<T> GetQueryWithIncludes<T>(IQueryable<T> query, Expression<Func<T, object>> arg)
{
// Tiny optimization
ParameterInfo[] parameters;
var includeInfo = typeof(EntityFrameworkQueryableExtensions).GetMethods().Where(info => info.Name == "Include" &&
(parameters = info.GetParameters()).Length == 2 &&
typeof(Expression).IsAssignableFrom(parameters[1].ParameterType)).Single();
// Retrieve then include that take first param as 'IIncludableQueryable<TEntity, ICollection<TPreviousProperty>>'
var thenIncludeInfo = typeof(EntityFrameworkQueryableExtensions).GetMethods().Where(info => info.Name == "ThenInclude").ToList()[1];
// Retrieve then include that take first param as 'IIncludableQueryable<TEntity, IEnumerable<TPreviousProperty>>'
var lastThenIncludeInfo = typeof(EntityFrameworkQueryableExtensions).GetMethods().Where(info => info.Name == "ThenInclude").ToList()[0];
// Retrieve all selection from input expression
var lambda = arg as LambdaExpression;
var method = arg.Body as MethodCallExpression;
var result = new List<Expression>();
while (method != null)
{
result.Add(Expression.Lambda(method.Arguments[0], lambda.Parameters[0]));
lambda = method.Arguments[1] as LambdaExpression;
method = lambda.Body as MethodCallExpression;
}
result.Add(lambda);
// Add Include and ThenInclude to IQueryable
for (int i = 0; i < result.Count; ++i)
{
var lambdaExp = result[i] as LambdaExpression;
query = i == 0
? includeInfo.MakeGenericMethod(lambdaExp.Parameters[0].Type, lambdaExp.ReturnType).Invoke(null, new object[] { query, lambdaExp }) as IQueryable<T>
: i == result.Count - 1
? lastThenIncludeInfo.MakeGenericMethod((result[0] as LambdaExpression).Parameters[0].Type, lambdaExp.Parameters[0].Type, lambdaExp.ReturnType).Invoke(null, new object[] { query, lambdaExp }) as IQueryable<T>
: thenIncludeInfo.MakeGenericMethod((result[0] as LambdaExpression).Parameters[0].Type, lambdaExp.Parameters[0].Type, lambdaExp.ReturnType).Invoke(null, new object[] { query, lambdaExp }) as IQueryable<T>;
}
return query;
}
By the way, method takes a one expression, but it can be lightly modified, so it will takes array of expression or you can directly invoke the method from a loop for all of expressions.
Code below is just usage. I wrote a tree small classes for testing:
public class Test
{
public int Id { get; set; }
public DateTime TestDate { get; set; }
public ICollection<Level> Levels { get; set; }
}
public class Level
{
public int Id { get; set; }
public ICollection<LevelDetail> LevelDetails { get; set; }
}
public class LevelDetail
{
public int Id { get; set; }
public DateTime LevelDate { get; set; }
}
...
// These results are the same and have the same expression trees
var resultByInclude = context.Tests
.Include(o => o.Levels)
.ThenInclude(p => p.LevelDetails).ToList();
var resultBySelect = GetQueryWithIncludes(context.Tests,
o => o.Levels.Select(p => p.LevelDetails)).ToList();
I hope it will helps you.
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 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 });
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();