18

I have a parent entity that I need to do a concurrency check (as annotated as below)

[Timestamp]
public byte[] RowVersion { get; set; }

I have a bunch of client processes that access readonly values out of this parent entity and primarily update its child entities.

The constraint

  1. Clients should not interfere with each other's work, (e.g. updating child records should not throw a concurrency exception on the parent entity).

  2. I have a server process that does update this parent entity, and in this case the client process needs to throw if the parent entity has been changed.

Note : The client's concurrency check is sacrificial, the server's workflow is mission critical.

The problem

I need to check (from the client process) if the parent entity has changed without updating the parents entity's row version.

It's easy enough to do a concurrency check on the parent entity in EF:

// Update the row version's original value
_db.Entry(dbManifest)
      .Property(b => b.RowVersion)
      .OriginalValue = dbManifest.RowVersion; // the row version the client originally read

// Mark the row version as modified
_db.Entry(dbManifest)
       .Property(x => x.RowVersion)
       .IsModified = true;

The IsModified = true is the deal breaker because it forces the row version to change. Or, said in context, this check from the client process will cause a row version change in the parent entity, which interferes needlessly with the other client processes' workflows.

A work around : I could potentially wrap the SaveChanges from the client process in a Transaction and then a subsequent read of the parent entity's row version, in-turn, rolling back if the row version has changed.

Summary

Is there an out-of-the-box way with Entity Framework where I can SaveChanges (in the client process for the child entities) yet also check if the parent entity's row version has changed (without updating the parent entities row version).

TheGeneral
  • 69,477
  • 8
  • 65
  • 107
  • Is it possible to use sqlserver rowversion feature? https://docs.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql?view=sql-server-2017 – ilkerkaran Jul 19 '19 at 06:29
  • @ilkerkaran yes i am using it, however its more a case of how to check for a concurrency change on a parent table with out changing the `rowversion` on that table so the `SaveChanges` fails in entity framework. – TheGeneral Jul 19 '19 at 06:32
  • Is this problem similar to yours? https://social.msdn.microsoft.com/Forums/en-US/4ec6844f-3a12-43c0-8a2c-84edb3d08c62/entity-framework-does-rowversion-checking-for-parent-tables?forum=adodotnetentityframework. – Andrei Odegov Jul 19 '19 at 09:52
  • Are there many methods that modify parents and children, or just a few? In other words, would your transaction work-around lead to repetitive code? – Gert Arnold Jul 22 '19 at 20:47
  • @GertArnold hi, Yeah the clients would need to protect the places where they update the child records, there is only a handful of places, maybe up to 10, this is not a deal breaker though. I could easily encapsulate a transaction logic in a method, it would be nice if there was another atomic way though. – TheGeneral Jul 22 '19 at 23:10
  • 1
    I think your transaction work-around isn't too bad. Esp. when encapsulated, the advantage is that it's all in one place, so it's clear what happens and it's not likely to have any side effects. Any other solution, for instance using EF's command tree interceptors, will consist of separated code parts, making it easy to break something at either side of the spectrum. F.e. it all fails if the parent rowids aren't pulled from the database, which is easy to forget when the code doesn't show why/where they're necessary. Also, maybe more importantly, it ties you to this version of EF. – Gert Arnold Jul 23 '19 at 07:23
  • You can use `[ConcurrencyCheck]` attribute above one of properties which causes a good concurrency control for the entire table based on my experience. – Amirhossein Mehrvarzi Jul 25 '19 at 14:35

3 Answers3

7

There is a surprisingly simple solution, "out-of-2-boxes", but it requires two modifications I'm not sure you can, or are willing to, make:

  • Create an updatable view on the Child table containing a ParentRowVersion column
  • Map the Child entity to this view

Let me show how this works. It's all pretty straightforward.

Database model:

