I'm still new to Linq so if you see something I really shouldn't be doing, please feel free to suggest a change.
I am working on a new system to allow officers to sign up for overtime. Part of the data is displayed on a map with search criteria filtering unwanted positions. In order to make the data easier to work with, it is read into a hierarchy object structure using Linq. In this example, a job can contain multiple shifts and each shift can have multiple positions available. The Linq statement to read them in looks like the following.
var jobs = (from j in db.Job
join s in db.Shift on j.Id equals s.JobId into shifts
select new JobSearchResult
{
JobNumber = j.Id,
Name = j.JobName,
Latitude = j.LocationLatitude,
Longitude = j.LocationLongitude,
Address = j.AddressLine1,
Shifts = (from shift in shifts
join p in db.Position on shift.Id equals p.ShiftId into positions
select new ShiftSearchResult
{
Id = shift.Id,
Title = shift.ShiftTitle,
StartTime = shift.StartTime,
EndTime = shift.EndTime,
Positions = (from position in positions
select new PositionSearchResult
{
Id = position.Id,
Status = position.Status
}).ToList()
}).ToList()
});
That works fine and has been tested. There may be a better way to do it and if you know of a way, feel free to suggest. My problem is this. After the query is created, search criteria will be added. I know that I could add it when the query is created but for this its easier to do it after. Now, I can easy add criteria that looks like this.
jobs = jobs.Where(j => j.JobNumber == 1234);
However, I am having trouble figuring out how to do the same for Shifts or Positions. In other words, how would I could it to add the condition that a shift starts after a particular time? The following example is what I am trying to accomplish but will not (obviously) work.
jobs = jobs.Shifts.Where(s = s.StartTime > JobSearch.StartTime) //JobSearch.StartTime is a form variable.
Anyone have any suggestions?
Step 1: create associations so you can have the joins hidden behind EntitySet properties.
http://msdn.microsoft.com/en-us/library/bb629295.aspx
Step 2: construct your filters. You have 3 queryables and the possibility of filter interaction. Specify the innermost filter first so that the outer filters may make use of them.
Here are all jobs (unfiltered). Each job has only the shifts with 3 open positions. Each shift has those open positions.
Expression<Func<Position, bool>> PositionFilterExpression =
p => p.Status == "Open";
Expression<Func<Shift, bool>> ShiftFilterExpression =
s => s.Positions.Where(PositionFilterExpression).Count == 3
Expression<Func<Job, bool>> JobFilterExpression =
j => true
Step 3: put it all together:
List<JobSearchResult> jobs = db.Jobs
.Where(JobFilterExpression)
.Select(j => new JobSearchResult
{
JobNumber = j.Id,
Name = j.JobName,
Latitude = j.LocationLatitude,
Longitude = j.LocationLongitude,
Address = j.AddressLine1,
Shifts = j.Shifts
.Where(ShiftFilterExpression)
.Select(s => new ShiftSearchResult
{
Id = s.Id,
Title = s.ShiftTitle,
StartTime = s.StartTime,
EndTime = s.EndTime,
Positions = s.Positions
.Where(PositionFilterExpression)
.Select(p => new PositionSearchResult
{
Id = position.Id,
Status = position.Status
})
.ToList()
})
.ToList()
})
.ToList();
Related
Suppose I have two Lists of locations. First has all available locations:
List<Location> locations = new List<Location> {
new Location { id = 1, address = "1 Main St.", selected = false },
...
}
and another is a List or Array of my locations:
List<int> myLocations = new List<int> { 1, 5, 8 };
(IDs are unpredictable, but my locations are guaranteed to be a subset of all locations).
I want to outer join two lists and get a result with selected = true for the locations where locations.id eq myLocations.
If I use Join() or Zip() then I get inner join, in other words, I lose elements that do not exist in myLocations, and if I use the following -
var result = from loc in locations
join my in myLocations
on loc.id equals my into myloc
from m in myloc.DefaultIfEmpty()
select new Location {
id = loc.id, address = loc.address, selected = true
};
then all locations are marked as selected; not to mention that it looks excessively complex.
Is there a way to do what I want without looping through list elements?
Because you need to set the selected property for each location that are already in memory then you can just use the ForEach extension method like code below :
locations.ForEach(location => location.selected = myLocations
.Any(id => id == location.id)
);
With this code, you are not creating a new instances of Location in memory like it will be by using projection (select new Location). The same instance into your locations are used.
Based on what you state
my locations are guaranteed to be a subset of all locations
you could implement LEFT JOIN instead of OUTER JOIN.
Try following
locations.Select(l => new Location
{
id = l.id,
adress = l.adress,
selected = myLocations.Any(ml => ml == l.id)
})
You are so close.
In the last query you actually implemented so called left outer join, but the more appropriate for this scenario would be group join.
Since the left outer join in LINQ is actually implemented via group join, all you need is to remove the line
from m in myloc.DefaultIfEmpty()
and use selected = myloc.Any() like this
var result = from loc in locations
join my in myLocations
on loc.id equals my into myloc
select new Location {
id = loc.id, address = loc.address, selected = myloc.Any()
};
CodeNotFound's answer is efficient if it's OK to modify the original Location objects.
If you require something that will act purely as a query with no side effects (which is often more in the spirit of Linq), then the following will work.
This is similar to tchelidze's answer, but avoids creating new instances of all the objects that don't match:
locations.Select(l => myLocations.Any(i => i == l.id) ?
new Location {id = l.id, address = l.address, selected = true } : l);
or using query syntax:
from location in locations
select myLocations.Any(i => i == location.id)
? new Location {
id = location.id,
address = location.address,
selected = true }
: location;
This query I wrote is failing and I am not sure why.
What I'm doing is getting a list of user domain objects, projecting them to a view model while also calculating their ranking as the data will be shown on a leaderboard. This was how I thought of doing the query.
var users = Context.Users.Select(user => new
{
Points = user.UserPoints.Sum(p => p.Point.Value),
User = user
})
.Where(user => user.Points != 0 || user.User.UserId == userId)
.OrderByDescending(user => user.Points)
.Select((model, rank) => new UserScoreModel
{
Points = model.Points,
Country = model.User.Country,
FacebookId = model.User.FacebookUserId,
Name = model.User.FirstName + " " + model.User.LastName,
Position = rank + 1,
UserId = model.User.UserId,
});
return await users.FirstOrDefaultAsync(u => u.UserId == userId);
The exception message
System.NotSupportedException: LINQ to Entities does not recognize the method 'System.Linq.IQueryable`1[WakeSocial.BusinessProcess.Core.Domain.UserScoreModel] Select[<>f__AnonymousType0`2,UserScoreModel](System.Linq.IQueryable`1[<>f__AnonymousType0`2[System.Int32,WakeSocial.BusinessProcess.Core.Domain.User]], System.Linq.Expressions.Expression`1[System.Func`3[<>f__AnonymousType0`2[System.Int32,WakeSocial.BusinessProcess.Core.Domain.User],System.Int32,WakeSocial.BusinessProcess.Core.Domain.UserScoreModel]])' method, and this method cannot be translated into a store expression.
Unfortunately, EF does not know how to translate the version of Select which takes a lambda with two parameters (the value and the rank).
For your query two possible options are:
If the row set is very small small, you could skip specifying Position in the query, read all UserScoreModels into memory (use ToListAsync), and calculate a value for Position in memory
If the row set is large, you could do something like:
var userPoints = Context.Users.Select(user => new
{
Points = user.UserPoints.Sum(p => p.Point.Value),
User = user
})
.Where(user => user.Points != 0 || user.User.UserId == userId);
var users = userPoints.OrderByDescending(user => user.Points)
.Select(model => new UserScoreModel
{
Points = model.Points,
Country = model.User.Country,
FacebookId = model.User.FacebookUserId,
Name = model.User.FirstName + " " + model.User.LastName,
Position = 1 + userPoints.Count(up => up.Points < model.Points),
UserId = model.User.UserId,
});
Note that this isn't EXACTLY the same as I've written it, because two users with a tied point total won't be arbitrarily assigned different ranks. You could rewrite the logic to break ties on userId or some other measure if you want. This query might not be as nice and clean as you were hoping, but since you are ultimately selecting only one row by userId it hopefully won't be too bad. You could also split out the rank-finding and selection of base info into two separate queries, which might speed things up because each would be simpler.
I retrieve data from two different repositories:
List<F> allFs = fRepository.GetFs().ToList();
List<E> allEs = eRepository.GetEs().ToList();
Now I need to join them so I do the following:
var EFs = from c in allFs.AsQueryable()
join e in allEs on c.SerialNumber equals e.FSerialNumber
where e.Year == Convert.ToInt32(billingYear) &&
e.Month == Convert.ToInt32(billingMonth)
select new EReport
{
FSerialNumber = c.SerialNumber,
FName = c.Name,
IntCustID = Convert.ToInt32(e.IntCustID),
TotalECases = 0,
TotalPrice = "$0"
};
How can I make this LINQ query better so it will run faster? I would appreciate any suggestions.
Thanks
Unless you're able to create one repository that contains both pieces of data, which would be a far preferred solution, I can see the following things which might speed up the process.
Since you'r always filtering all E's by Month and Year, you should do that before calling ToList on the IQueryable, that way you reduce the number of E's in the join (probably considerably)
Since you're only using a subset of fields from E and F, you can use an anonymous type to limit the amount of data to transfer
Depending on how many serialnumbers you're retrieving from F's, you could filter your E's by serials in the database (or vice versa). But if most of the serialnumbers are to be expected in both sets, that doesn't really help you much further
Reasons why you might not be able to combine the repositories into one are probably because the data is coming from two separate databases.
The code, updated with the above mentioned points 1 and 2 would be similar to this:
var allFs = fRepository.GetFs().Select(f => new {f.Name, f.SerialNumber}).ToList();
int year = Convert.ToInt32(billingYear);
int month = Convert.ToInt32(billingMonth);
var allEs = eRepository.GetEs().Where(e.Year == year && e.Month == month).Select(e => new {e.FSerialNumber, e.IntCustID}).ToList();
var EFs = from c in allFs
join e in allEs on c.SerialNumber equals e.FSerialNumber
select new EReport
{
FSerialNumber = c.SerialNumber,
FName = c.Name,
IntCustID = Convert.ToInt32(e.IntCustID),
TotalECases = 0,
TotalPrice = "$0"
};
I have some data in a List of User defined types that contains the following data:
name, study, group, result, date. Now I want to obtain the name, study and group and then a calculation based onthe result and date. The calculation is effectively:
log(result).where max(date) minus log(result).where min(date)
There are only two dates for each name/study/group, so the result from the maximum data (log) minus the result from the minumum date (log). here is what I have tried so far with no luck:
var result =
from results in sortedData.AsEnumerable()
group results by results.animal
into grp
select new
{
animal = results.animal,
study = results.study,
groupNumber = results.groupNumber,
TGI = System.Math.Log(grp.Select(c => c.volume)
.Where(grp.Max(c=>c.operationDate)))
- System.Math.Log(grp.Select(c => c.volume)
.Where(grp.Min(c => c.operationDate)))
};
Anybody any pointers? Thanks.
It isn't entirely clear how the grouping relates to your problem (what sense does it make to extract a property from a range variable after it has been grouped?), but the part you're having difficult with can be solved easily with MaxBy and MinBy operators, such as the ones that come with morelinq.
var result = from results in sortedData.AsEnumerable()
group results by results.animal into grp
select new
{
animal = grp.Key,
study = ??,
groupNumber = ??,
TGI = Math.Log(grp.MaxBy(c => c.operationDate).volume)
- Math.Log(grp.MinBy(c => c.operationDate).volume)
};
Otherwise, you can simulate these operators with Aggregate, or if you don't mind the inefficiency of sorting:
var result = from results in sortedData.AsEnumerable()
group results by results.animal into grp
let sortedGrp = grp.OrderBy(c => c.operationDate)
.ToList()
select new
{
animal = grp.Key,
study = ??,
groupNumber = ??,
TGI = sortedGrp.Last().volume - sortedGrp.First().volume
};
You have a few syntax problems, you cannot use the results parameter after your into grp line. So my initial attempt would be to change your statement like so
var result =
from results in sortedData.AsEnumerable()
group results by new
{
Animal = results.animal,
Study = results.study,
GroupNumber = results.groupNumber
}
into grp
select new
{
animal = grp.Key.Animal,
study = grp.Key.Study,
groupNumber = grp.Key.GroupNumber,
TGI = System.Math.Log(grp.OrderByDescending(c=>c.operationDate).First().volume)
- System.Math.Log(grp.OrderBy(c=>c.operationDate).First().volume)
};
I have two IList<Traffic> I need to combine.
Traffic is a simple class:
class Traffic
{
long MegaBits;
DateTime Time;
}
Each IList holds the same Times, and I need a single IList<Traffic>, where I have summed up the MegaBits, but kept the Time as key.
Is this possible using Linq ?
EDIT:
I forgot to mention that Time isn't necessarily unique in any list, multiple Traffic instances may have the same Time.
Also I might run into X lists (more than 2), I should had mentioned that as well - sorry :-(
EXAMPLE:
IEnumerable<IList<Traffic>> trafficFromDifferentNics;
var combinedTraffic = trafficFromDifferentNics
.SelectMany(list => list)
.GroupBy(traffic => traffic.Time)
.Select(grp => new Traffic { Time = grp.Key, MegaBits = grp.Sum(tmp => tmp.MegaBits) });
The example above works, so thanks for your inputs :-)
this sounds more like
var store = firstList.Concat(secondList).Concat(thirdList)/* ... */;
var query = from item in store
group item by item.Time
into groupedItems
select new Traffic
{
MegaBits = groupedItems.Sum(groupedItem => groupedItem.MegaBits),
Time = groupedItems.Key
};
or, with your rework
IEnumerable<IList<Traffic>> stores;
var query = from store in stores
from item in store
group item by item.Time
into groupedItems
select new Traffic
{
MegaBits = groupedItems.Sum(groupedItem => groupedItem.MegaBits),
Time = groupedItems.Key
};
You could combine the items in both lists into a single set, then group on the key to get the sum before transforming back into a new set of Traffic instances.
var result = firstList.Concat(secondList)
.GroupBy(trf => trf.Time, trf => trf.MegaBits)
.Select(grp => new Traffic { Time = grp.Key, MegaBits = grp.Sum()});
That sounds like:
var query = from x in firstList
join y in secondList on x.Time equals y.Time
select new Traffic { MegaBits = x.MegaBits + y.MegaBits,
Time = x.Time };
Note that this will join in a pair-wise fashion, so if there are multiple elements with the same time in each list, you may not get the results you want.