I am trying to build generic method which would look simething like this:
public static IQueryable<TModel> IncludeByUserCondition<TModel, TIncludable>(this IQueryable<TModel> query, Func<TModel, IQueryable<TIncludable>> includes, List<int> userIDs)
where TModel : class
where TIncludable: class
{
Expression<Func<TModel, object>> result = x => includes(x);
if(typeof(ASqlBase).IsAssignableFrom(typeof(TIncludable)))
{
result = x =>
includes(x)
.Select(prop => prop as ASqlBase)
.Where(prop =>
prop.DeleteDate == null
)
.Where(prop =>
userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
)
.Select(prop => prop as TIncludable);
}
query = query.Include(result);
return query;
}
This method would allow me to centrally check if user can read navigation property's value and, if so, include it in result. My applications read rights are conceived in hierarchical way: logged user can read his records and records of all users he had added to the system. Because of that, I cannot determine all visible records in compile-time and, thus, cannot use different database contexts for different groups of users. Also, since this is only one of many ways for filtering data, unfortunately I cannot make use of Global Filters.
I am trying to call the above method like this:
qry = qry.IncludeByUserCondition<AllocatedFund, AllocatedFundDetailPaymentMade>(p => p.AllocatedFundDetailPaymentsMade.AsQueryable(), allowedUserIDs);
However, when I try to invoke it in run-time, I get the following exception:
The expression 'Invoke(__includes_0, x).Select(prop => (prop As ASqlBase)).Where(prop => ((prop.DeleteDate == null))).Where(prop => True).Select(prop => (prop As AllocatedFundDetailPaymentMade))' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.
On another hand, when I try to run query manually, everything works fine:
qry = qry.Include(p =>
p.AllocatedFundDetailPaymentsMade
.AsQueryable()
.Where(prop =>
prop.DeleteDate == null
)
.Where(prop =>
userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
)
Since I have more than just one navigation property to include in this manner (and I have to perform query in similar manner for all 30+ other models I use in my application), I wouldn't want to manually write those where clauses in every query.
Does anybody know the solution for this problem? Any help would be kindly appreciated.
EDIT:
ASqlBase is just base, abstract class from which some other models interit (although not all of them - ie. User model does not inherit from ASqlBase).
ASqlBase looks like this:
public abstract class ASqlBase
{
[Key]
public int ID { get; set; }
[Required]
public int UserID { get; set; }
[ForeignKey("UserID")]
public virtual User User { get; set; }
public DateTime? DeleteDate { get; set; }
}
I plan to use that function to get data and then display it in report. I'm giving example for Person, then the method call would look something like this:
var qry = dbContext.Person.IncludeByUserCondition<Person, Athlete>(p => p.Athletes.AsQueryable(), athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Employee>(p => p.Employees.AsQueryable(), employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Student>(p => p.Students.AsQueryable(), studentAllowedUserIDs);
Person model looks something like this:
public class Person : ASqlBase
{
...
public virtual ICollection<Athlete> Athletes { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
All of the above models: Athlete, Employee and Student inherit from ASqlBase
EDIT 2:
Sorry for bad method naming, method should be called IncludeByUserCondition, not IncludeMultiple (as it was named before).
A little bit simplified usage. You do not need to call AsQueryable() and explicitly specify generic parameters:
var qry = dbContext.Person.IncludeByUserCondition(p => p.Athletes, athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition(p => p.Employees, employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition(p => p.Students, studentAllowedUserIDs);
And realization:
public static class IncludeExtensions
{
public static IQueryable<TModel> IncludeByUserCondition<TModel, TRelated>(this IQueryable<TModel> query,
Expression<Func<TModel, IEnumerable<TRelated>>> collectionProp, List<int> userIDs)
where TModel : class
where TRelated : ASqlBase
{
var relatedParam = Expression.Parameter(typeof(TRelated), "r");
// r.DeleteDate == null
var filterPredicate = (Expression)Expression.Equal(
Expression.PropertyOrField(relatedParam, nameof(ASqlBase.DeleteDate)),
Expression.Constant(null, typeof(DateTime?)));
if (userIDs?.Count > 0)
{
// r.DeleteDate == null && userIDs.Contains(r.UserID)
filterPredicate = Expression.AndAlso(filterPredicate,
Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(int) },
Expression.Constant(userIDs),
Expression.PropertyOrField(relatedParam, nameof(ASqlBase.UserID))));
}
// r => r.DeleteDate == null && userIDs.Contains(r.UserID)
var filterLambda = Expression.Lambda(filterPredicate, relatedParam);
// p => p.Navigation.Where(r => r.DeleteDate == null && userIDs.Contains(r.UserID))
var transformedProp = Expression.Lambda(Expression.Call(typeof(Enumerable), nameof(Enumerable.Where),
new[] { typeof(TRelated) }, collectionProp.Body, filterLambda), collectionProp.Parameters);
// query.Include(p => p.Navigation.Where(r => r.DeleteDate == null && userIDs.Contains(r.UserID)))
var includeExpression = Expression.Call(typeof(EntityFrameworkQueryableExtensions),
nameof(EntityFrameworkQueryableExtensions.Include),
new[] { typeof(TModel), typeof(IEnumerable<TRelated>) },
query.Expression,
Expression.Quote(transformedProp));
// instantiate new IQueryable<TModel>
var resultQuery = query.Provider.CreateQuery<TModel>(includeExpression);
return resultQuery;
}
}
Related
I have a code that fetching a table. Here's my sample code:
public IQueryable<MyItem> MyItems(){
return context.MyItem;
}
Here are the sample properties of MyItem
public int Id {get;set;}
public byte[] Image {get;set;}
public string Name {get;set;}
Since, byte[] can have multiple characters, I don't want to include then in searching because it will take so long if I have a records like 10,000 items.
Typically, I would Select like this:
public IQueryable<MyItem> MyItems(){
return context.MyItem.Select(item=>new MyItem{
Id=item.Id,
Name=item.Name
});
}
This is okay for few properties, but what I have a 10-20 properties, it would be hassle to write them one by one.
Is there any way like, I just Except the property Image in lambda for shorter code?
Create your own extension method SelectExcept:
public static IQueryable<T> SelectExcept<T, TKey>(this IQueryable<T> sequence,
Expression<Func<T, TKey>> excluder)
{
List<string> excludedProperties = new List<string>();
if (excluder.Body is MemberExpression memberExpression)
{
excludedProperties.Add(memberExpression.Member.Name);
}
else if (excluder.Body is NewExpression anonymousExpression)
{
excludedProperties.AddRange(anonymousExpression.Members.Select(m => m.Name));
}
var includedProperties = typeof(T).GetProperties()
.Where(p => !excludedProperties.Contains(p.Name));
return sequence.Select(x => Selector(x, includedProperties));
}
private static T Selector<T>(T obj, IEnumerable<PropertyInfo> properties)
{
var instance = Activator.CreateInstance<T>();
foreach (var property in properties)
property.SetValue(instance, property.GetValue(obj), null);
return instance;
}
Usage:
var result = context.MyItem.SelectExcept(x => x.Image);
You can exclude more than one property:
var result = context.MyItem.SelectExcept(x => new { x.Image, x.Name });
You can use AutoMapper for this. Just create a DTO class with all the properties excluding those which you don't want to query:
public class MyItemDTO
{
public int Id { get; set; }
public string Name { get; set; }
}
Then add some mapping:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<MyItem, MyItemDTO>();
});
Now you can query your entities like this:
context.MyItem
.OrderBy(m => m.Name)
.ProjectTo<MyItemDTO>();
This extension is available in AutoMapper.QueryableExtensions.
You can also create a DTO class with all the properties from an entity and then ignore some of them in mapping:
cfg.CreateMap<MyItem, MyItemDTO>().ForMember(d => d.Image, m => m.Ignore());
If you use EF Core you can instantiate entity class inside LINQ query so it's possible to map entity to itself and ignore some properties from it without creating additional DTO classes:
cfg.CreateMap<MyItem, MyItem>().ForMember(d => d.Image, m => m.Ignore());
Then you just use ProjectTo with the entity type which excludes unwanted properties from SQL query:
context.MyItem
.OrderBy(m => m.Name)
.ProjectTo<MyItem>();
Note that you may also need to create mappings for each navigation property of an entity.
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'm trying to reuse an EF6 extension method with an associated entity (one to many relationship). Contrived example:
public class Parent
{
public string State { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public string Value { get; set; }
public Parent Parent { get; set; }
}
public static ParentNamedScopes
{
public static IQueryable<Parent> IsReady(this IQueryable<Parent> queryable)
{
return queryable.Where(p => p.State == "Ready" || p.State == "New");
}
}
// ...
var children = db.Children
// my goal, but can't cast Parent to IQueryable<Parent>
// ------------------v
.Where(c => c.Parent.IsReady())
.Where(c => c.Value == "Foobar");
I've seen examples of using AsQueryable() on associated collections in sub queries, but that isn't an options since Parent is a single record. I'm sure I'm missing something obvious and I apologize since my google foo has not turned up the answer today.
One option would be to start your query with the Parents:
var children = db.Parents.IsReady()
.SelectMany(p => p.Children)
.Where(c => c.Value == "Foobar");
The idea that IsReady would convert an IQueryable seems a little off to me, though. If your use case gets more complex, you may need to change that to just give you an Expression<Func<Parent, bool>>, and use something like LINQKit to manipulate your query to make it reusable:
Expression<Func<Parent, bool>> parentIsReady = ParentCriteria.IsReady();
var readyParents = db.Parents.Where(parentIsReady);
var childrenWithReadyParents = db.Children.AsExpandable()
.Where(c => parentIsReady.Invoke(c.Parent))
.Where(c => c.Value == "Foobar");
Lets say we have the following model :
public class ReadingOrder
{
public virtual int Id { get; set; }
public virtual Order Order { get; set; }
}
Mapping:
Table("db_ReadingOrder");
Id(o => o.Id).Column("Id").GeneratedBy.Identity();
References(o => o.Order, "OrderId");
I want to get the ReadingOrder which has the orderId equal with 1 (eg).
But when I try a FirstOrDefault, the query returns null :
var readingO = _repositoryFactory.GetRepository<ReadingOrder>().FirstOrDefault(xz => xz.Order.Id == 1);
If I get all of them and after apply a FirstOrDefault works, but its stupid:
var readingOrderList1 = _repositoryFactory.GetRepository<ReadingOrder>()
.GetAll().FirstOrDefault(xz => xz.Order.Id == 1);
The method from repository has the following format:
public T FirstOrDefault(Expression<Func<T, bool>> predicate)
{
return _session.Query<T>().FirstOrDefault(predicate);
}
easy stuff, but not working. If I go for a normal property, like Id, all works as expected.
Also, if I get the generated query from log and put it in sqlite, it runs successfully and the reading order is returned. Is there a bug in NHibernate? Is a mapping problem? Or is it a problem with SQLite?
Ok, finally I found the problem : the name of the foreign key column "OrderId".
Nhibernate looks for "Hibernate.Order" in this case, I don`d know why, but after I changed the name of the column, the item is now retrieved from db. Thank you all for your answers! I gave the bounty to user Syed Farjad Zia Zaidi, because he helped me to isolate the problem. It was clear that was an Nhibernate issue, so thank you again.
There are two ways I see you can make this work either change your mapping as:
Table("db_ReadingOrder");
Id(o => o.Id).Column("Id").GeneratedBy.Identity();
References(o => o.Order);
and then query:
var readingO = _repositoryFactory.GetRepository<ReadingOrder>().FirstOrDefault(xz => xz.Order.Id == 1);
Otherwise you can also change your mapping as:
Table("db_ReadingOrder");
Id(o => o.Id).Column("Id").GeneratedBy.Identity();
References(o => o.Order).Column("OrderId");
and then query:
var readingO = _repositoryFactory.GetRepository<ReadingOrder>().FirstOrDefault(xz => xz.Order.OrderId == 1);
some times that's related to dynamic global filters, like a soft delete method by a IsDeleted prop so u need to fetching ignored items and using some methods like this .IgnoreQueryFilters() in ef.
The same scenario works fine with the following code at the repository.
public virtual IEnumerable<T> Get(
Expression<Func<T, bool>> filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
string includeProperties = "")
{
IQueryable<T> query = _context.Set<T>();
if (filter != null)
{
query = query.Where(filter);
}
if (includeProperties != null)
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
return query.ToList();
}
}
Modify your class like this:
public class ReadingOrder
{
public virtual int Id { get; set; }
public virtual Order Order { get; set; }
public virtual int OrderId { get; set; } // Explicit and direct foreign Key
}
and then retrieve your query using OrderId, instead of Order.Id:
var readingO = _repositoryFactory.GetRepository<ReadingOrder>()
.FirstOrDefault(xz => xz.OrderId == 1);
I'm trying to write this specific sql query in nHibernate QueryOver language, which I am not very familiar with:
SELECT MessageThreadId FROM MessageThreadAccesses
WHERE ProfileId IN (arr)
GROUP BY MessageThreadId
HAVING COUNT(MessageThreadId) = arr.Count
where arr is a array of integers(user Ids) I'm passing as argument and MessageThreadAccess entity looks like this:
public virtual MessageThread MessageThread { get; set; }
public virtual Profile Profile { get; set; }
....
After reading multiple stack overflow threads and experimenting I got this far with my query (trying to get MessageThread object - it should always be just one or none), but it still doesn't work and I'm not really sure what else to try. The query always seems to be returning the MessageThreadAccess object, but when reading it's MessageThread property it's always NULL.
var access = Session.QueryOver<MessageThreadAccess>()
.WhereRestrictionOn(x => x.Profile).IsIn(participants.ToArray())
.Select(Projections.ProjectionList()
.Add(Projections.Group<MessageThreadAccess>(x => x.MessageThread))
)
.Where(
Restrictions.Eq(Projections.Count<MessageThreadAccess>(x => x.MessageThread.Id), participants.Count)
)
.TransformUsing(Transformers.AliasToBean<MessageThreadAccess>())
.SingleOrDefault();
return Session.QueryOver<MessageThread>()
.Where(x => x.Id == access.MessageThread.Id)
.SingleOrDefault();
Can someone point me in the right direction, or explain what am I doing wrong?
Thanks in advance.
I guess you may try using a DTO for storing the result, instead of trying to fit the result in a MessageThreadAccess, when it is not one (no Profile).
Maybe you can try :
public class MessageThreadCountDTO
{
public MessageThread Thread { get; set; }
public int Nb { get; set; }
}
then
var profiles = new int[] { 1,2,3,4 };
MessageThreadCountDTO mtcDto = null;
var myResult =
_laSession.QueryOver<MessageThreadAccess>()
.WhereRestrictionOn(x => x.Profile.Id).IsIn(profiles)
.SelectList(list =>
list.SelectGroup(x => x.MessageThread).WithAlias(() => mtcDto.Thread).
SelectCount(x => x.MessageThread).WithAlias(() => mtcDto.Nb)
)
.Where(Restrictions.Eq(Projections.Count<MessageThreadAccess>(x => x.MessageThread), profiles.Count()))
.TransformUsing(Transformers.AliasToBean<MessageThreadCountDTO>())
.List<MessageThreadCountDTO>().FirstOrDefault();
would profiles be a Profile[], and not an int[], then the following line :
.WhereRestrictionOn(x => x.Profile.Id).IsIn(profiles)
should be :
.WhereRestrictionOn(x => x.Profile).IsIn(profiles)
Hope this will help