Encapsulate queries to Navigation Properties [duplicate] - c#

This question already has answers here:
LINQ to Entities does not recognize the method
(5 answers)
Closed 8 years ago.
To make my top layers more readable I usually make extension methods to encapsulate long hard-to-read queries into something as simple as db.Matches.By(period)
This 'By' method looks something like this:
public static IQueryable<PlayedMatch> By(this IQueryable<PlayedMatch> matches, Period period)
{
return matches.Where(pm => pm.Details.DateTime >= period.Start && pm.Details.DateTime < period.End);
}
Problem is that I would like to have something similar for querying Navigation Properties, so I could do something like this:
var query = Db.Players.Select( p => new
{
Player = p,
TotalPoints = p.Matches.By(period).Sum(m => m.Points)
});
Problem is that first of all Navigation Properties are of type ICollection<>. Second is that when I change the extension method to use IEnumerable<> or ICollection<> I get the following exception while running the query:
LINQ to Entities does not recognize the method 'System.Collections.Generic.IEnumerable'1[Match] By(System.Collections.Generic.ICollection`1[Match], Period)' method, and this method cannot be translated into a store expression.
Question:
Is there any other way for me to encapsulate queries on navigation properties like I do with my normal queries?

You'll need to add an extension method for each type:
public static IQueryable<PlayedMatch> By(this IQueryable<PlayedMatch> matches, Period period)
{
return matches.Where(pm => pm.Details.DateTime >= period.Start && pm.Details.DateTime < period.End);
}
public static ICollection<PlayedMatch> By(this ICollection<PlayedMatch> matches, Period period)
{
return matches.Where(pm => pm.Details.DateTime >= period.Start && pm.Details.DateTime < period.End);
}
public static IEnumerable<PlayedMatch> By(this IEnumerable<PlayedMatch> matches, Period period)
{
return matches.Where(pm => pm.Details.DateTime >= period.Start && pm.Details.DateTime < period.End);
}
The compiler will pick the most appropriate at compile time.

Linq-to-Entities cannot translate your By method into sql. It would work if you brought all the players into memory because then you'd be using Linq-to-Objects and it can work with your C# code:
var query = Db.Players
.AsEnumerable //pulls all players into memory
.Select( p => new
{
Player = p,
TotalPoints = p.Matches.By(period).Sum(m => m.Points)
});
But you probably don't want to pay the price of bringing all that data into memory....
If you want to encapsulate long hard-to-read queries, you could declare them as fields. Then you could do something like this:
Func<Bar, bool> NameIsTom = b => b.Name == "Tom";
Foos.Select(f => new { Foo = f, Toms = f.Bars.Where(NameIsTom) });

Related

Is it possible to put parts of a Linq statement in a variable?

I am coming from Perl doing first steps with C# and Linq.
My question is if there is a way to put parts of a Linq statement in a variable like this (Code only for short illustration, not tested) to use it several times more easily and flexible than writing it new every time. Also one could use different Order-Statements (Select-Case, Array) for the same MQuery.
var orderstatement = "orderby m.Reihenfolge ascending, m.Datum descending, m.Titel ascending";
var MQuery = (from m in _context.Movie
orderstatement
where m.Sichtbar == true && m.Gesperrt == false
select m;
Thanks for advising.
You can try something like this with extension methods. First, define the order by movies method
public static IQueryable<Movie> OrderMovies(this IQueryable<Movie> movies) {
return movies.OrderBy(m => m.Reihenfolge).ThenByDescending(m => m.Datum).ThenBy(m => m.Title);
}
And then use it in the movies query
var movies = _context.Movies.Where(m => m.Sichtbar == true && m.Gesperrt == false).OrderMovies().Select(m => m)

How to write a method for OrderBy lambda expression

I have the code:
var trips = _db.Trips
.OrderBy(u => u.TripStops.Where(p=>p.StopTypeId == destinationTypeId).OrderByDescending(c=>c.StopNo).Select(p=>p.Appt).FirstOrDefault())
and this part (sort by appt for the last TripStop with type = destinationTypeId) should be used in many places in code.
I want to write a method like:
private xxx LastTripStopAppt(...)
{
}
and then use it like:
var trips = _db.Trips
.OrderBy(LastTripStopAppt(u))
But a little confused how to implement this method correctly.
PS. I have tried to do it as
private DateTime? ReturnLastDeliveryAppointment(Trip u, int destinationTypeId)
{
return u.TripStops.Where(p => p.StopTypeId == destinationTypeId).OrderByDescending(c => c.StopNo).Select(p => p.Appt).FirstOrDefault();
}
and then
.OrderBy(u => ReturnLastDeliveryAppointment(u, destinationTypeId))
but I get an error:
LINQ to Entities does not recognize the method
'System.Nullable`1[System.DateTime]
ReturnLastDeliveryAppointment(Infrastructure.Asset.Trips.Trip, Int32)'
method, and this method cannot be translated into a store expression.
The signature is probably something like:
private Expression<Func<Trip, apptType>> LastTripStopAppt(...)
where appType is the type of p.Appt
You'll need to pass as a parameter to this method the destinationTypeId.
So, if appType is a string:
private static Expression<Func<Trip, string>> LastTripStopAppt(int destinationTypeId)
{
return u => u.TripStops.Where(p=>p.StopTypeId == destinationTypeId).OrderByDescending(c=>c.StopNo).Select(p=>p.Appt).FirstOrDefault();
}
SELECT *
FROM (VALUES(1), (4), (3)) t(v)
ORDER BY T;
Java 8:
Stream s = Stream.of(11, 41, 3);
s.sorted()
.forEach(System.out::println);
output:
3
11
41

LINQ how to query if a value is between a list of ranges?

Let's say I have a Person record in a database, and there's an Age field for the person.
Now I have a page that allows me to filter for people in certain age ranges.
For example, I can choose multiple range selections, such as "0-10", "11-20", "31-40".
So in this case, I'd get back a list of people between 0 and 20, as well as 30 to 40, but not 21-30.
I've taken the age ranges and populated a List of ranges that looks like this:
class AgeRange
{
int Min { get; set; }
int Max { get; set; }
}
List<AgeRange> ageRanges = GetAgeRanges();
I am using LINQ to SQL for my database access and queries, but I can't figure out how query the ranges.
I want to do something like this, but of course, this won't work since I can't query my local values against the SQL values:
var query = from person in db.People
where ageRanges.Where(ages => person.Age >= ages.Min && person.Age <= ages.Max).Any())
select person;
You could build the predicate dynamically with PredicateBuilder:
static Expression<Func<Person, bool>> BuildAgePredicate(IEnumerable<AgeRange> ranges)
{
var predicate = PredicateBuilder.False<Person>();
foreach (var r in ranges)
{
// To avoid capturing the loop variable
var r2 = r;
predicate = predicate.Or (p => p.Age >= r2.Min && p.Age <= r2.Max);
}
return predicate;
}
You can then use this method as follows:
var agePredicate = BuildAgePredicate(ageRanges);
var query = db.People.Where(agePredicate);
As one of your errors mentioned you can only use a local sequence with the 'Contains' method. One option would then be to create a list of all allowed ages like so:
var ages = ageRanges
.Aggregate(new List<int>() as IEnumerable<int>, (acc, x) =>
acc.Union(Enumerable.Range(x.Min,x.Max - (x.Min - 1)))
);
Then you can call:
People.Where(x => ages.Contains(x.Age))
A word of caution to this tale, should your ranges be large, then this will FAIL!
(This will work well for small ranges (your max number of accepted ages will probably never exceed 100), but any more than this and both of the above commands will become VERY expensive!)
Thanks to Thomas' answer, I was able to create this more generic version that seems to be working:
static IQueryable<T> Between<T>(this IQueryable<T> query, Expression<Func<T, decimal>> predicate, IEnumerable<NumberRange> ranges)
{
var exp = PredicateBuilder.False<T>();
foreach (var range in ranges)
{
exp = exp.Or(
Expression.Lambda<Func<T, bool>>(Expression.GreaterThanOrEqual(predicate.Body, Expression.Constant(range.Min)), predicate.Parameters))
.And(Expression.Lambda<Func<T, bool>>(Expression.LessThanOrEqual(predicate.Body, Expression.Constant(range.Max)), predicate.Parameters));
}
return query.Where(exp);
}
Much simpler implementation is to use Age.CompareTo()
I had a similar problem and solved it using CompareTo
In a database of houses, I want to find houses within the range max and min
from s in db.Homes.AsEnumerable()
select s;
houses = houses.Where( s=>s.Price.CompareTo(max) <= 0 && s.Price.CompareTo(min) >= 0 ) ;

