efcore: NumberToBytesConverter on ulong RowVersion property fails with .Include query
Sorry about the detail. but this has a few moving pieces.
(TLDR run this test in the repro)
Scenario
- using a Timestamp/row version
- rowversion is mapped to a
ulong
property with a NumberToBytesConverter.HasConversion(new NumberToBytesConverter<ulong>())
. - Models
public class Parent
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? ChildId { get; set; }
public Child Child { get; set; }
}
public class Child
{
public Guid Id { get; set; } = Guid.NewGuid();
public ulong ULongRowVersion { get; set; }
public Guid ParentId { get; set; }
public Parent Parent { get; set; }
}
- a single One To One Relationship. and map
ULongRowVersion
to a rowversion
var child = modelBuilder.Entity<Child>();
child.HasOne(_ => _.Parent)
.WithOne(_ => _.Child)
.HasForeignKey<Parent>(_ => _.ChildId);
child.Property(x => x.ULongRowVersion)
.HasConversion(new NumberToBytesConverter<ulong>())
.IsRowVersion()
.IsRequired()
.HasColumnType("RowVersion");
modelBuilder.Entity<Parent>();
- Data: single parent, no child
var parent = new Parent();
context.Add(parent);
context.SaveChanges();
- execute a query
FirstOrDefault
query onParent
and.Include
theChild
property
var firstOrDefault = context.Parents
.Include(x => x.Child)
.FirstOrDefault();
Outcome
InvalidOperationException
in NumberToBytesConverter.ReverseLong.
System.InvalidOperationException : An exception occurred while reading a database value for property 'Child.ULongRowVersion'. See the inner exception for more information.
---- System.IndexOutOfRangeException : Index was outside the bounds of the array.
at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityMaterializerSource.ThrowReadValueException[TValue](Exception exception, Object value, IPropertyBase property)
at lambda_method(Closure , DbDataReader )
at Microsoft.EntityFrameworkCore.Storage.Internal.TypedRelationalValueBufferFactory.Create(DbDataReader dataReader)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.BufferlessMoveNext(DbContext _, Boolean buffer)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.<_TrackEntities>d__17`2.MoveNext()
at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext()
at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at Tests.ULongRowVersion() in C:\Code\EfRowVersionRepro\DataModel\Tests.cs:line 46
----- Inner Stack Trace -----
at Microsoft.EntityFrameworkCore.Storage.ValueConversion.NumberToBytesConverter`1.ReverseLong(Byte[] bytes)
at lambda_method(Closure , DbDataReader )
The strange thing is, that with my understanding of the above scenario, NumberToBytesConverter
should never be called since there are no rows in that table.
Repro
https://github.com/SimonCropp/EfRowVersionRepro
The above failing scenario is in the ULongRowVersionContext and can be executed in the Tests.ULongRowVersion() method.
There are also two other working scenarios.
Using BytesRowVersion
This scenario still uses a ulong
for the row version, but with a custom. ULongRowVersionWithWorkAroundContext and can be executed in the Tests.ULongRowVersionWithWorkAround() method.
ULongToBytesConverter is a clone of NumberToBytesConverter. When debugging the exception was occurring in the code generated by the custom expressions, so it made it very difficult to understand what the problem was. When i initially created ULongToBytesConverter i got the same exception as with NumberToBytesConverter. But since i was able to debug i was able to see where the error was occurring and add a workaround.
static ulong ToNumber(byte[] bytes)
{
if (bytes == null)
{
return 0;
}
#region check added to avoid exception
if (bytes.Length == 0)
{
return 0;
}
#endregion
if (BitConverter.IsLittleEndian)
{
bytes = ReverseLong(bytes);
}
return BitConverter.ToUInt64(bytes, 0);
}
Note that i have no idea why this works 😉
Further technical details
- Microsoft.EntityFrameworkCore.SqlServer 2.1.1
- Microsoft.EntityFrameworkCore 2.1.1
- Microsoft SQL Server Express (64-bit) 14.0.1000.169
- Windows Server 2012 R2 Datacenter (6.3)
- TargetFramework: netcoreapp2.0
- Visual Studio 2017 15.7.4)
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 1
- Comments: 16 (11 by maintainers)
Commits related to this issue
- Fix issue#12518 Regression test already existed. Added another variation for #22256 Close #12518 — committed to dotnet/efcore by smitpatel 4 years ago
- Fix issue#12518 Regression test already existed. Added another variation for #22256 Close #12518 — committed to dotnet/efcore by smitpatel 4 years ago
- Fix issue#12518 (#22257) Regression test already existed. Added another variation for #22256 Close #12518 — committed to dotnet/efcore by smitpatel 4 years ago
This is working in 3.1
Providing a temporary workaround that worked in my case:
on this table:
using this builder config:
manually changing the default value to a non-zero length array in the migration fixes it: