Summarising existing data into newly created record - c#

I am experienced in SQL and less experienced in LINQ and OOP in general, the result being much frustration with LINQ, so please bear with me.
I'm using MVC / Entity Framework as per tags below.
I have two tables. One table called Header is bound to a grid. When I create a record to
be inserted into this Header table, I need to look up some matching related Detail records and summarise and show them in the grid.
I'll limit this to the LINQ aspects for now.
For example I have these detail records:
Date Segment Location Amount1 Amount2
2013-12-01 ABC ZZ 12 2
2013-12-02 ABC ZZ 50 3
2013-12-03 ABC ZZ 2 4
2013-12-01 DEF ZZ 7 5
and I create this header record in my grid:
DateFrom DateTo Segment Location DetailAmount1 DetailAmount2
2013-12-01 2013-12-07 ABC ZZ ( to be populated )
DetailAmount1 should be 64, DetailAmount2 should be 9
So my view calls the Grid_Create action in the controller to get a a viewmodel back with required data (which should have nothing except my summarised detail values and a DB generated key)
This is my controller:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Grid_Create(
[DataSourceRequest]DataSourceRequest request,
[Bind(Prefix = "models")]IEnumerable<Header_ViewModel> objects,
string Location,
int? Segment_ID,
DateTime? Start_Date,
DateTime? End_Date)
{
using (var MyDB = new DBEntities())
{
// Keep the inserted entitites here.
// Used to return the result later.
var entities = new List<Header_Table>();
if (ModelState.IsValid)
{
foreach (var obj in objects)
{
// PART A: Summarise estimates out of Detail
var est =
(from e in MyDB.Detail_Table
where e.SRC_System == Location
where e.Segment_ID == Segment_ID
where e.Transaction_Date >= Start_Date
where e.Transaction_Date <= End_Date
group e by e.Segment_ID into e
select
new Header_ViewModel
{
Amount1 = e.Sum(x => x.Amount1),
Amount2 = e.Sum(x => x.Amount2),
Amount3 = e.Sum(x => x.Amount3),
Amount4 = e.Sum(x => x.Amount4),
Amount5 = e.Sum(x => x.Amount5),
Amount6 = e.Sum(x => x.Amount6)
})
.FirstOrDefault();
// PART B: If there are no estimates, generate a 0
if (est == null)
{
est = new Header_ViewModel
{
Amount1 = 0,
Amount2 = 0,
Amount3 = 0,
Amount4 = 0,
Amount5 = 0,
Amount6 = 0
};
}
// PART C: Create a new entity
// and set its properties from the posted model
var entity = new Header_Table
{
Transaction_ID = obj.Transaction_ID,
Value1 = obj.Value1,
Value2 = obj.Value2,
Value3 = obj.Value3,
// Summary from detail table
Amount2 = est.Amount2,// obj.Amount2,
Amount3 = est.Amount3, // obj.Amount3,
Amount1 = est.Amount1,// obj.Amount1,
Amount4 = est.Amount4, //obj.Amount4,
Amount5 = est.Amount5, // obj.Amount5,
Amount6 = est.Amount6, // obj.Amount6,
Location = Location,
Segment_ID = Segment_ID,
Start_Date = Start_Date,
End_Date = End_Date,
// assign default values
Updated_By = User.Identity.Name,
Updated_Date = DateTime.Now
};
// Add the entity
MyDB.Header_Table.Add(entity);
// Store the entity for later use
entities.Add(entity);
}
// Insert the entities in the database
MyDB.SaveChanges();
}
// Return the inserted entities. Also return any validation errors.
return Json(
entities.ToDataSourceResult(
request,
ModelState, obj => new Header_ViewModel
{
Transaction_ID = obj.Transaction_ID,
Amount2 = obj.Amount2,
Value1 = obj.Value1,
Amount3 = obj.Amount3,
Amount1 = obj.Amount1
}));
}
}
The questions:
There aren't always detail records to be found. Whats a nice way to default the est object to a single item of zero? Currently in part B I am checking for est==null and loading it up manually. How do I make FirstOrDefault do this for me automatically? (therefore removing part B). I believe I should be able to pass in a type but I can't get the syntax correct, i.e. if I grab everything from new in part B and put it as an argument to FirstOrDefault I get System.Linq.IQueryable<Header_ViewModel> does not contain a definition for 'FirstOrDefault...
When getting the summary in part A, I don't actually need to group by Segment_ID, I just need the total summary for the table. However it appears I have to group by something to get an aggregate in LINQ. I've seen other posts mentioning group by e.GetType() but I get the error LINQ to Entities does not recognize the method 'System.Type GetType()'......
Given that this is a new record, should I be able to populate entity directly out of MyDB.Detail_Table (thereby combining part A and C)? I did try this but got an error every time. Apologies for not posting the exact error, but if someone thinks it's possible I will try again and post errors this time.

Question 1
The easiest solution I can think of is this (using your code as a base):
var est =
(from e in MyDB.Detail_Table
where e.SRC_System == Location
where e.Segment_ID == Segment_ID
where e.Transaction_Date >= Start_Date
where e.Transaction_Date <= End_Date
group e by e.Segment_ID into e
select
new Header_ViewModel
{
Amount1 = e.Sum(x => x.Amount1),
Amount2 = e.Sum(x => x.Amount2),
Amount3 = e.Sum(x => x.Amount3),
Amount4 = e.Sum(x => x.Amount4),
Amount5 = e.Sum(x => x.Amount5),
Amount6 = e.Sum(x => x.Amount6)
})
.FirstOrDefault() ?? new Header_ViewModel();
Question 2
Well, yeah... Not much can be done here, using a constant is possible as explained here. This seems to work:
var est =
MyDB.DetailTableSet.Where(e => e.SRC_System == Location && e.Segment_ID == Segment_ID
&& e.Transaction_Date >= Start_Date && e.Transaction_Date <= End_Date)
.GroupBy(e => 1)
.Select(e => new Header_ViewModel
{
Amount1 = e.Sum(x => x.Amount1),
Amount2 = e.Sum(x => x.Amount2),
})
.SingleOrDefault() ?? new Header_ViewModel();
Question 3
There is no good way to do this. The reason and some work around are explained here, but in your case I would not go that far. This is as far as I would go:
var est =
MyDB.DetailTableSet.Where(e => e.SRC_System == Location && e.Segment_ID == Segment_ID
&& e.Transaction_Date >= Start_Date && e.Transaction_Date <= End_Date)
.GroupBy(e => 1)
.Select(e => new Header_ViewModel
{
Amount1 = e.Sum(x => x.Amount1),
Amount2 = e.Sum(x => x.Amount2),
})
.SingleOrDefault() ?? new Header_ViewModel();
var entity = new HeaderTable()
{
Transaction_ID = obj.Transaction_ID,
Value1 = obj.Value1,
Value2 = obj.Value2,
Value3 = obj.Value3,
// Summary from detail table
Amount1 = est.Amount1,
Amount2 = est.Amount2,
Location = Location,
Segment_ID = Segment_ID,
Start_Date = Start_Date,
End_Date = End_Date,
// assign default values
Updated_By = "Myself",
Updated_Date = DateTime.Now
};
// Add the entity
MyDB.HeaderTableSet.Add(entity);
// Store the entity for later use
entities.Add(entity);

Related

EF core query group by date and subquery

I have the following table:
CREATE TABLE "OrderStatusLogs" (
"Id" UNIQUEIDENTIFIER NOT NULL,
"OrderId" UNIQUEIDENTIFIER NOT NULL,
"Status" INT NOT NULL,
"StartDateTime" DATETIMEOFFSET NOT NULL,
"EndDateTime" DATETIMEOFFSET NULL DEFAULT NULL,
PRIMARY KEY ("Id"),
FOREIGN KEY INDEX "FK_OrderStatusLogs_Orders_OrderId" ("OrderId"),
CONSTRAINT "FK_OrderStatusLogs_Orders_OrderId" FOREIGN KEY ("OrderId") REFERENCES "Orders" ("Id") ON UPDATE NO_ACTION ON DELETE CASCADE
)
;
For the following entity:
[DebuggerDisplay(nameof(OrderStatusLog) + " {Status} {StartDateTime} - {EndDateTime}" )]
public class OrderStatusLog
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public OrderStatus Status { get; set; }
public DateTimeOffset StartDateTime { get; set; }
public DateTimeOffset? EndDateTime { get; set; }
}
public enum OrderStatus
{
Unknown = 0,
Pending = 1,
Processing = 2,
Shipping = 3,
}
And i'm trying to generate a report which should show how many orders are set to a certain state for a given range.
For example, for the month oktober, we'd have the range 1 to 31 oktober.
The desired output would be something like this:
1/10/2021 Pending 21 orders
1/10/2021 Processing 23 orders
1/10/2021 Shipping 33 orders
1/10/2021 Unknown 0 orders
...
31/10/2021 Pending 1 orders
31/10/2021 Processing 3 orders
31/10/2021 Shipping 44 orders
31/10/2021 Unknown 5 orders
I'm having some difficulties writing a query in EF that would give me the right output. I can get things to work, but only client-side. I'm trying to make this work in the database instead.
So far i tried:
var logsByDayAndOrderId = orderStatusLogs.GroupBy(c => new { c.StartDateTime.Date, c.OrderId }, (key, values) => new
{
key.Date,
key.OrderId,
MaxStartDateTime = values.Max(x => x.StartDateTime)
});
var list = logsByDayAndOrderId.ToList();
var statusByDayAndOrderId = logsByDayAndOrderId.Select(c => new
{
c.Date,
c.OrderId,
orderStatusLogs.FirstOrDefault(x => x.StartDateTime == c.MaxStartDateTime && x.OrderId == c.OrderId).Status
});
//var statusByDayAndOrderId = logsByDayAndOrderId.Join(orderStatusLogs.def, inner => new { inner.OrderId, StartDateTime = inner.MaxStartDateTime }, outer => new { outer.OrderId, outer.StartDateTime }, (inner,outer) => new
//{
// inner.Date,
// inner.OrderId,
// outer.Status
//}); // TODO rem this query gives more results because of the join. we need an Outer join - but i could not get that to work. the version with select above works better, but then it does not use join so it may be slow(er).
var list1 = statusByDayAndOrderId.ToList();
var groupBy = statusByDayAndOrderId
.GroupBy(c => new { c.Date, c.Status })
.Select(c => new { c.Key.Date, c.Key.Status, Count = c.Count() });
var list2 = groupBy.ToList();
Another attempt:
var datesAndOrders = orderStatusLogs
.GroupBy(c => new { c.StartDateTime.Date, c.OrderId }, (key, values) => key);
var ordersByDateAndActiveStatusLog = orderStatusLogs
.Select(c => new
{
c.StartDateTime.Date,
c.OrderId,
ActiveStatusForDate = orderStatusLogs
.OrderByDescending(x => x.StartDateTime)
.FirstOrDefault(x => x.OrderId == c.OrderId && x.StartDateTime.Date == c.StartDateTime.Date)
.Status
});
var list = ordersByDateAndActiveStatusLog.ToList();
var orderCountByDateAndStatus = ordersByDateAndActiveStatusLog
.GroupBy(c => new { c.Date, c.ActiveStatusForDate }, (key, values) => new
{
key, count = values.Count()
});
var list1 = orderCountByDateAndStatus.ToList();
Both of these fail because of Cannot use an aggregate or a subquery in an expression used for the group by list of a GROUP BY clause..
This makes sense.
I'm hoping for someone that could help write a Linq query that generates the right data using ef core.
Notes:
I Solely use the fluent query syntax
I Have more places where i'd like to get data for each day so any other info or tips and tricks are welcome
I use net core 5 with ef core 5.0.11 with a MSSQL database
I would suggest to use EF Core extension linq2db.EntityFrameworkCore which has ability to work with local (in-memory) collections in database queries. Disclaimer: i'm one of the creators.
At first define function which generates days sequence:
public static IEnumerable<DateTime> GenerateDays(int year, int month)
{
var start = new DateTime(year, month, 1);
var endDate = start.AddMonths(1);
while (start < endDate)
{
yield return start;
start = start.AddDays(1);
}
}
Then we can use generated sequence in LINQ Query:
var days = GenerateDays(2021, 10).ToArray();
using var dc = ctx.CreateLinqToDbConnection();
var totalsQuery =
from d in days.AsQueryable(dc)
from l in orderStatusLogs.Where(l =>
(l.EndDateTime == null || l.EndDateTime >= d) && l.StartDateTime < d.AddDays(1))
.DefaultIfEmpty()
group l by new { Date = d, l.Status } into g
into g
select new
{
g.Key.Date,
g.Key.Status,
Count = g.Sum(x => x == null ? 0 : 1),
};
var result = totalsQuery.ToList();
The following SQL should be generated:
SELECT
[d].[item],
[e].[Status],
Sum(IIF([e].[OrderID] IS NULL, 0, 1))
FROM
(VALUES
('2021-05-01T00:00:00'), ('2021-05-02T00:00:00'),
('2021-05-03T00:00:00'), ('2021-05-04T00:00:00'),
('2021-05-05T00:00:00'), ('2021-05-06T00:00:00'),
('2021-05-07T00:00:00'), ('2021-05-08T00:00:00'),
('2021-05-09T00:00:00'), ('2021-05-10T00:00:00'),
('2021-05-11T00:00:00'), ('2021-05-12T00:00:00'),
('2021-05-13T00:00:00'), ('2021-05-14T00:00:00'),
('2021-05-15T00:00:00'), ('2021-05-16T00:00:00'),
('2021-05-17T00:00:00'), ('2021-05-18T00:00:00'),
('2021-05-19T00:00:00'), ('2021-05-20T00:00:00'),
('2021-05-21T00:00:00'), ('2021-05-22T00:00:00'),
('2021-05-23T00:00:00'), ('2021-05-24T00:00:00'),
('2021-05-25T00:00:00'), ('2021-05-26T00:00:00'),
('2021-05-27T00:00:00'), ('2021-05-28T00:00:00'),
('2021-05-29T00:00:00'), ('2021-05-30T00:00:00'),
('2021-05-31T00:00:00')
) [d]([item])
LEFT JOIN [OrderStatusLogs] [e] ON ([e].[EndDateTime] IS NULL OR [e].[EndDateTime] >= [d].[item]) AND [e].[StartDateTime] < DateAdd(day, 1, [d].[item])
GROUP BY
[d].[item],
[e].[Status]

