![]() |
Web Development »
ASP.NET »
Data
Intermediate
ASP.NET Optimistic Concurrency ControlBy TeoHow to implement the optimistic concurrency control without the DataSet |
C#, Windows, .NET1.0, .NET1.1, ASP.NET, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

Have you ever got a call from a user telling you that he/she edited a record and that the changes made are lost? If so, maybe your application is implementing "Last in Wins" concurrency control.
This article focuses on the "Optimistic concurrency control", doing it
manually with out the DataSet. On a second part, I will focus on
"Pessimistic concurrency control".
There are three types of concurrency control:
The full article can be found here.
While in a disconnected architecture the "Pessimistic concurrency control" can not be implemented using database locks, it can be implemented as follows:
Session_End.
We have to keep in mind that an application that holds locks for long periods is not scalable, but this concurrency control schema might have to be implemented on some portions of the application.
To implement this concurrency control, we do not have to do anything. But this might be unacceptable in some circumstances if different users start to access the same records frequently.
As explained at MSDN:
In the above scenario, the changes user B made were never seen by user A. Be sure that this situation is acceptable if you plan to use the "Last in wins" approach of concurrency control.
This article focuses on this approach and I will go deeply through it.
The optimistic concurrency control is based on a Record Version. Several users can open a page for the same record, but only the first one to save it will succeed.
The .NET DataSet uses the "All values" approach, comparing old vs. new
values for all the fields. This gives a version for the record, but if you are
not using the DataSet, it can be a nightmare. First, you have to consider the
possible null values and the where clause has to be changed if a new field is
added to the table.
Instead of "All values", we can use a single field to set the record version. For this purpose we can use a GUID string or a date-time value. I decided to use the date-time for the following reasons:
When updating the record, the version has to be added to the WHERE
clause:
UPDATE table SET fields = values WHERE pk
= pk_vlaue AND concurrency = concurrency_value
If the concurrency value given by the user does not match the concurrency value in the database, the record will not be updated and 0 rows affected will be returned.
With this SELECT we have to consider that, while a user was
modifying the record, another user might have deleted the same record. In
this case, the rows affected will also be 0.
If you want to give the user an accurate message about the situation, we can not tell that the record was updated by another user if it was deleted. To solve this, we have to check if a record with the primary key exists in the table.
SELECT COUNT(*) FROM table WHERE pk = pk_value
At this point, if the record count is zero, the record was deleted, other wise; a concurrency exception has to be thrown.
Finally, we have to inform the user that his/her changes were not successful because another user changed the data.
What to do now? Just display a JavaScript alert or a pop-up window?
What happens if user A changed 10 fields and user B (who saved first) only changed one? Will user A loose all the 10 fields?
The approach implemented here, is to reload the page and replace only the data that was modified by user B, leaving user A's changes intact as possible.
The example code was developed to use the Northwind database installed with SQL Server. The following stored procedures need to be created (The stored procedures are in the zip file):
CategoriesList
ProductsByCategory
SuppliersList
ProductsRead
ProductsUpdate
Add to the Products table the field Concurrency as DateTime.
Set the Concurrency field to some value. Example: 01/01/2003 for all
the records. It will not work with null values. When you create the INSERT
method, it has to set Concurrency to GETTIME().
Set the appropriate Data Source, uid, pwd values in the
Web.config file.
<appSettings>
<add key="SQLConnString"
value="Data Source=(local);uid= ;pwd= ;database=Northwind"/>
</appSettings>
You have to setup manually the virtual directory where the application will run. This can be done with "Internet Information Services" or just from the Windows Explorer. Right click on the folder, press properties, go to the "Web Sharing" tab and select "Share this folder".
ProductID.
Page_Load() is called and the categories and suppliers combo
boxes are loaded.
SetConcurrencyObject(product) method is
called. This stores the object as BLL.CRUD, as it was read from the
database so it can be compared later in case of a concurrency conflict.
Save() method is called. At this point is where the concurrency
control starts.
The Update method is called on the product:
try
{
// Update the product
product.Update();
// Redirect to the product's list
Response.Redirect("Default.aspx");
}
catch (DeletedRowInaccessibleException)
{
Response.Redirect("Error.aspx?msg=" + System.Web.HttpUtility.UrlEncode
("The product has been deleted by another user."));
}
catch (DBConcurrencyException)
{
ConcurrencyException();
}
catch (Exception ex)
{
throw ex;
}
If the product was deleted by another user, the flow is redirected to the
error page. If there was a concurrency exception, the method ConcurrencyException() is called to process the exception and
display the fields that are conflicting.
private void ConcurrencyException()
{
// Get the mapping controls - object properties
Hashtable controls = GetControlsMap();
// Update the page to show the fields that have concurrency conflicts
ShowConcurrencyFields(controls);
// Show the concurrency error label
lblConcurrencyMsg.Visible = true;
}
The method GetControlsMap(), gets a one-to-one mapping between
the web controls and the object properties. This allows changing the control's
look & feel if there is a concurrency conflict with any object's
property.
Any page that needs to process a concurrency exception has to inherit from
BasePage.

