Linq to entities - reusing predicates with navigation properties? - c#

I am trying to find a way to reuse predicates for filtering entities in EF 6.1.3. I've run into a problem filtering related properties using 'Where'.
E.g. if I have this interface IValidFromTo
public interface IValidFromTo
{
DateTime StartDate { get; set;}
DateTime EndDate { get; set; }
}
and a function that returns a predicate for Where :
public class Extensions
{
public static Expression<Func<T, bool>> Current<T>()
where T : IValidFromTo
{
var currentDate = DateTime.Now;
return x => x.StartDate <= currentDate && x.EndDate >= currentDate;
}
}
See http://www.albahari.com/nutshell/predicatebuilder.aspx for background.
When applied directly to DbSet, this method works.
var query = ctx.Items.Where(Extensions.Current<Item>()); // compiles
But how to make it work with a more complex query dealing with navigation properties?
E.g. if I have a DbSet<Person> with a collection of Item:
public class Person
{
...
public virtual ICollection<Item> Items { get; set; }
}
and I want to project it into an object containing the name of the person and just the current Items, I end up with some rather cluttered code:
var relationQuery = ctx.People.Select(x => new
{ Name = x.Name,
CurrentItems = x.Items.AsQueryable().Where(Extensions.Current<Item>())
});
I wonder if it is possible to improve this code, e.g. to be able to write something like
CurrentItems = x.Items.Current() // quasi an extension method on `ICollection<Item>`?
(writing an extension method on ICollection<IValidFromTo> doesn't work, because EFf wouldn't recognize this method and throw an error)
UPDATE
Seems like this is achievable via a Join (supposing that each Person can only have a single valid item):
var isCurrent= x => <<some condition on x>>;
...
var validItems = ctx.Items.Where(isCurrent);
var peopleWithCurrentItems = from person in ctx.Persons
join item in validItems on person.Id equals item.Owner.Id
select new { Person = person, Item = item };
If there may be more than one valid Item per Person, then
var grouped = peopleWithValid.GroupBy(x => x.Person);
However, this version of the query will exclude persons with no matching Items.

Related

Combining Linq-Entity statements

I have 2 repositories, Member and Person. The Person Model contains a nullable reference property to a Member Model. The Member Model is defined in the Member Repository and I would like to put in place a pattern that ensures this stays that way. however, when I call the methods in the member repo from the person repo I get the
LINQ to Entities does not recognize the method 'System.Linq.IQueryable 1[...IMember] Get(..Entities,System.Linq.Expressions.Expression 1[System.Func`2[..tblMember,System.Boolean]])' method, and this method cannot be translated into a store expression."
While I understand this can be easily solved by putting .asEnumerable() then a second select there is a cost that means your doing 2 queries instead of one and you loose the ability to expose the method as awaitable. Below is the code I have removed a few non essential parts to clearify. I am not using lambda because I have not found a good way to say let in a lambda expression. My goal is person.Member would simply be a nested select. Also Note that the Member side of things is a very ugly database that I have not control over and these repos are being separated for a reason. Thanks in advance
public class MemberRepository : Interfaces.IRepository<IMemberBase, string>
{
private Data.LSAEntities entities { get; set; }
public MemberRepository(Data.LSAEntities entities)
{
this.entities = entities;
}
internal static IQueryable<IMember> Get(Data.LSAEntities entities, Expression<Func<tblMember, bool>> predicate)
{
return (from t in entities.tblMembers.Where(predicate)
let options = entities.tblDataOptions.Where(o => o.DataName == "MemberStatus")
select new Member()
{
MemberID = t.MemberID,
...
});
}
}
public class PersonRepository : IRepository<IPerson, int>
{
private Data.LSAEntities entities { get; set; }
public PersonRepository(Data.LSAEntities entities)
{
this.entities = entities;
}
public IQueryable<IPerson> Get(int key)
{
return (from p in entities.tblPersons
where p.PersonId == key
select new Person()
{
PersonId = p.PersonId,
...
Member = MemberRepository.Get(entities, m=> p.MemberId == m.MemberID).FirstOrDefault()
});
}
}
This may help you:
Given
var ans = from a in table
let b = a.TotalPrice / a.Quantity
where b > 500
select new {
PriceEa = b,
a.ID,
a.Description
};
translates to
var ans = table.Select(a => new { b = a.TotalPrice / a.Quantity, a })
.Where(ba => ba.b > 500)
.Select(ba => new {
PriceEa = ba.b,
ba.a.ID,
ba.a.Description
});
the let clause in query comprehension syntax in LINQ is translated to a Select adding a new field to hold the let value when using lambda syntax.

