Skip to content

Commit d59f755

Browse files
committed
Added an extension method that changes the default ValueComparer so the comparison is done via reference equality.
1 parent 7e3c769 commit d59f755

File tree

9 files changed

+269
-44
lines changed

9 files changed

+269
-44
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Thinktecture.Database;
4+
5+
namespace Thinktecture.Benchmarking;
6+
7+
[MemoryDiagnoser]
8+
public class ReferenceEqualityValueComparer : IDisposable
9+
{
10+
private BenchmarkContext? _benchmarkContext;
11+
private IServiceScope? _scope;
12+
private SqlServerBenchmarkDbContext? _sqlServerDbContext;
13+
14+
private const int _BYTES_LENGTH = 1024;
15+
16+
private int _counter;
17+
private readonly byte[] _bytesBestCase = new byte[_BYTES_LENGTH];
18+
private readonly byte[] _bytesWorstCase = new byte[_BYTES_LENGTH];
19+
20+
private List<EntityWithByteArray> _entitiesWithDefaultComparer = null!;
21+
private List<EntityWithByteArrayAndValueComparer> _entitiesWithCustomComparer = null!;
22+
23+
[GlobalSetup]
24+
public void Initialize()
25+
{
26+
_benchmarkContext = new BenchmarkContext();
27+
_scope = _benchmarkContext.RootServiceProvider.CreateScope();
28+
_sqlServerDbContext = _scope.ServiceProvider.GetRequiredService<SqlServerBenchmarkDbContext>();
29+
30+
_sqlServerDbContext.Database.EnsureDeleted();
31+
_sqlServerDbContext.Database.EnsureCreated();
32+
33+
_sqlServerDbContext.EntitiesWithByteArray.BulkDelete();
34+
_sqlServerDbContext.EntitiesWithByteArrayAndValueComparer.BulkDelete();
35+
36+
var bytes = new byte[_BYTES_LENGTH];
37+
38+
for (var i = 0; i < 10_000; i++)
39+
{
40+
var id = new Guid($"66AFED1B-92EA-4483-BF4F-{i.ToString("X").PadLeft(12, '0')}");
41+
42+
_sqlServerDbContext.EntitiesWithByteArray.Add(new EntityWithByteArray(id, bytes));
43+
_sqlServerDbContext.EntitiesWithByteArrayAndValueComparer.Add(new EntityWithByteArrayAndValueComparer(id, bytes));
44+
}
45+
46+
_sqlServerDbContext.SaveChanges();
47+
_sqlServerDbContext.ChangeTracker.Clear();
48+
}
49+
50+
[GlobalCleanup]
51+
public void Dispose()
52+
{
53+
_scope?.Dispose();
54+
_benchmarkContext?.Dispose();
55+
}
56+
57+
[IterationSetup]
58+
public void IterationSetup()
59+
{
60+
_sqlServerDbContext!.ChangeTracker.Clear();
61+
_entitiesWithDefaultComparer = _sqlServerDbContext.EntitiesWithByteArray.ToList();
62+
_entitiesWithCustomComparer = _sqlServerDbContext.EntitiesWithByteArrayAndValueComparer.ToList();
63+
64+
_bytesBestCase[0] = _bytesWorstCase[^1] = (byte)(++_counter % Byte.MaxValue);
65+
}
66+
67+
[Benchmark]
68+
public async Task Default_BestCase()
69+
{
70+
_entitiesWithDefaultComparer.ForEach(e => e.Bytes = _bytesBestCase);
71+
72+
await _sqlServerDbContext!.SaveChangesAsync();
73+
}
74+
75+
[Benchmark]
76+
public async Task Default_WorstCase()
77+
{
78+
_entitiesWithDefaultComparer.ForEach(e => e.Bytes = _bytesWorstCase);
79+
80+
await _sqlServerDbContext!.SaveChangesAsync();
81+
}
82+
83+
[Benchmark]
84+
public async Task ReferenceEquality_BestCase()
85+
{
86+
_entitiesWithCustomComparer.ForEach(e => e.Bytes = _bytesBestCase);
87+
88+
await _sqlServerDbContext!.SaveChangesAsync();
89+
}
90+
91+
[Benchmark]
92+
public async Task ReferenceEquality_WorstCase()
93+
{
94+
_entitiesWithCustomComparer.ForEach(e => e.Bytes = _bytesWorstCase);
95+
96+
await _sqlServerDbContext!.SaveChangesAsync();
97+
}
98+
}

