If we're going to be updating or deleting records in the database then we probably care about concurrency.
What happens if we've got code like this;
static void Main(string[] args)
{
using (NorthwindContext ctx = new NorthwindContext("Name=NorthwindEntities"))
{
Shippers s = ctx.Shippers.Where("it.shipperid = 13").First();
s.Phone = "123";
Console.WriteLine("ready to make an update - go update the table to cause a concurrency problem");
Console.ReadLine();
ctx.SaveChanges(true);
}
}
And, in the middle of this piece of code at the point where I've got that Console.ReadLine() I go off and update the record where shipperid=13 in order to change its phone number to a different value.
What does the Entity Framework do in that situation? Nothing :-) The update simply goes through to the table.
So, is there no concurrency protection in the Entity Framework? Yep, there is - you just have to turn it on.
If I go an modify my .CSDL file for the Shippers EntityType to read;
<EntityType Name="Shippers">
<Key>
<PropertyRef Name="ShipperID" />
</Key>
<Property Name="ShipperID" Type="Int32" Nullable="false"/>
<Property Name="CompanyName" Type="String" Nullable="false" MaxLength="40" ConcurrencyMode="fixed" />
<Property Name="Phone" Type="String" MaxLength="24" ConcurrencyMode="fixed" />
<Property Name="ShipperType" Type="Int32" Nullable="false" />
<Property Name="ExtraInformation" Type="String" MaxLength="50" />
<NavigationProperty Name="Orders" Relationship="Northwind.FK_Orders_Shippers" FromRole="Shippers" ToRole="Orders" />
</EntityType>
Note - the addition here is the two ConcurrencyMode="fixed" (the other option there is None which is the default) to both the CompanyName and the Phone properties.
Now, with that in place I'm going to get an OptimisticConcurrencyException at the point where I call SaveChanges if either of the CompanyName or Phone columns have been updated by someone else. I'll try and handle that kind of exception here;
static void Main(string[] args)
{
using (NorthwindContext ctx = new NorthwindContext("Name=NorthwindEntities"))
{
Shippers s = ctx.Shippers.Where("it.shipperid = 13").First();
s.Phone = "123";
Console.WriteLine("ready to make an update - go update the table to cause a concurrency problem");
Console.ReadLine();
int tryCount = 0;
while (tryCount < 2)
{
tryCount++;
try
{
Console.WriteLine("trying to commit changes");
ctx.SaveChanges(true);
Console.WriteLine("succeeded");
}
catch (OptimisticConcurrencyException ex)
{
// Let's state the problem first...
List<object> failedObjects = new List<object>();
foreach (ObjectStateEntry entry in ex.StateEntries)
{
failedObjects.Add(entry.Entity);
Console.WriteLine("We hit a problem with an entity from set [{0}]",
entry.EntitySet.Name);
Console.WriteLine("Key values are [{0}]", KeyValuesToString(entry.EntityKey));
Console.WriteLine("Modified properties are [{0}]", ModifiedPropertiesToString(
entry.GetModifiedProperties()));
Console.WriteLine("Original Values");
DumpDataRecord(entry.OriginalValues);
Console.WriteLine("New Values");
DumpDataRecord(entry.CurrentValues);
}
// Now, how to fix this would depend on what the user wanted to do. One
// approach is to get the latest bits from the DB as "original" and
// keep our changes and then try again.
// "RefreshMode" here is a bit weird to me as it doesn't line up exactly
// with the MergeOption we use when Execute()ing a query. AFAIK,
// RefreshMode.ClientWins is like MergeOptions.PreserveChanges and
// RefreshMode.ServerWins is like MergeOptions.OverwriteChanges
ctx.Refresh(RefreshMode.ClientWins, failedObjects);
}
}
}
}
static void DumpDataRecord(DbDataRecord record)
{
for (int i = 0; i < record.FieldCount; i++)
{
Console.WriteLine("\t\t\t<Field [{0}], Value [{1}]>", record.GetName(i),
record[i]);
}
}
static string ModifiedPropertiesToString(IEnumerable<string> strings)
{
List<string> list = new List<string>(strings);
return (string.Concat(list.ToArray()));
}
static string KeyValuesToString(EntityKey key)
{
StringBuilder builder = new StringBuilder();
if (key.IsTemporary)
{
builder.Append("<Key=Temporary>");
}
else
{
foreach (KeyValuePair<string, object> pair in key.EntityKeyValues)
{
builder.AppendFormat("<Key={0},Value={1}>", pair.Key, pair.Value);
}
}
return (builder.ToString());
}
So, essentially that code is just trying to call SaveChanges and if it fails for concurrency reasons we try and print out as much information as we have as to why it's failed and then go and try and "fix it" by taking each failed Entity and updating it so that all its original values look as they currently do in the store. We then have another try at SaveChanges.
It'd be nice to be presented with that collection of failed objects in the OptimisticConcurrencyException - maybe you are and I just can't see it but it seemed like I had to iterate and build another list.
That's all I'll write here for now - I'm sure there's many more complex scenarios to think of but it's a starting point.
Posted
Mon, Aug 27 2007 4:45 PM
by
mtaulty