Spliting up long linq queries for improved maintainability - c#

I have a bunch of these Tasks that are all based on LINQ queries. I am looking for good way to refactor them and make them easier to read and allow me to change the queries depending on language/region etc.
var mailTaskOne = CreateTask(() => myService.Mail.Where(p => p.ProjectName == "Delta"
&& (p.MailLang== (int)MailLanguage.EU || p.MailLang == (int)MailLanguage.RU)
&& (p.DateEntered >= startDate && p.DateEntered <= endDate)
&& p.MailPriority == (int)MailPriority.High).Count());
One of the ways I thought would be convenient would be to split the query up into something like this.
var results = myService.Mail.Where(x => x.ProjectName == "Delta");
results = results.Where(p => p.MailLang== (int)MailLanguage.EU);
results = results.Where(p => p.DateModified >= startDate && p.DateModified <= endDate);
This would allow me to do this without having to repeat the whole query for each region.
if (MailLanguage == "English")
results = results.Where(p => p.MailLang== (int)MailLanguage.EU);
else
results = results.Where(p => p.MailLang== (int)MailLanguage.RU);
Is there anyone that knows a better solution for this? I end up having huge functions as I need to do maybe 20 of these queries depending on the requirements; such as Region, Project name etc.
Edit:
Due to some limitations I did not know of with the back-end (web service/api) I could unfortunately not use some of the awesome answers mentioned in this question.
For example this does not get translated properly, but in no ways because the answer incorrect, simply does not work with the API I am working against -- possibly because it is poorly implemented.
public bool IsValid(Type x)
{
return (x.a == b) && (x.c ==d) && (x.d == e);
}
Anyway, anyone looking for similar solutions all of these are valid answers, but in the end I ended up going with something similar to the solution snurre provided.

I would go with just splitting up the query onto different lines like you suggested, it means you can put comments per line to describe what it is doing. You are still only making 1 trip to the database so you aren't losing anything in terms of performance but gaining better readability.

Why not simply have a method for the purpose?
public static IQueryable<Mail> Count(this IQueryable<Mail> mails,
string projectName,
MailLanguage mailLanguage,
DateTime startDate,
DateTime endDate) {
return mails.Count(p=>
p.ProjectName == projectName
&& p.MailLang == mailLanguage
&& p.DateEntered >= startDate
&& p.DateEntered <= endDate
&& p.MailPriority == (int)MailPriority.High);
}
then you can simply use it like this
CreateTask(() => myService.Mail.Count("Delta",MailLanguage.EU,startDate,endDate));

You could turn project name, data modified, mail language and any other criteria into variables and guive them the value you want based on any condition. Then your query would use the variables not the literal values.
var projectName="Delta";
var mailLanguage=(int)MailLanguage.RU;
var results=myService.Mail.Where(x => x.ProjectName == projectName)
&& (p.MailLang== mailLanguage);
That way you can put most of the complexity in giving the values to the variables and the linq query would be easier to read and mantain.

You could create a parameter class like:
public class MailParameters
{
public DateTime EndTime { get; private set; }
public IEnumerable<int> Languages { get; private set; }
public int Priority { get; private set; }
public string ProjectName { get; private set; }
public DateTime StartTime { get; private set; }
public MailParameters(string projectName, DateTime startTime, DateTime endTime, MailLang language, Priority priority)
: this(projectName, startTime, endTime, new[] { language }, priority)
public MailParameters(string projectName, DateTime startTime, DateTime endTime, IEnumerable<MailLang> languages, Priority priority)
{
ProjectName = projectName;
StartTime = startTime;
EndTime = endTime;
Languages = languages.Cast<int>();
Priority = (int)priority;
}
}
Then add these extension methods:
public static int Count(this IQueryable<Mail> mails, MailCountParameter p)
{
return mails.Count(m =>
m.ProjectName == p.ProjectName &&
p.Languages.Contains(m.MailLang) &&
m.EnteredBetween(p.StartTime, p.EndTime) &&
m.Priority == p.Priority);
}
public static bool EnteredBetween(this Mail mail, DateTime startTime, DateTime endTime)
{
return mail.DateEntered >= startTime && mail.DateEntered <= endTime;
}
The usage would then be:
var mailParametersOne = new MailParameters("Delta", startDate, endDate, new[] { MailLang.EU, MailLang.RU }, MailPriority.High);
var mailTaskOne = CreateTask(() => myService.Mail.Count(mailParametersOne));