samples/Thinktecture.EntityFrameworkCore.Benchmarks/Database/BenchmarkDbContext.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ namespace Thinktecture.Database;
33
public abstract class BenchmarkDbContext : DbContext
44
{
55
public DbSet<TestEntity> TestEntities { get; set; } = null!;
6+
public DbSet<EntityWithByteArray> EntitiesWithByteArray { get; set; } = null!;
7+
public DbSet<EntityWithByteArrayAndValueComparer> EntitiesWithByteArrayAndValueComparer { get; set; } = null!;
68

79
protected BenchmarkDbContext(DbContextOptions options)
810
: base(options)
@@ -12,5 +14,17 @@ protected BenchmarkDbContext(DbContextOptions options)
1214
protected override void OnModelCreating(ModelBuilder modelBuilder)
1315
{
1416
modelBuilder.ConfigureTempTable<int, int>();
17+
18+
modelBuilder.Entity<EntityWithByteArray>(builder =>
19+
{
20+
builder.ToTable("EntitiesWithByteArray");
21+
});
22+
modelBuilder.Entity<EntityWithByteArrayAndValueComparer>(builder =>
23+
{
24+
builder.ToTable("EntitiesWithByteArrayAndValueComparer");
25+
26+
builder.Property(e => e.Bytes)
27+
.UseReferenceEqualityComparer();
28+
});
1529
}
1630
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Thinktecture.Database;
2+
3+
public class EntityWithByteArray
4+
{
5+
public Guid Id { get; set; }
6+
public byte[] Bytes { get; set; }
7+
8+
public EntityWithByteArray(Guid id, byte[] bytes)
9+
{
10+
Id = id;
11+
Bytes = bytes;
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Thinktecture.Database;
2+
3+
public class EntityWithByteArrayAndValueComparer
4+
{
5+
public Guid Id { get; set; }
6+
public byte[] Bytes { get; set; }
7+
8+
public EntityWithByteArrayAndValueComparer(Guid id, byte[] bytes)
9+
{
10+
Id = id;
11+
Bytes = bytes;
12+
}
13+
}

samples/Thinktecture.EntityFrameworkCore.Benchmarks/Program.cs

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,5 @@
44
// BenchmarkRunner.Run<CreateTempTable>();
55
// BenchmarkRunner.Run<BulkInsertIntoTempTable>();
66
// BenchmarkRunner.Run<ScalarCollectionParameter>();
7-
BenchmarkRunner.Run<ComplexCollectionParameter>();
8-
9-
// await ExecuteCreateTableAsync();
10-
// await ExecuteBulkInsertIntoTempTableAsync();
11-
12-
async Task ExecuteCreateTableAsync()
13-
{
14-
var benchmark = new CreateTempTable();
15-
16-
benchmark.Initialize();
17-
await benchmark.Sqlite_1_column();
18-
await benchmark.SqlServer_1_column();
19-
20-
await Execute(benchmark);
21-
22-
async Task Execute(CreateTempTable createTempTable)
23-
{
24-
for (var i = 0; i < 1000; i++)
25-
{
26-
await createTempTable.Sqlite_1_column();
27-
await createTempTable.SqlServer_1_column();
28-
}
29-
}
30-
}
31-
32-
async Task ExecuteBulkInsertIntoTempTableAsync()
33-
{
34-
var benchmark = new BulkInsertIntoTempTable();
35-
36-
benchmark.Initialize();
37-
await benchmark.Sqlite_1_column();
38-
await benchmark.SqlServer_1_column();
39-
40-
await Execute(benchmark);
41-
42-
async Task Execute(BulkInsertIntoTempTable bulkInsertIntoTempTable)
43-
{
44-
for (var i = 0; i < 1000; i++)
45-
{
46-
await bulkInsertIntoTempTable.Sqlite_1_column();
47-
await bulkInsertIntoTempTable.SqlServer_1_column();
48-
}
49-
}
50-
}
7+
// BenchmarkRunner.Run<ComplexCollectionParameter>();
8+
BenchmarkRunner.Run<ReferenceEqualityValueComparer>();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Runtime.CompilerServices;
2+
using Microsoft.EntityFrameworkCore.ChangeTracking;
3+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
4+
5+
namespace Thinktecture;
6+
7+
/// <summary>
8+
/// Extension methods for <see cref="PropertyBuilder{TProperty}"/>.
9+
/// </summary>
10+
public static class RelationalPropertyBuilderExtensions
11+
{
12+
/// <summary>
13+
/// Changes the <see cref="ValueComparer"/> so that values of type <typeparamref name="T"/> are compared using reference equality.
14+
/// </summary>
15+
/// <param name="propertyBuilder">The builder of property to set the <see cref="ValueComparer"/> for.</param>
16+
/// <typeparam name="T">The type of the property.</typeparam>
17+
/// <returns>The provided <paramref name="propertyBuilder"/> for chaining.</returns>
18+
public static PropertyBuilder<T> UseReferenceEqualityComparer<T>(this PropertyBuilder<T> propertyBuilder)
19+
where T : class
20+
{
21+
propertyBuilder.Metadata
22+
.SetValueComparer(new ValueComparer<T>((obj, otherObj) => ReferenceEquals(obj, otherObj),
23+
obj => RuntimeHelpers.GetHashCode(obj),
24+
obj => obj));
25+
26+
return propertyBuilder;
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Thinktecture.EntityFrameworkCore;
2+
using Thinktecture.TestDatabaseContext;
3+
4+
namespace Thinktecture.Extensions.RelationalPropertyBuilderExtensionsTests;
5+
6+
public class UseReferenceEqualityComparer : IntegrationTestsBase
7+
{
8+
public UseReferenceEqualityComparer(ITestOutputHelper testOutputHelper)
9+
: base(testOutputHelper, MigrationExecutionStrategies.EnsureCreated)
10+
{
11+
}
12+
13+
[Fact]
14+
public async Task Should_not_flag_entity_as_changed_if_property_unchanged()
15+
{
16+
ArrangeDbContext.EntitiesWithArrayValueComparer
17+
.Add(new EntityWithArrayValueComparer(new Guid("58AF51AD-2CB0-491D-88D2-64CAAC6A05B1"), new byte[] { 1, 2, 3 }));
18+
await ArrangeDbContext.SaveChangesAsync();
19+
20+
var entity = await ActDbContext.EntitiesWithArrayValueComparer.SingleAsync();
21+
22+
// no changes
23+
var affectedRows = await ActDbContext.SaveChangesAsync();
24+
25+
affectedRows.Should().Be(0);
26+
27+
var loadedEntity = await AssertDbContext.EntitiesWithArrayValueComparer.SingleAsync();
28+
loadedEntity.Bytes.Should().BeEquivalentTo(new byte[] { 1, 2, 3 });
29+
}
30+
31+
[Fact]
32+
public async Task Should_not_flag_entity_as_changed_if_reference_unchanged_but_content_changed()
33+
{
34+
ArrangeDbContext.EntitiesWithArrayValueComparer
35+
.Add(new EntityWithArrayValueComparer(new Guid("58AF51AD-2CB0-491D-88D2-64CAAC6A05B1"), new byte[] { 1, 2, 3 }));
36+
await ArrangeDbContext.SaveChangesAsync();
37+
38+
var entity = await ActDbContext.EntitiesWithArrayValueComparer.SingleAsync();
39+
entity.Bytes[2] = 42;
40+
41+
var affectedRows = await ActDbContext.SaveChangesAsync();
42+
43+
affectedRows.Should().Be(0);
44+
45+
var loadedEntity = await AssertDbContext.EntitiesWithArrayValueComparer.SingleAsync();
46+
loadedEntity.Bytes.Should().BeEquivalentTo(new byte[] { 1, 2, 3 });
47+
}
48+
49+
[Fact]
50+
public async Task Should_flag_entity_as_modified_if_reference_changed_but_content_unchanged()
51+
{
52+
ArrangeDbContext.EntitiesWithArrayValueComparer
53+
.Add(new EntityWithArrayValueComparer(new Guid("58AF51AD-2CB0-491D-88D2-64CAAC6A05B1"), new byte[] { 1, 2, 3 }));
54+
await ArrangeDbContext.SaveChangesAsync();
55+
56+
var entity = await ActDbContext.EntitiesWithArrayValueComparer.SingleAsync();
57+
entity.Bytes = new byte[] { 1, 2, 3 };
58+
59+
var affectedRows = await ActDbContext.SaveChangesAsync();
60+
61+
affectedRows.Should().Be(1);
62+
63+
var loadedEntity = await AssertDbContext.EntitiesWithArrayValueComparer.SingleAsync();
64+
loadedEntity.Bytes.Should().BeEquivalentTo(new byte[] { 1, 2, 3 });
65+
}
66+
67+
[Fact]
68+
public async Task Should_flag_entity_as_modified_if_reference_changed_and_content_changed()
69+
{
70+
ArrangeDbContext.EntitiesWithArrayValueComparer
71+
.Add(new EntityWithArrayValueComparer(new Guid("58AF51AD-2CB0-491D-88D2-64CAAC6A05B1"), new byte[] { 1, 2, 3 }));
72+
await ArrangeDbContext.SaveChangesAsync();
73+
74+
var entity = await ActDbContext.EntitiesWithArrayValueComparer.SingleAsync();
75+
entity.Bytes = new byte[] { 1, 2, 42 };
76+
77+
var affectedRows = await ActDbContext.SaveChangesAsync();
78+
79+
affectedRows.Should().Be(1);
80+
81+
var loadedEntity = await AssertDbContext.EntitiesWithArrayValueComparer.SingleAsync();
82+
loadedEntity.Bytes.Should().BeEquivalentTo(new byte[] { 1, 2, 42 });
83+
}
84+
}

tests/Thinktecture.EntityFrameworkCore.Relational.Tests/TestDatabaseContext/DbContextWithSchema.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class DbContextWithSchema : DbContext, IDbDefaultSchema
1010
#nullable disable
1111
public DbSet<TestEntity> TestEntities { get; set; }
1212
public DbSet<TestQuery> TestQuery { get; set; }
13+
public DbSet<EntityWithArrayValueComparer> EntitiesWithArrayValueComparer { get; set; }
1314
#nullable enable
1415
public Action<ModelBuilder>? ConfigureModel { get; set; }
1516

@@ -30,6 +31,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3031

3132
modelBuilder.Entity<TestQuery>().HasNoKey().ToView("TestQuery");
3233

34+
modelBuilder.Entity<EntityWithArrayValueComparer>(builder => builder.Property(e => e.Bytes)
35+
.UseReferenceEqualityComparer());
36+
3337
modelBuilder.HasDbFunction(() => TestDbFunction());
3438

3539
ConfigureModel?.Invoke(modelBuilder);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Thinktecture.TestDatabaseContext;
2+
3+
public class EntityWithArrayValueComparer
4+
{
5+
public Guid Id { get; set; }
6+
public byte[] Bytes { get; set; }
7+
8+
public EntityWithArrayValueComparer(Guid id, byte[] bytes)
9+
{
10+
Id = id;
11+
Bytes = bytes;
12+
}
13+
}

0 commit comments

Comments
 (0)