OrderBy nested property

I'm trying to use OrderBy for a nested property but I can't get it to work.
Models:
public class TPRenewalCycle
{
public virtual ICollection<TPCaseEvent> CaseEvents { get; set; }
}
public class TPCaseEvent
{
public DateTime? DueDate { get; set; }
}
Method:
List<TPRenewalCycle> cycles = renewalCycles
var nextRenewalCycle = cycles.OrderBy(cycle => cycle.CaseEvents.OrderBy(caseEvent => caseEvent.DueDate)).FirstOrDefault();
This gives me the runtime error:
At least one object must implement IComparable.
Is this due to the nullable DateTime or CaseEvents? How can I solve this?
In T-SQL I can do this:
SELECT CE.DueDate
FROM TPRenewalCycles RC
INNER JOIN TPCaseEvents CE on (CE.BusinessSystemId = RC.BusinessSystemId and CE.CaseId = RC.CaseId and CE.Action = RC.Action and CE.Cycle = RC.Cycle)
Order by CE.DueDate
Since OrderBy expression needs to supply a value to be used as the comparison key for the entire record, you need to select the earliest due date in the list:
var nextRenewalCycle = cycles
.OrderBy(cycle => cycle.CaseEvents.Select(caseEvent => caseEvent.DueDate).Min())
.FirstOrDefault();
If you are looking for the earliest date, as in your SQL query, you could use SelectMany instead:
var nextRenewalCycle = cycles
.SelectMany(cycle => cycle.CaseEvents)
.Select(caseEvent => caseEvent.DueDate)
.Min();

Member is "not supported in LINQ to Entities"

