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

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; }
}
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>();

image

  • Data: single parent, no child
var parent = new Parent();
context.Add(parent);
context.SaveChanges();
  • execute a query FirstOrDefault query on Parent and .Include the Child 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

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 1
  • Comments: 16 (11 by maintainers)

Commits related to this issue

Most upvoted comments

This is working in 3.1

Providing a temporary workaround that worked in my case:

on this table:

public class MyTable {
  public ulong RowVersion { get; set; }
}

using this builder config:

builder.Entity<MyTable>()
                .Property(p => p.RowVersion)
                .HasConversion<byte[]>()
                .IsRowVersion();
migrationBuilder.AddColumn<byte[]>(
                name: "RowVersion",
                table: "MyTable",
                nullable: false,
                defaultValue: new byte[] { });

manually changing the default value to a non-zero length array in the migration fixes it:

migrationBuilder.AddColumn<byte[]>(
                name: "RowVersion",
                table: "MyTable",
                nullable: false,
                defaultValue: new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });