StackOverflowException using Linq(Kit) for nested data - c#

I'm trying to build a nested query using Linq/LinqKit. In theory this seems to be easy. But I am stuck with the practical part.
In my database I have a table which has a self-reference to its parent. In my linq-query I now want to select all parents of a given element (and the parents of this one and so on).
In my code I have the following expression in partial class of MyTable:
public static Expression<Func<MyTable, IEnumerable<MyTable>>> Parents => (entity) => entity.ParentId != null ? new[]{entity.ParentEntity}.Union(Parents.Invoke(entity.ParentEntity) : new MyEntity[]{};
which should select the parent of a given entity and those parents when the ParentId is set.
The query itself (simplified):
dbContext
.MyTable
.AsExpandable()
.Where(x => x.Id == myId)
.Select(x => new
{
Parents = MyTable.Parents.Invoke(x, dbContext)
});
Running this code ends up in an StackOverflowException as the stop-condition is not hit and therefore the Parents-call is nested endlessly until the stack is full.
Any ideas how this can be done or is this not possible? Or is there an other way for fetching nested data using Linq/LinqKit within one query?
I already tried passing the context to the expression in order to create a sub-query (also not working):
public static Expression<Func<MyTable, MyContext, IEnumerable<MyTable>>> Parents => (entity, dbContext) => entity.ParentId != null ? new[]{entity.ParentEntity}.Union(Parents.Invoke(dbContext.MyTable.FirstOrDefault(x => x.Id == entity.ParentId), dbContext) : new MyEntity[]{};

As mentioned in comments, currently it's not possible to create a recursive expandable (i.e. non invokable) expression.
However, if you can limit the maximum depth, one possible solution would be to build expression like this (utilizing the EF navigation property):
Parents = new MyTable [] { x.Parent, x.Parent.Parent, x.Parent.Parent.Parent, ...}
.Where(e => e != null)
dynamically:
static Expression<Func<MyTable, IEnumerable<MyTable>>> ParentsSelector(int maxLevels)
{
var parameter = Expression.Parameter(typeof(MyTable), "x");
var parents = new Expression[maxLevels];
for (int i = 0; i < parents.Length; i++)
parents[i] = Expression.Property(i > 0 ? parents[i - 1] : parameter, "Parent");
Expression<Func<MyTable, bool>> predicate = x => x != null;
var result = Expression.Call(
typeof(Enumerable), "Where", new[] { parameter.Type },
Expression.NewArrayInit(parameter.Type, parents), predicate);
return Expression.Lambda<Func<MyTable, IEnumerable<MyTable>>>(result, parameter);
}
and use it as follows:
var parents = ParentsSelector(10);
var query = dbContext.MyTable
.AsExpandable()
.Where(x => x.Id == myId)
.Select(x => new
{
Parents = parents.Invoke(x)
});

Related

Ef Core Table relationship include count Error

I'm new to using .NET and EF Core. I cannot count the like table associated with the post table.
What I want to do is show the total number of likes on the post.
PostManager:
var posts = await _unitOfWork.Posts
.GetAllAsync(p => p.UserId == userId &&
p.IsActive == true,
l => l.Likes.Count());
EfBaseRepository:
public async Task<IList<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> predicate = null, params Expression<Func<TEntity, object>>[] includeProperties)
{
IQueryable<TEntity> query = _context.Set<TEntity>();
if (predicate != null)
{
query = query.Where(predicate);
}
if (includeProperties.Any())
{
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
}
return await query.ToListAsync();
}
Error:
System.InvalidOperationException: The expression 'Convert(l.Likes.AsQueryable().Count(), Object)' 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. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.
If you are new to EF Core, do not create generic repository it is useless pattern and just increase complexity.
Correct and fastest query is:
var likesCount = await _context.Posts
.Where(p => p.UserId == userId && p.IsActive == true)
.SelectMany(p => p.Likes)
.CountAsync();
do this instead. Basically, you want to achieve "Lazy Loading". In your "GetAllAsync" method, the "includeProperties" is expecting a property. In this case, the "Likes" property so it "l.Likes" not "l.Likes.Count()". You can do your count afterwards.
var posts = await _unitOfWork.Posts
.GetAllAsync(p => p.UserId == userId &&
p.IsActive == true,
l => l.Likes).ToListAsync();
var likesCount = posts.Likes.Count();

How to include all levels of a object in EF? [duplicate]

This question already has an answer here:
loading a full hierarchy from a self referencing table with EntityFramework.Core
(1 answer)
Closed 2 years ago.
I have an object that defines a network structure, I want to send over all children of that object and the children of the children and so on.
Right now I have this:
var Data = await _context.Scans
.Include(c => c.networkDevices)
.ThenInclude(d => d.ports)
.ThenInclude(p => p.Service)
.Include(c => c.root)
.ThenInclude(d => d.children).ThenInclude(p => p.children).ThenInclude(c => c.children)
.ToListAsync();
return Data;
This code will get most levels but if a network has many different layers it won't get all of them.
How can i make it so that all layers get included.
I think there is no built in way to load "all" layers because in theory it'd be possible that you have cyclic references.
This snippet will create a query for the number of layers
namespace System.Data.Entity
{
using Linq;
using Linq.Expressions;
using Text;
public static class QueryableExtensions
{
public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
int levelIndex, Expression<Func<TEntity, TEntity>> expression)
{
if (levelIndex < 0)
throw new ArgumentOutOfRangeException(nameof(levelIndex));
var member = (MemberExpression)expression.Body;
var property = member.Member.Name;
var sb = new StringBuilder();
for (int i = 0; i < levelIndex; i++)
{
if (i > 0)
sb.Append(Type.Delimiter);
sb.Append(property);
}
return source.Include(sb.ToString());
}
}
}
Usage:
var children = await DbContext.Roots
.Include(3, a => a.Children)
.SingleOrDefaultAsync(a => a.Id == 5);
I think the easiest way to accomplish this is by lazy loading,
take a look at this post

