zzzprojects / EntityFramework-Plus

Entity Framework Plus extends your DbContext with must-haves features: Include Filter, Auditing, Caching, Query Future, Batch Delete, Batch Update, and more
https://entityframework-plus.net/
MIT License
2.27k stars 318 forks source link

TimeOnly support in IncludeFilter #773

Closed fectus closed 1 year ago

fectus commented 1 year ago

1. Description

I am using the NuGet package Z.EntityFramework.Plus.EFCore v6.22.4 in my application that is built on .NET 6.0 with Microsoft.EntityFrameworkCore v6.0.20 and targets MS SQL Server 2019.

I have a class that I would like to persist to the database and one of its properties is of type TimeOnly. I was able to get it working for regular LINQ to SQL with the help of the NuGet package ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly that bridges some missing features in EF Core 6.

However, when I tried to integrate the .IncludeFilter extension method from EF Plus package to one of my LINQ to SQL queries I ran into an issue deeper in the codebase of EF Plus where it apparently attempts to map a string value to a TimeOnly property and it fails with an InvalidCastException thrown. See below the stack trace.

I am not sure if the support for TimeOnly is missing in the version 6.22.4 of the package or if I am missing something and there is a known workaround. I have not been able to find any so far.

Any help will be much appreciated. Thank you.

2. Exception

If you are seeing an exception, include the full exception details (message and stack trace).

Exception message:
System.InvalidCastException: Unable to cast object of type 'System.String' to type 'System.TimeOnly'.
Stack trace:

at Z.EntityFramework.Plus.CreateEntityDataReader.GetFieldValue[T](Int32 ordinal)
   at lambda_method277(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
   at Z.EntityFramework.Plus.QueryFutureEnumerable`1.SetResult(IEnumerator`1 enumerator)
   at Z.EntityFramework.Plus.QueryFutureEnumerable`1.SetResult(DbDataReader reader)
   at Z.EntityFramework.Plus.QueryFutureBatch.ExecuteQueries()
   at Z.EntityFramework.Plus.QueryFutureEnumerable`1.GetEnumerator()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Z.EntityFramework.Plus.QueryIncludeFilterParentQueryable`1.CreateEnumerable()
   at Z.EntityFramework.Plus.QueryIncludeFilterParentQueryable`1.<GetAsyncEnumerator>b__25_1()
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at Z.EntityFramework.Plus.LazyAsyncEnumerator`1.FirstMoveNextAsync()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

3. Fiddle or Project

N/A

4. Any further technical details

I was able to decompile the code with JetBrains Rider to see where exactly the InvalidCastException exception is thrown and the method looked like this. I was able to step through the code in debug mode as well. I highlighted the invalid cast with an inline comment below.

public override T GetFieldValue<T>(int ordinal)
{
  object fieldValue = this.GetValue(ordinal);
  if (typeof (T) == typeof (DateTimeOffset) && fieldValue is DateTime)
    fieldValue = (object) new DateTimeOffset((DateTime) fieldValue);
  else if (typeof (T) == typeof (Guid) && fieldValue is byte[] b && b.Length == 16)
    fieldValue = (object) new Guid(b);
  else if (typeof (T) == typeof (TimeSpan) && fieldValue is string s)
  {
    TimeSpan result;
    TimeSpan.TryParse(s, out result);
    fieldValue = (object) result;
  }
  return (T) fieldValue;    // <-- This cast is throwing the InvalidCastException
}
JonathanMagnan commented 1 year ago

Hello @fectus ,

The support for TimeOnly and DateOnly has been added in the latest version released today.

If you have the chance to test it, let us know if that is now working correctly.

Thank you for all your information, I forgot to answer you, but you gave enough information to let us easily reproduce and fix this issue.

Best Regards,

Jon


Are you finding this library useful? If so, please consider supporting its continued development by becoming a sponsor.

Additionally, if you think your enterprise would benefit from this library, we encourage you to suggest they become a sponsor as well. Your support will help ensure this library remains alive and well-supported.