I'm trying to create a method that will generate EF Core linq queries dynamically which I intend to use on a generic page in my app.
So far I have this...
public static IQueryable<object> LoadGridData(this DbContext context, GridLoadParameters parameters)
{
List<string> includes = new List<string>();
foreach (string str in parameters.Columns)
{
string path = str.StripLastElement('.');
if (!string.IsNullOrEmpty(path) && !includes.Contains(path))
includes.Add(path);
}
includes = includes.OrderBy(x => x).ToList();
IQueryable<object> linq = ((IQueryable)context.PropertyByName(parameters.Table.Pluralize())).OfType<object>();
foreach (string path in includes)
linq = linq.Include(path);
foreach (OrderColumn order in parameters.Order)
if (order.Direction == OrderDirection.Ascending)
linq = ((IQueryable)linq).Cast<object>().OrderBy(x => x.PropertyByName(order.ColumnPath));
else
linq = ((IQueryable)linq).Cast<object>().OrderByDescending(x => x.PropertyByName(order.ColumnPath));
linq = linq.Skip(parameters.ItemsToSkip).Take(parameters.ItemsToTake)
.Select(CreateLambdaObjectArraySelect<object>(parameters.Columns));
return linq;
}
The problem I have is with the Include(path) line. To keep the query dynamic I have had to cast the result to type 'object'. This is fine when accessing the properties because I have an extension method 'PropertyByName' which uses reflection to retrieve the value.
However, the Include(path) line needs to understand the entity type in order to work and as all the results have been cast to object it fails with the error 'The Include operation 'Include("Period")' is not supported. 'Period' must be a navigation property defined on an entity type.'
I have considered using a dynamic linq library but I really wanted to see if I could solve the problem without it first. After all, if the library can do it, there must be a way!
Related
I use Entity framework 6. I have a Transaction object with several navigation properties. It is easy to implement eager loading using multiple Include.
var aa = db.Transactions.Include(p => p.Account).Include(p => p.Instrument);
How can I implement the same if the fields to be included are parameters?
var aa = db.Transactions.IncludeMore(delegatesToBeIncluded);
If delegatesToBeIncluded is null then there is nothing to be included.
https://stackoverflow.com/a/38823723/5852947 This is similar what I want but it uses string instead of delegates.
https://stackoverflow.com/a/35889204/5852947 This is also interesting.
How to pass lambda 'include' with multiple levels in Entity Framework Core? This focuses on multiple level (I have one level)
https://stackoverflow.com/a/52156692/5852947 This is promising also.
Which direction should I go?
Revision 1: Why I need this?
Based on the elements of aa new objects will be created. I realized that at each object creation EF reads the DB (lazy loading is used). It is just 50 ms, but it is repeated n times.
This function is implemented in a template class, so Transactions is also a parameter.
Revision 2: In the full code there is filtering (pagination to be exact), and ToList() at then end. The tricky part that it is implemented in a template function. dbTableSelector is a delegate: readonly Func<MainDbContext, DbSet<TDbTable>> dbTableSelector;
var myList = dbTableSelector(db).Where(WhereCondition).
Skip(numberOfSkippedRows).Take(PageSize).OrderBy(OrderByCondition).ToList();
After that I transform each element of myList to another type of object. This is where lazy loading is activated one by one for each element. That is why I try to use Include. If dbTableSelector(db) returns Transactions I have to Include different elements when it returns let us say Instruments. So IncludeMore should have a List parameter which defines the fields to be included.
Here is the solution. It is based on this.
public static class IQueryableExtensions
{
public static IQueryable<T> IncludeMultiple<T, TProperty>(this IQueryable<T> query,
Expression<Func<T, TProperty>>[] includeDelegates) where T : class
{
foreach (var includeDelegate in includeDelegates)
query = query.Include(includeDelegate);
return query;
}
}
This is the calling:
var pathsA = new Expression<Func<ViewTransaction, object>>[2] { p => p.Account, p => p.Instrument };
var pathsB = new Expression<Func<ViewTransaction, object>>[1] { p => p.Account};
var pathsC = Array.Empty<Expression<Func<ViewTransaction, object>>>();
var a = db.ViewTransactions.IncludeMultiple(pathsA).Single(e => e.Id == 100);
another option instead using AsEnumerable() in linq EF, when using custom method in linq
I have below code
class Program
{
static void Main(string[] args)
{
string keyword = "m";
using (TestingEntities1 db = new TestingEntities1())
{
var models = db.Teachers.AsEnumerable().Where(a => RemoveA(a.Name).Contains(keyword));
foreach (var item in models)
{
Console.WriteLine(item.Name);
}
}
}
static string RemoveA(string input)
{
return input.Replace('a', ' ');
}
}
as you can see in my code i must use AsEnumerable() to use custom function works because if i dont use i will get something error saus
"LINQ to Entities does not recognize the method ' ' method, and this method cannot be translated into a store expression."
and then i ralized that asenumerable make slow becaseu i found that here How to improve AsEnumerable performance in EF says "you're doing is that you're downloading the whole BilBillMasters and BilBillDetails tables and then doing some processing on those in your application, rather than on the SQL server. This is bound to be slow."
please see first anwer for more detail, So is there any way to make my code faster, my case i have more than one million data in my database
Entity Framework needs to translate your query into a SQL query, which means it needs to know how to convert every part of it into SQL. Your query contains calling a RemoveA function, which Entity Framework doesn't know how to deal with. You can solve this by converting your code into
class Program
{
static void Main(string[] args)
{
string keyword = "m";
using (TestingEntities1 db = new TestingEntities1())
{
var models = db.Teachers.Where(a => a.Name.Replace("a", " ").Contains(keyword));
foreach (var item in models)
{
Console.WriteLine(item.Name);
}
}
}
}
See this MSDN page for which functions you can use inline in a LINQ to Entities query.
Alternatively, another option would be to convert your function into an Expression that Entity Framework can understand, you can see an example of it in this StackOverflow answer.
In the following code, the type of domainObject varies (but ends with DO, which I trim then to get the corresponding table name). Having the name of the table and its type, I want to update an existing object - its name is the same as the tableName due to the EF - in the database with the new property values from domainObject. Therefore, I have to find the POCO in the table with the same ID first to overwrite this. This is the code so far:
public void Update(object domainObject)
{
Type type = domainObject.GetType();
string tableName = type.Name.Substring(0, type.Name.Length - 2);
PropertyInfo tableProp = typeof(MyDbContext).GetProperty(tableName);
Type tableType = tableProp.PropertyType;
Type pocoType = tableType.GetGenericArguments()[0];
int id = (int)type.GetProperty("ID").GetValue(domainObject);
using (var context = new MyDbContext())
{
object table = tableProp.GetValue(context);
MethodInfo singleMethod = tableType.GetMethod("Single");
}
}
Usually, knowing the actual table and not just its type, I would now get the POCO via
var poco = context.TableName.Single(item => item.ID == id);
There's 2 problems here:
(1) Single is an extension method.
(2) I don't have an idea how to get the lambda expression in form of an object to pass it to the Invoke of Single.
Is there any way to do this at all with Reflection, or do I have to work around this? (For example, I could iterate through the items in table and check manually [which would load everything from the DB into memory and thus should be avoided], or maybe configure the EF to do some kind of 'override' whenever I just Add and object whose ID is already present if this is possible). Even supposing I could work around this, I'd still like to know a definitive answer to this question, since it's pretty interesting for me!
If you want to use reflection and to find given entity by ID then, if ID is primary key this is fairly simple as this is all you have to do:
object entity = context.Set(domainObject.GetType()).Find(id);
If your property is not primary key then you need to do it as follows:
ParameterExpression p = Expression.Parameter(domainObject.GetType());
Expression property = Expression.Property(p, "ID");
Expression c = Expression.Constant(id);
Expression body = Expression.Equal(property, c);
Expression exp = Expression.Lambda(body, new ParameterExpression []{ p });
MethodInfo singleMethod = typeof(Queryable).GetMethods()
.Single(m => m.Name == "Single" && m.GetParameters().Count() == 2)
.MakeGenericMethod(domainObject.GetType());
DbSet dbSet = context.Set(domainObject.GetType());
object entity = singleMethod.Invoke(null, new object[]{ dbSet, exp });
First with Expression class you build expression that will be passed to Single method (in your case this will be p => p.ID == id). Then you search proper Single method from Queryable class. The last thing is to invoke this method with proper parameters. This way you may do any linq queries with use of Reflection.
You simply need to make a generic method, with a type parameter that represents the type of your entity and use the corresponding DbSet.
public int Update<TEntity>(TEntity domainObject)
{
int id = domainObject.Id; // Needs interface !!
using (var context = new MyDbContext())
{
var objectInDb
= ctx.DbSet<TEntity>.Single(e => e.Id == id); // Needs interface !!
// Use ValueInjecter (not AutoMapper) to copy the properties
objectInDb.InjectFrom(domainObject); // needs ValueInjecter Nuget Package
context.SaveChanges();
}
return userId;
}
As you see in the code comments, your entities need to implement an interface so that you can access the Id property:
public interface IId
{
public int Id { get; set; }
}
And then you need to include the generic method in a generic class that has the corresponding type constraint:
public RepoClass<TEntity>
where TEntity : IId
{
// Define the generic method here
}
In this way you don't have to resort to Reflection.
If you're using some kind of T4 template,or whatever, to create your POCOs, make them partial classes, so that you can declare the interface in a separate file, like this:
public partial MyDomainClass : IId
{
}
In this wya, the interface won't be lost when you update your Db Context objects.
And finally, download an use ValueInjecter, for example using Nuget Package Manager, or running Install-Package ValueInjecter in the Nuget Package Manager console.
When you include using Omu.ValueInjecter; namespace in your code, you'll get an InjectFrom extension method on all objects, that allows to automatically copy all the properties from a source object (by matching their names). Don't use AutoMapper, or you'll have to solve other problems.
Alternatively, you can check that the object exists in the DB (for security) and use the original object, without copying the properties, i.e.
var updatedObject = ctx.Set<TEntity>().Attach(domainObject);
ctx.Entry(updatedObject).State = EntityState.Modified;
ctx.SaveChanges();
I prefer this solution, better than the previous one.
I tried to use expandoobjects in LINQ queries to have the ability to query against properties that are created during runtime, for example the headers from a csv file. It all worked fine if typing the LINQ query direct in the code as in the example:
// initialize testdata
List<ExpandoObject> hans = new List<ExpandoObject>();
string[] names = {"Apfel", "Birne", "Banane", "Orange"};
int[] ids = { 1, 2, 3, 4 };
for (int i = 0; i < 4; i++)
{
dynamic horst = new ExpandoObject();
((IDictionary<string, object>)horst).Add("Fruit", names[i]);
((IDictionary<string, object>)horst).Add("ID", ids[i]);
hans.Add(horst);
}
// try some LINQ queries, both are working as intended
var klaus = from dynamic x in hans where x.ID < 3 select x;
//var klaus = hans.Where(x => x.ID < 3).Select(x => x);
Then i tried to read the query from the commandline and create a dynamic LINQ query using a slighty modified version of the evaluant linq compiler.
string expression = System.Console.ReadLine();
LinqCompiler lc = new LinqCompiler(expression);
lc.AddSource<ExpandoObject>("hans", hans);
IEnumerable<ExpandoObject> klaus = (IEnumerable<ExpandoObject>)lc.Evaluate();
As long as as i donĀ“t use WHERE or ORDER BY statements, everything is fine, but if any WHERE or ORDER BY is included in the query, i get an error when compiling the codedom code in the linq compiler: CS1963: An expression tree may not contain a dynamic operation.
The code for the query is created using the following line:
doRequestMethod.Statements.Add(new CodeMethodReturnStatement(new CodeSnippetExpression(Query)));
I suppose that the codedom compiler is building the expression tree in some way different to the way a direct typed in LINQ query is parsed. Any idea to get this to work would be appretiated, including other ideas to dynamically create queries for runtime-generated objects.
To get the error you're getting, I had to fix the LINQ Compiler to support dynamic, by telling it to use C# 4.0 and add a reference to Microsoft.CSharp.dll, so I assume you have done the same.
The problem is a source in LINQ compiler can be any collection, including IQueryable<T>. And if IQueryable<T> is supposed to work correctly, you actually need to treat it as IQueryable<T>, not IEnumerable<T>. The way LINQ compiler solves this is that it treats any source as IQuerybale<T>by using the AsQueryable() extension method.
This means the generated code looks like this:
public object DoRequest(System.Linq.IQueryable<System.Dynamic.ExpandoObject> hans) {
return from dynamic x in hans where x.ID < 3 select x;
}
The problem with this code is that tries to use IQuerybale<T> versions of LINQ methods, that use Expressions. And, as the error message tells you, Expressions don't support dynamic.
I think the easiest way to fix this is to modify LINQ Compiler to use IEnumerable<T>, instead of IQuerybale<T> by changing the AddSource() into:
public void AddSource<T>(string name, IEnumerable<T> source)
{
this.sources.Add(new SourceDescription(name, typeof(IEnumerable<T>), source));
}
Of course, this means it won't work well for database queries, but you can't make database queries work with dynamic anyway.
I saw this code work with LINQ to SQL but when I use Entity Framework, it throws this error:
LINQ to Entities does not recognize the method 'System.Linq.IQueryable'1[MyProject.Models.CommunityFeatures] GetCommunityFeatures()' method, and this method cannot be translated into a store expression.`
The repository code is this:
public IQueryable<Models.Estate> GetEstates()
{
return from e in entity.Estates
let AllCommFeat = GetCommunityFeatures()
let AllHomeFeat = GetHomeFeatures()
select new Models.Estate
{
EstateId = e.EstateId,
AllHomeFeatures = new LazyList<HomeFeatures>(AllHomeFeat),
AllCommunityFeatures = new LazyList<CommunityFeatures>(AllCommFeat)
};
}
public IQueryable<Models.CommunityFeatures> GetCommunityFeatures()
{
return from f in entity.CommunityFeatures
select new CommunityFeatures
{
Name = f.CommunityFeature1,
CommunityFeatureId = f.CommunityFeatureId
};
}
public IQueryable<Models.HomeFeatures> GetHomeFeatures()
{
return from f in entity.HomeFeatures
select new HomeFeatures()
{
Name = f.HomeFeature1,
HomeFeatureId = f.HomeFeatureId
};
}
LazyList is a List that extends the power of IQueryable.
Could someone explain why this error occurs?
Reason:
By design, LINQ to Entities requires the whole LINQ query expression to be translated to a server query. Only a few uncorrelated subexpressions (expressions in the query that do not depend on the results from the server) are evaluated on the client before the query is translated. Arbitrary method invocations that do not have a known translation, like GetHomeFeatures() in this case, are not supported.
To be more specific, LINQ to Entities only support Parameterless constructors and Initializers.
Solution:
Therefore, to get over this exception you need to merge your sub query into the main one for GetCommunityFeatures() and GetHomeFeatures() instead of directly invoking methods from within the LINQ query. Also, there is an issue on the lines that you were trying to instantiate a new instance of LazyList using its parameterized constructors, just as you might have been doing in LINQ to SQL. For that the solution would be to switch to client evaluation of LINQ queries (LINQ to Objects). This will require you to invoke the AsEnumerable method for your LINQ to Entities queries prior to calling the LazyList constructor.
Something like this should work:
public IQueryable<Models.Estate> GetEstates()
{
return from e in entity.Estates.AsEnumerable()
let AllCommFeat = from f in entity.CommunityFeatures
select new CommunityFeatures {
Name = f.CommunityFeature1,
CommunityFeatureId = f.CommunityFeatureId
},
let AllHomeFeat = from f in entity.HomeFeatures
select new HomeFeatures() {
Name = f.HomeFeature1,
HomeFeatureId = f.HomeFeatureId
},
select new Models.Estate {
EstateId = e.EstateId,
AllHomeFeatures = new LazyList<HomeFeatures>(AllHomeFeat),
AllCommunityFeatures = new LazyList<CommunityFeatures>(AllCommFeat)
};
}
More Info: Please take a look at LINQ to Entities, what is not supported? for more info.
Also check out LINQ to Entities, Workarounds on what is not supported for a detailed discussion on the possible solutions.
(Both links are the cached versions because the original website is down)