The heart of the concurrency handle is at ShowConcurrency().
The original object as it was read is retrieved from the view state:
BLL.CRUD userObject = (BLL.CRUD) ViewState[CONCURRENCY_OBJECT];
Then the object's new data has to be read so it can be compared with the
original data read before the concurrency conflict. This is done with generic
code, invoking the Read() method on a BLL.CRUD object; to do this,
the objects must inherit from BLL.CRUD.

// Instantiate an object of the same type and read its properties
Type type = userObject.GetType();
BLL.CRUD dbObject = (BLL.CRUD) type.Assembly.CreateInstance(type.FullName);
dbObject.ID = userObject.ID;
if (!dbObject.Read())
Response.Redirect("Error.aspx?msg=" + System.Web.HttpUtility.UrlEncode
("The record has been deleted by another user."));
Once the new object is read, the differences are gotten using reflection:
IList differences = BLL.ObjectDifference.GetDifferences(dbObject,
userObject);
At last, the web controls styles are changed to show the user, the conflicting fields:
foreach (BLL.ObjectDifference diff in differences)
{
// Get the control
WebControl ctrl = controls[diff.PropertyName] as WebControl;
if (ctrl != null)
{
:
:
:
}
}
All the stored procedures are basically SELECTs to retrieve data. So I will
only focus on ProductsUpdate:
CREATE PROCEDURE ProductsUpdate
(
@ProductID int,
@CategoryID int,
@SupplierID int,
@Name varchar(40),
@QuantityPerUnit varchar(20),
@UnitPrice decimal(19,4),
@UnitsInStock smallint,
@UnitsOnOrder smallint,
@ReorderLevel smallint,
@Discontinued bit,
@Concurrency datetime
)
AS
UPDATE
Products
SET
ProductName = @Name,
SupplierID = @SupplierID,
CategoryID = @CategoryID,
QuantityPerUnit = @QuantityPerUnit,
UnitPrice = @UnitPrice,
UnitsInStock = @UnitsInStock,
UnitsOnOrder = @UnitsOnOrder,
ReorderLevel = @ReorderLevel,
Discontinued = @Discontinued,
Concurrency = GETDATE() -- When updated, set the
Concurrency to the server's date
WHERE
ProductID = @productID AND
Concurrency = @Concurrency
IF @@ROWCOUNT = 0
BEGIN
IF EXISTS( SELECT ProductID FROM products
WHERE ProductID = @productID )
RETURN 2 -- Concurrency conflict
ELSE
RETURN 1 -- The record has been deleted
END
ELSE
RETURN 0 -- The record could be updated
You can notice that the WHERE clause for the UPDATE, the ProductID and Concurrency are queried together. The ProductID is the primary key and the Concurrency field guaranties that we are
not updating a record modified by another user since the data was gathered.
If the @@ROWCOUNT is greater than zero (It should be 1 because
of the primary key), the record could be updated without concurrency
conflicts.
If the @@ROWCOUNT is zero, there are two possibilities:
IF
EXISTST
For each case, the stored procedure returns a value:
0: The record could be updated
1: The record has been deleted
2: Concurrency conflict
When the update query is executed, the return values are checked in
BLL.Product.cs Update() and the appropriate exception is thrown.
// Check for success
switch ( (UpdateRecordStatus) parms[11].Value)
{
case UpdateRecordStatus.Concurrency:
throw new DBConcurrencyException("The record has
been modified by another user or process.");
case UpdateRecordStatus.Deleted:
throw new DeletedRowInaccessibleException();
}
To try out the concurrency control:
When accepting the second page, the fields that were changed on the first page will become gray and the fields that are not conflicting will be unchanged.
Concurrency control increases the application's complexity, debugging and maintenance, but your users will have a better experience using it.
Remember that it is not mandatory to implement the optimistic concurrency control for all the application updates. You might need to mix the 3 mechanisms within a single application based on the needs.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 19 Aug 2003 Editor: Smitha Vijayan |
Copyright 2003 by Teo Everything else Copyright © CodeProject, 1999-2010 Web18 | Advertise on the Code Project |