Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int

public override Type GetFieldType(int ordinal) => _inner.GetFieldType(ordinal);

public override T GetFieldValue<T>(int ordinal) => Wrap(() => _inner.GetFieldValue<T>(ordinal));

public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken) =>
WrapAsync(() => _inner.GetFieldValueAsync<T>(ordinal, cancellationToken));

public override float GetFloat(int ordinal) => _inner.GetFloat(ordinal);

public override Guid GetGuid(int ordinal) => _inner.GetGuid(ordinal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ protected override (TestDbContext ctx, SqlCapture capture) CreateContextWithCapt

protected override async Task ResetDatabaseAsync(TestDbContext ctx)
{
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM `ModelsWithDateTimeOffset`");
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM `OrderLines`");
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM `Products`");
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM `Categories`");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ protected override (TestDbContext ctx, SqlCapture capture) CreateContextWithCapt
}

protected override Task ResetDatabaseAsync(TestDbContext ctx) =>
ctx.Database.ExecuteSqlRawAsync("""TRUNCATE "OrderLines", "Products", "Categories" RESTART IDENTITY CASCADE""");
ctx.Database.ExecuteSqlRawAsync(
"""TRUNCATE "OrderLines", "Products", "Categories", "ModelsWithDateTimeOffset" RESTART IDENTITY CASCADE"""
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ protected override (TestDbContext ctx, SqlCapture capture) CreateContextWithCapt

protected override async Task ResetDatabaseAsync(TestDbContext ctx)
{
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM [ModelsWithDateTimeOffset]");
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM [OrderLines]");
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM [Products]");
await ctx.Database.ExecuteSqlRawAsync("DELETE FROM [Categories]");
await ctx.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('[ModelsWithDateTimeOffset]', RESEED, 0)");
await ctx.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('[OrderLines]', RESEED, 0)");
await ctx.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('[Products]', RESEED, 0)");
await ctx.Database.ExecuteSqlRawAsync("DBCC CHECKIDENT ('[Categories]', RESEED, 0)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<NoWarn>$(NoWarn);EF1001</NoWarn>
<IsPackable>false</IsPackable>
<IsTestProject>false</IsTestProject>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,32 @@ public async Task ForUpdate_ToList_ReturnsAllMatchingRows()
products.Should().HaveCount(3);
await tx.RollbackAsync();
}

[Fact]
public async Task ForUpdate_WithDateTimeOffsetColumn_MaterializesProviderValue()
{
await using var ctx = CreateContext();
var expected = DateTimeOffset.UtcNow.AddMinutes(-10);
var entity = new ModelWithDateTimeOffset { Date = expected };
ctx.ModelsWithDateTimeOffset.Add(entity);
await ctx.SaveChangesAsync();

await using var tx = await ctx.Database.BeginTransactionAsync();
var result = ctx
.ModelsWithDateTimeOffset.ForUpdate()
.Where(m => m.Date < DateTimeOffset.UtcNow.AddMinutes(-1))
.FirstOrDefault(m => m.Id == entity.Id);

result.Should().NotBeNull();
result!.Date.Should().BeCloseTo(expected, TimeSpan.FromMilliseconds(1));

result = await ctx
.ModelsWithDateTimeOffset.Where(m => m.Date < DateTimeOffset.UtcNow.AddMinutes(-1))
.ForUpdate()
.FirstOrDefaultAsync(m => m.Id == entity.Id);

result.Should().NotBeNull();
result!.Date.Should().BeCloseTo(expected, TimeSpan.FromMilliseconds(1));
await tx.RollbackAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(
public DbSet<Category> Categories => Set<Category>();
public DbSet<Product> Products => Set<Product>();
public DbSet<OrderLine> OrderLines => Set<OrderLine>();
public DbSet<ModelWithDateTimeOffset> ModelsWithDateTimeOffset => Set<ModelWithDateTimeOffset>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand All @@ -30,6 +31,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
b.Property(ol => ol.UnitPrice).HasColumnType("decimal(18,2)");
b.HasOne(ol => ol.Product).WithMany(p => p.OrderLines).HasForeignKey(ol => ol.ProductId);
});

modelBuilder.Entity<ModelWithDateTimeOffset>(b =>
{
b.HasKey(m => m.Id);
b.Property(m => m.Date).IsRequired();
});
}
}

Expand Down Expand Up @@ -59,3 +66,9 @@ public class OrderLine
public decimal UnitPrice { get; set; }
public Product Product { get; set; } = null!;
}