Improving speed of method after adding value from different table aspn.net

I've got two tables: Index and Codes
when one condition is true, I need to check whether the Index is still valid and for that I need to get EndDate of the code which is in Codes table (as I've got only id of code in Index table)
This is how I do that:
1) First I get all Codes that have ended already (approx 3k+ items)
var goods = _context.Codes.Select(a =>
new Codes
{
Loid = a.Loid,
Pid = a.Pid,
Code = a.Code,
Startdate = a.Startdate,
Enddate = a.Enddate
})
.Where(x => x.Enddate < DateTime.Now)
.ToList();
then I'm taking my Index and adding values there as well as CodeEnd from Codes list above:
query = _context.VpAbcIndex
.AsNoTracking()
.Select(e => new VpAbcIndex
{
Id = e.Id,
ParentName = e.ParentName ?? "!",
ParentEndDate = e.ParentEndDate,
ParentStartDate = e.ParentStartDate,
ParentCode = e.ParentCode,
ParentNote = e.ParentNote ?? "",
ParentStatus = e.ParentStatus,
ChildName = e.ChildName ?? "!",
ChildId = e.ChildId,
ChildEndDate = e.ChildEndDate,
ChildStartDate = e.ChildStartDate,
CodeEnd = (filter.Level == 0) ? goods
.FirstOrDefault( x => x.Code == e.ParentCode).Enddate :
(filter.Level == 1) ? goods
.FirstOrDefault(x => x.Code == e.ChildCode).Enddate :
(filter.Level == 2) ?
goods
.FirstOrDefault(x => x.Code == e.GrandChildCode).Enddate : null
})
;
}
That's approx 10k items. It doesn't seem that much however it takes quite a long time for them to appear in my browser. Am I doing something wrong? Is there a faster way to join these values?

Count within a formatted DateTime with LINQ

I populate the following List from database, where I like to get the percentage of Good or Bad by only day (ignoring time).
I cannot group the data by data.GroupBy(p => p.DateTime.ToString("YYYY-MM-dd")) or String.Format("YYYY-MM-dd", p.DateTime) or count from it, let alone doing the .Where clause to match a Content to be Good or Bad and count from it.
It is even possible in LINQ?
I can use SQL where I had to format each DateTime, perform multiple Array and Unique conversion and multiple SQL calls to retrieve the count for each variables to calculate a ratio, but that is so dumb.
DateTime Content
-------------------------------------
2018-03-16 17:59:26.000 Good
2018-03-16 18:05:04.000 Bad
2018-03-16 19:23:26.000 Bad
2018-03-17 03:19:02.000 Good
2018-03-17 06:20:32.000 Bad
What I want to get
2018-03-16 Good: 33% Bad: 66%
2018-03-17 Good: 50% Bad: 50%
Basically you want to take advantage of DateTime.Date
DateTime.Date Property
Gets the date component of this instance.
Given
var list = new List<MyClass>()
{
new MyClass()
{
Content = "Good",
MyDateTimeField = DateTime.Now
},
new MyClass()
{
Content = "Good",
MyDateTimeField = DateTime.Now
},
new MyClass()
{
Content = "Bad",
MyDateTimeField = DateTime.Now
}
};
Group By
var results = list.GroupBy(x => x.MyDateTimeField.Date)
.Select(x =>
{
var count = x.Count();
var good = x.Count(y => y.Content == "Good");
var bad = x.Count(y => y.Content == "Bad");
var result = new
{
Date = x.Key,
Good = good / (decimal)count,
Bad = bad / (decimal)count
};
return result;
});
foreach (var item in results)
{
Console.WriteLine(string.Format("Date = {0}, Good = {1:P2}, Bad = {2:P2}", item.Date.Date, item.Good, item.Bad));
}
Output
Date = 3/24/2018 12:00:00 AM, Good = 66.67 %, Bad = 33.33 %
Full Demo Here
Note : i'm using Format Specifiers in C# P2 to format the percent, however you can apply your own rounding and logic to tidy it up
You could try something like
var result = from p in dataContext.Table
group p by p.DateTime.Date into g
select new {
Date = g.Key,
NumberOfGood = g.Count(p => p.Content == "Good"),
NumberOfBad = g.Count(p => p.Content == "Bad"),
PercentageGood = ((decimal)g.Count(p => p.Content == "Good") / ((decimal)g.Count(p => p.Content == "Good") + (decimal)g.Count(p => p.Content == "Bad"))),
PercentageBad = ((decimal)g.Count(p => p.Content == "Bad") / ((decimal)g.Count(p => p.Content == "Good") + (decimal)g.Count(p => p.Content == "Bad")))
};

Convert OrderBy Case statement in LINQ

I'm trying to convert the following SQL from Oracle into a Linq to Entity query.
ORDER by
case when(e.prev_co = 'ABC' and(nvl(co_seniority, '1-jan-2099') < to_date('10-apr-2001')))
then '2001-04-01'
else to_char(nvl(co_seniority, '1-jan-2099'), 'YYYY-MM-DD') end,
nvl(co_seniority, '1-jan-2099'),
birth_dt
I was hoping I could use a function to pass in some parameters and have it return the correct date. I tried creating an new property called SortDate and then created a function on my page that would take in the parameters and return the correct date but that didn't work. I get and exception that says "LINQ to Entities does not recognize the method GetSortDate".
Model
SortByDate = GetSortDate(e.PREV_CO, e.CO_SENIORITY),
Function
public static DateTime GetSortDate(string PreviousCo, DateTime? CoSeniorityDate)
{
//set variable to default date
DateTime sortDate = System.DateTime.Parse("2001-04-01");
//set default date if NULL
if (CoSeniorityDate == null)
{
CoSeniorityDate = System.DateTime.Parse("2099-01-01");
}
if (PreviousCo == "ABC" && (CoSeniorityDate < System.DateTime.Parse("2001-04-10")))
{
sortDate = System.DateTime.Parse("2001-04-01");
}
else
{
sortDate = System.DateTime.Parse(CoSeniorityDate.ToString());
}
return sortDate;
}
Here is my complete EF
using (DataContext db = new DataContext())
{
db.Configuration.AutoDetectChangesEnabled = false; //no changes needed so turn off for performance.
var workStatus = new string[] { "1", "3" };
var company = new string[] { "EX", "SM" };
var eventReason = new string[] { "21", "22", "23" };
data = (from e in db.EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& company.Contains(e.CO.Substring(0, 2))
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
select new Employee
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY == null ? DateTime.MaxValue : e.CO_SENIORITY,
BirthDate = e.BIRTH_DT,
SortByDate = GetSortDate(e.PREV_CO, e.CO_SENIORITY),
PreviousCo = e.PREV_CO
}).OrderBy(o => o.SortByDate).ThenBy(o => o.SeniorityDate).ThenBy(o => o.BirthDate).ToList();
}
Anyone have a suggestion on how I can convert this OrderBy?
UPDATED QUESTION
At the moment I have my query working correctly by using a secondary SELECT like #Markus showed. The first query just pulls the data and then all the formatting and calling of a method to get the correct SortByDate.
However, my manager would really prefer to do the sorting in the DB versus in memory. He let this one go because there are very few people calling this seniority list and only once a month.
For learning purposes I would like to see if I could get the DB to do all the sorting as #IvanStoev shows below. So, going back that route I’m not able to get the OrderBy to work exactly like it should.
If you look at the original SQL I’m trying to convert it first looks to see if the person had a previous company of “ABC” and if they do then look at the SeniorityDate (set a default date if that’s NULL) and compare it to an acquisition date. If that condition isn’t met then just use their SeniorityDate (set it’s default if NULL). Tricky….I know.
Using the suggested OrderBy in LinqPad and then looking at the returned SQL I can see that the first part of the OrderBy looks at the previous company and then the SeniorityDate and sets a value. Then it looks at the acquisition date. I need to somehow group some conditions to look at first which I don’t know it that’s possible.
SELECT t0.ABV_POSITION_TITLE, t0.BIRTH_DT, t0.CITY_CODE, t0.CO, t0.CO_SENIORITY, t0.EMP, t0.FIRST_NAME, t0.LAST_NAME, t0.PART_TIME_IND, t0.PREV_CO, t0.WORKSTAT
FROM SAP_EMPLOYEE t0
WHERE ((((t0.WORKSTAT IN (:p0, :p1) AND (t0.PERS_SUB_AREA = :p2)) AND SUBSTR(t0.CO, 0 + 1, 2) IN (:p3, :p4)) AND (t0.CO <> :p5)) AND ((t0.EVENT_TYP <> :p6) OR ((t0.EVENT_TYP = :p6) AND NOT t0.EVENT_RSN IN (:p7, :p8, :p9))))
ORDER BY (CASE WHEN ((t0.PREV_CO = :p10) AND (t0.CO_SENIORITY IS NULL)) THEN :p11 WHEN (t0.CO_SENIORITY < :p12) THEN :p13 ELSE COALESCE(t0.CO_SENIORITY, :p11) END), COALESCE(t0.CO_SENIORITY, :p11), t0.BIRTH_DT
-- p0 = [1]
-- p1 = [3]
-- p2 = [200A]
-- p3 = [EX]
-- p4 = [SM]
-- p5 = [EXGS]
-- p6 = [35]
-- p7 = [21]
-- p8 = [22]
-- p9 = [23]
-- p10 = [ABC]
-- p11 = [1/1/2099 12:00:00 AM]
-- p12 = [4/10/2001 12:00:00 AM]
-- p13 = [4/1/2001 12:00:00 AM]
I need to come up with something like
ORDER BY (CASE WHEN ((t0.PREV_CO = :p10) AND (COALESCE(t0.CO_SENIORITY, :p11) < :p12) THEN :p13 ELSE COALESCE(t0.CO_SENIORITY, :p11) END)
Here is the code I used in LinqPad.
void Main()
{
var workStatus = new string[] { "1", "3" };
var company = new string[] { "EX", "SM" };
var eventReason = new string[] { "21", "22", "23" };
var baseDate = new DateTime(2001, 4, 10); // 10-apr-2001
var minDate = new DateTime(2001, 4, 1); // 1-apr-2001
var abcDate = new DateTime(2001, 4, 10); // 10-apr-2001
var maxDate = new DateTime(2099, 1, 1); // 1-jan-2099
var data = (from e in SAP_EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& e.PERS_SUB_AREA == "200A"
&& company.Contains(e.CO.Substring(0, 2))
&& e.CO != "EXGS"
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
orderby e.PREV_CO == "ABC" && e.CO_SENIORITY == null ? maxDate : e.CO_SENIORITY < abcDate ? minDate : e.CO_SENIORITY ?? maxDate,
e.CO_SENIORITY ?? maxDate,
e.BIRTH_DT
select new Employee
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY == null ? maxDate :
e.PREV_CO == "ABC" && e.CO_SENIORITY < twaDate ? maxDate : e.CO_SENIORITY,
LOA = e.WORKSTAT == "1" ? "LOA" : "",
ABC = e.PREV_CO == "ABC" ? "ABC" : "",
BirthDate = e.BIRTH_DT,
PreviousCo = e.PREV_CO
}).ToList();
data.Dump();
}
The reason for the exception is that entity framework generates the SQL query when you execute it. In your case, this happens with the call to ToList() at the end. In order to generate the SQL query, entity framework analyzes the query and transforms it into SQL. As entity framework does not know your function, it cannot generate the SQL statements for it.
In order to solve this, you need to first execute the query and do the sort operation in memory on the results. In order to limit the amount of data that is transferred to the client, you should execute the query including the where clause and also tell EF which fields you are interested in to avoid a SELECT * FROM ... that includes all fields of the table.
You could change your query approximately as follows:
data = (from e in db.EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& company.Contains(e.CO.Substring(0, 2))
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
select new ()
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY,
BirthDate = e.BIRTH_DT,
PreviousCo = e.PREV_CO
}).ToList().Select(x => new Employee()
{
Co = x.Co,
CityCode = x.CityCode,
EmployeeNumber = x.EmployeeNumber,
LastName = x.LastName,
FirstName = x.FirstName,
Position = x.Position,
EmploymentType = x.EmploymentType,
SeniorityDate = x.SeniorityDate ?? DateTime.MaxValue,
BirthDate = x.BirthDate,
SortByDate = GetSortDate(x.PreviousCo, x.SeniorityDate),
PreviousCo = x.PreviousCo
}).OrderBy(o => o.SortByDate)
.ThenBy(o => o.SeniorityDate)
.ThenBy(o => o.BirthDate).ToList();
This query first filters the data as specified in the where clause and then uses an anonymous type to retrieve only the relevant fields - including the ones that are later used as an input to the GetSortDate method with its original values. After the first ToList the results are present in memory and you can first add a new select that creates the Employee objects including the sort date. These objects are then ordered by sort date and so on.
A small hint for the GetSortDate method: specifying DateTime constants as a string that is parsed is not a good idea as parsing is dependent on the culture of the thread (if no culture is specified).
// Culture dependent
sortDate = System.DateTime.Parse("2001-04-01");
// Better
sortDate = new DateTime(2001, 04, 01);
As you already noticed (the hard way), in LINQ to Entities query you cannot use local methods like in LINQ to Objects. If you want the whole query to be executed in the database, you need to embed the logic inside the query using only the supported constructs.
With that being said, the equivalent of your SQL query should be something like this
var baseDate = new DateTime(2001, 4, 10); // 10-apr-2001
var minDate = new DateTime(2001, 4, 1); // 1-apr-2001
var maxDate = new DateTime(2099, 1, 1); // 1-jan-2099
data = (from e in db.EMPLOYEE
where workStatus.Contains(e.WORKSTAT)
&& company.Contains(e.CO.Substring(0, 2))
&& ((e.EVENT_TYP != "35") || (e.EVENT_TYP == "35" && !eventReason.Contains(e.EVENT_RSN)))
let seniorityDate = e.CO_SENIORITY ?? maxDate
let sortDate =
e.CO_SENIORITY == null ? maxDate :
e.PREV_CO == "ABC" && e.CO_SENIORITY < baseDate ? minDate :
e.CO_SENIORITY
orderby sortDate, seniorityDate, e.BIRTH_DT
select new Employee
{
Co = e.CO,
CityCode = e.CITY_CODE,
EmployeeNumber = e.EMP,
LastName = e.LAST_NAME,
FirstName = e.FIRST_NAME,
Position = e.ABV_POSITION_TITLE,
EmploymentType = e.PART_TIME_IND == "X" ? "PT" : "FT",
SeniorityDate = e.CO_SENIORITY,
BirthDate = e.BIRTH_DT,
PreviousCo = e.PREV_CO
}).ToList();
Update: For learning purposes I've updated the answer with using let clauses.
Now regarding the concrete ordering. I could have written the "SortDate" part exactly the way you did it, but I believe my way is a better equivalent. Why?
Here is my "SortDate" interpretation in pseudo code
if (CoSeniorityDate == null)
SortDate = #2099-01-01#
else if (PreviousCo == "ABC" && CoSeniorityDate < #2001-04-10#)
SortDate = #2001-04-01#
else
SortDate = CoSeniorityDate
And here is your function
if (CoSeniorityDate == null) CoSeniorityDate = #2099-01-01#
if (PreviousCo == "ABC" && CoSeniorityDate < #2001-04-10#)
SortDate = #2001-04-01#
else
SortDate = CoSeniorityDate
Let CoSeniorityDate == null. Then, according to your logic, let substitute CoSeniorityDate = #2099-01-01#:
if (PreviousCo == "ABC" && #2099-01-01# < #2001-04-10#)
SortDate = #2001-04-01#
else
SortDate = #2099-01-01#
Since #2099-01-01# < #2001-04-10# is always false, it becomes simple
SortDate = #2099-01-01#
i.e. exactly like the first part of my criteria. In the else part we already know CoSeniorityDate is not null and can just check the other conditions.
Anyway, doing it your way would be like this
let sortDate = e.PREV_CO == "ABC" && seniorityDate < baseDate ? minDate : seniorityDate

Transforming T-SQL Query into C# LINQ with joins on multiple conditions and also grouping on multiple conditions

First I want to say hello, I'm new to this site ;-)
My problem is to transform the following sql-query into a c# linq-query.
( I HAVE searched hard for an existing answer but I'm not able to combine the solution for
the joins on multiple conditions and the grouping / counting ! )
The sql-query :
DECLARE #datestart AS DATETIME
DECLARE #dateend AS DATETIME
SET #datestart = '01.04.2014'
SET #dateend = '30.04.2014'
SELECT md1.value AS [controller],md2.value AS [action], COUNT(md2.value) AS [accesscount], MAX(re.TIMESTAMP) AS [lastaccess] FROM recorderentries AS re
INNER JOIN messagedataentries AS md1 ON re.ID = md1.recorderentry_id AND md1.position = 0
INNER JOIN messagedataentries AS md2 ON re.ID = md2.recorderentry_id AND md2.position = 1
WHERE re.TIMESTAMP >= #datestart AND re.TIMESTAMP <= #dateend
AND re.messageid IN ('ID-01','ID-02' )
GROUP BY md1.value,md2.value
ORDER BY [accesscount] DESC
Any suggestions are welcome ...
What i have so far is this :
var _RecorderActionCalls = (from r in _DBContext.RecorderEntries
join m1 in _DBContext.MessageDataEntries on
new {
a = r.ID,
b = 0
} equals new {
a = m1.ID,
b = m1.Position
}
join m2 in _DBContext.MessageDataEntries on
new {
a = r.ID,
b = 0
} equals new {
a = m2.ID,
b = m2.Position
}
where r.TimeStamp >= StartDate & r.TimeStamp <= EndDate & (r.MessageID == "VAREC_100_01" | r.MessageID == "VAAUTH-100.01")
group r by new { md1 = m1.Value, md2 = m2.Value } into r1
select new { controller = r1.Key.md1, action = r1.Key.md2, count = r1.Key.md2.Count() }).ToList();
But this throws an exception ( translated from german ) :
DbExpressionBinding requires an input expression with a Listing Result Type ...
UPDATE : Back with headache ... ;-)
I found a solution to my problem :
var _RecorderActionCalls = _DBContext.RecorderEntries
.Where(r => r.TimeStamp >= StartDate & r.TimeStamp <= EndDate & (r.MessageID == "VAREC_100_01" | r.MessageID == "VAAUTH-100.01"))
.GroupBy(g => new { key1 = g.MessageData.FirstOrDefault(md1 => md1.Position == 0).Value, key2 = g.MessageData.FirstOrDefault(md2 => md2.Position == 1).Value })
.Select(s => new {
ControllerAction = s.Key.key1 + " - " + s.Key.key2,
Value = s.Count(),
Last = s.Max(d => d.TimeStamp)
}).ToList();
With this syntax it works for me. Thank you for thinking for me :-)
Something like that:
List<string> messageIdList = new List<string> { "ID-01", "ID-02" };
from re in RecorderEntries
from md1 in MessageDataEntries
from md2 in MessageDataEntries
where re.ID = md1.recorderEntry_id && md1.position == 0
where re.ID = md2.recorderEntry_id && md2.position == 1
where idList.Contains(re.messageid)
let joined = new { re, md1, md2 }
group joined by new { controller = joined.md1.value, action = joined.md2.value } into grouped
select new {
controller = grouped.Key.controller,
action = grouped.Key.action,
accesscount = grouped.Where(x => x.md2.value != null).Count(),
lastaccess = grouped.Max(x => x.re.TimeStamp) }

Categories

Resources