Consider moving the complex comparisons into a function. For exanple, instead of
Results.Where(x => (x.a == b) && (x.c == d) && (x.d == e))
consider
Results.Where(x => IsValid(x))
...
public bool IsValid(Type x)
{
return (x.a == b) && (x.c ==d) && (x.d == e);
}
The code becomes more readable and IsValid is easy to test using an automated testing framework.

My final solution is based on an article by ScottGu.
http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx
I build the LINQ query like this.
var linqStatements = new List<String>();
linqStatements.Add(parser.StringToLinqQuery<Project>("ProjectId", report.Project));
linqStatements.Add(parser.StringToLinqQuery<Region>("RegionId", report.Region));
linqStatements.Add(parser.StringToLinqQuery<Status>("Status", report.Status));
linqStatements.Add(parser.StringToLinqQuery<Priority>("Priority", report.Priority));
linqStatements.Add(parser.StringToLinqQuery<Category>("CategoryId", report.Category));
linqStatements.Add(AccountIdsToLinqQuery(report.PrimaryAssignment));
string baseQuery = String.Join(" AND ", linqStatements.Where(s => !String.IsNullOrWhiteSpace(s)));
var linqQuery = service.Mail.Where(baseQuery).Cast<Mail>();
The StringToLinqQuery looks something like this (simplified version).
public string StringToLinqQuery<TEnum>(string field, string value) where TEnum : struct
{
if (String.IsNullOrWhiteSpace(value))
return String.Empty;
var valueArray = value.Split('|');
var query = new StringBuilder();
for (int i = 0; i < valueArray.Count(); i++)
{
TEnum result;
if (Enum.TryParse<TEnum>(valueArray[i].ToLower(), true, out result))
{
if (i > 0)
query.Append(" OR ");
query.AppendFormat("{0} == {1}", field, Convert.ToInt32(result));
}
else
{
throw new DynoException("Item '" + valueArray[i] + "' not found. (" + type of (TEnum) + ")",
query.ToString());
}
}
// Wrap field == value with parentheses ()
query.Insert(0, "(");
query.Insert(query.Length, ")");
return query.ToString();
}
And the end result would look something like this.
service.Mail.Where("(ProjectId == 5) AND (RegionId == 6 OR RegionId == 7) AND (Status == 5) and (Priority == 5)")
In my project I store the values in an XML file, and then feed them into the above LINQ query. If an field is empty it will be ignored. It also support multiple values using the | sign, e.g. EU|US would translate to (Region == 5 OR Region == 6).

Related

Increment outer loop from inner loop in C# using Foreach

