Skip to content

Commit

Permalink
Use structural equality for concurrency check in in-memory database
Browse files Browse the repository at this point in the history
Fixes #12214

Fix is to use structural equality all the time for concurrency checks. This matches more closely the way the check works on real database systems.
  • Loading branch information
ajcvickers committed Jun 27, 2018
1 parent 0dc07a6 commit 0887a13
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 6 deletions.
29 changes: 23 additions & 6 deletions src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -66,10 +67,7 @@ public virtual void Delete(IUpdateEntry entry)

for (var index = 0; index < properties.Count; index++)
{
if (properties[index].IsConcurrencyToken && !Equals(_rows[key][index], entry.GetOriginalValue(properties[index])))
{
concurrencyConflicts.Add(properties[index], _rows[key][index]);
}
IsConcurrencyConflict(entry, properties[index], _rows[key][index], concurrencyConflicts);
}

if (concurrencyConflicts.Any())
Expand All @@ -85,6 +83,25 @@ public virtual void Delete(IUpdateEntry entry)
}
}

private static bool IsConcurrencyConflict(
IUpdateEntry entry,
IProperty property,
object rowValue,
Dictionary<IProperty, object> concurrencyConflicts)
{
if (property.IsConcurrencyToken
&& !StructuralComparisons.StructuralEqualityComparer.Equals(
rowValue,
entry.GetOriginalValue(property)))
{
concurrencyConflicts.Add(property, rowValue);

return true;
}

return false;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
Expand All @@ -101,11 +118,11 @@ public virtual void Update(IUpdateEntry entry)

for (var index = 0; index < valueBuffer.Length; index++)
{
if (properties[index].IsConcurrencyToken && !Equals(_rows[key][index], entry.GetOriginalValue(properties[index])))
if (IsConcurrencyConflict(entry, properties[index], _rows[key][index], concurrencyConflicts))
{
concurrencyConflicts.Add(properties[index], _rows[key][index]);
continue;
}

valueBuffer[index] = entry.IsModified(properties[index])
? entry.GetCurrentValue(properties[index])
: _rows[key][index];
Expand Down
10 changes: 10 additions & 0 deletions src/EFCore.Specification.Tests/TestModels/UpdatesModel/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ public class Product
public Guid Id { get; set; }
public int? DependentId { get; set; }
public string Name { get; set; }

[ConcurrencyCheck]
public decimal Price { get; set; }
}

public class ProductWithBytes
{
public Guid Id { get; set; }
public string Name { get; set; }

[ConcurrencyCheck]
public byte[] Bytes { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class UpdatesContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductWithBytes> ProductWithBytes { get; set; }

public UpdatesContext(DbContextOptions options)
: base(options)
Expand All @@ -23,6 +24,7 @@ public static void Seed(UpdatesContext context)
context.Add(new Category { Id = 78, PrincipalId = 778 });
context.Add(new Product { Id = productId1, Name = "Apple Cider", Price = 1.49M, DependentId = 778 });
context.Add(new Product { Id = productId2, Name = "Apple Cobler", Price = 2.49M, DependentId = 778 });
context.Add(new ProductWithBytes { Id = productId1, Name = "MegaChips", Bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 } });

context.SaveChanges();
}
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Specification.Tests/UpdatesFixtureBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
modelBuilder.Entity<Category>()
.Property(e => e.Id)
.ValueGeneratedNever();

modelBuilder.Entity<ProductWithBytes>()
.Property(e => e.Id)
.ValueGeneratedNever();

#if !Test20
modelBuilder.Entity<LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly>(eb =>
{
Expand Down
106 changes: 106 additions & 0 deletions src/EFCore.Specification.Tests/UpdatesTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,112 @@ public virtual void Save_partial_update_on_concurrency_token_original_value_mism
() => context.SaveChanges()).Message);
});
}

[Fact]
public virtual void Update_on_bytes_concurrency_token_original_value_mismatch_throws()
{
var productId = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146");

ExecuteWithStrategyInTransaction(
context =>
{
var entry = context.ProductWithBytes.Attach(
new ProductWithBytes
{
Id = productId,
Name = "MegaChips",
Bytes = new byte[] { 8, 7, 6, 5, 4, 3, 2, 1 }
});
entry.Entity.Name = "GigaChips";
Assert.Throws<DbUpdateConcurrencyException>(
() => context.SaveChanges());
},
context =>
{
Assert.Equal("MegaChips", context.ProductWithBytes.Find(productId).Name);
});
}

[Fact]
public virtual void Update_on_bytes_concurrency_token_original_value_matches_does_not_throw()
{
var productId = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146");

ExecuteWithStrategyInTransaction(
context =>
{
var entry = context.ProductWithBytes.Attach(
new ProductWithBytes
{
Id = productId,
Name = "MegaChips",
Bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }
});
entry.Entity.Name = "GigaChips";
Assert.Equal(1, context.SaveChanges());
},
context =>
{
Assert.Equal("GigaChips", context.ProductWithBytes.Find(productId).Name);
});
}

[Fact]
public virtual void Remove_on_bytes_concurrency_token_original_value_mismatch_throws()
{
var productId = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146");

ExecuteWithStrategyInTransaction(
context =>
{
var entry = context.ProductWithBytes.Attach(
new ProductWithBytes
{
Id = productId,
Name = "MegaChips",
Bytes = new byte[] { 8, 7, 6, 5, 4, 3, 2, 1 }
});
entry.State = EntityState.Deleted;
Assert.Throws<DbUpdateConcurrencyException>(
() => context.SaveChanges());
},
context =>
{
Assert.Equal("MegaChips", context.ProductWithBytes.Find(productId).Name);
});
}

[Fact]
public virtual void Remove_on_bytes_concurrency_token_original_value_matches_does_not_throw()
{
var productId = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146");

ExecuteWithStrategyInTransaction(
context =>
{
var entry = context.ProductWithBytes.Attach(
new ProductWithBytes
{
Id = productId,
Name = "MegaChips",
Bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }
});
entry.State = EntityState.Deleted;
Assert.Equal(1, context.SaveChanges());
},
context =>
{
Assert.Null(context.ProductWithBytes.Find(productId));
});
}
#endif

[Fact]
Expand Down

0 comments on commit 0887a13

Please sign in to comment.