So I am new to C#, LINQ, and MVC. I am trying to get a list of Ages, but it says
The specified type member 'Age' is not supported in LINQ to Entities.
Only initializers, entity members, and entity navigation properties
are supported.
For a previous tutorial, they use this exact same logic, except they check a string, not an int (Age). Why is this giving me a fit, and how can I fix it?
public ActionResult SearchIndex(string ageValue, string searchString)
{
if (!string.IsNullOrEmpty(ageValue))
{
var AgeList = new List<string>();
var AgeListQry = from d in db.Actors orderby d.Age select d.Age.ToString();
AgeList.AddRange(AgeListQry.Distinct());
}
// other stuff
}
I want to learn what is going on, so that I can avoid this in the future!
Entity Model code
public class Actor
{
public int ID { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public int Age
{
get {
return (int)(DateTime.Now - BirthDate).TotalDays / 365;
}
}
public decimal NetValue { get; set; }
}
public class ActorDBContext : DbContext
{
public DbSet<Actor> Actors { get; set; }
}
As mentioned in the comments, you can't call ToString() in a Linq to Entities query. Instead do it like this:
var AgeList = new List<string>();
//retrieve as whatever type Age is, no conversion in SQL Server
var AgeListQry = (from d in db.Actors orderby d.Age select d.Age).ToList();
//convert them after the fact, using Linq to Objects
AgeList.AddRange(AgeListQry.Select(a => a.ToString()).Distinct());
EDIT
I saw your latest update that does show that Age is not a database column. You are then required to do something like this (assuming BirthDate is properly mapped):
var AgeList = new List<string>();
//retrieve BirthDate from SQL Server and use ToList() to get it to run immediately
var AgeListQry = (from d in db.Actors orderby d.BirthDate select d.BirthDate).ToList();
//convert them after the fact, using Linq to Objects
AgeList.AddRange(AgeListQry.Select(bd => ((int)(DateTime.Now - bd).TotalDays / 365).ToString()).Distinct());
Linq to Entities maps your expressions to SQL statements and there is nothing for it to map to when you use your Age property. Instead, you need to get what you can from SQL Server (BirthDate) and then do the translation to Age yourself. You could replace the inline code with a method call like this if you'd rather:
AgeList.AddRange(AgeListQry.Select(bd => CalculateAge(bd)).Distinct());
//...
private string CalculateAge(DateTime birthday)
{
return ((int)(DateTime.Now - bd).TotalDays / 365).ToString();
}
You haven't the Age in you DB scheme and it is impossible to convert LINQ to DB query.
You must order the Age collection in client side or add calculated column to your table.
There is another way. Have a converter file, where you pass the object, works with the birthdate and produces the age, returns the same object. That also means, that you can't search the database for the age column

How to make expression treat value type as a reference type?

I wanted to store a collection of expressions accessing object's properties. For example:
class Entity
{
public int Id { get; set; }
public Entity Parent { get; set; }
public string Name { get; set; }
public DateTime Date { get; set; }
public decimal Value { get; set; }
public bool Active { get; set; }
}
static void Main(string[] args)
{
var list = new List<Expression<Func<Entity, object>>>();
list.Add(e => e.Id);
list.Add(e => e.Name);
list.Add(e => e.Parent);
list.Add(e => e.Date);
list.Add(e => e.Value);
list.Add(e => e.Active);
StringBuilder b = new StringBuilder();
list.ForEach(f => b.AppendLine(f.ToString()));
Console.WriteLine(b.ToString());
Console.ReadLine();
}
This code outputs:
e => Convert(e.Id)
e => e.Name
e => e.Parent
e => Convert(e.Date)
e => Convert(e.Value)
e => Convert(e.Active)
It does add Convert to value types.
As far as in the end I wanted to use those expressions with LINQ to SQL, I need not to have that Convert in expressions, for them to be successfully translated to SQL.
How can I achieve this?
P.S.: expressions from this collection are later used as arguments to OrderBy and ThenBy methods.
If you create a function generic in the proeprty type you can avoid the Convert:
private static LambdaExpression GetExpression<TProp>
(Expression<Func<Entity, TProp>> expr)
{
return expr;
}
then you can change the type of list:
var list = new List<LambdaExpression>();
list.Add(GetExpression(e => e.Id));
list.Add(GetExpression(e => e.Name));
This will require you to create your OrderBy and ThenBy expressions using reflection e.g.
LambdaExpression idExpr = list[0];
Type keyType = idExpr.ReturnType;
var orderByMethod = typeof(Queryable).GetMethods()
.Single(m => m.Name == "OrderBy" && m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(Entity), keyType);
var ordered = (IQueryable<Entity>)
orderByMethod.Invoke(null, new object[] { source, idExpr });
I patched up a EF code first attempt at using your code like this
public class Entity
{
public int Id { get; set; }
public Entity Parent { get; set; }
public string Name { get; set; }
public DateTime Date { get; set; }
public decimal Value { get; set; }
public bool Active { get; set; }
}
public class EntityContext : DbContext
{
public EntityContext()
: base(new SqlCeConnection("Data Source=Database.sdf;Persist Security Info=False;"),
contextOwnsConnection: true)
{
// Using a SQL Compact database as backend
}
public DbSet<Entity> Entities { get; set; }
}
and attempted some linq on the context
static void Main(string[] args)
{
var list = new List<Expression<Func<Entity, object>>>();
list.Add(e => e.Date);
list.Add(e => e.Name);
using (var c = new EntityContext())
{
//each time a new record is added
var data = new Entity
{
Name = string.Format("Data{0}", c.Entities.Count()),
Date = DateTime.Now
};
c.Entities.Add(data);
c.SaveChanges();
// sort by date
foreach (var e in c.Entities.OrderBy(list.First().Compile()))
Console.WriteLine(string.Format("{0} - {1}", e.Name, e.Date));
// sort by name .. in reverse
foreach (var e in c.Entities.OrderByDescending(list.Last().Compile()))
Console.WriteLine(string.Format("{0} - {1}", e.Name, e.Date));
}
Console.ReadLine();
}
There were no issues running the code.
UPDATE The same holds true for LINQ to SQL: I built a table in a local SQL Server with the same structure as the class, and tried to OrderBy it : no problem.
My answer is "You don't need to worry about that".
Thank's to the answer by Alex I found out for myself that, when ordering the data I can use two different methods, depending on the specified argument:
Queryable.OrderBy Method with Expression<Func<TSource, TKey>>
Enumerable.OrderBy Method with Func<TSource, TKey>
When Queryable.OrderBy is used, LINQ compiles the OrderBy clause into the SQL statement, executed over the database. So when I try to give it a Expression<Func<TEntity, object>> that looks like e => Convert(e.Field), LINQ throws an InvalidOperationException, saying Cannot order by type 'System.Object'.
When Enumerable.OrderBy is used, LINQ does not compile the OrderBy clause into the SQL query, but executes the current query and applies sorting on the enumerable of entities, returned by the query, in the program's memory. Here no problem with ordering by Func<TEntity, object>.
So I found two alternatives here:
Query the database without sorting and order the returned result set
Provide better expressions to LINQ, that it could compile the SQL query, and then apply sorting in the database layer; here the answer by Lee suggests one way..
In my exact case sorting is the last operation to execute, and I don't see much harm, if I order the result set in the programm's memory...I'm not going to expect huge amounts of data to be returned...
Though in a more common case, probably it's still better to do all possible operations in the database layer...
P.S.: SO: Order a linq query - a close discussion...

Changing selected objects inside a query

I have a class that needs a property set inside a LINQ-to-SQL query. My first attempt was to have a "setter" method that would return the object instance and could be used in my select, like this:
public partial class Foo
{
public DateTime RetrievalTime { get; set; }
public Foo SetRetrievalTimeAndReturnSelf ( DateTime value )
{
RetrievalTime = value;
return this;
}
}
....
from foo in DataContext.GetTable<Foo> select foo.SetRetrievalTimeAndReturnSelf();
Unfortunately, such a query throws an exception like this: "System.NotSupportedException: Method 'Foo.SetRetrievalTime(System.DateTime)' has no supported translation to SQL".
Is there any alternative to converting the result to a list and iterating over it? The query is used in a custom "Get" method that wraps the DataContext.GetTable method, so will be used as the base for many other queries. Immediately converting a potentially-large result set to a list would not be optimal.
UPDATE
Here's a better example of what I'm trying to do, updated with Jason's proposed solution:
protected IQueryable<T> Get<T>() where T : class, ISecurable
{
// retrieve all T records and associated security records
var query = from entity in DataContext.GetTable<T> ()
from userEntityAccess in DataContext.GetTable<UserEntityAccess> ()
where userEntityAccess.SysUserId == CurrentUser.Id
&& entity.Id == userEntityAccess.EntityId
&& userEntityAccess.EntityClassName == typeof ( T ).Name
select new { entity, userEntityAccess };
return query.AsEnumerable ()
.Select ( item =>
{
item.entity.CanRead = item.userEntityAccess.CanRead;
item.entity.CanWrite = item.userEntityAccess.CanWrite;
item.entity.CanDelete = item.userEntityAccess.CanDelete;
return item.entity;
} ).AsQueryable ();
}
public interface ISecurable
{
int Id { get; set; }
bool CanRead { get; set; }
bool CanWrite { get; set; }
bool CanDelete { get; set; }
}
UserEntityAccess is a cross-reference table between a user and a business object record (i.e. an entity). Each record contains fields like "CanRead", "CanWrite", and "CanDelete", and determines what a specific user can do with a specific record.
ISecurable is a marker interface that must be implemented by any LINQ-to-SQL domain class that needs to use this secured Get method.
var projection = DataContext.GetTable<Foo>
.AsEnumerable()
.Select(f => f.SetRetrievalTimeAndReturnSelf());
This will then perform the invocation of SetRetrievalTimeAndReturnSelf for each instance of Foo in DataContext.GetTable<Foo> when the IEnumerable<Foo> projection is iterated over.
What do you need to know the time that object was yanked of the database for? That's potentially smelly.

Categories