okarpov okarpov - 1 year ago 82
C# Question

EF Count() > 0 but First() throws exception

I have faced a strange problem. When user comes to any page of my web app
I do check if user has permissions to access it, and provide trial period if its first time to come.

Here is my piece of code:

List<string> temp_workers_id = new List<string>();
if (temp_workers_id.Count > 6)
System.Data.SqlTypes.SqlDateTime sqlDate = new System.Data.SqlTypes.SqlDateTime(DateTime.Now.Date);
var rusers = dbctx.tblMappings.Where(tm => temp_workers_id.Any(c => c == tm.ModelID));
var permissions = dbctx.UserPermissions
.Where(p => rusers
.Any(ap => ap.UserID == p.UserID)
&& p.DateStart != null
&& p.DateEnd != null
&& p.DateStart <= sqlDate.Value
&& p.DateEnd >= sqlDate.Value);

if (permissions.Count() < 1)
permissions = dbctx.UserPermissions
.Where(p => rusers
.Any(ap => ap.UserID == p.UserID)
&& p.DateStart == null
&& p.DateEnd == null);

var used = dbctx.UserPermissions
.Where(p => rusers
.Any(ap => ap.UserID == p.UserID)
&& p.DateStart != null
&& p.DateEnd != null);

if (permissions.Count() > 0 && used.Count() < 1)
var p = permissions.First();
using (Models.TTTDbContext tdbctx = new Models.TTTDbContext())
var tp = tdbctx.UserPermissions.SingleOrDefault(tup => tup.UserID == p.UserID);
tp.DateStart = DateTime.Now.Date;
tp.DateEnd = DateTime.Now.Date.AddDays(60);

here the
method throws exception:

Sequence contains no elements

how that even could be?

I dont think that user opens two browsers and navigate here at the same time, but could be the concurrency issue?

Answer Source

You claim you only found this in the server logs and didn't encounter it during debugging. That means that between these lines:

if (permissions.Count() > 0)
    var p = permissions.First();

Some other process or thread changed your database, so that the query didn't match any documents anymore.

This is caused by permissions holding a lazily evaluated resource, meaning that the query is only executed when you iterate it (which Count() and First()) do.

So in the Count(), the query is executed:


Which returns, at that moment, one row. Then the data is modified externally, causing the next query (at First()):

SELECT n1, n2, ... WHERE ...

To return zero rows, causing First() to throw.

Now for how to solve that, is up to you, and depends entirely on how you want to model this scenario. It means the second query was actually correct: at that moment, there were no more rows that fulfilled the query criteria. You could materialize the query once:

permissions = query.Where(...).ToList()

But that would mean your logic operates on stale data. The same would happen if you'd use FirstOrDefault():

var permissionToApply = permissions.FirstOrDefault();
if (permissionToApply != null)
    // rest of your logic

So it's basically a lose-lose scenario. There's always the chance that you're operating on stale data, which means that the next code:

 tdbctx.UserPermissions.SingleOrDefault(tup => tup.UserID == p.UserID);

Would throw as well. So every time you query the database, you'll have to write the code in such a way that it can handle the records not being present anymore.