CREATE TABLE [dbo].[Parent]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Parent] ADD CONSTRAINT [PK_Parent] PRIMARY KEY CLUSTERED  ([ID]) ON [PRIMARY]

CREATE TABLE [dbo].[Child]
(
[ID] [int] NOT NULL IDENTITY(1, 1),
[Name] [nvarchar] (50) NOT NULL,
[RowVersion] [timestamp] NOT NULL,
[ParentID] [int] NOT NULL
) ON [PRIMARY]
ALTER TABLE [dbo].[Child] ADD CONSTRAINT [PK_Child] PRIMARY KEY CLUSTERED  ([ID]) ON [PRIMARY]
GO
CREATE VIEW [dbo].[ChildView]
WITH SCHEMABINDING
AS
SELECT Child.ID
, Child.Name
, Child.ParentID
, Child.RowVersion
, p.RowVersion AS ParentRowVersion
FROM dbo.Child
INNER JOIN dbo.Parent p ON p.ID = Child.ParentID

The view is updatable because it meets the conditions for Sql Server views to be updatable.

Data

SET IDENTITY_INSERT [dbo].[Parent] ON
INSERT INTO [dbo].[Parent] ([ID], [Name]) VALUES (1, N'Parent1')
SET IDENTITY_INSERT [dbo].[Parent] OFF

SET IDENTITY_INSERT [dbo].[Child] ON
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (1, N'Child1.1', 1)
INSERT INTO [dbo].[Child] ([ID], [Name], [ParentID]) VALUES (2, N'Child1.2', 1)
SET IDENTITY_INSERT [dbo].[Child] OFF

Class model

public class Parent
{
    public Parent()
    {
        Children = new HashSet<Child>();
    }
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] RowVersion { get; set; }
    public ICollection<Child> Children { get; set; }
}

public class Child
{
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] RowVersion { get; set; }

    public int ParentID { get; set; }
    public Parent Parent { get; set; }
    public byte[] ParentRowVersion { get; set; }
}

Context

public class TestContext : DbContext
{
    public TestContext(string connectionString) : base(connectionString){ }

    public DbSet<Parent> Parents { get; set; }
    public DbSet<Child> Children { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Parent>().Property(e => e.RowVersion).IsRowVersion();
        modelBuilder.Entity<Child>().ToTable("ChildView");
        modelBuilder.Entity<Child>().Property(e => e.ParentRowVersion).IsRowVersion();
    }
}

Bringing it together

This piece of code updates a Child while a fake concurrent user updates its Parent:

using (var db = new TestContext(connString))
{
    var child = db.Children.Find(1);

    // Fake concurrent update of parent.
    db.Database.ExecuteSqlCommand("UPDATE dbo.Parent SET Name = Name + 'x' WHERE ID = 1");

    child.Name = child.Name + "y";
    db.SaveChanges();
}

Now SaveChanges throws the required DbUpdateConcurrencyException. When the update of the parent is commented out the child update succeeds.

I think the advantage of this method is that it's pretty independent of a data access library. All you need a an ORM that supports optimistic concurrency. A future move to EF-core won't be a problem.

spottedmahn
  • 11,379
  • 7
  • 75
  • 144
Gert Arnold
  • 93,904
  • 24
  • 179
  • 256
  • 1
    This is actually a very neat solution and creative solution, also a novel way of achieving the desired result with the view. ill play around with it at work today, thanks ! – TheGeneral Jul 23 '19 at 21:22
  • 1
    "out-of-2-boxes" - lol, I like that! It would have been nice if EF allows us to create such "view" with code (expression type column?), but it doesn't, so this seems to be the best combination of the "two worlds" :) – Ivan Stoev Jul 24 '19 at 17:50
  • 1
    @Ivan tnx! Yeah, EF could do with a little more graph/aggregate awareness, although I think EF-core is a "small step" in that direction. I consider you capable of realizing this expression-type column idea into a pull request making for a "giant leap" in this area. Only takes a bit of time... – Gert Arnold Jul 24 '19 at 18:55
  • This answers the question best in the spirit of what i was asking for – TheGeneral Jul 26 '19 at 06:33
  • Great answer! Just curious how to create the view when using EF Code First for the model definition. – sjb-sjb Aug 30 '19 at 12:12
  • @sjb-sjb "how to create the view" encompasses a lot of things. It's better to ask a new question is this is an issue for you. – Gert Arnold Aug 30 '19 at 12:26