Using Entity Framework dynamically sort by a column in a child

I call dynamically sort rows of a table when the orderby column is in the parent table doing the following...
public List<ServiceRequest> SortSRsByParentFields(string p_Criteria,
bool p_sortDescending,
bool p_ShowAll = true) {
var propertyInfo = typeof(ServiceRequest).GetProperty(p_Criteria);
var sortedList1 = new List<ServiceRequest>();
var sortedList2 = new List<ServiceRequest>();
var myServiceRequests = GetMyServiceRequests();
var otherServiceRequests = GetOthersServiceRequests();
if (p_sortDescending)
{
sortedList1 = myServiceRequests
.AsEnumerable()
.OrderByDescending(x => propertyInfo.GetValue(x, null)).ToList();
sortedList2 = otherServiceRequests.AsEnumerable()
.OrderByDescending(x => propertyInfo.GetValue(x, null))
.ThenBy(x => x.Client.LastNameFirst).ToList();
}
else
{
sortedList1 = myServiceRequests.AsEnumerable()
.OrderBy(x => propertyInfo.GetValue(x, null)).ToList();
sortedList2 = otherServiceRequests.AsEnumerable()
.OrderBy(x => propertyInfo.GetValue(x, null))
.ThenBy(x => x.Client.LastNameFirst).ToList();
}
var allSRs = p_ShowAll == false ? sortedList1.Concat(sortedList2).Take(1000)
.ToList() : sortedList1.Concat(sortedList2).ToList();
return allSRs;
}
But I can't seem to make this method work if the orderby column is in a child table (a table related to the parent though an FKey).
So the question is how do I make that work?
EF isn't really designed with dynamic sorting in mind. But there are alternatives you can use for cases like this without replacing the rest of your EF code.
For example, with Tortuga Chain you can write:
ds.From("ServiceRequests", [filter]).WithSorting (new SortExpression(p_Criteria, p_sortDescending)).ToCollection<ServiceRequest>().Execute();
You can also just generate SQL directly, but I don't recommend that approach because you have to carefully check the sort expression to ensure it is actually a column name and not a SQL injection attack.

Get Method is null off IQueryable (Entity Framework)

