1

I am having a problem returning a default DateTime value from a complex Linq-to-Sql query.

Hopefully the following simplified example shows the problem (although I haven't run this exact code):

users.Select(u =>
  new MyDomainObject(
     u.Id,
     u.Transactions
        .Where(t => false) // empty results set
        .Select(t => t.TransactionTime) // TransactionTime is DATETIME NOT NULL
        .OrderByDescending(x => x)
        .FirstOrDefault() // I want DateTime.MinValue (or SqlDateTime.MinValue)
  )
); 

So I want the last timestamp, or some MinValue timestamp if there are no results.

Enumerating the above query gives the error

The null value cannot be assigned to a member with type System.DateTime

UPDATE

Ok I'm not sure that my above example was sufficient to illustrate the problem. I believe the error may be something to do with how I'm trying to do a subquery on a third linked table.

The following example recreates the exact error:

enter image description here

So I have a car, which I can take to the mechanic, which sometimes (but not always) is serviced by the mechanic.

The requirement is to find via a query to the car table how many mechanic visits each car has had since it was last serviced. The issue is when the car has never been serviced, so the data looks like:

Car
------------- 
Id: 1         


MechanicVisit          
------------- 
Id: 1 
CarId: 1 
ServiceRecordId: NULL
VisitDate: 1 Jan 2011    


ServiceRecord
------------- 
<empty>

So a simple example that shows the error is a query to get the list of last service times:

 var test = _dataContext.GetTable<Car>
                 .Select(c => 
                     c.MechanicVisits
                      .Select(m => m.ServiceRecord)
                      .Select(s => s.ServiceDate)
                      .OrderByDescending(d => d)
                      .FirstOrDefault()
                  ).ToList();

This gives the previously described error of trying to assign null to a non-nullable type, where what I need to do is return DateTime.MinValue or SqlDateTime.MinValue when the date is null (so I can do the actual query which is number of mechanic visits since the last service)


SOLUTION

I used a variation of what Jon Skeet suggested, using a cast to DateTime? and null coalescing operator:

 var test = _dataContext.GetTable<Car>
                     .Select(c => 
                         c.MechanicVisits
                          .Select(m => m.ServiceRecord)
                          .Select(s => (DateTime?)s.ServiceDate)
                          .OrderByDescending(d => d)
                          .FirstOrDefault() ?? new DateTime(1900, 1, 1)
                      ).ToList();

Note the use of the paramatered constructor for the "default" date - DateTime.MinValue can't be used here as it throws an out of range exception when converted to SQL, and SqlDateTime.MinValue can't be used as it is non-nullable (so the coalesce operator becomes invalid).

I still don't really understand why the original error was occuring and this solution does feel a bit hacky, but I've been unable to find any neater way of fixing it.

fearofawhackplanet
  • 52,166
  • 53
  • 160
  • 253

4 Answers4

3

I'd be very tempted to actually use a nullable DateTime for this. For example, from your "Car" sample:

var test = _dataContext.GetTable<Car>
                       .Select(c => 
                           c.MechanicVisits
                            .Select(m => m.ServiceRecord)
                            .Select(s => (DateTime?) s.ServiceDate)
                            .OrderByDescending(d => d)
                            .FirstOrDefault()
                       ).ToList();

That way I suspect you'll end up with it working and giving you null DateTime? values. You could always transform that later if you wanted:

var test = _dataContext.GetTable<Car>
                       .Select(c => 
                           c.MechanicVisits
                            .Select(m => m.ServiceRecord)
                            .Select(s => (DateTime?) s.ServiceDate)
                            .OrderByDescending(d => d)
                            .FirstOrDefault()
                       ).AsEnumerable()
                        .Select(dt => dt ?? DateTime.MinValue)
                        .ToList();

Original answer (doesn't work)

Hmm. I won't claim to fully understand the reasons for this, but here's a potential workaround:

users.Select(u =>
  new MyDomainObject(
     u.Id,
     u.Transactions
        .Where(t => false)
        .Select(t => t.TransactionTime)
        .OrderByDescending(x => x)
        .DefaultIfEmpty(DateTime.MinValue)
        .First()
  )
); 

In other words, if the result set is empty, use the specified default - and then take the first result of the now-definitely-not-empty sequence.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • That looks like what I need but it's throwing `System.NotSupportedException: Unsupported overload used for query operator 'DefaultIfEmpty'`. Now I'm even more confused! – fearofawhackplanet Jun 27 '11 at 16:41
  • @fearofawhack: Drat. Okay, that's unhelpful... will have a think. – Jon Skeet Jun 27 '11 at 16:46
  • @Jon ok, I'm thinking there must be something going on which I haven't spotted. I'm trying to do an end to end example, I'll update the question when I have something which demonstrates the problem a bit better. I'd appreciate it if you could check back on this question a bit later or tomorrow sometime. – fearofawhackplanet Jun 27 '11 at 16:53
  • @fearofawhackplanet: A complete example would be great, thanks. I suggest you add a comment to this answer when you're done, or I may forget to check :) – Jon Skeet Jun 27 '11 at 16:54
  • @Jon: ok I have added an update which recreates the error in full. Would appreciate your input if you do find this thread again and it's still unsolved. I'm sure it must be something obvious but I can't spot it! thanks – fearofawhackplanet Jun 27 '11 at 19:20
  • I would be very surprised if the updated answer did not work! Was going to suggest the cast myself. However, I would suggest the field is made nullable in the db. As mentioned, the value isn't guaranteed so nullable seems sensible. – Hux Jun 27 '11 at 20:01
  • @MiG: I don't think so - it's only because we're collecting a *bunch* of these that we're ending up with a nullable value. It sounds like it should logically be non-nullable in the db. – Jon Skeet Jun 27 '11 at 20:28
  • @Jon Skeet - My bad, I didn't see that `ServiceRecordId` was nullable. – Hux Jun 27 '11 at 20:37
  • @Jon: thanks for your help, I've used a variation on your suggestion (shown in the updated question). I still don't really understand why Linq couldn't parse the original code into a valid query, but at least I've got something working now. – fearofawhackplanet Jun 28 '11 at 10:40
1

The error implies that t.TransactionTime is nullable (i.e. DateTime?). Even if the field can't be null in the database if the property is nullable and the query returns no rows FirstOrDefault() will return null as this is the default value of t.TransactionTime

EDIT

further to your comments, that is very strange: the below code outputs True

List<DateTime> dates = new List<DateTime>();
DateTime date = dates.FirstOrDefault();
Console.WriteLine(date == DateTime.MinValue);

And i would expect your code to do the same.

Ben Robinson
  • 21,601
  • 5
  • 62
  • 79
1
users.Select(u =>
  new MyDomainObject(
     u.Id,
     u.Transactions
        .Where(t => false) // empty results set
        .Select(t => t.TransactionTime)
        .Any() ?
     u.Transactions
        .Where(t => false) // empty results set
        .Select(t => t.TransactionTime) // TransactionTime is DATETIME NOT NULL
        .OrderByDescending(x => x)
        .FirstOrDefault() : // I want DateTime.MinValue (or SqlDateTime.MinValue)
     DateTime.MinValue
  )
);

This will supply DateTime.MinValue if there are no transactions available for that object.

Chris Snowden
  • 4,982
  • 1
  • 25
  • 34
0

This will supply DateTime.MinValue if there are no transactions available for that object, example:

users.Select(u =>
  new MyDomainObject(
     u.Id,
     u.Transactions
        .Where(t => false) // empty results set
        .Select(t => t.TransactionTime)
        .Any() ?
     u.Transactions
        .Where(t => false) // empty results set
        .Select(t => t.TransactionTime) // TransactionTime is DATETIME NOT NULL
        .OrderByDescending(x => x)
        .FirstOrDefault() : // I want DateTime.MinValue (or SqlDateTime.MinValue)
     DateTime.MinValue
  )
);
Dani
  • 1,825
  • 2
  • 15
  • 29
Chris Snowden
  • 4,982
  • 1
  • 25
  • 34