public class ModelWithDateTimeOffset
{
public int Id { get; set; }
public DateTimeOffset Date { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Collections;
using System.Data.Common;
using AwesomeAssertions;
using EntityFrameworkCore.Locking.Abstractions;
using EntityFrameworkCore.Locking.Exceptions;
using EntityFrameworkCore.Locking.Internal;
using Xunit;

namespace EntityFrameworkCore.Locking.Tests;

public class TranslatingDbDataReaderTests
{
[Fact]
public void GetFieldValue_DelegatesProviderSpecificTypedReadToInnerReader()
{
var expected = new DateTimeOffset(2026, 5, 14, 10, 15, 30, TimeSpan.Zero);
var reader = new TranslatingDbDataReader(
new ProviderSpecificDateTimeOffsetReader(expected),
new NoopTranslator()
);

var value = reader.GetFieldValue<DateTimeOffset>(0);

value.Should().Be(expected);
}

[Fact]
public async Task GetFieldValueAsync_DelegatesProviderSpecificTypedReadToInnerReader()
{
var expected = new DateTimeOffset(2026, 5, 14, 10, 15, 30, TimeSpan.Zero);
var reader = new TranslatingDbDataReader(
new ProviderSpecificDateTimeOffsetReader(expected),
new NoopTranslator()
);

var value = await reader.GetFieldValueAsync<DateTimeOffset>(0);

value.Should().Be(expected);
}

private sealed class NoopTranslator : IExceptionTranslator
{
public LockingException? Translate(Exception exception) => null;
}

private sealed class ProviderSpecificDateTimeOffsetReader(DateTimeOffset value) : DbDataReader
{
public override int FieldCount => 1;
public override object this[int ordinal] => GetValue(ordinal);
public override object this[string name] => GetValue(GetOrdinal(name));
public override int Depth => 0;
public override bool HasRows => true;
public override bool IsClosed => false;
public override int RecordsAffected => 1;

public override bool Read() => true;

public override Task<bool> ReadAsync(CancellationToken cancellationToken) => Task.FromResult(true);

public override bool NextResult() => false;

public override Task<bool> NextResultAsync(CancellationToken cancellationToken) => Task.FromResult(false);

public override bool GetBoolean(int ordinal) => throw new NotSupportedException();

public override byte GetByte(int ordinal) => throw new NotSupportedException();

public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) =>
throw new NotSupportedException();

public override char GetChar(int ordinal) => throw new NotSupportedException();

public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) =>
throw new NotSupportedException();

public override string GetDataTypeName(int ordinal) => "timestamp with time zone";

public override DateTime GetDateTime(int ordinal) => value.UtcDateTime;

public override decimal GetDecimal(int ordinal) => throw new NotSupportedException();

public override double GetDouble(int ordinal) => throw new NotSupportedException();

public override Type GetFieldType(int ordinal) => typeof(DateTime);

public override T GetFieldValue<T>(int ordinal)
{
if (typeof(T) == typeof(DateTimeOffset))
return (T)(object)value;

return base.GetFieldValue<T>(ordinal);
}

public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken) =>
Task.FromResult(GetFieldValue<T>(ordinal));

public override float GetFloat(int ordinal) => throw new NotSupportedException();

public override Guid GetGuid(int ordinal) => throw new NotSupportedException();

public override short GetInt16(int ordinal) => throw new NotSupportedException();

public override int GetInt32(int ordinal) => throw new NotSupportedException();

public override long GetInt64(int ordinal) => throw new NotSupportedException();

public override string GetName(int ordinal) => ordinal == 0 ? "Date" : throw new IndexOutOfRangeException();

public override int GetOrdinal(string name) => name == "Date" ? 0 : throw new IndexOutOfRangeException(name);

public override string GetString(int ordinal) => throw new NotSupportedException();

public override object GetValue(int ordinal) => value.UtcDateTime;

public override int GetValues(object[] values)
{
values[0] = GetValue(0);
return 1;
}

public override bool IsDBNull(int ordinal) => false;

public override IEnumerator GetEnumerator()
{
yield break;
}
}
}
Loading