EF Core & Nullable Timespans

We'll keep the first post short 😅

I fell into a bit of trap working with a nullable TimeSpan and EF Core 3.1

Situation

Say you have a basic class which your going to use as a model in EF Core with a nullable TimeSpan.

public class Record 
{
    public int Id { get; set; }
    public string Name { get; set; }
    public TimeSpan? Value { get; set; } 
}

Now you wire it up up in your old DbContext

public class TestContext : DbContext
{
   public DbSet<Record> Records { get; set; }

   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
       base.OnModelCreating(modelBuilder);

       modelBuilder.Entity<Record>(entity =>
       {
       	entity.HasKey(x => x.Id);
           entity.Property(x => x.Id).UseIdentityColumn();
       });
   }
}

Create your migration and create your database.

The problem

When you try to add a new record to the database.

await context.Records.AddAsync(new Record {
    Name = "How long did i take to eat the pizza",
    Value = TimeSpan.FromDays(2)
});
await context.SaveChangesAsync();

This will error.

EF Core maps TimeSpans to the MSSQL Server Time format which is limited to 24 hours.

The Fix

EF Core has the concept of Value Conversions which you can apply to properties of your models to change the type they are stored as in SQL Server.

For TimeSpans there are two:

Problem in this example is we want a nullable TimeSpan. Luckily you can make your own and MS are great at letting you look at the code for their stuff.

So lets take TimeSpanToTicksConverter and update it so we can work with nullable TimeSpans.

public class NullableTimeSpanToTicksConverter : ValueConverter<TimeSpan?, long?>
{
    public NullableTimeSpanToTicksConverter(ConverterMappingHints mappingHints = null) : 
        base(v => v.HasValue ? v.Value.Ticks : (long?)null , v => v.HasValue ? new TimeSpan(v.Value) :
            (TimeSpan?)null, mappingHints)
            {
            }
    
    public static ValueConverterInfo DefaultInfo { get; } = 
    new ValueConverterInfo(typeof(TimeSpan?), typeof(long?), i => new NullableTimeSpanToTicksConverter(i.MappingHints));
}

Now all we do is add into our OnModelCreating for our Record

entity.Property(p => p.Value).HasConversion(new NullableTimeSpanToTicksConverter());

Recreate our migrations and a fresh database.

Now our nullable TimeSpan will be converted to a nullable long which can be any time length.

Enjoy!

Jonathan Dent