Skip to content

Concurrency

Simon Mourier edited this page Feb 19, 2020 · 1 revision

Concurrency control refers to handling cases where multiple users load the same entity, work on it, and then save their changes. In such cases you commonly have these three choices:

  1. Apply the "last one always wins" rule, meaning that we'll override and loose the previous user's changes,

  2. Let the whole process follow its normal course because most of the time it should work and if ever it doesn't, we'll raise an error,

  3. Forbid other users to access this entity when one is using it, as it might lead to a concurrency problem.

When choosing rule #1, this means the developer chooses not to handle concurrency: the last user writing data always wins, and ones accept some data might be lost. Choosing rule #2 is handling concurrency in an optimistic mode which is the default mode in the Business Object Model (BOM). Finally, choosing rule #3 corresponds to a pessimistic concurrency control. All three options are available, and configurable on each entity at design time, through the entity “Concurrency Mode” attribute; however, the pessimistic concurrency control is not supported by any out of the box producers:

Concurrency - Picture 307

By default, concurrency control in the BOM is handled in an optimistic mode. From a developer's point of view this means that when saving an entity, he might get a CodeModelerConcurrencyException if ever a newer version of his entity exists in the database. This version check is done by comparing the entity's RowVersion. Each entity has a RowVersion column in the database layer (for SQL Server, or equivalent in other database systems), which corresponds to the RowVersion property in the BOM. At each write, the row version is updated, and if ever a user tries to save an instance with a row version which doesn't match the current one, it will fail.

The concurrency exception warns the developer when a concurrency error occurred, and its management should be implemented by the developer.

In some cases, even if the user hasn't an up-to-date instance, a developer will still want the user's action to succeed. For instance, in a sample application we might want to allow a user to delete an instance even though he hasn't the last version of this entity. The BOM provides by default a Reload method for such cases.

The Reload method takes as a parameter an enumeration named ReloadOption which can be a combination of those values:

  • BinaryLargeObjects: Binary Large Objects (BLOB) are reloaded (this flag is only used in Service Oriented Architecture, or Micro Services contexts for example),

  • Default: the default value (currently defined as Everything),

  • Everything: everything is reloaded (equivalent to Properties | RowVersion | BinaryLargeObjects),

  • Nothing: nothing is reloaded,

  • Properties: model properties are reloaded,

  • RowVersion: only the row version is reloaded (this flag is only used if the entity handles concurrency),

  • ShadowProperties: shadow properties are reloaded (not supported yet).

In the scenario described here before, the developer will be able to handle it by reloading the entity using the RowVersion flag and call the Delete method afterwards. This way he will be sure the user's instance always gets deleted even though the user didn't have the last version of it.

Here's a sample illustrating the described scenario with a Product entity, and an instance of it:

Concurrency - Picture 308

With the following OnBeforeDelete implementation in the snippet:

Concurrency - Picture 309

This is the generated Business Object Model:

// Snippet method 'OnBeforeDeleteSnippet'
private void OnBeforeDelete()
{
    Reload(ReloadOptions.RowVersion);
}
 
public virtual bool Delete()
{
    bool ret = false;
    this.OnBeforeDelete();
    CodeModeler.Runtime.EntityActionEventArgs evt = new CodeModeler.Runtime.EntityActionEventArgs(this, CodeModeler.Runtime.EntityAction.Deleting, true);
    this.OnEntityAction(evt);
    if ((evt.Cancel == true))
    {
        return ret;
    }
    if ((this.EntityState == CodeModeler.Runtime.EntityState.Deleted))
    {
        return ret;
    }
    if ((this.RowVersion == null))
    {
        return ret;
    }
    CodeModeler.Runtime.CodeModelerPersistence persistence = CodeModelerContext.Get(Sample.Constants.SampleStoreName).Persistence;    
    persistence.CreateStoredProcedureCommand("Product_Delete");
    persistence.AddParameter("@Product_Id", this.Id, ((int)(-1)));
    persistence.AddParameter("@_rowVersion", this.RowVersion);
    persistence.ExecuteNonQuery();
    this.EntityState = CodeModeler.Runtime.EntityState.Deleted;
    this.OnEntityAction(new CodeModeler.Runtime.EntityActionEventArgs(this, CodeModeler.Runtime.EntityAction.Deleted, false, false));
    ret = true;
    return ret;
}

And here is some sample client code:

static void Main(string[] args)
{
    CodeModeler.Runtime.CodeModelerPersistence persistence = CodeModelerContext.Get(Sample.Constants.SampleStoreName).Persistence;
    Console.WriteLine("Currently using this connection string: " + persistence.ConnectionString);
    Console.WriteLine();
    Console.WriteLine("Loading the 'Chocolate' product...");
    Product chocolate = Product.LoadByCode("PRD1");
    if (chocolate == null)
    {
        Console.WriteLine("The chocolate product wasn't found in database.");
        Console.ReadKey();
        return;
    }
    Console.WriteLine(chocolate.Trace());
    Console.WriteLine();
    Console.WriteLine("Deleting the chocolate product..");
    // Calling Delete() will call the OnBeforeDelete rule which will reload the entity,
    // making sure we never hit a CodeModelerConcurrencyException.
    chocolate.Delete();
    Console.WriteLine("Delete succeeding.");
    Console.ReadKey();
    return;
}
Clone this wiki locally