I am trying to search through a group of days, and determine if a worker has worked that day, and get a total of days worked. The below works, but is terribly inefficient since even after it finds a guy worked a day it keeps looking through the rest of those days. If I could somehow increment the outer ForEach loop when the inner condition (day worked) is satisfied it would surely be faster.
totalDaysWorked is what I'm after below:
public class StationSupportRequest
{
public string RequestNum;
public string Status;
public string Prefix;
public string PlantLoc;
public DateTime Date;
public string Departmnt;
public DateTime Time;
public string StationID;
public string Fixture;
public string Supervisor;
public string PartNo;
public string SerialNum;
public string FailedStep;
public string Reason;
public string OtherReason;
public string Details;
public string Urgency;
public DateTime Date_1;
public DateTime Time_1;
public DateTime Date_2;
public DateTime Time_2;
public string ProblemFound;
public string SolutionCode;
public string Solution;
public double ServiceTechHrs;
public double ServiceEngHrs;
public string DocHistory;
public DateTime CloseDate;
public DateTime IniDate;
public DateTime IniTime;
public string MOT;
public string Initiator;
public string Notification;
public string ServiceTech;
public string ServiceEng;
public string SolutionCode_1;
public string Solution_1;
public string UpdatedBy;
public List<string> UpdatedByList;
public string Revisions;
public List<DateTime> RevisionsDateTime;
public List<WorkedDatapoint> WorkedDataPointsList;
}
public class WorkedDatapoint
{
public string AssignerName { get; set; }
public string AssigneeName { get; set; }
public DateTime Date { get; set; }
public bool AssignedToOther { get; set; }
}
var DateRange = SSRList.Where(y => y.IniDate >= IniDate && y.CloseDate < EndDate);
//DateRange = DateRange.Where(dr => dr.Fixture != null && dr.Fixture.Length == 6); //To get valid fixtures if pivoting on "Fixture"
var groupedData = DateRange.GroupBy(x => new { DS = x.ServiceTech }).Select(x =>
{
double totalSsrsWorkedOn = x.Select(y => y.RequestNum).Count();
IEnumerable<TimeSpan> hoursWorked = x.Select(y => y.CloseDate - y.IniDate.AddDays(GetWeekendDaysToSubtract(y.IniDate, y.CloseDate)));
var averageReactionTimeMinutes = x.Where(d => d.IniDate != null && d.Revisions != null)
.Average(d => ((DateTime.Parse(d.Revisions.Split(',')[0]) - (DateTime)d.IniDate)).Minutes);
double[] listOfMinutesOpenTime = x.Where(d => d.IniDate != null && d.Revisions != null)
.Select(d => Convert.ToDouble(((DateTime.Parse(d.Revisions.Split(',')[0]) - (DateTime)d.IniDate)).Minutes))
.ToArray();
double[] listOfDaysOpenTime = x.Where(d => d.IniDate != null && d.CloseDate != null)
.Select(d => ((DateTime)d.CloseDate - (DateTime)d.IniDate.AddDays(GetWeekendDaysToSubtract(d.IniDate, d.CloseDate))).TotalDays)
.ToArray();
string testtech = x.Select(y => y.ServiceTech).FirstOrDefault();
List<DateTime> totalDaysInDateRange = Enumerable.Range(0, 1 + EndDate.Subtract(IniDate).Days)
.Select(offset => IniDate.AddDays(offset)).ToList();
double totalHoursLogged = x.Sum(d => d.ServiceEngHrs) + x.Sum(d => d.ServiceTechHrs);
int assignedToOthersCount = x.SelectMany(y => y.WorkedDataPointsList)
.Where(z => z.AssignerName.Contains(testtech) && z.AssignedToOther == true)
.Count();
int brokenWiresFixed = x.Where(d => d.SolutionCode != null)
.Where(d => d.SolutionCode.Contains("A01 -") ||
d.SolutionCode.Contains("F01 -") ||
d.SolutionCode.Contains("S01 -")).Count();
int npfResults = x.Where(d => d.ProblemFound != null).Where(d => d.ProblemFound.Contains("NPF")).Count();
int totalDaysWorked = 0;
List<DateTime> workingDatesList = new List<DateTime>();
totalDaysInDateRange.ForEach((day) =>
{
x.Select(y => y.WorkedDataPointsList).ForEach((WorkedDataPoint) =>
{
IEnumerable<WorkedDatapoint> dateList = WorkedDataPoint
.Where(y => testtech == y.AssignerName)
.DistinctBy(z => z.Date.Date);
foreach ( WorkedDatapoint date in dateList)
{
if (x.Any(b => b.Date.Date.Date == date.Date.Date.Date))
{
workingDatesList.Add(date.Date.Date.Date);
break;
}
}
});
});
workingDatesList.Dump("WorkingDatesList");
totalDaysWorked = workingDatesList.DistinctBy(b => b.Date).Count();
/*int totalDaysWorked = 0;
totalDaysInDateRange.ForEach((day) =>
{
if (AssignersList.Where(d => testtech.Contains(d.AssignerName))
.DistinctBy(d => d.Date.Date)
.Any(d => d.Date.Date == day.Date))
{
totalDaysWorked++;
}
}); TODO: Delete this once new is working*/
return new
{
//SSRs = x,
//Station = x.Select(d => d.StationID).FirstOrDefault(),
//Fixture = x.Select(d => d.Fixture).FirstOrDefault(),
//ProductTested = x.Select(d => d.Details).FirstOrDefault(),
TestTech = testtech,
//TestEng = x.Select(d => d.ServiceEng).Distinct().Where(d => d.Length > 0),
TotalSSRsWorkedOn = Math.Round(totalSsrsWorkedOn, 4),
TotalHoursLogged = Math.Round(totalHoursLogged, 4),
AssignedToOthersCount = assignedToOthersCount,
AssignedToOthersPercentage = 100 * Math.Round(assignedToOthersCount / (assignedToOthersCount + totalSsrsWorkedOn), 4),
//AverageReactionTimeMinutes = averageReactionTimeMinutes,
AverageTimeToCompleteHours = x.Where(y => y.CloseDate != null && y.Time_1 != null && y.Time_1 != DateTime.MinValue).Select(z => (z.CloseDate - z.Time_1).TotalHours).Average(),
//Close = x.Where(y => y.CloseDate != null && y.Time_1 != null).Select(z => (z.CloseDate)),
//Time = x.Where(y => y.CloseDate != null && y.Time_1 != null).Select(z => (z.Time_1)),
MedianDaysRequestOpen = Math.Round(GetMedian(listOfDaysOpenTime), 3),
DaysWorkedPerDateRange = totalDaysWorked,
AveSSRsClosedPerWorkedDay = Math.Round(totalSsrsWorkedOn / totalDaysWorked, 3),
AveHoursLoggedPerRequest = Math.Round((x.Select(y => y.ServiceTechHrs + y.ServiceEngHrs).Sum()) / totalSsrsWorkedOn, 3),
BrokenWiresFixed = brokenWiresFixed,
PercentageBrokenWires = 100 * Math.Round(brokenWiresFixed / totalSsrsWorkedOn, 4),
NPFResults = npfResults,
PercentageNPF = 100 * Math.Round(npfResults / totalSsrsWorkedOn, 4),
};
}).OrderByDescending(x => x.TotalSSRsWorkedOn)
.Dump("Summary");
return;
Sample output, with the duplicate dates evaluated (workingDatesList):
8/1/2017 12:00:00 AM
8/1/2017 12:00:00 AM
8/1/2017 12:00:00 AM
8/2/2017 12:00:00 AM
A couple of comments on the code you posted:
Since you don't ever use the day variable from the outermost loop, simply remove that loop altogether.
Why are you testing whether x.Any(...) within a loop that iterates over y? This seems fundamentally flawed.
I can't discern from your problem statement what your data structures are, nor what it is that you are actually trying to do. Your problem statement is currently worded as:
I am trying to search through a group of days, and determine if a worker has worked that day, and get a total of days worked.
It appears you are taking some input called testtech (String) and totalDaysInDateRange (List<DateTime>), then want to find all entries in some data structure x (I can't infer what this is) where String.equalsIgnoreCase(y.AssignerName, testtech) && totalDaysInDateRange.contains(y.Date). Is this interpretation correct?
If so, simply iterate over the entries in whatever your x data structure is, and run the above logic. If this doesn't solve your problem, then please give us more information on the layout of the data structure x and how information about each worker is actually associated with the other data about that worker.
BEGIN EDIT
OK, now that you have provided more information, I think you want to replace the totalDaysInDateRange.ForEach statement with the following:
x.Select(y => y.WorkedDataPointsList).ForEach((wdp) =>
{
if (testtech == wdp.AssignerName && IniDate.Date <= wdp.Date.Date
&& wdp.Date.Date <= EndDate.Date)
{
workingDatesList.Add(wdp.Date.Date);
}
});
After changing your implementation, simply delete totalDaysInDateRange. I also recommend changing the type of workingDatesList to HashSet<DateTime>, since you don't seem to care about duplicate dates. Be sure to convert workingDatesList to a list and sort it once the loop is complete if you want the dates printed in chronological order.

Using T-SQL DATEFROMPARTS() in C# Linq

I have the following T-SQL query which I'm trying to convert into C# code:
SELECT
*, DATEFROMPARTS(Jaar, Maand, 1)
FROM
EAN_Budget_Meetdata_Factuur
WHERE
EID = 1 AND
Bron = 'B' AND
DATEFROMPARTS(Jaar, Maand, 1) BETWEEN '20161101' AND '20170101'
The table contains a column Year and Month, and I'm querying the table based on two known DateTimes (a period).
The query is meant to retrieve the table rows that apply to this period based on the years and months between the two DateTimes.
What is the best way to convert this query into C#? I've so far come up with the following, without success:
var query = from b in CT2.EAN_Budget_Meetdata_Factuur
let JaarTest = b.Jaar + "-" + b.Maand + "-01"
where
b.EID == 1 &&
b.Bron == "B" &&
JaarTest == "something"
select new
{
JaarTest = b.Jaar + "-" + b.Maand + "-01"
};
If you're using DATEFROMPARTS on table columns you've already lost any opportunity for indexes to be useful on the database side - so I'd instead recommend working in total months.1
(year * 12) + months is a simple calculation that you can perform both with the table columns and whatever start/end points you want to query for and you then can perform simple int range checks.
1In the alternative, if you hadn't realised that this was going to deny you any indexing ability, you might want to consider adding a computed column on which you apply an index - in which case the column definition would use the function (either the total months calculation I give here or the original DATEFROMPARTS one) and you'd then query that column easily.
Try following :
class Program
{
static void Main(string[] args)
{
var query = EAN_Budget_Meetdata_Factuur.factuurs
.Where(x => (x.eid == 1) && (x.bron == "B") && (x.date >= new DateTime(2016, 11, 1)) && (x.date <= new DateTime(2017, 1, 1)))
.Select(x => new { eid = x.eid, bron = x.bron, date = x.date })
.ToList();
}
}
public class EAN_Budget_Meetdata_Factuur
{
public static List<EAN_Budget_Meetdata_Factuur> factuurs = new List<EAN_Budget_Meetdata_Factuur>();
public int eid { get; set; }
public string bron { get; set; }
public DateTime date { get; set; }
}

LINQ doesn't accept static function in a predicate: LINQ to Entities does not recognize the method

I know this question looks like a duplicate but it seems like each case has it's own solution and I can't find the right explanation for this Exception:
LINQ to Entities does not recognize the method 'Boolean
DateReservationOnTheSameWeek(System.DateTime)' method, and this method
cannot be translated into a store expression.
I just want to compare if two dates are on the same iso week of year, here's my code:
public class Reservation
{
public DateTime DateReservation { get; set; }
// other methods and properties
public static bool IsOnQuota(Reservation item)
{
using (var ctx = new SchedulingToolContext())
{
float qteSem = ctx.Reservations.Where(k => k.CltNameAlpha == item.CltNameAlpha
&& k.DateReservation.Year == item.DateReservation.Year
&& item.DateReservationOnTheSameWeek(k.DateReservation) // this causes the exception
).Sum(k => k.Qte);
var q = ctx.Quotas.Where(k => k.Id == item.IdQuota).FirstOrDefault();
return q.QteMaxJour >= item.Qte && q.QteMaxSemaine >= qteSem;
}
}
private static int GetIso8601WeekOfYear(DateTime time)
{
DayOfWeek day = System.Globalization.CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(time);
if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday)
{
time = time.AddDays(3);
}
return System.Globalization.CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(time, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
private bool DateReservationOnTheSameWeek(DateTime date)
{
return GetIso8601WeekOfYear(this.DateReservation) == GetIso8601WeekOfYear(date);
}
}
I can't figure out why this predicate fails.
EDIT: I can't calculate the predicate out of the query because, it depends on k that is part of the lambda function.
Perhaps using SqlFunctions.DatePart and ISO_WEEK will help
using (var ctx = new SchedulingToolContext())
{
float qteSem = ctx.Reservations.Where(k => k.CltNameAlpha == item.CltNameAlpha
&& k.DateReservation.Year == item.DateReservation.Year
&& SqlFunctions.DatePart("isowk", this.DateReservation) == SqlFunction.DatePart("isowk", k.DateReservation)
).Sum(k => k.Qte);
var q = ctx.Quotas.Where(k => k.Id == item.IdQuota).FirstOrDefault();
return q.QteMaxJour >= item.Qte && q.QteMaxSemaine >= qteSem;
}

Efficient LINQ to Entities query

I have an entity collection of Readings.
Each Reading is linked to an entity called Meter.
(And each Meter holds multiple readings).
each Reading holds a field for meter id (int) and a field for time.
Here is some simplified code to demonstrate it:
public class Reading
{
int Id;
int meterId;
DateTime time;
}
public class Meter
{
int id;
ICollection<Readings> readings;
}
Given a specific period and list of meterids,
what would be the most efficient way to get for each Meter
the first and last reading in that time period?
I am able to iterate through all meters and for each meter to obatin
first and last reading for the period,
but I was wandering if there is a more efficient way to acheive this.
And a bonus question: same question, but with multiple periods of time to get data for,
instead of just one period.
I am not exactly sure how you want this data, but you could project it into an anonymous type:
var metersFirstAndLastReading = meters.Select(m => new
{
Meter = m,
FirstReading = m.readings.OrderBy(r => r.time).First(),
LastReading = m.readings.OrderBy(r => r.time).Last()
});
You can then read your result list like this (this example is just meant as an illustration):
foreach(var currentReading in metersFirstAndLastReading)
{
string printReadings = String.Format("Meter id {0}, First = {1}, Last = {2}",
currentReading.Meter.id.ToString(),
currentReading.FirstReading.time.ToString(),
currentReading.LastReading.time.ToString());
// Do something...
}
Another option would be to create properties in Meter which dynamically return the first and last readings:
public class Meter
{
public int id;
public List<Reading> readings;
public Reading FirstReading
{
get
{
return readings.OrderBy(r => r.time).First();
}
}
public Reading LastReading
{
get
{
return readings.OrderBy(r => r.time).Last();
}
}
}
EDIT: I misunderstood the question a little.
Here is the implementation to determine the first and last readings for a meter including a date range (assuming meterIdList is an ICollection<int> of IDs and begin and end is the specified date range)
var metersFirstAndLastReading = meters
.Where(m => meterIdList.Contains(m.id))
.Select(m => new
{
Meter = m,
FirstReading = m.readings
.Where(r => r.time >= begin && r.time <= end)
.OrderBy(r => r.time)
.FirstOrDefault(),
LastReading = m.readings
.Where(r => r.time >= begin && r.time <= end)
.OrderByDescending(r => r.time)
.FirstOrDefault()
});
You won't be able to use properties now (as you need to supply parameters) so methods will work just fine as an alternative:
public class Meter
{
public int id;
public List<Reading> readings;
public Reading GetFirstReading(DateTime begin, DateTime end)
{
var filteredReadings = readings.Where(r => r.time >= begin && r.time <= end);
if(!HasReadings(begin, end))
{
throw new ArgumentOutOfRangeException("No readings available during this period");
}
return filteredReadings.OrderBy(r => r.time).First();
}
public Reading GetLastReading(DateTime begin, DateTime end)
{
var filteredReadings = readings.Where(r => r.time >= begin && r.time <= end);
if(!HasReadings(begin, end))
{
throw new ArgumentOutOfRangeException("No readings available during this period");
}
return filteredReadings.OrderBy(r => r.time).Last();
}
public bool HasReadings(DateTime begin, DateTime end)
{
return readings.Any(r => r.time >= begin && r.time <= end);
}
}
I have a very similar data model where this code is used to get the oldest readings, i just changed it to also include the newest.
I use query syntax to do something like this:
var query = from reading in db.Readings
group reading by reading.meterId
into readingsPerMeter
let oldestReadingPerMeter = readingsPerMeter.Min(g => g.time)
let newestReadingPerMeter = readingsPerMeter.Max(g => g.time)
from reading in readingsPerMeter
where reading.time == oldestReadingPerMeter || reading.time == newestReadingPerMeter
select reading; //returns IQueryable<Reading>
That would result in a only the newest and oldest reading for each meter.
The reason i think this is efficient is because its one lookup to the DB to get all the readings for each meter, instead of several lookups for each meter. We have ~40000 meters with ~30mil readings. i just tested the lookup on our data it took about 10s
The sql preformed is a crossjoin between two sub selects for each of the min and max dates.
UPDATE:
Since this is queryable you should be able to supply a period after, like this:
query.Where(r=>r.time > someTime1 && r.time < someTime2)
Or put it into the original query, i just like it seperated like this. The query isnt executed yet since we havent performed an action that fetches the data yet.
Create a new class as the return type called Result, which looks like this
public class Result
{
public int MeterId;
public Readings Start;
public Readings Last;
}
I emulated your situation by making a list of Meters and populating some data, your query should be pretty much the same though
var reads = Meters.Where(x => x.readings != null)
.Select(x => new Result
{
MeterId = x.id,
Start = x.readings.Select(readings => readings).OrderBy(readings=>readings.time).FirstOrDefault(),
Last = x.readings.Select(readings=>readings).OrderByDescending(readings=>readings.time).FirstOrDefault()
});
public IEnumerable<Reading> GetFirstAndLastInPeriod
(IEnumerable<Reading> readings, DateTime begin, DateTime end)
{
return
from reading in readings
let span = readings.Where(item => item.time >= begin && item.time <= end)
where reading.time == span.Max(item => item.time)
|| reading.time == span.Min(item => item.time)
select reading;
}
meters.Where(mt=>desiredMeters.Contains(mt)).Select(mt=>
new{
mt.Id,
First = mt.Readings.Where(<is in period>).OrderBy(rd=>rd.Time).FirstOrDefault(),
Last = mt.Readings.Where(<is in period>).OrderBy(rd=>rd.Time).LastOrDefault()
});
If you have lots of readings per meter, this will not perform well, and you should consider Readings to be of SortedList class.
my solution will return exact what u want (List of all Meters containing Readings within given Time Period)
public IList<Reading[]> GetFirstAndLastReadings(List<Meter> meterList, DateTime start, DateTime end)
{
IList<Reading[]> fAndlReadingsList = new List<Reading[]>();
meterList.ForEach(x => x.readings.ForEach(y =>
{
var readingList = new List<Reading>();
if (y.time >= startTime && y.time <= endTime)
{
readingList.Add(y);
fAndlReadingsList.Add(new Reading[] { readingList.OrderBy(reading => reading.time).First(), readingList.OrderBy(reading => reading.time).Last() });
}
}));
return fAndlReadingsList;
}
I got some very nice leads, thank to all the responders.
Here is the solution that worked for me:
/// <summary>
/// Fills the result data with meter readings matching the filters.
/// only take first and last reading for each meter in period.
/// </summary>
/// <param name="intervals">time intervals</param>
/// <param name="meterIds">list of meter ids.</param>
/// <param name="result">foreach meter id , a list of relevant meter readings</param>
private void AddFirstLastReadings(List<RangeFilter<DateTime>> intervals, List<int> meterIds, Dictionary<int, List<MeterReading>> result)
{
foreach (RangeFilter<DateTime> interval in intervals)
{
var metersFirstAndLastReading = m_context.Meter.Where(m => meterIds.Contains(m.Id)).Select(m => new
{
MeterId = m.Id,
FirstReading = m.MeterReading
.Where(r => r.TimeStampLocal >= interval.FromVal && r.TimeStampLocal < interval.ToVal)
.OrderBy(r => r.TimeStampLocal)
.FirstOrDefault(),
LastReading = m.MeterReading
.Where(r => r.TimeStampLocal >= interval.FromVal && r.TimeStampLocal < interval.ToVal)
.OrderByDescending(r => r.TimeStampLocal)
.FirstOrDefault()
});
foreach (var firstLast in metersFirstAndLastReading)
{
MeterReading firstReading = firstLast.FirstReading;
MeterReading lastReading = firstLast.LastReading;
if (firstReading != null)
{
result[firstLast.MeterId].Add(firstReading);
}
if (lastReading != null && lastReading != firstReading)
{
result[firstLast.MeterId].Add(lastReading);
}
}
}
}
}

LINQ to SQL Conditional where clause

I have the following controller code that returns a Json list object to my view that draws a pie chart.
There are 4 input parameters and i have it working with 3 of them.
However, the fist parameter entitled 'SiteTypeId' needs to be included in the where.
My problem is how to include this neatly in the code, i'd like to avoid an override of the function.
The required additional logic is:
if SiteTypeId = -1 (then this means show all so nothing is to be changed)
if SiteTypeId = 0 (then i.SiteTypeId == 0 needs to be added)
if SiteTypeId = 1 (then i.SiteTypeId == 1 needs to be added)
If 2 and 3 above were all that was required it would be easy I guess. I'm thinking there must be a neat expression for this or a neat way of splitting the LINQ into 2 with a condition perhaps.
I'm new to LINQ - can anyone advise me, here is the controller code i need to modify:
public JsonResult GetChartData_IncidentsBySiteStatus(string SiteTypeId, string searchTextSite, string StartDate, string EndDate)
{
if (searchTextSite == null)
searchTextSite = "";
DateTime startDate = DateTime.Parse(StartDate);
DateTime endDate = DateTime.Parse(EndDate);
var qry = from s in _db.Sites
join i in _db.Incidents on s.SiteId equals i.SiteId
where s.SiteDescription.Contains(searchTextSite)
&& (i.Entered >= startDate && i.Entered <= endDate)
group s by s.SiteStatus.SiteStatusDescription + "[" + s.SiteTypeId.ToString() + "]"
into grp
select new
{
Site = grp.Key,
Count = grp.Count()
};
return Json(qry.ToList() , JsonRequestBehavior.AllowGet);
}
Sounds like you could use LINQKit and its PredicateBuilder. You use it to build dynamic conditional WHERE clauses. It's also used in LinqPad, and it's free.
Try this:
public JsonResult GetChartData_IncidentsBySiteStatus(string SiteTypeId, string searchTextSite, string StartDate, string EndDate)
{
if (searchTextSite == null)
searchTextSite = "";
DateTime startDate = DateTime.Parse(StartDate);
DateTime endDate = DateTime.Parse(EndDate);
var incidentsQry = _db.Incidents;
if(SiteTypeId > -1)
{
incidentsQry = incidentsQry.Where(a=>a.SiteTypeId == SiteTypeId);
}
var qry = from s in _db.Sites
join i in incidentsQry on s.SiteId equals i.SiteId
where s.SiteDescription.Contains(searchTextSite)
&& (i.Entered >= startDate && i.Entered <= endDate)
group s by s.SiteStatus.SiteStatusDescription + "[" + s.SiteTypeId.ToString() + "]"
into grp
select new
{
Site = grp.Key,
Count = grp.Count()
};
return Json(qry.ToList() , JsonRequestBehavior.AllowGet);
}
Simply add the following to your where clause
(SiteTypeId == -1 || i.SiteTypeId == SiteTypeId)

Categories

Resources