I am trying to do a 'left outer join' in linq using GroupJoin and SelectMany, but then I also want to aggregate the result using GroupBy and Sum.
But when I execute the code below, I get:
System.NotSupportedException: 'The entity or complex type '...tableB' cannot be constructed in a LINQ to Entities query.'
Repo<tableA>().All()
.Where(i =>
(i.Date >= dateF && i.Date <= dateT)
&&
i.EndOfMonth
)
.GroupJoin(
Repo<tableB>().All().Where(i => (i.fieldX = ...somevalue... )),
dt => dt.DayIndex, scd => scd.DayIndex, (dt, scd) =>
new {
dt = dt,
scd = scd,
}
)
.SelectMany(
jn => jn.scd.DefaultIfEmpty( new tableB { Count1 = 0, Count2 = 0 }), // runtime error here
(dt,scd) => new { dt=dt.dt, scd = scd}
)
.GroupBy(i => i.dt)
.Select(i => new CountListItem
{
Date = i.Key.Date,
CountField1 = i.Sum(o => o.scd.Count1),
CountField2 = i.Sum(p => p.scd.Count2)
})
.OrderBy(i => i.Date)
.ToList()
When I just do DefaultIfEmpty() I get the error:
System.InvalidOperationException: 'The cast to value type 'System.Int32' failed because the materialized value is null. Either the result type's generic parameter or the query must use a nullable type.'
I am assuming, I must admit, this is because Sum encounters null values.
I tried i.Sum(o => o.scd.Count1 ?? 0, but then it says:
Operator '??' cannot be applied to operands of type 'int' and 'int' [sic]
I also tried DefaultIfEmpty(new { Count1 = 0, Count2 =0}) but this gives me>
... type cannot be inferred.
How can I get this to work?
Avoiding the first exception is easy - simply use the standard pattern for left outer join with parameterless DefaultIfEmpty().
The second problem originates from the difference between SQL and LINQ (C#) query data types. SQL queries support NULL values natively and can return NULL even if the source expression is not nullable. As you noticed with the last attempt, LINQ (and in particular C# compiler) is not happy with that syntax.
The trick is to promote the non nullable type to nullable using the C# cast operator and then apply the null-coalescing operator to the resulting expression:
CountField1 = i.Sum(o => (int?)o.scd.Count1 ?? 0),
CountField2 = i.Sum(o => (int?)o.scd.Count2 ?? 0),
Related
I'm trying to use LINQ-to-entities to query my DB, where I have 3 tables: Room, Conference, and Participant. Each room has many conferences, and each conference has many participants. For each room, I'm trying to get a count of its conferences, and a sum of all of the participants for all of the room's conferences. Here's my query:
var roomsData = context.Rooms
.GroupJoin(
context.Conferences
.GroupJoin(
context.Participants,
conf => conf.Id,
part => part.ConferenceId,
(conf, parts) => new { Conference = conf, ParticipantCount = parts.Count() }
),
rm => rm.Id,
data => data.Conference.RoomId,
(rm, confData) => new {
Room = rm,
ConferenceCount = confData.Count(),
ParticipantCount = confData.Sum(cd => cd.ParticipantCount)
}
);
When I try and turn this into a list, I get the error:
The cast to value type 'System.Int32' failed because the materialized value is null. Either the result type's generic parameter or the query must use a nullable type.
I can fix this by changing the Sum line to:
ParticipantCount = confData.Count() == 0 ? 0 : confData.Sum(cd => cd.ParticipantCount)
But the trouble is that this seems to generate a more complex query and add 100ms onto the query time. Is there a better way for me to tell EF that when it is summing ParticipantCount, an empty list for confData should just mean zero, rather than throwing an exception? The annoying thing is that this error only happens with EF; if I create an empty in-memory List<int> and call Sum() on that, it gives me zero, rather than throwing an exception!
You may use the null coalescing operator ?? as:
confData.Sum(cd => cd.ParticipantCount ?? 0)
I made it work by changing the Sum line to:
ParticipantCount = (int?)confData.Sum(cd => cd.ParticipantCount)
Confusingly, it seems that even though IntelliSense tells me that the int overload for Sum() is getting used, at runtime it is actually using the int? overload because the confData list might be empty. If I explicitly tell it the return type is int? it returns null for the empty list entries, and I can later null-coalesce the nulls to zero.
Use Enumerable.DefaultIfEmpty:
ParticipantCount = confData.DefaultIfEmpty().Sum(cd => cd.ParticipantCount)
Instead of trying to get EF to generate a SQL query that returns 0 instead of null, you change this as you process the query results on the client-side like this:
var results = from r in roomsData.AsEnumerable()
select new
{
r.Room,
r.ConferenceCount,
ParticipantCount = r.ParticipantCount ?? 0
};
The AsEnumerable() forces the SQL query to be evaluated and the subsequent query operators are client-side LINQ-to-Objects.
I want to pass a lambda to my .Select() method depending on a condition.
I set my lambda up like this:
Func<Monthly, int?> f = x => x.CLDD;
I then set up my .Select() like this:
IQueryable query =
db.Monthlies
.GroupBy(o => o.Date.Value.Year)
.Select(
o => new {
Year = o.Key,
MaxDate = o.Max(x => x.Date),
Data = o.Sum(f)
}
)
.Where(o => o.Year != currentYear)
.OrderBy(o => o.Year);
The code compiles and runs fine but the query does not send back any results. When I debug and watch query I see it says:
+ base {"Internal .NET Framework Data Provider error 1025."}
System.SystemException {System.InvalidOperationException}
Note if instead I do:
Expression<Func<Monthly, int?>> f = x => x.CLDD;
Then o.Sum(f) errors saying:
Error 1 Instance argument: cannot convert from
'System.Linq.IGrouping<int,MyWeb.Models.Monthly>' to
System.Linq.IQueryable<MyWeb.Models.Monthly>'
Thank you!
You were close, Entity Framework needs an Expression to work not Func, but the Sum extension method that receives an Expression works only with IQueryable.
Now inside Select you are getting an IGrouping from GroupBy which does not implement IQueryable only IEnumerable.
So you just need to cast it to get the right extension method:
Data = o.AsQueryable().Sum(f)
I have 2 queries. One to find where Value column is greater than 150,000 and i need the count of entries. The second one is the sum of that rather than count.
The Count works perfectly but the sum crashes and provides this error
{"The cast to value type 'System.Decimal' failed because the
materialized value is null. Either the result type's generic parameter
or the query must use a nullable type."}
Working code:
var excessCount = closedDealNonHost.Any() ? closedDealNonHost.Where(x => x.Value > 150000).Count() : 0;
Crashing Code:
var excessSum = CloseDealNonHost = closedDealNonHost.Any() ? closedDealNonHost.Where(x => x.Value > 150000).Sum(x => x.Value) : 0;
You can solve the issue by explicitly casting to decimal? in Sum like:
var excessSum = CloseDealNonHost = closedDealNonHost.Any() ? closedDealNonHost
.Where(x => x.Value > 150000)
.Sum(x => (decimal?) x.Value) : 0;
The issue is due to generated SQL from LINQ expression, and at C# end it will try to return decimal which can't accommodate a null value, hence the error.
You may see: Linq To Entities: Queryable.Sum returns Null on an empty list
So I tried to follow this example to have a sub-query in the where clause of this LINQ query.
var innerquery =
from app in context.applications
select new { app.app_id };
IEnumerable<postDatedCheque> _entityList = context.postDatedCheques
.Where(e => innerquery.Contains(e.appSancAdvice.application.app_id));
The objective was to select those records from postDatedCheques that have app_id in applications table.
But I am getting following erros inside the where clause:
Delegate 'System.Func' does not
take 1 arguments
Cannot convert lambda expression to type 'string' because it is not
a delegate type
'System.Linq.IQueryable' does not contain a
definition for 'Contains' and the best extension method overload
'System.Linq.ParallelEnumerable.Contains(System.Linq.ParallelQuery,
TSource)' has some invalid arguments
Instance argument: cannot convert from
'System.Linq.IQueryable' to
'System.Linq.ParallelQuery'
What am I coding incorrect?
I think a simple join would do the job. It will filter out the 'cheques' that have no relative 'app':
var _entitylist =
from cheque in context.postDatedCheques
join app in context.applications on cheque.appSancAdvice.application equals app
select cheque;
Edit:
Solutions using a .Contains(...) will be translated into a SQL IN statement. Which will be very inefficient. Linq join is translated into SQL INNER JOIN which is very efficient if your DB schema is well trimmed (FKs, index)
What about?
IEnumerable<postDatedCheque> _entityList = context.postDatedCheques.Where(
e => context.applications.Any(
x => e.appSancAdvice.application.app_id == x.app_id));
And if you want to use two statements, set the first as an expression function.
Expression<Func<string, bool>> innerQuery =
x => context.applications.Any(y => y.app_id == x);
IEnumerable<postDatedCheque _entityList =
context.postDatedCheques.Where(
x => innerQuery(x.appSancAdvice.application.app_id));
innerquery is a IQueryable of anonymous type that contains an app_id.
The line Contains(e.appSancAdvice.application.app_id) doesn't make sense since e.appSancAdvice.application.app_id and the anonymous type are not the same type.
Simply do:
var _entityList = context.postDatedCheques
.Where(e =>
context.applications
.Select(a => a.app_id)
.Contains(e.appSancAdvice.application.app_id));
Try this instead:
var innerquery =
from app in context.applications
select new { app.app_id };
IEnumerable<postDatedCheque> _entityList = context.postDatedCheques
.Where(e => innerquery.Any(a => a.app_id == e.appSansAdvice.application.app_id));
I have a Linq collection of Things, where Thing has an Amount (decimal) property.
I'm trying to do an aggregate on this for a certain subset of Things:
var total = myThings.Sum(t => t.Amount);
and that works nicely. But then I added a condition that left me with no Things in the result:
var total = myThings.Where(t => t.OtherProperty == 123).Sum(t => t.Amount);
And instead of getting total = 0 or null, I get an error:
System.InvalidOperationException: The null value cannot be assigned to
a member with type System.Decimal which is a non-nullable value type.
That is really nasty, because I didn't expect that behavior. I would have expected total to be zero, maybe null - but certainly not to throw an exception!
What am I doing wrong? What's the workaround/fix?
EDIT - example
Thanks to all for your comments. Here's some code, copied and pasted (not simplified). It's LinqToSql (perhaps that's why you couldn't reproduce my problem):
var claims = Claim.Where(cl => cl.ID < 0);
var count = claims.Count(); // count=0
var sum = claims.Sum(cl => cl.ClaimedAmount); // throws exception
I can reproduce your problem with the following LINQPad query against Northwind:
Employees.Where(e => e.EmployeeID == -999).Sum(e => e.EmployeeID)
There are two issues here:
Sum() is overloaded
LINQ to SQL follows SQL semantics, not C# semantics.
In SQL, SUM(no rows) returns null, not zero. However, the type inference for your query gives you decimal as the type parameter, instead of decimal?. The fix is to help type inference select the correct type, i.e.:
Employees.Where(e => e.EmployeeID == -999).Sum(e => (int?)e.EmployeeID)
Now the correct Sum() overload will be used.
To get a non-nullable result, you need to cast the amount to a nullable type, and then handle the case of Sum returning null.
decimal total = myThings.Sum(t => (decimal?)t.Amount) ?? 0;
There's another question devoted to the (ir)rationale.
it throws an exception because the result of the combined sql query is null and this cant be assigned to the decimal var. If you did the following then your variable would be null (I assume ClaimedAmount is decimal):
var claims = Claim.Where(cl => cl.ID < 0);
var count = claims.Count(); // count=0
var sum = claims.Sum(cl => cl.ClaimedAmount as decimal?);
then you should get the functionality you desire.
You could also do ToList() at the point of the where statement and then the sum would return 0 but that would fall foul of what has been said elsewhere about LINQ aggregates.
If t has a property like a 'HasValue', then I would change the expression to:
var total =
myThings.Where(t => (t.HasValue) && (t.OtherProperty == 123)).Sum(t => t.Amount);