3

Well, what you need to do is check the concurrency token (Timestamp) of the parent entity when you write to the child entity. The only challenge is that the parent timestamp is not in the child entities.

You didn't state explicitly but I'm assuming that you are using EF Core.

Looking at https://docs.microsoft.com/en-us/ef/core/saving/concurrency, it seems that EF Core will throw the concurrency exception if an UPDATE or DELETE affects zero rows. To implement concurrency testing, EF adds a WHERE clause testing the concurrency token and then tests whether or the correct number of rows were impacted by the UPDATE or DELETE.

What you could try would be to add an additional WHERE clause to the UPDATE or DELETE that tests the value of the parent's RowVersion. I think you might be able to do this using the System.Diagnostics.DiagnosticListener class to intercept the EF Core 2. There is an article on it at https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core and a discussion at Can I configure an interceptor yet in EntityFramework Core? . Evidently EF Core 3 (I think it is coming in September/October) will include an interception mechanism similar to that which was in EF pre-Core, see https://github.com/aspnet/EntityFrameworkCore/issues/15066

Hope this is useful for you.

sjb-sjb
  • 961
  • 4
  • 13
  • 1
    Well, first this is *not* EF Core, but EF6 - I specifically asked OP and they added the correct tag, which should have been there at the time of answering. Second, regarding the *only challenge* - to me it is quite a big challenge. While overall the idea seems to be correct, I would like to see that implemented in a generic fashion, especially with the quite counterintuitive EF6 metadata model. – Ivan Stoev Jul 22 '19 at 08:35
  • Yeah this is definitely entity framework not core, ill take a further look at this approach today and get my head around to see if its applicable. – TheGeneral Jul 22 '19 at 23:14
  • The last link cited in my answer above restates the problem and it names the classes that are relevant to solving the problem pre-Core. – sjb-sjb Jul 25 '19 at 02:43
3

From project to project I meet this problem on a broad platforms (not only .Net). From architecture perspective I can propose several decisions that aren't peculiar to EntityFramework. (As for me #2 is better)

OPTION 1 to implement optimistic locking approach. In general idea sounds like: "Let's update Client then check state of the parent". You have already mentioned idea "To use transaction", but optimistic locking can just reduce time needed to retain Parent entity. Something like:

var expectedVersion = _db.Parent...First().RowVersion;
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required))
{
    //modify Client entity there
    ...
    //now make second check of Parent version
    if( expectedVersion != _db.Parent...First().RowVersion )
        throw new Exception(...);
    _db.SaveChanges();
}

Note! Depending on SQL server settings (isolation levels) you may need to apply to Parent entity select-for-update pls see there how to do it. How to implement Select For Update in EF Core

OPTION 2 As for me better approach instead of EF to use explicit SQL something like:

UPDATE 
    SET Client.BusinessValue = :someValue -- changes of client
    FROM Client, Parent
         WHERE Client.Id = :clientToChanges -- restrict updates by criteria
         AND Client.ParentId = Parent.Id -- join with Parent entity
         AND Parent.RowVersion = :expectedParent

After this query in .Net code you need to check that exactly 1 row was affected (0 means that Parent.Rowversion has been changed)

if(_db.ExecuteSqlCommand(sql) != 1 )
    throw new Exception();

Also try to analyse "Global Lock" design pattern with help of additional DB-table. You may read about this approach there http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

Dewfy
  • 21,895
  • 12
  • 66
  • 114