Custom function in Entity Framework query sometimes translates properly, sometimes doesn't

I have this function:
public static IQueryable<Article> WhereArticleIsLive(this IQueryable<Article> q)
{
return q.Where(x =>
x != null
&& DateTime.UtcNow >= x.PublishTime
&& x.IsPublished
&& !x.IsDeleted);
}
And it works just fine in this query:
from a in Articles.WhereArticleIsLive()
where a.Id == 5
select new { a.Title }
But it doesn't work in this only slightly more complex query:
from s in Series
from a in Articles.WhereArticleIsLive()
where s.Id == a.SeriesId
select new { s, a }
I get this error message:
NotSupportedException: LINQ to Entities does not recognize the method 'System.Linq.IQueryable1[TheFraser.Data.Articles.Article] WhereArticleIsLive(System.Linq.IQueryable1[TheFraser.Data.Articles.Article])' method, and this method cannot be translated into a store expression.
Any idea why? Is there another way to consolidate query parameters like this?
Thanks in advance.
EDIT: corrections by Craig.
I'm leaving this here, because I think it's a valuable tool: Use linqkit! But not for solving this question though :-)
Instead of returning IQueryable, use Expression to factor out predicates. E.g. you could define the following static method on Article:
public static Expression<Func<Article,bool>> IsLive()
{
return x =>
x != null
&& DateTime.UtcNow >= x.PublishTime
&& x.IsPublished
&& !x.IsDeleted
}
Then, ensure to store a reference to this expression when building your query, something along the lines of (not tested):
var isLive = Article.IsLive();
from s in Series
from a in Articles.Where(isLive)
where s.Id == a.SeriesId
select new { s, a }

