Things can go awry in many ways when you’re querying or updating entities. The Entity Data Model (EDM), for example, could have a problem that the validator fails to detect but that makes it difficult for a query to be compiled. You might attempt to save entities to the database that are missing related data—a reservation without a trip, perhaps, or a reservation without a customer. The database might have a constraint that is not reflected in the model; if a user saves data that breaks the rule of the constraint, the database will throw an error. Or someone may have modified or even deleted a record while another user was editing it.
The Entity Framework includes a specialized set of exceptions for capturing problems like these that arise during query and command execution.
In your application, you should embed each query execution or call
you make to SaveChanges
in some sort of
mechanism for catching these exceptions, most likely a Try
/Catch
block. When one of these exceptions is raised, it is up to your code to
handle it.
In this chapter, we’ll look at exception handling that is unique to the Entity Framework. Some exceptions are Entity Framework-specific exceptions, and others are .NET exceptions caused by faulty operations in the Entity Framework. You’ll also spend a good deal of time focusing on concurrency exceptions, which are raised by a database conflict is caused when multiple users edit the same data concurrently.
You should strongly consider exception handling for any application you write. If this is a new topic for you, plenty of resources are available on the Web, in the MSDN documentation, and in a variety of books to teach you accepted patterns and practices for implementing exception handling in .NET.
In Object Services, you can get exceptions from the moment you try
to instantiate an ObjectContext
to
the time you call SaveChanges
. Use
Try
/Catch
blocks around this functionality to
capture exceptions. For instance, you can dispose the context in the Finally
clause in case it was instantiated
before the error occurred, as shown in Example 18-1.
Example 18-1. Catching an exception and disposing the ObjectContext in Finally
VB
Dim context As BAEntities
Try
context = New BAEntities
Dim res = context.Reservations.First
Return res
Catch ex As Exception
Throw 'Throw or handle exception
Finally
context = Nothing
End Try
C#
BAEntities context = null;
try
{
context = new BAEntities();
var res = context.Reservations.First();
return res;
}
catch (Exception ex)
{
throw; //throw or handle exception}
finally
{
context = null;
}
When you employ the using
block
in the context’s instantiation, as shown in Example 18-2, the context and
any resources that it controls will be disposed at the end of the
block.
Example 18-2. Catching an exception when ObjectContext is automatically disposed
VB
Try
Using context As New BAEntities
Dim res = context.Reservations.First
End Using
Catch ex As Exception
Throw 'Throw or handle exception
End Try
C#
try
{
using (BAEntities context = new BAEntities())
{
var res = context.Reservations.First;
}
}
catch (Exception ex)
{
throw; //Throw or handle exception
}
Exceptions can occur when you’re creating connections and
executing commands with EntityClient
.
It’s equally important to capture those exceptions and be sure the
connections and DataReader
s are
properly disposed. Example 18-3 shows the code for
catching an exception when using EntityClient
.
Example 18-3. Catching an exception when using EntityClient
VB
Dim esql = "SELECT VALUE r FROM BAEntities.Reservations AS r"
Try
Using econn As New EntityConnection("Name = BAEntities")
Dim eComm = econn.CreateCommand
Dim eReader As EntityClient.EntityDataReader
With eComm
.CommandText = esql
econn.Open()
eReader = .ExecuteReader(CommandBehavior.SequentialAccess)
econn.Close()
Do While eReader.Read
'process results
Loop
End With
End Using
Catch ex As Exception
Throw 'Throw or handle exception
End Try
C#
var esql = "SELECT VALUE r FROM BAEntities.Reservations AS r";
try
{
using (EntityConnection econn = new EntityConnection("Name = BAEntities"))
{
var eComm = econn.CreateCommand();
EntityClient.EntityDataReader eReader = null;
eComm.CommandText = esql;
econn.Open();
eReader = eComm.ExecuteReader(CommandBehavior.SequentialAccess);
econn.Close();
while (eReader.Read())
{
//process results
}
}
}
catch (Exception ex)
{
throw; //Throw or handle exception
}
A number of problems are the result of a missing, misinformed, or
even malformed EntityConnectionString
.
The first step in most activities involving entities is to
create an ObjectContext
. As you’ve
learned, this requires an EntityConnection
, which in turn depends on
the EntityConnectionString
.
If the default or specified connection string cannot be found
either in the application’s .config file or in other locations that
you’ve designated, a System.Argument
Exception
will be thrown that reads as
follows:
The specified named connection is either not found in the configuration, not intended to be used with the EntityClient provider, or not valid.
You will get this error if you are creating the EntityConnection
directly and are passing in
a connection string name with the same problem.
This is easier to deal with during debug mode than at runtime.
In debug mode, check your entity connection string. If you are relying
on the default in the .config
file, make sure its name matches the EntityContainer
name of your model. In our
examples, the EntityContainer
is
BAEntities
and the connection
string should have the same name:
<connectionStrings> <add name="BAEntities" .... />
If that’s not the problem, check that the string doesn’t contain some type of invalid formatting.
The metadata attribute of the connection string has the names and paths of the model files (e.g., BAModel.csdl, BAModel.msl, BAModel.ssdl) hardcoded into it. The path could be a file path:
res:C:/BAModel.csdl|C:/BAModel.ssdl|C:/BAModel.msl
or a notation that indicates the files are embedded in an assembly:
res://*/BAModel.csdl|res://*/BAModel.ssdl|res://*/BAModel.msl
Many times in the ObjectContext
life cycle these files need to
be read so that when you instantiate the context, it looks for the
files. If the files in the metadata tag cannot be found in the
designated file path or in one of the referenced assemblies, a
System.Data.MetadataException
will be
thrown.
If you have changed the name of the EDMX file in your solution, this can cause problems with the metadata attribute.
In many of the following examples, the exact code for dealing
with a problem may not be specified. If you put no code in a
Catch
statement, the exception
will be caught but will not be thrown to the calling code, thereby
going completely undetected. Minimally, you should call Throw
to cause the exception to be raised
to the calling code.
Example 18-4
shows how to prepare for possible exceptions the aforementioned
problems can throw. It looks for the message related to a missing or
invalid ConnectionString
as well as the
MetadataException
. You can handle
the errors right in the code or throw them to the calling
code.
Example 18-4. Catching a connection string problem in an ArgumentException
VB
Catch ex As ArgumentException
If ex.Message.Contains("specified named connection is either not found") Then
Throw 'replace with handling code
Else
Throw 'handle or Throw the exception
End If
Catch ex As MetadataException
Throw 'replace with handling code
Catch ex As Exception
Throw
End Try
C#
catch (ArgumentException ex)
{
if (ex.Message.Contains("specified named connection is either not found"))
throw; //replace with handling code
else
throw; //handle or Throw the exception
}
catch (MetadataException ex)
{
throw; // replace with handling code
}
catch (Exception ex)
{
throw;
}
It’s not easy to resolve these types of problems in your code,
and your best resolution in your exception handler is to exit out of
the method elegantly, provide the end user with some information, and
log the error in such a way that an administrator or support person
can assist with the issue. The System.Data.MetadataException
or System.ArgumentException
contains no
special information other than the message itself. You will benefit by using
standard exception-handling methods, such as reporting the message
along with the connection string and where the message came
from.
If the connection succeeds, the next thing you will probably do that involves an Entity Framework-related exception is to create and execute a query.
LINQ’s syntax has the benefit of IntelliSense and compile-time checking, so it is less prone to runtime errors. However, certain syntax will be valid to the compiler, but not when it comes time to process the query.
A good example of this is the use of the .NET method ToShortDateString()
. The following LINQ
query passes through the compiler’s checks because ToShortDateString
is a valid method for a
Date
type:
From r In context.Reservations Select r.ReservationDate.ToShortDateString()
But at runtime, when it attempts to compile the query into a
native store command, it will discover that ToShortDateString
has no direct mapping to
any function in the store. Thus, the store command cannot be created,
which results in a System.NotSupportedException
with the
following message:
LINQ to Entities does not recognize the method 'System.String ToShortDateString()' method, and this method cannot be translated into a store expression.
You should be able to catch these during debug mode.
EntitySQLException
will
probably be the most common exception you will encounter as you are
learning Entity SQL and debugging your applications. So, you might as
well make friends with it straight away. Also, remember that the query
builder methods can help you with a good portion of your Entity SQL
queries, even though they provide only a subset of the operators and
functions you can use when you write Entity SQL directly.
EntitySQLException
is thrown
when your Entity SQL expression cannot be parsed or processed. You
should be testing every one of the Entity SQL expressions you write so
that you don’t have any runtime surprises. However, if you are
building dynamic queries, chances are greater that bad syntax will
sneak in. And there are always the “what if” scenarios that you can’t
even imagine until they occur, but that you might lie awake worrying
about at night. So, rather than lose sleep, you can hedge your bets by
making sure you catch any of the exceptions properly.
Don’t forget about eSqlBlast, the tool for testing Entity SQL queries that I mentioned in Chapter 3. It’s a great help for testing Entity SQL expressions without having to constantly debug your code to do so.
Here’s an example of a malformed expression where the AS
operator is missing; a common mistake.
The expression should be using AS
con
after contacts
; but
with that missing, the variable con
used elsewhere in the expression has no meaning.
SELECT VALUE con FROM BAEntities.contacts WHERE left(con.Lastname,1)='S'
The exception passes back some very helpful information contained in its properties.
The exception details are as follows:
Column= 61
In the preceding code, column 61
is where con.Lastname
begins.
ErrorContext= multipart
identifier
multipart identifier
refers to the fact that multiple items (con
and Lastname
) exist and the parser has
an issue with one (or more) of them. If the expression had
selected a single value (SELECT VALUE
con
), the ErrorContext
would be a
singlepart identifier.
ErrorDescription =
"'con.Lastname' could not be resolved in the current scope or
context. Make sure that all referenced variables are in scope,
that required schemas are loaded, and that namespaces are
referenced correctly."
Again, this is saying that the parser just can’t figure
out what con.Lastname
is,
and is listing all of the possible causes.
Another example occurs when you use incorrect functions or
operators. Even the provider-specific functions and
operators will be checked here. For instance, the following expression
will throw an error because it incorrectly uses SqlServer.AVERAGE
instead of the correct
function, SqlServer.AVG
:
SELECT VALUE SQLServer.AVERAGE(p.amount) FROM BAEntities.Payments AS p
The exception’s message will read as follows:
'SqlServer.AVERAGE' cannot be resolved into a valid type constructor or function, near function, method or type constructor, line 1, column 30."
The line break is not accounted for in the message. In addition, the parsing occurs long before any attempts to touch the database are made.
Example 18-5
shows the Catch
block for a method
that creates an EntityConnection
and attempts to execute it.
Example 18-5. Checking for a connection string problem in an ArgumentException
VB
Catch ex As EntitySqlException
Throw 'TODO: Replace with handling code
Catch ex As ArgumentException
If ex.Message.Contains("specified named connection is either not found") Then
Throw 'TODO: Replace with handling code
Else
Throw 'handle or Throw the exception
End If
Catch ex As MetadataException
Throw 'handle or Throw the exception
Catch ex As Exception
Throw 'handle or Throw the exception
End Try
C#
catch (EntitySqlException ex)
{
throw; //TODO: Replace with handling code
}
catch (ArgumentException ex)
{
if (ex.Message.Contains("specified named connection is either not found"))
{
throw; //TODO: Replace with handling code
}
else
{
throw; //handle or Throw the exception
}
}
catch (MetadataException ex)
{
throw; //handle or Throw the exception
}
catch (Exception ex)
{
throw; //handle or Throw the exception
}
Command compilation occurs when the Entity Framework creates the
command tree to represent a store query. It’s possible that the
provider compiling the query is causing a problem. In this case, an
EntityCommandCompilationException
will be thrown with the following message, and no additional
details:
An error occurred while preparing the command definition.
This is another tricky one to solve, although you won’t be able to solve it in your code. The best you can do is to log the exception, inform the user if necessary, and gracefully exit the method.
If you have a defined system for handling or perhaps logging
errors, you wouldn’t want to rewrite that handling code for every query.
Instead, you could build a set of wrappers to execute queries and handle
particular exceptions. Each wrapper method would take either an ObjectQuery
or a LINQ to Entities query as an
argument and return either a single object or some type of enumerable
collection of objects. If the query execution fails, the method could
provide code to handle the different types of exceptions.
Here are the signatures of two methods, each with the two
overloads for ObjectQuery
or LINQ to
Entities queries. The first method returns a single entity and the
second returns a List
of
entities:
VB
Public Function ExecuteFirstorDefault(Of TEntity) _
(ByVal objectQuery As ObjectQuery(Of TEntity)) As TEntity
Public Function ExecuteFirstorDefault(Of TEntity) _
(ByVal L2EQuery As IQueryable(Of TEntity)) As TEntity
Public Function ExecuteList(Of TEntity) _
(ByVal objectQuery As ObjectQuery(Of TEntity)) As List(Of TEntity)
Public Function ExecuteList (Of TEntity) _
(ByVal objectQuery As ObjectQuery(Of TEntity)) As List(Of TEntity)
C#
public TEntity ExecuteFirstorDefault<TEntity>(ObjectQuery<TEntity> objectQuery)
public TEntity ExecuteFirstorDefault<TEntity>(IQueryable<TEntity> L2EQuery)
public List<TEntity> ExecuteList<TEntity>(ObjectQuery<TEntity> objectQuery)
public List<TEntity> ExecuteList<TEntity>( IQueryable <TEntity> L2EQuery)
Each method executes the given query and returns the requested
result. For example, the first method would call return objectQuery.FirstOrDefault()
or return
L2EQuery.FirstOrDefault()
to perform the
execution. The ObjectQuery
methods
can take an ObjectQuery
whether it
was created using context.CreateQuery
, new ObjectQuery
, or a
QueryBuilder
method. Just be sure not to do anything to execute that query in the
calling code. The methods that accept LINQ to Entities queries can take
straight LINQ to Entities queries or those that were created by invoking
a CompiledQuery
.
Example 18-6
shows one of the methods with all of its Exceptions
stubbed out. Remember that the
ObjectQuery
queries can throw
EntitySQLExceptions
, while the LINQ
to Entities queries can throw InvalidOperationExceptions
.
Example 18-6. The ExecuteFirstorDefault wrapper method for executing ObjectQuery queries
VB
Public Function ExecuteFirstorDefault(Of TEntity) _
(ByVal objectQuery As ObjectQuery(Of TEntity)) As TEntity
Try
Return objectQuery.FirstOrDefault()
Catch ex As EntitySqlException
Throw ex 'TODO: Replace with handling code
Catch ex As ArgumentException
If ex.Message.Contains("specified named connection is either not found") Then
Throw 'TODO: Replace with handling code
Else
Throw 'handle or Throw the exception
End If
Catch ex As MetadataException
Throw 'handle or Throw the exception
Catch ex As Exception
Throw 'handle or Throw the exception
End Try
End Function
C#
public TEntity ExecuteFirstorDefault<TEntity>(ObjectQuery<TEntity> objectQuery)
{
try
{
return objectQuery.FirstOrDefault();
}
catch (EntitySqlException ex)
{
throw ex; //TODO: Replace with handling code
}
catch (ArgumentException ex)
{
if (ex.Message.Contains("specified named connection is either not found"))
throw; //TODO: Replace with handling code
else
throw; //handle or Throw the exception
}
catch (MetadataException ex)
{
throw ; //handle or Throw the exception
}
catch (Exception ex)
{
throw; //handle or Throw the exception
}
}
Example 18-7 shows code that
calls the ExecuteList
method overload to execute a
LINQQuery.
Example 18-7. Executing a LINQ to Entities query with the ExecuteList method
VB
Dim linqquery = context.Contacts.OfType(Of Customer) _
.Include("Reservations") _
.Where(Function(c) c.Reservations.Any())
Dim custlist = dal.ExecuteList(Of Customer)(linqquery)
C#
var linqquery = context.Contacts.OfType<Customer>()
.Include("Reservations")
.Where(c => c.Reservations.Any());
var custlist = dal.ExecuteList<Customer>(linqquery );
You will still need some error handling in the calling code to
handle particular scenarios, but the Execution query helper methods
allow you to avoid repeating exception handling code that you may want
to repeat for every query. Note that you don’t necessarily need to
return EntityObject
s from the queries. You
could return a single or set of DbDataRecord
s or any other predefined
class. The only thing that you can’t return would be an anonymous type
that results from a LINQ to Entities projection. You’ll see these
query execution methods used in later chapters.
When it’s time to save changes back to the database you have
another set of problems to be aware of. The connection issues raised
earlier in this chapter will come into play if you are establishing a
new ObjectContext
or EntityConnection
to perform the update. But
the data itself causes other problems. The Entity Framework will catch
some of the problems and prevent the data from going to the database.
The database will detect others and will return an error to the
application.
If you have done something such as violated a constraint built
into the model, an UpdateException
will be thrown.
Relationships are constraints. If an association defines a 1:*
(One to Many) relationship between two entities, any child in that
relationship that is being saved needs to have some evidence of a
parent. Even if the parent entity is not attached to the child, the
EntityReference
must have an
EntityKey
. Example 18-8 shows a new
reservation being created in memory and added to the context, which
then calls SaveChanges
. But no
Customer
is associated with the
context, not even an EntityKey
for
the CustomerReference
. As a result, this
call to SaveChanges
will
fail.
Example 18-8. A SaveChanges call that will fail because the new reservation has no Customer identified
VB
Dim res = New Reservation
res.TripReference.EntityKey = trip.EntityKey
res.ReservationDate = Today
Using context As New BAEntities
context.AddToReservations(res)
context.SaveChanges()
End Using
C#
var res = new Reservation();
res.TripReference.EntityKey = trip.EntityKey;
res.ReservationDate = DateTime.Today;
using (BAEntities context = new BAEntities())
{
context.AddToReservations(res);
context.SaveChanges();
}
This code assigns the trip’s EntityKey
to TripReference
, rather than just stating
res.Trip=trip
. Remember that when
the reservation is added to the context, the context will also try
to add anything attached to that reservation. If the trip, which
came from the database, is attached to the reservation, you’ll get
an error as the context attempts to assign a new temporary EntityKey
to that trip.
SaveChanges
throws an
UpdateException
with the following
message:
Entities in 'BAEntities.Reservations' participate in the FK_Reservations_Customers' relationship. 0 related 'Customers' were found. 1 'Customers' is expected.
The ObjectContext
doesn’t do
this type of validation when you add the reservation to the context,
because you might attach a Customer
or its EntityKey
later. Therefore,
the only time it’s confident that you have no intention of adding a
Customer
is when you are calling
SavingChanges
, and that’s when it
does its check.
You should detect this type of problem before your code goes
into production. You can also employ your own business rules to
perform these types of checks before it’s time to call SaveChanges
.
For example, you could have specific rules in the Reservation
class that test to see whether
a Customer
is defined by checking
for the presence of the CustomerReference.EntityKey
or by checking
that the Customer
property is not
null. The code behind SaveChanges
uses the MetadataWorkspace
to read the model,
identify the constraints, and then check the entities in the cache
to see whether they pass or fail the constraints. You could write
similar generic code to perform this type of function as well, if it
makes sense for you to do so.
Other constraints may not be defined in the model or handled by
any business logic. Such is the case with the dependency that a
Payment
must contain a valid
ReservationID
.
The database does not define a cascading delete to delete any payments
related to a reservation if that reservation is being deleted.
Therefore, if an attempt is made to delete a reservation that would
leave orphaned Payment
records, the
database will throw an error and will not execute the delete command.
That error is passed back to the client. If the client is the Entity
Framework and the error was a result of a SaveChanges
call, an UpdateException
will be thrown with the
following message:
The DELETE statement conflicted with the REFERENCE constraint "FK_Payments_Reservations". The conflict occurred in database "BreakAway", table "dbo.Payments", column 'ReservationID'.
In Chapter 16,
you learned that SaveChanges
is
wrapped in an implicit transaction. If an UpdateException
is thrown during the call to
SaveChanges
, this halts the entire
SaveChanges
method and causes any previously executed commands to be rolled
back.
UpdateException
is part of the
System.Data.Entity
API and is an
Object Services exception. It inherits from .NET’s DataException
class and adds to it a valuable
piece of information: the ObjectStateEntry
of the entity being processed
when the error occurred.
Figure 18-1 shows
the UpdateException
thrown by
SaveChanges
in Example 18-2, where the
Reservation
has no Customer
.
In the exception, you can see the StateEntries
property. Multiple entries can
appear in this property; for example, if you have relationships to other
entities being managed by the context, the RelationshipEntry
objects will be in this
collection. But only the primary entry will be displayed in the debug
window. And in this window you can see the ObjectStateEntry
that is related to the
Reservation
, and that the entry has a
pointer back to the entity.
If you want to you can log this information or present it to the user.
Remember that the context might be tracking a number of entities, so it’s not always going to be obvious which entity caused the problem. By having this information returned in the exception, you can handle the exception intelligently.
A number of other exceptions derive from the generic System.Data.EntityException
. Although some
of these exceptions are internal, some may be raised simply as an
EntityException
.
For example, if there is a problem with the database server
during command execution, an EntityException
with the following message
could be thrown:
"An error occurred while starting a transaction on the provider connection. See the inner exception for details."
The InnerException
will
contain the actual error from the database, such as the following
error in which, for dramatic effect, the SQL Server service was paused
on the server:
"SQL Server service has been paused. No new connections will be allowed. To resume the service, use SQL Computer Manager or the Services application in Control Panel. Login failed for user 'domainjulie'. A severe error occurred on the current command. The results, if any, should be discarded."
Not all entity-related exceptions are query and execution related.
One example you may encounter is the System.InvalidOperationException
. This will be
thrown when, for instance, you try to add an object to a context that
already has an EntityKey
. ObjectContext.Add
is used
to add new entities, and part of its job is to create an EntityKey
. This is the
type of exception you can avoid through testing and debugging. But you
may certainly encounter it during testing and debugging.
In Example 18-9,
the code that calls AddToContacts
will throw an InvalidOperationException
. The error
results from the fact that the entity already has an EntityKey
. The appropriate method to use is
Attach
.
Example 18-9. Trying to add an entity that already has an EntityKey
VB
Dim con = context.Contacts.First
context.Detach(con)
context.AddToContacts(con)
C#
var con=context.Contacts.First();
context.Detach(con);
context.AddToContacts(con);
InvalidOperationException
is
another exception you may want to plan for when working with
entities.
Concurrency issues are the bane of data access developers. In any sizable organization, it is not uncommon for multiple users or services to be processing the same sets of information. Not infrequently, different users may be updating the same piece of data concurrently, and a conflict occurs. For instance, a salesperson could be modifying a payment at the same time an accounting system is processing it. Or a scheduler might delete a calendar item while another person in a different department was in the middle of editing the same item.
These are two very different types of concurrency problems. In the first problem, you need to consider whose changes are saved. Does the accounting system rule over the salesperson, or vice versa? Do you just take the last changes that were sent to the database, overriding the changes that were just saved? In some organizations the focus is on a single record, whereas other organizations might get as granular as worrying about which fields in the record were updated by whom.
In the second example, the problem occurs when the second user tries to save her changes but the record no longer exists in the database. What do you do then? You can’t assume that her changes are unimportant just because the record was deleted. You might at least want to inform the user about the problem and give her an opportunity to take further action, if she has the proper permissions.
It is a tangled web of conundrums and decision making on the part of the application designer. Once your organization has devised the rules, it is up to the developer to implement them. But how?
In the database world, there are two ways to deal with concurrency conflicts. One involves pessimistic concurrency, where you expect the worst; the other is optimistic concurrency, where you hope for the best.
With pessimistic concurrency the database row is locked when a user retrieves that data and is then released when the user is finished working with that row. Nobody else can touch that data until the row is unlocked. It greatly reduces the potential of conflicts, but it comes at a price. Pessimistic concurrency is not scalable, because it maintains open connections and it can cause deadlocks.
A number of data access technologies do not support pessimistic concurrency because of the overhead involved. ADO.NET does not support it, nor does the ADO.NET Entity Framework. Therefore, this chapter will not cover pessimistic concurrency, but will focus instead on optimistic concurrency.
Optimistic concurrency does not lock the database rows, and relies on you, the developer, to provide logic in your application to handle potential update conflicts.
Concurrency is an age-old problem for anybody who designs line-of-business applications, and there is no silver bullet solution. You need to understand your business rules, be aware that these scenarios will need to be considered, and build your business logic to follow the rules you desire.
The Entity Framework does not magically solve the problem for you, either; however, it does provide tools for you to implement your business logic.
Given that pessimistic concurrency is not an option for the Entity Framework, what are your options when multiple people (or processes) are concurrently editing data? First we’ll survey the lay of the land, and then we’ll dig into implementation.
A few solutions are commonly used; however, a narrower field of applications will process concurrency conflicts in a very granular way, which is not as common.
We’ll look at these options as they are generally used in software and then focus on how the Entity Framework addresses them.
Many small systems don’t even worry about these conflicts. When a user saves her changes, they are written to the database regardless of what anybody else is doing or has done.
The Entity Framework’s default commands play an interesting role
here. Because an Entity Framework update will update only the fields
the user edited, it’s possible that concurrent editing won’t even
cause a problem. Imagine that User A retrieves a Customer
record, and
while she is editing that record User B edits the same Customer
, changing the BirthDate
property. User B saves his
changes. User A modifies the Customer
’s Notes
field and saves. The Entity Framework
will write a command to update the Notes
field for that Customer
. It won’t touch the BirthDate
, so all of the edits by both users
are safe.
When using stored procedures to update, however, this scenario
won’t be so rosy. The procedure will most likely update every field
regardless of its status. So, in that case, the original BirthDate
value will be saved back to the
database, and User B’s changes will disappear.
In a system designed to alert you of conflicts, the system would
alert you when User A attempts to save her data, indicating that the
record has been modified since the time she initially retrieved it. In
the Entity Framework, one option in this case is to force the current
user’s updates to the database. This is also referred to as ClientWins
. It’s different from ignoring the
conflict, however, because it will update all of the values in the
entity, even those that have not been edited. The impact in the
scenario described in the preceding section is that the BirthDate
field and every other field in the
database record will be changed to reflect the user’s version of the
data. It would have the same effect as the stored procedure.
In this resolution, when the conflict is detected the user’s
data is refreshed from the server. The entity that she has just
modified in her application will be updated to reflect the server’s
current version of the data. Any edits she made will be lost. That may
sound malicious, but if this is the expected behavior of the
application, it shouldn’t be a problem. The application can alert the
user and she can apply her edits to the Notes
field again (or the application can do
that for her if, for example, the changes still exist in memory, or
even in the Text
value of a user
control), and then she can save again. If a process that doesn’t
involve a user is making the updates, you should apply logic that
doesn’t require user interface.
You can discover whether data has changed on the server while a user is in the process of editing the same data in a number of ways:
To do this, you would need to compare every field in the
property to see whether that row was edited. The Entity
Framework supports this using its ConcurrencyMode
property. Another
mechanism that developers use is a database function called
checksum
that computes a hash
value from all of the data in a particular row. The Entity
Framework doesn’t have direct support for checksum
, but you can access it using
Entity SQL, because of the way Entity SQL allows you to use
database functions.
Here you need to focus on only one or more specific fields
that would indicate a change has been made. Database rowversion
fields are great for this.
But you may really be interested in a few specific properties.
For example, with an employee record, you may determine that the
only piece of data in which a conflict would create a problem is
the Social Security number. Rather than using rowversion
, which indicates that
something changed with no regard to which field that may have
been, you could specifically watch only the Social Security
number field. If it was updated during a concurrent operation,
it’s time to raise a flag.
The Entity Framework does not support this directly. You would have to do additional queries to check in this way.
SQL Server’s timestamp
type
is the same as the ISO standard rowversion
type and its name is a bit
confusing. Microsoft will be changing the name of this type to
rowversion
in a future version of SQL Server.
“The timestamp syntax is deprecated. This feature will be removed in a future version of Microsoft SQL Server. Avoid using this feature in new development work, and plan to modify applications that currently use this feature.”
The simplest mechanism for detecting that anything in a database
row has changed is to use a rowversion
field. A rowversion
is a binary field that is
automatically updated whenever changes are made to the row. Many
databases have a specific field type that is used for this. As noted
earlier, SQL Server’s timestamp
is
a rowversion
field and will
eventually be renamed to rowversion
in a future version. With databases that do not have an explicit
rowversion
type, patterns are available for creating triggers to update fields in
your database.
If you use rowversion
fields
in your database and they are surfaced as properties in your entities,
the Entity Framework will need to check only that single field to
detect whether a change was made to the data in the data store.
Because the BreakAway database is a SQL Server database and
its field type is timestamp
, I
will use that term frequently in this discussion.
A number of entities in the BreakAway model have TimeStamp
properties, which map to timestamp
fields in the database.
In the EDM, the TimeStamp
property is a non-nullable binary field. In the store schema in the
EDMX, the field is also non-nullable (Nullable=false
) and its StoreGeneratedPattern
is Computed
, so the Entity
Framework does not need to worry about managing this field:
CSDL <Property Name="TimeStamp" Type="Binary" Nullable="false" MaxLength="8" FixedLength="true" /> SSDL <Property Name="TimeStamp" Type="timestamp" Nullable="false" StoreGeneratedPattern="Computed" />
When working with disconnected data, a rowversion
field—whether your database
inherently supports it or you have to use a binary field with
triggers—is one of the most important tools you have in your arsenal
for dealing with concurrent systems. Otherwise, if you want to
identify that a change has been made to a table row, you may have to
consider a less efficient method of concurrency checking, such as
checking every single field in the row.
In the Entity Framework, enabling optimistic concurrency checks requires two steps:
Define which property or properties will be used to perform the concurrency check.
Handle the OptimisticConcurrencyException
that
is thrown when the check fails.
First we’ll look at the various methods and effects of identifying the properties for the concurrency checks, and after that we’ll dig into the exception handling.
Because concurrency is defined on a property-by-property basis
in the Entity Framework, the first step is to identify the property or
properties that you will use for concurrency checking. We’ll use the
Contact
entity’s TimeStamp
field.
The ConcurrencyMode
property
of an entity is used to flag a property for concurrency checking and
can be found right in the Properties window. Its options are None,
which is the default, and Fixed, as shown in Figure 18-2.
By setting Concurrency Mode to Fixed, you ensure that the
property is validated when an entity that was retrieved from the
database is updated during a call to SaveChanges
.
When Object Services prepares an Update
or Delete
command, it uses any properties
marked for concurrency checking as a filter in the command along with
the identity key.
With the ConcurrencyMode
of
Contact.TimeStamp
set to Fixed
, anytime a Contact
is updated the Update
command will look for the Contact
using its EntityKey
and its TimeStamp
property.
For example, if Charles Petzold is knighted, his Title
property will change from Mr.
to Sir
, as shown in Example 18-10.
Example 18-10. Changing a property of an entity with a concurrency checking property
VB
Using context As New BAEntities
Dim con = context.Contacts _
.Where(Function(c) c.LastName = "Petzold" And c.FirstName = "Charles") _
.FirstOrDefault
con.Title = "Sir"
context.SaveChanges()
End Using
C#
using (BAEntities context = new BAEntities())
{
var con = context.Contacts
.Where(c => c.LastName == "Petzold" && c.FirstName == "Charles")
.FirstOrDefault();
con.Title = "Sir";
context.SaveChanges();
}
When SaveChanges
is called,
the command shown in Example 18-11 will be sent to
the database.
Example 18-11. The T-SQL Update command when using a TimeStamp field for optimistic concurrency checking
exec sp_executesql N'update [dbo].[Contact] set [Title] = @0 where (([ContactID] = @1) and ([TimeStamp] = @2)) select [TimeStamp] from [dbo].[Contact] where @@ROWCOUNT > 0 and [ContactID] = @1',N'@0 nchar(3),@1 int,@2 binary(8)',@0=N'Sir',@1=850,@2=0x000000000000791A
That last value in Example 18-11 is the binary
TimeStamp
field. The command is
attempting to update a Contact
record where ContactID=1
and
TimeStamp=0x000000000000791A
, the
original value of TimeStamp
when
the contact was first retrieved.
If that TimeStamp
value had
changed since the first query, due to someone else editing that
record, the Update
command will not
find a matching record and will throw an error back to the client. The
Entity Framework will report this as an OptimisticConcurrencyException
.
This same type of concurrency check will also happen if the user
is attempting to delete the contact. It will add the TimeStamp
to the WHERE
clause of the command. Every property
that is marked as ConcurrencyMode=Fixed
in your entity will be
incorporated into the WHERE
clause
of Update
and Delete
commands in this way.
You’ll read more about OptimisticConcurrencyException
after
reviewing some other options.
Although the rowversion
concurrency checks are the most common methods developers use, you may
not have that field available as an option.
In that case, you can use the ModifiedDate DateTime
fields, but you need
to be sure they are being updated. Although the SavingChanges
example in Chapter 10 ensures that a particular model’s
ObjectContext
always updates the
ModifiedDate
fields, this does not
give you full coverage. You need to be sure that any application,
process, or even user accessing the database directly updates that
field every time, or the concurrency check will not detect a
change.
Another method is to mark every property in the entity as
FIXED
. Although this does the
trick, it makes your commands less efficient because all of the
properties’ original values will become part of every WHERE
clause.
With the assumption that you are thinking about CheckSum
because you are unable to modify
the database to use timestamp
fields, a last resort is to use a QueryView
or a Store Schema Definition Layer
(SSDL) function, and to write store function queries (store queries
written directly in the SSDL, as you saw in Chapter 13) directly into
your model.
If you have checksum functions in the data store or you are
implementing them in the SSDL, you still need to consider the actual
act of performing the update so that you can get a concurrency check.
If the CheckSum
value is directly
in the data table, it is represented in your entity as a binary
property. It can be marked as a Fixed
field and used for concurrency checks
in the same way you use the TimeStamp
or any other property in an
entity.
If you have used QueryView
or
a store function to query data that includes a checksum value, you
will need to use stored procedures for the update and delete
operations, and these stored procedures will need to perform the
concurrency checking.
If an entity has navigation properties that point to a parent or
“One” side of a relationship, the concurrency check will still take
place if that relationship changes. In the database, a foreign key
value represents the relationship. So, as long as something changes
that value and causes the TimeStamp
field to change, when your application attempts to update the same row
the change will be detected.
Version 1 of the Entity Framework does not support concurrency
checks in derived types, period. You will see this quickly if you
attempt to change the ConcurrencyMode
property of any property in
a derived type.
With inherited types, however, you may only use properties from
the base type for concurrency checks. If you set the ConcurrencyMode
of any property in a derived
type to Fixed
, you will get a
validation error on the model that says that new concurrency
requirements are not allowed for subtypes of base EntitySet
types.
Given that you can’t perform concurrency checks on derived types, it’s important to see what behavior you can expect if the base type has any concurrency properties.
When you edit an inherited type that has a concurrency check in its base type, concurrency checking will happen, but only on the base type itself. Let’s take a closer look at this.
In the BreakAway model, Customer
inherits from Contact
and Contact
has a TimeStamp
field that is now being used for
concurrency checks. What happens when a Customer
entity is being edited and a field
that is specific to Customer
has
been modified? (See Example 18-12.)
Example 18-12. Modifying a derived entity whose base entity has a concurrency checking property
VB
Dim cust = context.Contacts.OfType(Of Customer).First
cust.InitialDate = cust.InitialDate.Value.AddDays(1)
context.SaveChanges()
C#
var cust = context.Contacts.OfType<Customer>.First();
cust.InitialDate = cust.InitialDate.Value.AddDays(1);
context.SaveChanges();
In this case, two commands will be sent to the database. The
first will test to see whether anything in the Contact
has changed. It is an update command
that first declares a new variable (@p
) and then attempts to update it using the
ContactID
and TimeStamp
filter, as shown in Example 18-13.
Example 18-13. T-SQL checking for a change in the table related to the base entity before updating the derived entity
exec sp_executesql N'declare @p int update [dbo].[Contact] set @p = 0 where (([ContactID] = @0) and ([TimeStamp] = @1)) select [TimeStamp] from [dbo].[Contact] where @@ROWCOUNT > 0 and [ContactID] = @0',N'@0 int,@1 binary(8)',@0=1,@1=0x00000000000016B9
If a contact with a matching ContactID
and TimeStamp
is found, the context will send
the next command, which is the one to update the InitialDate
field in the Customer
table, to be executed in the
database. If the contact is not found, the command will throw an error
back to the ObjectContext
, which
will in turn throw an OptimisticConcurrencyException
.
There is a problem that you need to keep an eye on here. This
mechanism assumes that all updates are being made through this model
and that anytime something in the Customer
table is changed, the TimeStamp
field of the Contact
table will be modified.
However, other applications may be using the same data, or even
other EDMs that map to this data where the Customer
is not a derived type. If one of
the Customer
table fields in the
database is modified, the concurrency check will not detect it.
In this case, if you do need the check to be performed, you may want to rely on stored procedures for your DML commands.
If you have mapped stored procedures to the insert, update, and
delete functions of an entity, any properties marked as Fixed
in that entity will not automatically
be used in concurrency checks. However, you can define concurrency checking in the
function mappings; the stored procedure that the functions are based
on needs to be designed correctly.
If your stored procedure has one or more parameters that take in values to be used for concurrency checking, when mapping to these parameters you can force the original value to be sent to that parameter with the Use Original Value checkbox.
There is one other important requirement for the stored procedure and the mapping. The database needs to return the new timestamp value to the entity. That way, if you need to use the original value again, it will be the correct version.
Adding an additional SELECT
statement after the UPDATE
command will impact the procedure’s ability to return an error,
because the SELECT
statement will
most likely succeed. Therefore, between the UPDATE
command and the SELECT
command, you will need to test to
see whether the update was successful. If it was, continue with the
SELECT
; otherwise, the procedure
will be finished and the error will be returned to your application.
The UpdatePayment
stored
procedure you already mapped to the Payment
entity is written for you to use
in this way. Example 18-14
displays the stored procedure.
Example 18-14. The UpdatePayment stored procedure
ALTER PROCEDURE [dbo].[UpdatePayment] @PaymentID INT, @date DATETIME, @reservationID INT, @amount MONEY, @modifiedDate DATETIME, @timestamp timestamp AS UPDATE payments SET paymentdate=@date,reservationID=@reservationID,amount=@amount WHERE paymentid=@paymentid AND TIMESTAMP=@timestamp IF @@ROWCOUNT>0 SELECT timestamp AS newTimeStamp FROM payments WHERE paymentid=@paymentid
In Chapter 7, when you mapped the
UpdatePayment
function, you
selected Use Original Value next to the TimeStamp
property and you defined the
function to capture the timestamp value that the stored procedure
returned.
If you want to see this in action, now you can test the
concurrency checking with Example 18-15, a short
routine you can use to test the UpdatePayment
procedure.
Example 18-15. Testing for update conflicts with function mappings
VB
Using context As New BAEntities
Dim pmt = context.Payments.First
pmt.PaymentDate = pmt.PaymentDate.Value.AddDays(1)
Dim origTS = pmt.TimeStamp
Try
context.SaveChanges()
Dim newTS = pmt.TimeStamp
If newTS Is origTS Then
Console.WriteLine("timestamp not updated")
Else
Console.WriteLine("timestamp updated")
End If
Catch ex As OptimisticConcurrencyException
Console.WriteLine("Concurrency Exception was thrown")
End Try
End Using
C#
using (BAEntities context = new BAEntities())
{
var pmt = context.Payments.First;
pmt.PaymentDate = pmt.PaymentDate.Value.AddDays(1);
var origTS = pmt.TimeStamp;
try
{
context.SaveChanges();
var newTS = pmt.TimeStamp;
if (newTS == origTS)
Console.WriteLine("timestamp not updated");
else
Console.WriteLine("timestamp updated");
}
catch (OptimisticConcurrencyException ex)
{
Console.WriteLine("Concurrency Exception was thrown");
}
}
You’ll see that the Payment
entity’s TimeStamp
property is
updated when the update is successful. If you test the collision by
editing the database manually at the suggested breakpoint, an
OptimisticConcurrencyException
will be thrown.
Now it’s time to look at the other piece of the concurrency puzzle: handling the exception that is thrown when a concurrency check fails.
When a check fails and a System.Data.OptimisticConcurrencyException
is
thrown, this is where you can inject your business logic to determine
how to deal with the issue.
The most common resolutions, as described earlier, are to force
the client-side data to the server, or to pull the server-side data to
the client and lose the client’s edits. You can perform either of these
actions using ObjectContext.Refresh
.
While many applications handle concurrency conflicts, it is more common
to have a sweeping rule rather than to have logic handle very narrow
cases. We’ll look at both scenarios, but we’ll spend more time on the
more commonly used patterns.
ObjectContext.Refresh
allows
you to refresh entities in the context from the database. You can also
use it in other places in your application. Here we’ll focus on using
it to handle OptimisticConcurrencyException
s.
You can use Refresh
to force
either a ClientWins
scenario or a
StoreWins
scenario with your
updates.
Refresh
takes two parameters.
The first is RefreshMode
, which has
the options RefreshMode.ClientWins
and RefreshMode.StoreWins
. The second parameter
is either a single entity or an IEnumerable
of entities. The IEnumerable
can be something such as a
List
or an Array
, or even an IQueryable
(LINQ to Entities query) or
ObjectQuery
:
context.Refresh(RefreshMode.ClientWins, aTrip)
If the RefreshMode
is
ClientWins
, a query will be
executed against the database to get the current server values for the
entity. Then it will push those values into the original values of the
entity. That will make the entity think it started out with those
server values and it will build the update commands accordingly when
SaveChanges
is
called again.
StoreWins
will replace all of
the current and original values of the entity with the data from the
server. The user will lose her edits, and instead the cached data will
be in sync with the database. Entities that are refreshed with
StoreWins
will be ignored by
SaveChanges
until they are edited again.
To get your first look at this, let’s focus on a single entity
and see what Refresh
looks like in
a basic scenario.
In Example 18-16, a
simple query returns a single entity. If a conflict arises during a
call to SaveChanges
, that same
entity is passed into the Refresh
method.
Example 18-16. A ClientWins refresh on a single entity
VB
Using context As New BAEntities
Dim con = context.Contacts _
.Where(Function(c) c.LastName = "Petzold" And c.FirstName = "Charles") _
.FirstOrDefault
con.Title = "Sir"
Try
context.SaveChanges()
Catch ex As OptimisticConcurrencyException
'Refresh the contact entity,using ClientWins
context.Refresh(RefreshMode.ClientWins, aTrip)
'SaveChanges again
context.SaveChanges()
End Using
C#
using (BAEntities context = new BAEntities())
{
var con = context.Contacts
.Where(c => c.LastName == "Petzold" && c.FirstName == "Charles")
.FirstOrDefault();
con.Title = "Sir";
try
{
context.SaveChanges();
}
catch (OptimisticConcurrencyException)
{
//Refresh the contact entity,using ClientWins;
context.Refresh(RefreshMode.ClientWins, con);
//SaveChanges again;
context.SaveChanges();
}
}
Before SaveChanges
is called,
if another user has edited the same contact, causing the timestamp
field to be updated, an OptimisticConcurrencyException
will be
thrown when SaveChanges
is
called.
To test the OptimisticConcurrencyException
, you’ll
need to emulate an edit being made by another user or process. To do
this, place a breakpoint on context.SaveChanges
. When the breakpoint
is hit, open the Contact
table in
the Solution Explorer and edit the matching record.
Figure 18-3
shows the state of that contact using the ObjectStateEntry
visualizer (which you built
in Chapter 17) before
the Refresh
is executed.
The only changed field is Title
, with the current value of Sir
and original value of Mr.
.
Next, Refresh
is called,
which executes a query to retrieve the current values of this entity
in the database, including the new timestamp. Figure 18-4 shows the
contact after Refresh
has been
called.
Figure 18-4. ObjectContext.Refresh with ClientWins refreshing all of the original values of the designated entities, even those that have not changed, leaving every property modified
All of the original fields have been updated to reflect the
latest server values, and you can see that on the server side some
naughty person changed Mr. Petzold’s first name to Chuck, causing the
TimeStamp
field to be
updated.
Because every property was modified in this entity, each
property’s EntityState
was changed
to Modified
. That means when
SaveChanges
is called again, every
value will be sent to the server for updating. This time the record
will be found because you have the new value of the TimeStamp
for the WHERE
clause. The update succeeds and we now
have Sir Charles Petzold, which has quite a nice ring to it.
Let’s take the same scenario and see what happens when you
choose the StoreWins
option.
Figure 18-5
shows the state of the Contact
entity after Refresh(StoreWins,
con)
has been called. The entity’s state is Unchanged
and the Current
and Original
values have been replaced with the
server-side values. The local entity has lost its nice title of Sir
and has acquired the nickname Chuck. When SaveChanges
is called again, it will do
nothing because this entity is now Unchanged
.
Figure 18-5. ObjectContext.Refresh with StoreWins refreshing the entities by completely synchronizing them with the database
In this case there is no need to call SaveChanges
again, because you have done a
StoreWins
refresh on the only entity in the ObjectContext
. However, if you are building
a generic routine, it’s safer to call SaveChanges
anyway, as you may have other
entities in the ObjectContext
that
you need to deal with. It doesn’t waste any resources if there is
nothing to change.
You also can use Refresh
with
a collection of entities. The easiest scenario with which to use this
overload is when you already have a set of entities encapsulated in a
collection. For example, if you are working with a list of Contact
entities, you can refresh the entire
list at once. This makes an assumption that your business rules don’t
require any granular decision making to determine whether this type of
update is appropriate for every entity in that collection.
Example 18-17
shows a simple query that changes any contact with a FirstName
of Chuck
to Charles
. Then, if there is a concurrency
exception, it uses the brute force of a ClientWins
refresh to ensure that this
change is made to the database.
Example 18-17. Doing a ClientWins Refresh on a set of entities
VB
Using context As New BAEntities
Dim cons = context.Contacts.Where("(it.FirstName)='Chuck'").ToList()
For Each con In cons
con.FirstName = "Charles"
Next
Try
context.SaveChanges()'breakpoint here to modify server data
Catch ex As OptimisticConcurrencyException
context.Refresh(RefreshMode.ClientWins, cons)
context.SaveChanges()
End Try
End Using
C#
using (BAEntities context = new BAEntities())
{
var cons = context.Contacts.Where("(it.FirstName)='Chuck'").ToList();
foreach (var con in cons)
con.FirstName = "Charles";
try
{
context.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
context.Refresh(RefreshMode.ClientWins, cons);
context.SaveChanges();
}
}
In this case, we are passing the entire list of Contact
entities to the Refresh
method.
As I explained earlier in the book, I recommend that you not
work directly with the query unless you want to execute it again and
instead that you create a set of results, such as a List
. This is to avoid accidental query
execution. However, I did test to see what would happen if I passed
an ObjectQuery
and a LINQ
IQueryable
directly into a
Refresh
command. Did it wreak
havoc? No. It made no attempt to execute the query again. The
behavior was no different from passing in the List
, as in Example 18-17.
Refresh
builds a query to
retrieve the current store contact data by placing the EntityKey
s into one big
WHERE
clause so that it is a single
query:
SELECT 1 AS [C1], CASE WHEN (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL)) THEN '0X0X' ELSE '0X1X' END AS [C2], [Extent1].[ContactID] AS [ContactID], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], etc........ WHERE ([Extent1].[ContactID] = 468) OR ([Extent1].[ContactID] = 471) OR ([Extent1].[ContactID] = 474) OR ([Extent1].[ContactID] = 480) OR ([Extent1].[ContactID] = 489) OR ([Extent1].[ContactID] = 492) OR ([Extent1].[ContactID] = 498) OR ([Extent1].[ContactID] = 506) OR ([Extent1].[ContactID] = 578) OR ([Extent1].[ContactID] = 581) OR ([Extent1].[ContactID] = 587) OR ([Extent1].[ContactID] = 596) OR ([Extent1].[ContactID] = 805)
This way, it is able to refresh all of the items in the collection at once.
Remember that this means any other fields of any of these
records that were modified in the meantime will be overwritten by
the values in the entities in the context. However, only the
originally modified entities will be updated when SaveChanges
is called
again. Any Unchanged
entities that were
refreshed will still be Unchanged
after the refresh and
therefore overlooked by SaveChanges
.
If a modified entity is within a graph and it causes a
concurrency exception, be cautious about which entities you pass into
the Refresh
method. Refresh
will only refresh
the parent node of a graph and will not impact any related entities in
the graph.
Beware! Refresh does not impact graphs.
For instance, in Example 18-18, addressGraph
is a graph whose main entity is
an address that contains a contact. If the contact’s update throws a
concurrency exception when SaveChanges
is called, you might want to
solve that by calling Refresh
on
the addressGraph
.
Example 18-18. Refreshing a graph—not the results you might expect
VB
Dim addressGraph = context.Addresses.Include("Contact").First
add.Contact.FirstName = _
New String(add.Contact.FirstName.Trim.Reverse.ToArray)
Try
context.SaveChanges()
Catch ex As OptimisticConcurrencyException
context.Refresh(RefreshMode.StoreWins, add)
context.SaveChanges()
End Try
C#
var addressGraph = context.Addresses.Include("Contact").First();
addressGraph.Contact.Title = "Dr.";
try
{ context.SaveAllChanges(); }
catch (OptimisticConcurrencyException)
{
context.Refresh(RefreshMode.StoreWins, addressGraph);
context.SaveAllChanges();
}
But only the parent entity of the graph, the Address
entity, will be refreshed. The
contact will continue to cause the exception every time you save
changes.
But there is a way to attack this problem. Remember that the
exception returns ObjectStateEntry
objects for the entity
that was causing the problem.
This means that in the exception, you will have the ObjectStateEntry
for the contact, which
contains a reference to the entity. You can extract that entry’s
entity and call Refresh
on the
contact, and then call SaveChanges
again if necessary. Example 18-19 shows the code for
this.
Example 18-19. Getting a graph child to refresh
VB
Catch ex As OptimisticConcurrencyException
Dim contact=ex.StateEntries(0).Entity
context.Refresh(RefreshMode.ClientWins, contact)
context.SaveChanges()
End Try
C#
catch (OptimisticConcurrencyException ex)
{
var contact = ex.StateEntries[0].Entity;
context.Refresh(RefreshMode.ClientWins, contact);
context.SaveAllChanges();
}
It’s important to realize that when handling these exceptions,
SaveChanges
won’t just continue on
its merry way, updating the next entity in the context. If you hit the
exception, the SaveChanges
method
rolls back whatever it has already done and then halts.
In the exception handler you can call SaveChanges
again. However, if that call
fails, you need to catch it again. If you are pushing a lot of changes
in one SaveChanges
call and a
number of exceptions are in there, each time you call SaveChanges
you may have fixed the last
problem but you will then hit the next one.
So again, you need to trap that error, handle it, and call
SaveChanges
again. You will end up
with code that looks like the VB code in Example 18-20 (the C# version
would require quite a lot of braces).
Example 18-20. Catching a number of concurrency exceptions
VB
Try
Context.SaveChanges
Catch ex As OptimisticConcurrencyException
'do some work, then try again
Try
context.SaveChanges()
Catch ex As OptimisticConcurrencyException
'do some work, then try again
Try
context.SaveChanges()
Catch ex As OptimisticConcurrencyException
'do some work, then try again
Try
context.SaveChanges()
Catch ex As OptimisticConcurrencyException
'and so on and so forth....
End Try
End Try
End Try
End Try
You will be better off having your own method that calls
SaveChanges
and contains the
exception handler. Then you can call the method recursively as needed.
Example 18-21 shows a
custom method, SaveMyChanges
. Further on, you’ll see a
twist on this, which is a method added to the partial class for
BAEntities
,
allowing you to call context.SaveMyChanges
without having to
pass the context in as a parameter.
Example 18-21. Handling concurrency exceptions recursively
VB
Private Sub SaveMyChanges(ByVal context As Objects.ObjectContext)
Try
context.SaveChanges()
Catch ex As OptimisticConcurrencyException
'handle concurrency exception here
'then try again
SaveMyChanges(context)
Catch ex As Exception
'handle other exceptions
End Try
End Sub
C#
private void SaveMyChanges(Objects.ObjectContext context)
{
try
{
context.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
//handle concurrency exception here
//then try again
SaveMyChanges(context);
}
catch (Exception ex)
{
//handle other exceptions
}
}
Although I prefer the separate and reusable method, SaveMyChanges
, you can find other ways to
recursively call SaveChanges
in
the MSDN documentation. One example, which is combined with a
System.Transaction.TransactionScope
(more on transactions and exceptions later in this chapter), is in
the topic titled “How to: Manage Object Services Transactions
(Entity Framework).”
The code in Example 18-22 reassigns a payment to a different reservation.
Example 18-22. Reassigning a payment’s ReservationReference
VB
Dim pmt = context.Payments.FirstOrDefault
pmt.ReservationReference.EntityKey = _
New EntityKey("BAEntities.Reservations", "ReservationID", 13)
SaveMyChanges(context)
C#
var pmt = context.Payments.FirstOrDefault();
pmt.ReservationReference.EntityKey =
new EntityKey("BAEntities.Reservations", "ReservationID", 13);
SaveMyChanges(context);
Figure 18-6
shows the
before calling
ObjectStateEntr
iesSaveChanges
. The Payment
itself is Unchanged
. There is a Relationship
Entry
for the original relationship to
Reservation
10
. That relationship was deleted when the new relationship
was added.
The Entity Framework uses the added relationship and the entity
that it belongs to (Payment
) to
build an update command, changing the RelationshipID
property in the Payment
record.
If a conflict occurs, the exception that results contains these same two entries that were used to build the command, as shown in Figure 18-7.
In a simple Refresh
, you only
need to refresh the payment; therefore, the existing code that
refreshes StateEntries(0)
works
perfectly.
However, if you need more granular logic, there’s great information to be found in the entries returned by the exception.
Using the details from the exception, you can create a log error or even a message to a user that describes the conflict in detail. The client-side data is readily available to create this report. If you need even more details from the server, you’ll have to hit the server to get details about what actually changed there, although this is not a common scenario.
Reporting the exception could be as simple as alerting the user that there was a conflict when updating this payment.
The EntryDetails
extension
method, which you can find in the downloads on the book’s website,
creates a string of property names and values from a given ObjectStateEntry
. The
particular method is designed to supply information to an end user, so
it doesn’t include EntityKey
values
or any binary data.
For example, calling this extension method on the Payment
entry from the exception shown in
Figure 18-7 would
return the following:
PaymentDate: 4/6/2008 12:00:00 AM Amount: 800.0000 ModifiedDate: 5/1/2008 08:23:16 AM
The method is generalized, but you can enhance it even further to fine-tune the details.
If a user modified a variety of data, knowing which specific piece of data was causing the problem could be useful in letting the user decide whether her edits should be sent to the server or whether she would rather have the latest data from the server.
The level of information to access is up to you. Do you want the exception handler to retrieve the current store values as well? Should the user know who made that last change and when? These are common decisions that have to be made for handling concurrent data access, and again, they are not new to the Entity Framework.
Although the generic ClientWins
and StoreWins
will suffice for many
applications, in some applications more granular exception handling is
defined. It’s definitely difficult to come up with rules for automating
intricate exception handling, but because of the information in the
exception, if you do need to go to this level, a lot of possibilities
are open to you. The rest of this chapter will explore some more
heavy-duty exception handling.
You may have your own concurrency rules that don’t require a
user to get involved. Perhaps for Payment
entities, your rule is that if the
client is editing the amount, the client’s data should win; otherwise,
refresh the payment information from the server. You may decide that
the client should update all contact data. You may not even place a
concurrency check on the contact for this reason, but you may have a
best practice that requires concurrency checks on every
entity.
Because the rule in this case is that all contacts should get a
ClientWins
, it doesn’t make sense
to hit them one at a time. So, on the first occurrence of a conflict
with a contact, the code will refresh all contacts in the ObjectContext
. This will require a little
trickery that involves digging through the ObjectStateManager
to get a collection of
the contacts.
Let’s see what the exception code looks like for these scenarios. First, you can separate the logic for the various types into their own methods, as shown in Example 18-23.
Example 18-23. Subroutines for handling exceptions differently for payments than for contacts
VB
Private Sub PaymentRefresh _
(ByRef entry As ObjectStateEntry, ByRef context As ObjectContext)
'rule - if amount was changed locally,
' then clientwins, otherwise, storewins
If entry.GetModifiedProperties.Contains("Amount") Then
context.Refresh(RefreshMode.ClientWins, entry.Entity)
Else
context.Refresh(RefreshMode.StoreWins, entry.Entity)
End If
End Sub
Private Sub ContactRefresh _
(ByRef entry As ObjectStateEntry, ByRef context As ObjectContext)
'Contacts will always have a ClientWins refresh
'Refresh all of the contacts when the first Contact conflict occurs
Dim contactsinContext = context.EntitiesFromContext(Of Contact)()
context.Refresh(RefreshMode.ClientWins, contactsinContext)
End Sub
C#
private void PaymentRefresh(ref ObjectStateEntry entry, ref ObjectContext context)
{
//rule - if amount was changed locally,
//then clientwins, otherwise, storewins;
if (entry.GetModifiedProperties().Contains("Amount"))
context.Refresh(RefreshMode.ClientWins, entry.Entity);
else
context.Refresh(RefreshMode.StoreWins, entry.Entity);
}
private void ContactRefresh(ref ObjectStateEntry entry, ref ObjectContext context)
{
//Contacts will always have a ClientWins refresh
//Refresh all of the contacts when the first Contact conflict occurs
var contactsinContext = context.EntitiesFromContext<Contact>();
context.Refresh(RefreshMode.ClientWins, contactsinContext);
}
EntitiesFromContext
is
another custom extension method that creates a list of entities of a
particular type by pulling the entries for that type out of the
ObjectStateManager
and then placing
the entity object for each ObjectStateEntry
into a List
. This way, you’ll have the whole set of
contacts that exist in the context. Example 18-24 shows this method,
which extends ObjectContext
. If
during the course of the call to SaveChanges
another concurrency
conflict arises with a contact, all of the contacts will be refreshed
again.
Example 18-24. Extracting entities from a context using an extension method
VB
<Extension()> _
Public Function EntitiesFromContext(Of TEntity)(ByVal context As ObjectContext) _
As List(Of TEntity)
Dim entries = context.ObjectStateManager.GetObjectStateEntries(Of TEntity)()
Dim list As New List(Of TEntity)
For Each e In entries
list.Add(CType(e.Entity, TEntity))
Next
Return list
End Function
C#
public List<TEntity> EntitiesFromContext<TEntity>(ObjectContext context)
{
var entries = context.ObjectStateManager.GetObjectStateEntries<TEntity>();
List<TEntity> list = new List<TEntity>();
foreach (var e in entries)
{
list.Add((TEntity)e.Entity);
}
return list;
}
The updated exception code inside SaveMyChanges
now farms out the Refresh
call to the appropriate method after
it tests to be sure the entry is not a relationship, as shown in Example 18-25.
Example 18-25. Updated exception handling for calling subroutines
VB
Dim conflictEntry = ex.StateEntries(0)
If Not conflictEntry.IsRelationship Then
Dim entryEntityType = conflictEntry.Entity.GetType.Name
Select Case entryEntityType
Case GetType(Contact).Name 'this should refresh customers, too
ContactRefresh(conflictEntry, context)
Case GetType(Payment).Name
PaymentRefresh(conflictEntry, context)
Case Else 'business rule is to ClientWins refresh anything else
context.Refresh(RefreshMode.ClientWins, conflictEntry)
End Select
End If
SaveMyChanges(context)
C#
var conflictEntry = ex.StateEntries[0];
if (! conflictEntry.IsRelationship)
{
var entryEntityType = conflictEntry.Entity.GetType().Name;
if (entryEntityType == typeof(Contact).Name) //this should refresh customers, too
ContactRefresh(conflictEntry, context);
else if (entryEntityType == typeof(Payment).Name)
PaymentRefresh(conflictEntry, context);
else
context.Refresh(RefreshMode.ClientWins, conflictEntry);
}
You can use a lot of variations of this once you’ve gotten into the exception and you know how to drill into the details and make some decisions based on what you’ve found.
The default method of conflict resolution in the Entity
Framework has a few downsides. The first is that none of the data in
the context will be saved until every conflict has been resolved. If
you are updating a lot of records in a highly concurrent system, your
SaveChanges
operation may go
through many loops before all of the commands execute successfully.
The user may or may not notice the delay, but the delay could cause
other conflicts.
Another downside is that you can’t easily gather a list of all of the conflicts to present to the user at a later time for resolution. You might want to give the user a list of the conflicts, rather than giving the user one conflict at a time, with no indication of how many more there might be. This is because you have to resolve the first conflict encountered before you can get a report of the next conflict; otherwise, that first conflict will keep coming back.
One way to set the conflicting entities aside is to remove them from the context and save all of the entities that don’t pose any conflicts. Then, as soon as the save is complete, pull them back into the context in such a way that you can reconstruct their state. This is not a simple task, but you have already learned the necessary steps to pull it off.
When all of the conflicting entries have been removed from the
context, SaveChanges
will succeed
and the other data changes will be applied to the data store. On the
book’s website, you can find a PersistedStateEntry
class in both VB and
C# that achieves this pattern. This class takes advantage of many of
the things you learned regarding MetadataWorkspace
and ObjectStateManager
. It also uses
reflection because the ObjectContext
(and therefore the
ObjectStateManager
) are not
available for setting properties.
There are two principal functions. The first is to store the
state entry information. This is done by storing the main ObjectStateEntry
’s EntityKey
, original values, and entity in
the constructor and then adding the information for each RelationshipEntry
that also came back in
the StateEntries
. The second main
function is performed by the NewEntityfromOrig
method which
reconstructs the object with its state and the EntityReference
s that
were defined by the RelationshipEntries
in the exception’s
StateEntries
.
Along with the code for the PersistedEntry
class on the book’s
website, you will find an example of a Save routine that uses the
class. Essentially, the routine instantiates a list of PersistedEntry
objects, and any time an
OptimisticConcurrencyException
is
encountered, a new PersistedEntry
is created from the ObjectStateEntry
that caused the problem.
Its Entity
is detached from the
context and added into the list. The method repeats this process
until all of the exceptions are encountered, and then on a final
call to SaveChanges
, the valid
updates are persisted to the database. The entities that were
persisted are reconstructed and reattached to the context along with
their relationship information. All of the information about the
entity along with the exception’s message and any inner exception
information are available from the PersistedEntry
class for building an
informational UI for the end user, for logging the problems, or any
other task you may want to perform as part of your exception
handling.
This is one pattern for separating conflicting data from good data that not only allows you to get the good data into the database more quickly, but also provides you an opportunity to present all of the conflicts to a user at once.
When you allow the Entity Framework to provide its default
transactions, rollbacks and commits will occur automatically. In
addition, ObjectContext.AcceptChanges
will be called at the end of a successful SaveChanges
so that the state of the entities
becomes Unchanged
.
If, however, you are using your own transactions as described in
Chapter 16, you will
need to roll back and commit the transactions yourself depending on the
success or failure of the call to SaveChanges
.
Depending on your application architecture and business rules, you
may even choose to commit changes that have already been sent to the
database, rather than rolling them back. You will also need to call
AcceptAllChanges
manually when the
commands are completed successfully. Example 18-26 shows a basic
pattern for using your own transaction with an OptimisticConcurrencyException
.
Example 18-26. Handling an exception in a manual transaction
VB
Using tran As New System.Transactions.TransactionScope
Try
context.SaveChanges()
tran.Complete()
context.AcceptAllChanges()
Catch ex As OptimisticConcurrencyException
'TODO: add code for handling exception
context.SaveChanges()
End Try
End Using
C#
using (var tran = new System.Transactions.TransactionScope())
{
try
{
context.SaveChanges();
tran.Complete();
context.AcceptAllChanges();
}
catch (OptimisticConcurrencyException ex)
{
//TODO: add code for handling exception
context.SaveChanges();
}
}
Although Complete
and AcceptAllChanges
won’t be executed anytime an
exception is thrown, you still may want to separate those calls from the
SaveChanges
loop.
Example 18-27 shows a pattern that allows you to shift the location of some of the logic.
Example 18-27. Moving the transaction completion into a Finally clause
VB
Dim ChangesSaved as Boolean
Using tran As New System.Transactions.TransactionScope
Try
context.SaveChanges()
ChangesSaved=True
Catch ex As OptimisticConcurrencyException
'add code for handling exception
context.SaveChanges()
Finally
If ChangesSaved Then
tran.Complete()
context.AcceptAllChanges()
End If
End Try
End Using
C#
bool ChangesSaved = false;
using (var tran = new System.Transactions.TransactionScope())
{
try
{
context.SaveChanges();
ChangesSaved = true;
}
catch (OptimisticConcurrencyException ex)
{
//TODO: add code for handling exception
context.SaveChanges();
}
finally
{
if (ChangesSaved)
{
tran.Complete();
context.AcceptAllChanges();
}
}
}
In this chapter, you saw that there are many opportunities for exceptions to be thrown when querying or updating entities. You’ll need to catch these exceptions and do something about them, or you will have some very unhappy end users.
The patterns in the chapter concentrated on handling the
exceptions within the code where they occurred. Another common pattern
is to raise exceptions to a common ExceptionHandler
that you can use throughout
your application. This is not specific to the Entity Framework, and you
can find plenty of guidance on .NET exception handling in the
documentation, articles, and other books that focus on handling
exceptions.
Rules for handling concurrency problems vary among enterprises and applications. Because it is difficult to even come up with rules for resolving these issues at a granular level, you’ll find that most commonly, the three sweeping solutions—client always wins with no concurrency checks, client wins with a complete replacement of the server data, and server wins with a complete replacement of the client data—are the ones chosen.
But you do have some options for handling exceptions in a more detailed way, and hopefully you’ll find the patterns that I laid out in the final pages of the chapter both interesting and useful.
3.144.94.190