I'm trying to pass lambda expressions and a type to my DAL. I have this statement:
(entities).GetType().GetMethod("Where")
"entities" is the Table of entities on the DataContext.
When I run the statement I get a null even though Linq.Table inherits IQueryable.
Anyone have an idea?
Here is the entire method:
public object GetResultSet(Dictionary<Type, Func<object, bool>> values)
{
using (ICSDataContext db = DataContextFactory.CreateDataContext<ICSDataContext>(DataContexts.ICS))
{
foreach (var entry in values)
{
var property = db.GetType().GetProperty(entry.Key.Name + "s");
IQueryable entities = (IQueryable)property.GetValue(db, null);
var whereMethod = (entities).GetType().GetMethod("Where")
.MakeGenericMethod(Type.GetType(entry.Key.AssemblyQualifiedName));
return whereMethod.Invoke(entities, new object[] { entry.Value });
}
}
return null;
}
Thanks
As an alternative you could do something like
db.Set<Type>()
which will return you the DBSet of the appropriate type, with Where accessible without reflection. Also you may want to use Expression> rather than Func, expressions work on queryables where as funcs work on enumerables. If you pass a func into a Where clause it pulls the entire dbset down and processes it in memory.
Typed expressions are also a little easier to work with (intellesence, type checking).
Expression<Func<User,bool>> filter = c=>c.FirstName == "Bob";
As another alternative you can look into System.Linq.Dynamic, ScottGu has a write up on it here. The article and the code are old, but it works with EF 6. It allows things like
.Where("CategoryId=2 and UnitPrice>3")
From answer by LukeH under here:
var where1 = typeof(Queryable).GetMethods()
.Where(x => x.Name == "Where")
.Select(x => new { M = x, P = x.GetParameters() })
.Where(x => x.P.Length == 2
&& x.P[0].ParameterType.IsGenericType
&& x.P[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
&& x.P[1].ParameterType.IsGenericType
&& x.P[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))
.Select(x => new { x.M, A = x.P[1].ParameterType.GetGenericArguments() })
.Where(x => x.A[0].IsGenericType
&& x.A[0].GetGenericTypeDefinition() == typeof(Func<,>))
.Select(x => new { x.M, A = x.A[0].GetGenericArguments() })
.Where(x => x.A[0].IsGenericParameter
&& x.A[1] == typeof(bool))
.Select(x => x.M)
.SingleOrDefault();
Then this:
var gmi = where1.MakeGenericMethod(typeof(T));

How do I define a SELECT TOP using LINQ with a dynamic query?

I want to pass dynamic lambda expressions to the function below, but I'm not sure how to define the .Take() or .OrderByDescending() on the expression object.
If I want to call the function below, then I want to be able to do this:
dbprovider.Query = (x => x.ConfigurationReference == "172.16.59.175")
.Take(100)
.OrderByDescending(x.Date)
FindEntities(db, dbprovider.Query)
But I can't (this syntax is invalid). Any ideas?
public static List<T> FindEntities<T>(TrackingDataContext dataContext, System.Linq.Expressions.Expression<Func<T, bool>> find) where T : class
{
try
{
var val = dataContext.GetTable<T>().Where(find).ToList<T>();
return val;
}
catch (Exception ex)
{
throw ex;
}
}
The parameter is of type:
System.Linq.Expressions.Expression<Func<T, bool>> find
That means it can take a predicate (the "where" clause), and only a predicate. Thus the only bit you can pass in there is the filter:
x => x.ConfigurationReference == "172.16.59.175"
To do what you want, you would need to add the rest of the code in FindEntities, so that it becomes:
var val = dataContext.GetTable<T>().Where(find)
.OrderByDescending(x => x.Date).Take(100).ToList<T>();
(note also that the Take should really be after the OrderByDescending)
One way you could do that would be:
public static List<T> FindEntities<T>(TrackingDataContext dataContext,
System.Linq.Expressions.Expression<Func<T, bool>> find,
Func<IQueryable<T>, IQueryable<T>> additonalProcessing = null
) where T : class
{
var query = dataContext.GetTable<T>().Where(find);
if(additonalProcessing != null) query = additonalProcessing(query);
return query.ToList<T>();
}
and call:
var data = FindEntities(db, x => x.ConfigurationReference == "172.16.58.175",
q => q.OrderByDescending(x => x.Date).Take(100));
However, frankly I'm not sure what the point of this would be... the caller could do all of that themselves locally more conveniently, without using FindEntities at all. Just:
var data = db.GetTable<T>()
.Where(x => x.ConfigurationReference == "172.16.58.175")
.OrderByDescending(x => x.Date).Take(100).ToList();
or even:
var data = db.SomeTable
.Where(x => x.ConfigurationReference == "172.16.58.175")
.OrderByDescending(x => x.Date).Take(100).ToList();
or just:
var data = (from row in db.SomeTable
where row.ConfigurationReference == "172.16.58.175"
orderby row.Date descending
select row).Take(100).ToList();

Categories

Resources