How to refactor multiple similar Linq-To-Sql queries?

Suppose I have the two following Linq-To-SQL queries I want to refactor:
var someValue1 = 0;
var someValue2= 0;
var query1 = db.TableAs.Where( a => a.TableBs.Count() > someValue1 )
.Take( 10 );
var query2 = db.TableAs.Where( a => a.TableBs.First().item1 == someValue2)
.Take( 10 );
Note that only the Where parameter changes. There is any way to put the query inside a method and pass the Where parameter as an argument?
All the solutions posted in the previous question have been tried and failed in runtime when I try to enumerate the result.
The exception thrown was: "Unsupported overload used for query operator 'Where'"
Absolutely. You'd write:
public IQueryable<A> First10(Expression<Func<A,bool>> predicate)
{
return db.TableAs.Where(predicate).Take(10);
}
(That's assuming that TableA is IQueryable<A>.)
Call it with:
var someValue1 = 0;
var someValue2= 0;
var query1 = First10(a => a.TableBs.Count() > someValue1);
var query2 = First10(a => a.TableBs.First().item1 == someValue2);
I believe that will work...
The difference between this and the answers to your previous question is basically that this method takes Expression<Func<T,bool>> instead of just Func<T,bool> so it ends up using Queryable.Where instead of Enumerable.Where.
If you really want reusability you can try to write your own operators. E.g. instead of repeatedly writing:
var query =
Products
.Where(p => p.Description.Contains(description))
.Where(p => p.Discontinued == discontinued);
you can write simple methods:
public static IEnumerable<Product> ByName(this IEnumerable<Product> products, string description)
{
return products.Where(p => p.Description.Contains(description));
}
public static IEnumerable<Product> AreDiscontinued(IEnumerable<Product> products, bool isDiscontinued)
{
return products.Where(p => p.Discontinued == discontinued);
}
and then use it like this:
var query = Products.ByName("widget").AreDiscontinued(false);

Categories