So far you have seen how to use Entity Framework to query for data from the database and save changes to those entities back to the database. You’ve seen how Entity Framework will keep track of any changes you make to entities that are being tracked by a context. It is the responsibility of the Change Tracker to keep track of these changes as you make them.
In this chapter you will learn about using the Change Tracker API to access the information that Entity Framework is storing about the entities it is tracking. This information goes beyond the values stored in the properties of your entities and includes the current state of the entity, the original values from the database, which properties have been modified, and other data. The Change Tracker API also gives you access to additional operations that can be performed on an entity, such as reloading its values from the database to ensure you have the latest data.
You’ve already seen bits of the Change Tracker API in action in
earlier chapters. In Chapter 2 you saw how
to perform explicit loading using the DbContext.Entry
. In Chapter 3 you saw how to get the
Change Tracker to scan your entities for changes using the DbContext.ChangeTracker.DetectChanges
method. In
Chapter 4 you saw how to
set the state of an entity, mark individual properties as modified, and work
with original values using the Entry
method. You also saw how to look at all entities being tracked by the
context using the DbContext.ChangeTracker.Entries
method.
We’ll start this chapter by taking a tour of all the information and operations that are available in the Change Tracker API. Then we’ll wrap up the chapter by looking at how these operations can be combined to save time logging and resolving concurrency conflicts.
The easiest way to get access to the change tracking information for
an entity is using the Entry
method on
DbContext
. Entry
returns a DbEntityEntry
instance, which gives you access
to the information and operations available for the entity. There are two
overloads of Entry
. One is generic
(Entry<TEntity>
) and will return
a strongly typed DbEntityEntry<TEntity>
:
public
DbEntityEntry
<TEntity> Entry<TEntity>(TEntity entity)
The other overload is nongeneric and returns DbEntityEntry
:
public
DbEntityEntry
Entry(object
entity);
Both of these provide access to exactly the same information and
operations. Because the strongly typed DbEntityEntry<TEntity>
knows the type of
entity it represents, it allows you to use lambda expressions when
drilling into property details, so that you get IntelliSense and
additional compile-time checks. You don’t need to worry about selecting
the correct overload—the compiler will take care of this for you. If the
entity you pass to Entry
is typed as
object
, you will get the nongeneric
DbEntityEntry
. If the entity you pass
in is typed as something more specific than object
(for example, Destination
), you will get the generic DbEntityEntry<TEntity>
, where TEntity
is the same type as the entity you pass
in. You’ll see both of these overloads in action in the next couple of
sections.
One of the core pieces of change tracking information is what state
the entity is currently in: Added
,
Unchanged
, Modified
, or Deleted
. This can be determined using the
State
property on DbEntityEntry
. To see how this works, add the
PrintState
method shown in Example 5-1.
private static void
PrintState() {using
(var
context =new
BreakAwayContext
()) {var
canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single();DbEntityEntry
<Destination
> entry = context.Entry(canyon);Console
.WriteLine("Before Edit: {0}"
, entry.State); canyon.TravelWarnings ="Take lots of water."
;Console
.WriteLine("After Edit: {0}"
, entry.State); } }
The code retrieves the Grand Canyon destination from the database
and then locates the change tracking entry for it. The canyon
variable is strongly typed as Destination
, so the compiler selects the generic
overload of Entry
and we get a strongly
typed DbEntityEntry<Destination>
returned. The code then prints out the state of canyon
as recorded in the change tracker. Next,
the TravelWarnings
property is modified
and then the State
is printed out
again. Update the Main
method to call
the PrintState
method and run the
application. The console window will display the following:
Before Edit: Unchanged After Edit: Modified
As expected, the canyon
object is
reported as Unchanged
after it is
retrieved from the database. After modifying one of its properties, the
object is seen by the change tracker as being in the Modified
state.
Back in Chapter 3,
we enabled Destination
as a
change tracking proxy, meaning that changes to any
instances of Destination
are reported
to the context in real time. Entities that are not change tracking proxies
require an explicit or implicit call to DetectChanges
to scan for any changes to the
object. Most of the methods on DbContext
will automatically call DetectChanges
for you. Entry
is one of those methods. But reading the
State
property will not cause an
automatic DetectChanges
. If Destination
was not a change tracking proxy, you
would need to call DetectChanges
after
setting the TravelWarnings
property to
get the correct state reported. More information on DetectChanges
is available in the Using Snapshot
Change Tracking section of Chapter 3. You can avoid the
need to call DetectChanges by calling Entry
each time you need the entry, rather than
keeping a reference to it. For example, rather than storing the entry in
the entry
variable as you did in Example 5-1, you could use Entry
each time you want the state:
Console
.WriteLine("Before Edit: {0}"
, context.Entry(canyon).State); canyon.TravelWarnings ="Take lots of water."
;Console
.WriteLine("After Edit: {0}"
, context.Entry(canyon).State);
The State
property also exposes
a public setter, meaning you can assign a state to an entity. Setting
the state is useful when you are working with disconnected graphs of
entities—typically in N-Tier scenarios. Chapter 4 of this book is
dedicated to learning about the various ways to set the state of
entities, including setting the State
property.
Along with the current state of an entity, DbEntityEntry
gives you access to the entity’s
current, original, and database values. The DbPropertyValues
type is used to represent each
of these sets of properties. Current values are the values that are
currently set in the properties of the entity. Original values are the
values for each property when the entity was originally attached to the
context; for example, when the entity was first retrieved from the
database. Database values are the values currently stored in the database,
which may have changed since you queried for the entity. Accessing
database values involves Entity Framework performing a behind-the-scenes
query for you.
There is a bug in Entity Framework 4.1 and 4.2 that blocks you
from using the GetDatabaseValues
method for an entity that is not in the same namespace as your context.
The Entity Framework team has fixed this bug in the Entity Framework 4.3
release. If you are using Entity Framework 4.2 or earlier, you will need
to modify the namespace of your context class to be the same as your
domain classes. Failure to make this change will result in an EntitySqlException
if you attempt to retrieve
the database values for an entity.
Let’s start by writing a method that will output these various
values for any given Lodging
. Add the
PrintChangeTrackingInfo
method shown in
Example 5-2.
private static void
PrintChangeTrackingInfo(BreakAwayContext
context,Lodging
entity) {var
entry = context.Entry(entity);Console
.WriteLine(entry.Entity.Name);Console
.WriteLine("State: {0}"
, entry.State);Console
.WriteLine(" Current Values:"
); PrintPropertyValues(entry.CurrentValues);Console
.WriteLine(" Original Values:"
); PrintPropertyValues(entry.OriginalValues);Console
.WriteLine(" Database Values:"
); PrintPropertyValues(entry.GetDatabaseValues()); }private static void
PrintPropertyValues(DbPropertyValues
values) {foreach
(var
propertyNamein
values.PropertyNames) {Console
.WriteLine(" - {0}: {1}"
, propertyName, values[propertyName]); } }
The code starts by looking up the change tracking entry for the
supplied Lodging
. Because the lodging
variable is strongly typed as Lodging
, the compiler will select the generic
overload of Entry
. The code then prints
out the Name
of the Lodging
. Of course, we could have just gotten
the name from the lodging
variable, but
we are using the Entity
property on the
entry for demonstration purposes. Because the entry is strongly typed, the
Entity
property provides strongly typed
access to the Destination
, which is why we can call
entry.Entity.Name
.
The code then loops through the current, original, and database
values and writes the value for each property using the PrintPropertyValues
helper method. These
collections of values are all the DbPropertyValues
type. Note that there is no way
to directly iterate over the values, so you need to iterate over the names
of the properties and look up the value for each property. GetDatabaseValues
will send a query to the
database at the time it is called, to determine what values are currently
stored in the database. A new query will be sent to the database every
time you call the method.
This method for accessing current, original, and database values uses a string to identify the property to get the value for. In Working with Individual Properties, you will learn about a strongly typed way to specify the property.
Now let’s write a method to test out our change tracking logic. Go
ahead and add the PrintLodgingInfo
method shown in Example 5-3.
private static void
PrintLodgingInfo() {using
(var
context =new
BreakAwayContext
()) {var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single(); hotel.Name ="Super Grand Hotel"
; context.Database.ExecuteSqlCommand(@"UPDATE Lodgings
SET Name = 'Not-So-Grand Hotel'
WHERE Name = 'Grand Hotel'"
); PrintChangeTrackingInfo(context, hotel); } }
This new method locates an existing Lodging
from the database and modifies its
Name
property. The code then uses
Database.ExecuteSqlCommand
to run some
raw SQL to modify the name of the Lodging
in the database. You’ll learn more about
executing raw SQL against the database in Chapter 8. Finally, the hotel
instance is passed to our PrintChangeTrackingInfo
method. Update the
Main
method to call PrintLodgingInfo
and run the application, which
will output the following to the console:
Super Grand Hotel State: Modified Current Values: - LodgingId: 1 - Name: Super Grand Hotel - Owner: - MilesFromNearestAirport: 2.50 - DestinationId: 1 - PrimaryContactId: - SecondaryContactId: Original Values: - LodgingId: 1 - Name: Grand Hotel - Owner: - MilesFromNearestAirport: 2.50 - DestinationId: 1 - PrimaryContactId: - SecondaryContactId: Database Values: - LodgingId: 1 - Name: Not-So-Grand Hotel - Owner: - MilesFromNearestAirport: 2.50 - DestinationId: 1 - PrimaryContactId: - SecondaryContactId:
As expected, the current name of the hotel is printed out. Since we
changed the Name
, the State
of the entity is displayed as Modified
. The current value for Name
shows the new name that we assigned in
PrintLodgingInfo
(“Super Grand Hotel”).
The original value of Name
shows the
value when we retrieved the Lodging
from the database (“Grand Hotel”). The database value of Name
shows the new value we assigned in the
database using the raw SQL command, after the hotel was retrieved from the
database (“Not-So-Grand Hotel”).
The code from Example 5-2 works nicely for
existing entities, because they have current, original, and database
values. But this isn’t true for new entities or entities that are marked
for deletion. New entities don’t have original values or database values.
Entity Framework doesn’t track current values for entities that are marked
for deletion. If you try to access such values, Entity Framework will
throw an exception. Let’s add some conditional logic in PrintChangeTrackingInfo
to account for these
restrictions. Replace the code that prints out current, original, and
database values with the code in Example 5-4.
if
(entry.State !=EntityState
.Deleted) {Console
.WriteLine(" Current Values:"
); PrintPropertyValues(entry.CurrentValues); }if
(entry.State !=EntityState
.Added) {Console
.WriteLine(" Original Values:"
); PrintPropertyValues(entry.OriginalValues);Console
.WriteLine(" Database Values:"
); PrintPropertyValues(entry.GetDatabaseValues()); }
The updated code now checks the State
of the entry and skips printing out
current values for entities that are marked for deletion. The code also
skips printing original and database values for newly added entities.
Let’s also update the PrintLodgingInfo
method so that we can see these changes in action (Example 5-5).
private static void
PrintLodgingInfo() {using
(var
context =new
BreakAwayContext
()) {var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single(); PrintChangeTrackingInfo(context, hotel);var
davesDump = (from
din
context.Lodgingswhere
d.Name =="Dave's Dump"
select
d).Single(); context.Lodgings.Remove(davesDump); PrintChangeTrackingInfo(context, davesDump);var
newMotel =new
Lodging
{ Name ="New Motel"
}; context.Lodgings.Add(newMotel); PrintChangeTrackingInfo(context, newMotel); } }
The updated code now also locates Dave’s Dump and marks it for
deletion. The code also adds New Motel to the context. PrintChangeTrackingInfo
is called for both of
these entities. If you run the application again, you will see that the
relevant change tracking information is successfully displayed for all
three locations.
The PrintChangeTrackingInfo
method will currently only work for Lodging
s, because the entity
parameter is strongly typed. But most of
the code in the method is not specific to the Lodging
type. Let’s change the PrintChangeTrackingInfo
method to accept any
entity type by changing the entity parameter to be typed as object
rather than Lodging
:
private static void
PrintChangeTrackingInfo(BreakAwayContext
context,object
entity)
Since the entity may not be a Lodging
, the compiler now selects the nongeneric
Entry
method, which returns the
nongeneric DbEntityEntry
. Because we no
longer know what type the entity is, we can’t be sure that there is a
Name
property to print—in fact, the
compiler will give us an error if we try to. Remove the line that printed
out the name of the Lodging
and replace
it with a line that prints out the type of the entity instead:
Console
.WriteLine("Type: {0}"
, entry.Entity.GetType());
Go ahead and run the application. You will see that the updated code continues to successfully display change tracking information.
So far, our examples have used the indexer
syntax to get the value for a property out of DbPropertyValues
. The indexer syntax is where
you used square braces on an object to specify the key/index of the value
you want to retrieve (in other words, entry.CurrentValues[“Name”]
). Because there is
no way to know what type will be returned from each property, the return
type of the indexer on DbPropertyValues
is object
. There may be times where you
do know what type the value will be. Rather than casting the return value
to the required type, you can use the GetValue<TValue>
method to specify the
type of the value. For example, you may want to find the original value
that was assigned to the Name
property
of a Lodging
when it was retrieved from
the database. Add the PrintOriginalName
method shown in Example 5-6.
private static void
PrintOriginalName() {using
(var
context =new
BreakAwayContext
()) {var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single(); hotel.Name ="Super Grand Hotel"
;string
originalName = context.Entry(hotel) .OriginalValues .GetValue<string
>("Name"
);Console
.WriteLine("Current Name: {0}"
, hotel.Name);Console
.WriteLine("Original Name: {0}"
, originalName); } }
The code retrieves a Lodging
from
the database and changes its Name
property. The code then looks up the original value for the Name
property using the GetValue
method on the OriginalValues
for the entity. Because we know
that Name
is a string
property, the code specifies string
as the TValue
when calling GetValues
. The original value is returned as a string
and the current and original value for
the Name
property are then printed to
the console.
Let’s look at how you can work with DbPropertyValues
when you have a property on
your entity that uses a complex type. Remember that complex types allow
you to group multiple scalar values into a class. A property that
references a complex type is known as a complex
property. For example, the BAGA model uses an Address
complex type to group address related
properties (Example 5-7).
[ComplexType
]public class
Address
{public int
AddressId {get
;set
; } [MaxLength
(150)] [Column
("StreetAddress"
)]public string
StreetAddress {get
;set
; } [Column
("City"
)]public string
City {get
;set
; } [Column
("State"
)]public string
State {get
;set
; } [Column
("ZipCode"
)]public string
ZipCode {get
;set
; } }
Code First convention recognizes complex types when the type has
no key property. Since Address
has
a property that Code First will recognize as a key, AddressId
, and therefore will infer this to
be an entity type, the class is explicitly marked as a ComplexType
.
This complex type is then used by the Address
property in the Person
class (Example 5-8). Person.Address
is therefore a complex
property. Note that PersonInfo
is
also a complex type.
[Table
("People"
)]public class
Person
{public
Person() { Address =new
Address
(); Info =new
PersonalInfo
{ Weight =new
Measurement
(), Height =new
Measurement
() }; }public int
PersonId {get
;set
; } [ConcurrencyCheck
]public int
SocialSecurityNumber {get
;set
; }public string
FirstName {get
;set
; }public string
LastName {get
;set
; }public
Address
Address {get
;set
; }public
PersonalInfo
Info {get
;set
; }public
List
<Lodging
> PrimaryContactFor {get
;set
; }public
List
<Lodging
> SecondaryContactFor {get
;set
; } [Required
]public
PersonPhoto
Photo {get
;set
; }public
List
<Reservation
> Reservations {get
;set
; } }
To demonstrate how DbPropertyValues
handles complex types, let’s
create a new Person
and pass it to
our PrintChangeTrackingInfo
method.
Add the PrintPersonInfo
method shown
in Example 5-9.
private static void
PrintPersonInfo() {using
(var
context =new
BreakAwayContext
()) {var
person =new
Person
{ FirstName ="John"
, LastName ="Doe"
, Address =new
Address
{ State ="VT"
} }; context.People.Add(person); PrintChangeTrackingInfo(context, person); } }
When we get the value for a complex property from DbPropertyValues
, it’s going to return another
DbPropertyValues
that contains the
values from the complex type. Let’s update the PrintPropertyValues
helper method to account
for this (Example 5-10).
private static void
PrintPropertyValues(DbPropertyValues
values,int
indent = 1) {foreach
(var
propertyNamein
values.PropertyNames) {var
value = values[propertyName];if
(valueis
DbPropertyValues
) {Console
.WriteLine("{0}- Complex Property: {1}"
,string
.Empty.PadLeft(indent), propertyName); PrintPropertyValues( (DbPropertyValues
)value, indent + 1); }else
{Console
.WriteLine("{0}- {1}: {2}"
,string
.Empty.PadLeft(indent), propertyName, values[propertyName]); } } }
For each property being printed, the code now checks if the value
is a DbPropertyValues
. If it is, the
code prints out the name of the complex property and then recursively
calls PrintPropertyValues
with the
values for the complex type. PrintPropertyValues
also allows an indent
level to be supplied, which is used to indent the values of a complex
property. If an indent is not supplied, a default indent of 1
is used. Update the Main
method to call PrintPersonInfo
and run the application. The
console will display the following output:
Type: Model.Person State: Added Current Values: - PersonId: 0 - SocialSecurityNumber: 0 - FirstName: John - LastName: Doe - Complex Property: Address - AddressId: 0 - StreetAddress: - City: - State: VT - ZipCode: - Complex Property: Info - Complex Property: Weight - Reading: 0 - Units: - Complex Property: Height - Reading: 0 - Units: - DietryRestrictions:
The console output is displaying the property values contained in
the Address
property. You’ll also
notice that the code works for nested complex types. Person.Info
is a complex property that
references the PersonInfo
complex
type. PersonInfo
also has complex
properties for a Person
’s Height
and Weight
. From the printout you can see that the
DbPropertyValues
for the Info complex
property returned another DbPropertyValues
for its Weight
and Height
properties.
Having a single object, like DbPropertyValues
, that represents a set of
values is handy. However, we usually want to write application logic in
terms of our domain classes, rather than a type such as DbPropertyValues
. For example, we might have a
method that defines how we display a Destination
for the user of our application to
see. We can pass any instance of a Destination into the method to print
out its current values, but it would be good if we could use that same
method to print out the original and database values as well. Add the
PrintDestination
method shown in
Example 5-11.
private static void
PrintDestination(Destination
destination) {Console
.WriteLine("-- {0}, {1} --"
, destination.Name, destination.Country);Console
.WriteLine(destination.Description);if
(destination.TravelWarnings !=null
) {Console
.WriteLine("WARNINGS!: {0}"
, destination.TravelWarnings); } }
If you want to display the current values for a Destination
, you can pass the actual instance
to the method. But there may be scenarios where you want to display the
original values fetched from the database or perhaps the current
database values to the end user. One such scenario is resolving
concurrency conflicts during a save. We’ll look at that particular
scenario in more detail later in this chapter.
DbPropertyValues
includes a
ToObject
method that will copy the
values into a new instance of the entity without overwriting the
existing instance as you would with a query to the database. To see how
this works, add the TestPrintDestination
method shown in Example 5-12.
private static void
TestPrintDestination() {using
(var
context =new
BreakAwayContext
()) {var
reef = (from
din
context.Destinationswhere
d.Name =="Great Barrier Reef"
select
d).Single(); reef.TravelWarnings ="Watch out for sharks!"
;Console
.WriteLine("Current Values"
); PrintDestination(reef);Console
.WriteLine(" Database Values"
);DbPropertyValues
dbValues = context.Entry(reef) .GetDatabaseValues(); PrintDestination((Destination
)dbValues.ToObject()); } }
The code retrieves the Great Barrier Reef Destination
from the database and changes its
TravelWarnings
property. Then it
passes the Destination
to the
PrintDestination
method to print out
the current values. Next, it gets the values from the database and uses
ToObject
to construct a Destination
that contains the values from the
database. This new Destination
is
then passed to PrintDestination
to
print the database values. Update the Main
method to call TestPrintDestination
and run the
application:
Current Values -- Great Barrier Reef, Australia -- Beautiful coral reef. WARNINGS!: Watch out for sharks! Database Values -- Great Barrier Reef, Australia -- Beautiful coral reef
The current and database values are printed to the screen and you
can see that the updated TravelWarnings
property is printed out in the
current values. The second instance of Destination
, which was created by calling
ToObject
, is not attached to the
context. Any changes to this second instance will not be persisted
during SaveChanges
.
ToObject
will only clone the
values from scalar properties; all navigation properties on the entity
will be left unassigned. This makes ToObject
useful for cloning a single object,
but it will not clone an entire object graph for you.
DbPropertyValues
isn’t a
read-only type. You can also use it to update values that are stored in
an instance. When you set values using CurrentValues
or OriginalValues
, this will also update the
current and original values in the change tracker. Additionally,
updating the CurrentValues
will
change the values that are stored in the properties of your entity
instance.
In Recording Original Values, you saw how the
OriginalValues
could be individually
updated. As each value was updated, the Change Tracker worked out which
properties had been modified. Let’s take a look at setting the current
values. Add the ChangeCurrentValue
method shown in Example 5-13.
private static void
ChangeCurrentValue() {using
(var
context =new
BreakAwayContext
()) {var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single(); context.Entry(hotel) .CurrentValues["Name"
] ="Hotel Pretentious"
;Console
.WriteLine("Property Value: {0}"
, hotel.Name); } }
The code loads the Grand Hotel Lodging
from the database. The code then gets
the CurrentValues
for the hotel
instance and modifies the value stored
for the Name
property. Finally, the
code writes out the value stored in the Name
property on the entity. Update the
Main
method to call ChangeCurrentValue
and run the application,
which will result in the following output:
Property Value: Hotel Pretentious
We see from the output that updating the value of a property in
the CurrentValues
has also updated
the value stored in the property of the entity.
Back in Working with Change Tracking, you
learned that POCO entities require a call to DetectChanges
to scan for changes in the
properties of the entity. You also learned that DbContext
takes care of calling DetectChanges
for you, but that you can
disable this behavior if you want to control when DetectChanges
is called.
Remember that in most cases it is best to let DbContext
automatically call DetectChanges
for you. More information on
manually calling DetectChanges
and
the use of change tracking proxies is available
in Chapter 4.
If you make changes using the Change Tracker API, there is no need
for DetectChanges
to be called,
because the Change Tracker is aware of the change being made. To see
this in action, update the ChangeCurrentValue
method, as shown in Example 5-14.
private static void
ChangeCurrentValue() {using
(var
context =new
BreakAwayContext
()) { context.Configuration.AutoDetectChangesEnabled =false
;var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single(); context.Entry(hotel) .CurrentValues["Name"
] ="Hotel Pretentious"
;Console
.WriteLine("Property Value: {0}"
, hotel.Name);Console
.WriteLine("State: {0}"
, context.Entry(hotel).State); } }
The updated code now disables automatic calling of DetectChanges
. The code also prints out the
state of the hotel
entity, as
recorded by the Change Tracker, after the current value for Name
has been updated. Go ahead and run the
application again:
Property Value: Hotel Pretentious State: Modified
If we had updated the Name
property on the entity, we would expect the state to be Unchanged
, since a call to DetectChanges
would be required to discover
the updated property. However, because the Name
property was updated using the Change
Tracker API, the state is correctly recorded as Modified
without calling DetectChanges
.
There may be times when you want to have an editable copy of the
current or original values but you don’t want changes to be recorded in
the Change Tracker. You’ll see one such scenario when we look at
resolving concurrency conflicts later in this chapter. The Clone
method will return a copy of any
DbPropertyValues
instance. Be aware
that when you clone current or original values, the resulting copy will
not be hooked up to the change tracker. Add the CloneCurrentValues
method shown in Example 5-15 to see how cloning works.
private static void
CloneCurrentValues() {using
(var
context =new
BreakAwayContext
()) {var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single();var
values = context.Entry(hotel).CurrentValues.Clone(); values["Name"
] ="Simple Hotel"
;Console
.WriteLine("Property Value: {0}"
, hotel.Name);Console
.WriteLine("State: {0}"
, context.Entry(hotel).State); } }
The code loads the Grand Hotel Lodging
from the database and then clones its
current values. The value stored for the Name
property in the cloned values is updated,
and then the value of the Name
property in the entity is written out. Update the Main
method to call CloneCurrentValues
and run the application to
see the following output in the console:
Property Value: Grand Hotel State: Unchanged
As expected, updating the cloned values has no impact on the values or the state of the entity they were cloned from.
In Chapter 4,
you learned that you can copy the contents of one DbPropertyValues
into another using the
SetValues
method. For example, you
may want users of a client application with access to the change
tracker to be able to roll back changes they’ve made to an entity. The
easiest way to do this is to copy the original values (when the entity
was retrieved from the database) back into the current values. Add the
UndoEdits
method shown in Example 5-16.
private static void
UndoEdits() {using
(var
context =new
BreakAwayContext
()) {var
canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single(); canyon.Name ="Bigger & Better Canyon"
;var
entry = context.Entry(canyon); entry.CurrentValues.SetValues(entry.OriginalValues); entry.State =EntityState
.Unchanged;Console
.WriteLine("Name: {0}"
, canyon.Name); } }
The code retrieves the Grand Canyon Destination
from the database and changes
its Name
. The code then undoes this
edit by locating the entry and copying the original values back into
the current values. Entity Framework isn’t smart enough to detect that
these new values match the original values, so the code also manually
swaps the state back to Unchanged
.
Finally, the name property is printed out to verify that the changes
were reverted. Update the Main
method to call UndoEdits
and run
the application. As expected, the changes to the Name
property are reverted and displayed in
the console like this:
Name: Grand Canyon
SetValues
doesn’t just
accept DbPropertyValues
, but can
accept any object. SetValues
will
attempt to overwrite the property values with the values in the
object that’s been passed in. This is done by matching the names of
the object’s properties with the names of the DbPropertyValues
instance. If a property
with the same name is found, the value is copied. If the property on
the object is not of the same type as the value stored in the
DbPropertyValues
, an InvalidOperationException
is thrown. Any
properties in the object that don’t match the name of a value
already stored in the DbPropertyValues
are ignored.
Many applications allow you to enter a new record by cloning an
existing record. Let’s see how to use SetValues
to accomplish this task. You might
be a fan of Dave, of Dave’s Dump fame, and want to create a new Dave’s
Campsite Lodging
using Dave’s Dump
as a starting point. Add the CreateDavesCampsite
method shown in Example 5-17.
private static void
CreateDavesCampsite() {using
(var
context =new
BreakAwayContext
()) {var
davesDump = (from
din
context.Lodgingswhere
d.Name =="Dave's Dump"
select
d).Single();var
clone =new
Lodging
(); context.Lodgings.Add(clone); context.Entry(clone) .CurrentValues .SetValues(davesDump); clone.Name ="Dave's Camp"
; context.SaveChanges();Console
.WriteLine("Name: {0}"
, clone.Name);Console
.WriteLine("Miles: {0}"
, clone.MilesFromNearestAirport);Console
.WriteLine("Contact Id: {0}"
, clone.PrimaryContactId); } }
The code retrieves Dave’s Dump from the database. Then it
creates a new Lodging
for Dave’s
Campsite and adds it to the context. The current values for the new
Campsite are then copied from Dave’s Dump, using the SetValues
method. The code then overwrites
the name, since we don’t want this new Lodging
to have the same name, and saves to
the database. Finally, some of the properties of the new Lodging
are written out to the console.
Update the Main
method to call
CreateDavesCampsite
and run the
application:
Name: Dave's Camp Miles: 32.65 Contact Id: 1
As expected, the displayed values are the same as Dave’s dump,
except for the Name
property that
we overwrote.
DbPropertyValues
is a great way
to work with complete sets of values, but there may be times when you just
want to work with the change tracking information for one property. You
can of course access the values of a single property using DbPropertyValues
, but that uses string-based
names for the property. Ideally you should be using strongly typed lambda
expressions to identify the property, so that you get compile-time
checking, IntelliSense, and refactoring support.
You can use the Property
,
Complex
, Reference
, and Collection
methods on an entry to get access to
the change tracking information and operations for an individual
property:
The Property
method is used
for scalar and complex properties.
The Complex
method is used to
get additional operations that are specific to complex
properties.
The Reference
and Collection
methods are used for navigation
properties.
There is also a Member
method, which can be used for any type of property. The Member
method is not strongly typed and only
provides access to information that is common to all
properties.
Let’s start with the Property
method. The Property
method allows
you to read and write the original and current value. It also lets you
know whether an individual property is marked as Modified
, something that isn’t possible with
DbPropertyValues
. The WorkingWithPropertyMethod
method shown in
Example 5-18 will allow
you to begin exploring the Property
method.
private static void
WorkingWithPropertyMethod() {using
(var
context =new
BreakAwayContext
()) {var
davesDump = (from
din
context.Lodgingswhere
d.Name =="Dave's Dump"
select
d).Single();var
entry = context.Entry(davesDump); entry.Property(d => d.Name).CurrentValue ="Dave's Bargain Bungalows"
;Console
.WriteLine("Current Value: {0}"
, entry.Property(d => d.Name).CurrentValue);Console
.WriteLine("Original Value: {0}"
, entry.Property(d => d.Name).OriginalValue);Console
.WriteLine("Modified?: {0}"
, entry.Property(d => d.Name).IsModified); } }
The code retrieves Dave’s Dump from the database and locates the
representative entry from the context. It then uses the Property
method to change the current value
for the Name
property. Then the code
prints out the current and original values plus the IsModified
flag. The IsModified
flag tells us if the property is
marked as Modified
and will be
updated when SaveChanges
is called.
You can update the Main
method to
call WorkingWithPropertyMethod
and
run the application to see these results in the console:
Current Value: Dave's Bargain Bungalows Original Value: Dave's Dump Modified?: True
The current and original values are displayed as expected, since
we changed the value of the Name
property. You can see that the property is also marked as modified.
There is also a weakly typed overload of Property
that accepts a string property name
rather than a lambda expression. In fact, all the methods you will see
in this section have a string-based overload.
The strongly typed lambda overloads are recommended because they
give you a compile-time check, but the string-based overloads can be
useful when writing generalized code. For example, you might want to
find out which properties are currently marked as Modified
. You can get the names of all
properties using CurrentValues
and
then check if they are modified using the Property
method. The FindModifiedProperties
method shown in Example 5-19 demonstrates
this.
private static void
FindModifiedProperties() {using
(var
context =new
BreakAwayContext
()) {var
canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single(); canyon.Name ="Super-Size Canyon"
; canyon.TravelWarnings ="Bigger than your brain can handle!!!"
;var
entry = context.Entry(canyon);var
propertyNames = entry.CurrentValues.PropertyNames;IEnumerable
<string
> modifiedProperties =from
namein
propertyNameswhere
entry.Property(name).IsModifiedselect
name;foreach
(var
propertyNamein
modifiedProperties) {Console
.WriteLine(propertyName); } } }
The code retrieves the Grand Canyon Destination
from the database and changes a
couple of properties. The code then locates the entry for the Grand
Canyon and gets a list of all property names using CurrentValues
. Then a LINQ query is used to
find which of those property names are marked as modified. The where
section of the LINQ query uses the
string-based overload of Property
to
get the change tracking information for the property. Finally, the
modified properties are written out to the console. When you run
FindModifiedProperties
, the names of
the two edited properties are written out to the console:
Name TravelWarnings
The Property
method also gives
you access to the name of the property and the change tracker entry for
the entity containing the property. This information is provided in the
Name
and EntityEntry
properties (Figure 5-1).
In the code you’ve seen so far, we’ve always known this
information because we started with the change tracking entry and then
the property name to find the information for a property. In Chapters
6 and 7 you
will see how the Name
and EntityEntry
properties are useful in
Validation scenarios.
When working with complex properties, you use the ComplexProperty
method to get access to change
tracking information and operations. The same operations that you just
learned about for scalar properties are all available for complex
properties. You can also use the Property
method to drill into individual
scalar properties on the complex type. The WorkingWithComplexMethod
shown in Example 5-20 demonstrates
interacting with the properties of the Address
complex property in a Person
instance.
private static void
WorkingWithComplexMethod() {using
(var
context =new
BreakAwayContext
()) {var
julie = (from
pin
context.Peoplewhere
p.FirstName =="Julie"
select
p).Single();var
entry = context.Entry(julie); entry.ComplexProperty(p => p.Address) .Property(a => a.State) .CurrentValue ="VT"
;Console
.WriteLine("Address.State Modified?: {0}"
, entry.ComplexProperty(p => p.Address) .Property(a => a.State) .IsModified);Console
.WriteLine("Address Modified?: {0}"
, entry.ComplexProperty(p => p.Address).IsModified);Console
.WriteLine("Info.Height.Units Modified?: {0}"
, entry.ComplexProperty(p => p.Info) .ComplexProperty(i => i.Height) .Property(h => h.Units) .IsModified); } }
The code loads data for Julie from the database and locates the
change tracking entry from the context. Next it drills into the Address
complex property and then into the
scalar State
property within Address
. The code changes the current value
assigned to Address.State
and then
prints out the IsModified
flag for
Address.State
and for the complex
Address
property. Finally, the code
drills into a nested complex property to check the IsModified
flag for Info.Height.Units
. You can see that ComplexProperty
calls can be chained together
to drill into a complex property that is defined in another complex
property. Following are the console results after running the WorkingWithComplexMethod
:
Address.State Modified?: True Address Modified?: True Info.Height.Units Modified?: False
An alternative syntax to access the change tracking information
for a complex property is to specify the full path to the property in a
single Property
call. For example,
you could also change the current value of Address.State
using the following code:
entry.Property(p => p.Address.State).CurrentValue = "VT"
;
You can also specify the full path if you are using the
string-based overload of Property
:
entry.Property("Address.State"
).CurrentValue ="VT"
;
When working with complex properties, Entity Framework tracks that
state for the complex type, but not for its individual properties. If
you check the state of any property within the complex type (for
example, the City
property of
Address
), Entity Framework will
return the state of the complex type (Address
). After changing the Address.State
property, every property of
Address
will be marked as
modified.
So far in this section, we have always modified the scalar values
of an existing complex type instance. You can also assign a new complex
type instance to a complex property. In Example 5-20, instead of editing
the State
property of the existing
Address
instance, we could have
replaced it with a new instance:
entry.ComplexProperty(p => p.Address) .CurrentValue =new
Address
{ State ="VT"
};
Replacing the value assigned to a complex property with a new instance will mark the entire complex property as modified.
You may have noticed in Figure 5-1 that a ParentProperty
is available after calling
Property
or ComplexProperty
. For properties that are
defined directly on an entity, this will always return null
. For properties that are defined within
a complex property, ParentProperty
will return the change tracking information for the parent complex
property. For example, if you are looking at the information for the
City
property inside Address
, ParentProperty
will give you the information
for Address
. In the examples in
this chapter, we always know the parent property because we started
with the entity, then drilled into the complex property, followed by
its subproperties.
Now it’s time to look at how to access the change tracking
information and operations associated with a navigation property.
Instead of the Property method, you use the Reference
and Collection
methods to get to navigation
properties:
Reference
is used when the
navigation property is just a reference to a single entity (for
example, Lodging.Destination
).
Collection
is used when the
navigation property is a collection (for example, Destination.Lodgings
).
These methods give you the ability to do several things:
Read and write the current value assigned to the navigation property
Load the related data from the database
Get a query representing the contents of the navigation property
In Explicit Loading, you saw how the Load
method can be used to load the contents
of a navigation property from the database. You also learned that the
IsLoaded
flag can be used to
determine if the entire contents of a navigation property (for example,
Destination.Trips
) have already been
loaded. In Chapter 2 you also saw how
to use the Query
method to run a LINQ
query against the contents of the navigation property. This was in Querying Contents of a Collection Navigation Property.
The Reference
method gives
you access to change tracking information and operations for a
navigation property. One piece of information that is available is the
value currently assigned to the navigation property. This is accessed
via the CurrentValue
property. If
the navigation property hasn’t been loaded from the database, CurrentValue
will return null
. You can also set the CurrentValue
property to change the entity
assigned to the navigation property, therefore changing the
relationship. Add the code for WorkingWithReferenceMethod
, shown in Example 5-21.
private static void
WorkingWithReferenceMethod() {using
(var
context =new
BreakAwayContext
()) {var
davesDump = (from
din
context.Lodgingswhere
d.Name =="Dave's Dump"
select
d).Single();var
entry = context.Entry(davesDump); entry.Reference(l => l.Destination) .Load();var
canyon = davesDump.Destination;Console
.WriteLine("Current Value After Load: {0}"
, entry.Reference(d => d.Destination) .CurrentValue .Name);var
reef = (from
din
context.Destinationswhere
d.Name =="Great Barrier Reef"
select
d).Single(); entry.Reference(d => d.Destination) .CurrentValue = reef;Console
.WriteLine("Current Value After Change: {0}"
, davesDump.Destination.Name); } }
The code retrieves Dave’s Dump Lodging
from the database and locates the
change tracking entry from the context. Then it drills into the
Destination
reference and
explicitly loads the related data using Reference().Load()
. The name of the Destination
that Dave’s Dump is assigned is
then written out to the console using the CurrentValue
property. Next, we change
CurrentValue
by assigning the Great
Barrier Reef Destination
. Finally,
we’ll write out the name of the Destination
that Dave’s Dump is assigned
again, but this time by accessing the navigation property on the
davesDump
entity itself.
Update the Main
method to
call WorkingWithReferenceMethod
and
run the application to see the following results:
Current Value After Load: Grand Canyon Current Value After Change: Great Barrier Reef
CurrentValue
allowed you to
read and write the Destination
that
Dave’s Dump is assigned to. Changing the CurrentValue
also updated the navigation
property on the davesDump
entity.
Earlier, when working with scalar properties, you saw that
DetectChanges
was not required when
making changes through the change tracker. The same is true for
reference navigation properties. Change detection and relationship
fix-up occur without DetectChanges
being called. To see this in action, update the WorkingWithReferenceMethod
method to disable
automatic change detection and lazy loading. Add the following code
immediately before the LINQ query that retrieves Dave’s Dump from the
database:
context.Configuration.AutoDetectChangesEnabled =false
; context.Configuration.LazyLoadingEnabled =false
;
Let’s also print some additional information to the console to
observe how the context is tracking the changes. Add the following
code after the final Console.WriteLine
call in the existing
method:
Console
.WriteLine("State: {0}"
, entry.State);Console
.WriteLine("Referenced From Current Destination: {0}"
, reef.Lodgings.Contains(davesDump));Console
.WriteLine("Referenced From Former Destination: {0}"
, canyon.Lodgings.Contains(davesDump));
The code prints out the state of Dave’s Dump as recorded by the
change tracker. Then it prints out whether Dave’s Dump is present in
Lodgings
collection on the current
Destination
(reef
) and the former Destination
(canyon
). Go ahead and run the application
again, which will print these results in the console:
After Load: Grand Canyon After CurrentValue Change: Great Barrier Reef State: Modified Referenced From Current Destination: True Referenced From Former Destination: False
The change tracker is aware that Dave’s Dump is Modified
without calling DetectChanges
. The change tracker has also
taken care of updating the Destination
reference on Dave’s Dump,
removing Dave’s Dump from the Lodgings
collection on the former Destination
, and adding it to the new
Destination
.
More information on relationship fix-up is available in Using DetectChanges to Trigger Relationship Fix-up.
You’ve seen many features of working with a reference navigation
using the Reference
method. The
same operations are available when using the Collection
method to interact with a
collection navigation property. The WorkingWithCollectionMethod
method, shown in
Example 5-22, runs
through some of the same tasks, but this time with a navigation
property that points to a collection. We’re using Reservation.Payments
as our collection
navigation property rather than Destination.Lodgings
. Back in Chapter 3, we set up
Destination
to use a dynamic change
tracking proxy so that changes would be automatically reported to the
change tracker. But Reservation
is
not set up to use a change tracking proxy. This will allow us to
explore how the Collection
method
behaves with change detection and relationship fix-up.
private static void
WorkingWithCollectionMethod() {using
(var
context =new
BreakAwayContext
()) {var
res = (from
rin
context.Reservationswhere
r.Trip.Description =="Trip from the database"
select
r).Single();var
entry = context.Entry(res); entry.Collection(r => r.Payments) .Load();Console
.WriteLine("Payments Before Add: {0}"
, entry.Collection(r => r.Payments).CurrentValue.Count);var
payment =new
Payment
{ Amount = 245 }; context.Payments.Add(payment); entry.Collection(r => r.Payments) .CurrentValue .Add(payment);Console
.WriteLine("Payments After Add: {0}"
, entry.Collection(r => r.Payments).CurrentValue.Count); } }
The method loads a Reservation
from the database and locates
its change tracking entry from the context. Then it drills into the
Payments
property using the
Collection
method and, just as we
did with the Reference
, uses the
Load
method to explicitly load any
related Payments
from the database.
The method then calls the Collection.CurrentValue.Count
property to
count how many payments are in the collection and prints out the
count. Finally, the method adds a new payment and prints out the count
again. Update the Main
method to
call WorkingWithCollectionMethod
and run the application. Here is what you’ll see in the
console:
Payments Before Add: 1 Payments After Add: 2
With a Collection
, you can
use the CurrentValue
property to
read and write from the relevant collection navigation property (in
this case, Payments
). CurrentValue
on collection navigation
properties returns the instance of the collection assigned to the
navigation property. In the case of Reservation.Payments
, that’s the List<Payment>
that gets created in the
constructor of Reservation
.
Therefore, adding or removing from the CurrentValue
behaves the same as adding or
removing from the navigation property itself. This means that unless
the entity is a change tracking proxy, you’ll need DetectChanges
to get change detection and
relationship fix-up to occur. Let’s see how this affects the results
of the WorkingWithCollectionMethod
method by adding a line of code to disable automatic change detection.
Add the following line of code immediately before the LINQ query that
retrieves the Reservation
from the
database:
context.Configuration.AutoDetectChangesEnabled = false
;
Also add the following code after the final
Console.WriteLine
call. This new code calls
DetectChanges
after adding the
Payment
. The value assigned to the
foreign key on the new Payment
is
printed out to the console on either side of the DetectChanges
call:
Console
.WriteLine("Foreign Key Before DetectChanges: {0}"
, payment.ReservationId); context.ChangeTracker.DetectChanges();Console
.WriteLine("Foreign Key After DetectChanges: {0}"
, payment.ReservationId);
Go ahead and run the application again. You can see the effect
of setting AutoDetectChangesEnabled
to false and the explicit DetectChanges
call in the console
output:
Count Before Add: 1 Count After Add: 2 Foreign Key Before DetectChanges: 0 Foreign Key After DetectChanges: 1
The addition of the Payment
to the Payments
collection of the
Reservation
was not automatically
detected. Relationship fix-up was not triggered until DetectChanges
was called.
Throughout this book you have seen how to load data from the database and work with it in-memory. So far, you have only loaded the data one time, but there may be times when you want to refresh or reload a given entity. For example you may have had an entity in memory for a long period of time and want to make sure you have the latest data before displaying it to a user.
Entity Framework includes a Reload method on DbEntityEntry
that can be used to refresh an
entity with the latest data from the database. The ReloadLodging
method shown in Example 5-23 uses this
method.
private static void
ReloadLodging() {using
(var
context =new
BreakAwayContext
()) {var
hotel = (from
din
context.Lodgingswhere
d.Name =="Grand Hotel"
select
d).Single(); context.Database.ExecuteSqlCommand(@"UPDATE dbo.Lodgings
SET Name = 'Le Grand Hotel'
WHERE Name = 'Grand Hotel'"
);Console
.WriteLine("Name Before Reload: {0}"
, hotel.Name);Console
.WriteLine("State Before Reload: {0}"
, context.Entry(hotel).State); context.Entry(hotel).Reload();Console
.WriteLine("Name After Reload: {0}"
, hotel.Name);Console
.WriteLine("State After Reload: {0}"
, context.Entry(hotel).State); } }
The method retrieves the Grand Hotel Lodging
from the database and then, for the sake
of demoing this feature, issues a raw SQL query to update its Name
in the database. The code then calls
Reload
to refresh with the latest data
from the database. Notice that the code prints out the value assigned to
the Name
property and the state of the
entity before and after calling Reload
.
Update the Main
method to call ReloadLodging
and run the application to see the
effect. This is the output in the console:
Name Before Reload: Grand Hotel State Before Reload: Unchanged Name After Reload: Le Grand Hotel State After Reload: Unchanged
You can see that the new value for the Name
property was retrieved from the
database.
Reload
will also overwrite any
changes you have in memory. To see this effect, update the ReloadLodging
method to edit the Lodging
before reloading. Add the following line
of code immediately after the LINQ query that populates the hotel
variable:
hotel.Name = "A New Name"
;
The code now modifies the Name
property of the entity in memory before calling Reload
. This will now be the output of the
method:
Name Before Reload: A New Name State Before Reload: Modified Name After Reload: Le Grande Hotel State After Reload: Unchanged
Because we edited the Name
property, the entity state is Modified
before the Reload
. After the Reload
, the entity is now marked as Unchanged
, because any changes were overwritten
with data from the database.
So far you have seen how to get access to the DbEntityEntry
for a single entity. Sometimes you
might want to get access to entries for all entities or a subset of the
entries tracked by the context. You can do this using the DbContext.ChangeTracker.Entries
method. There is
a generic Entries<TEntity>
overload that returns a collection of DbEntityEntry<TEntity>
records for all
entities that are of the type specified for TEntity
. The nongeneric overload of Entries
does not allow you to specify the type,
and it returns a collection of DbEntityEntry
records for all of the tracked
entities.
We’ll start by looking at all entries known by the change tracker
using the nongeneric overload using the PrintChangeTrackerEntries
method shown in Example 5-24.
private static void
PrintChangeTrackerEntries() {using
(var
context =new
BreakAwayContext
()) {var
res = (from
rin
context.Reservationswhere
r.Trip.Description =="Trip from the database"
select
r).Single(); context.Entry(res) .Collection(r => r.Payments) .Load(); res.Payments.Add(new
Payment
{ Amount = 245 });var
entries = context.ChangeTracker.Entries();foreach
(var
entryin
entries) {Console
.WriteLine("Entity Type: {0}"
, entry.Entity.GetType());Console
.WriteLine(" - State: {0}"
, entry.State); } } }
The code retrieves a Reservation
from the database and then uses explicit loading to bring its related
Payments
into memory. The code also
creates a new Payment
and adds it to
the Payments
collection of the Reservation
. Then we use the Entries
method to retrieve all change tracked
entries from the context. As the code iterates over the entries, it prints
out the type of entity and its current state. Calling PrintChangeTrackerEntries
from the Main
method results in this console
output:
Entity Type: Model.Payment - State: Added Entity Type: Model.Reservation - State: Unchanged Entity Type: Model.Payment - State: Unchanged
The Entries
method returns an
entry for the Reservation
and its
existing Payment
, as well as the new
Payment
we added. You’ll notice that
the entries aren’t returned in the order they began being tracked by the
context.
You shouldn’t rely on the order that entries are returned in, as it may change between versions of Entity Framework.
You can also use LINQ to Objects to query the result of the Entries
method. Replace the line of code in PrintChangeTrackerEntries
that populates the
entries
variable to use a LINQ
query:
var
entries =from
ein
context.ChangeTracker.Entries()where
e.State ==EntityState
.Unchangedselect
e;
This updated code uses a LINQ query to select only the entries for
entities that are tracked in the Unchanged
state. If you run the application you
will see that the information for the Added
Payment
is no longer displayed:
Entity Type: Model.Reservation - State: Unchanged Entity Type: Model.Payment - State: Unchanged
Another way to filter is to use the generic overload of Entries
to specify which types you want entries
for. Change the Entries
call in
PrintChangeTrackerEntries
again along
with the code, which writes to the console:
var
entries = context.ChangeTracker.Entries<Payment
>();foreach
(var
entryin
entries) {Console
.WriteLine("Amount: {0}"
, entry.Entity.Amount);Console
.WriteLine(" - State: {0}"
, entry.State); }
The call now uses the generic overload of Entries
to specify that we are only interested
in entries for the Payment
type. Thanks
to the generic overload, the Entity
property on the returned entries is now strongly typed as Payment
. We’ll print out the payment Amount
instead of the type of the entity. If you
run the application again, you will see that only information for the two
Payment
entities is printed:
Amount: 245 - State: Added Amount: 150.00 - State: Unchanged
The type that you supply to the generic overload of Entries
does not need to be a type that is
included in your model. For example, in Chapter 4, you saw the generic
overload used to get all entries for entities that implemented a given
interface:
context.ChangeTracker.Entries<IObjectWithState
>()
We’ve covered a lot of functionality in this chapter, so let’s look at a couple of examples of how that functionality can be used in an application.
You’ll see how you can use the Change Tracker API to resolve
concurrency conflicts and also to log changes that are made during
SaveChanges
.
A concurrency conflict occurs when you attempt to update a record in the database but another user has updated that same record since you queried for it. By default, Entity Framework will always update the properties that you have modified regardless of whether or not there is a concurrency conflict. However, you can configure your model so that Entity Framework will throw an exception when a concurrency conflict occurs. You do this by specifying that a specific property should be used as a concurrency token.
How to configure your model for optimistic concurrency is covered in detail in Programming Entity Framework, 2e (for EDMX models) and in Programming Entity Framework: Code First (for models defined using Code First).
During SaveChanges
Entity
Framework will check if the value in the corresponding database column
has been updated since the record was bought into memory. A concurrency
exception is thrown if the value in the database has changed.
The BAGA model includes two examples of concurrency tokens. The
SocialSecurityNumber
property on
Person
is marked with the ConcurrencyCheck
attribute. When updating an
existing Person
, Entity Framework
will check that the SSN allocated to the Person
when the record was retrieved from the
database remains the same in the database. If another user has changed
the SSN, SaveChanges
will fail and a
DbUpdateConcurrencyException
will be
thrown. The Trip
class includes a
RowVersion
property that is marked
with the Timestamp
attribute.
Timestamp properties are treated the same as other concurrency check
properties, except the database will automatically generate a new value
for this property whenever any column in the record is updated. This
means that when saving changes to an existing Trip
, a concurrency exception will be thrown
if another user has updated any properties since the Trip
was retrieved from the database.
Let’s write a method that will cause a DbUpdateConcurrencyException
to be thrown when
trying to save a change to an existing Trip
. When the exception is thrown, we’ll ask
the end user of our application to tell us how they want to resolve the
conflict. Go ahead and add the ConcurrencyDemo
method shown in Example 5-25.
private static void
ConcurrencyDemo() {using
(var
context =new
BreakAwayContext
()) {var
trip = (from
tin
context.Trips.Include(t => t.Destination)where
t.Description =="Trip from the database"
select
t).Single(); trip.Description ="Getaway in Vermont"
; context.Database.ExecuteSqlCommand(@"UPDATE dbo.Trips
SET CostUSD = 400
WHERE Description = 'Trip from the database'"
); SaveWithConcurrencyResolution(context); } }private static void
SaveWithConcurrencyResolution(BreakAwayContext
context) {try
{ context.SaveChanges(); }catch
(DbUpdateConcurrencyException
ex) { ResolveConcurrencyConflicts(ex); SaveWithConcurrencyResolution(context); } }
The example retrieves an existing Trip
from the database and changes its
Description
property. It then issues
a raw SQL query to update the CostUSD
column of the same Trip
in the
database. Executing this statement will cause the database to generate a
new value for the RowVersion
column
in the database. Issuing a raw SQL statement is not a recommended
practice and is just used to simulate another user changing data. Next,
the code calls the SaveWithConcurrencyResolution
helper method.
This helper method calls SaveChanges
,
which will issue an UPDATE command to apply our changes to the Trip
. As part of the update process, Entity
Framework will check if the RowVersion
column in the database still has
the same value as it did when we queried for the Trip
. Because of change we made with ExecuteSqlCommand
, the RowVersion
will have changed and a DbUpdateConcurrencyException
will be thrown.
The example code catches this exception and attempts to resolve the
conflict with a custom method, ResolveConcurrencyConflict
. Once the conflict
is resolved, the code makes a recursive call to SaveWithConcurrencyResolution
. The recursive
call is used to ensure that we handle any further concurrency conflicts
that occur after the first conflict is resolved. Example 5-26 shows the ResolveConcurrencyConflict
method.
private static void
ResolveConcurrencyConflicts(DbUpdateConcurrencyException
ex) {foreach
(var
entryin
ex.Entries) {Console
.WriteLine("Concurrency conflict found for {0}"
, entry.Entity.GetType());Console
.WriteLine(" You are trying to save the following values:"
); PrintPropertyValues(entry.CurrentValues);Console
.WriteLine(" The values before you started editing were:"
); PrintPropertyValues(entry.OriginalValues);var
databaseValues = entry.GetDatabaseValues();Console
.WriteLine(" Another user has saved the following values:"
); PrintPropertyValues(databaseValues);Console
.Write("[S]ave your values, [D]iscard you changes or [M]erge?"
);var
action =Console
.ReadKey().KeyChar.ToString().ToUpper();switch
(action) {case
"S"
: entry.OriginalValues.SetValues(databaseValues);break
;case
"D"
: entry.Reload();break
;case
"M"
:var
mergedValues = MergeValues( entry.OriginalValues, entry.CurrentValues, databaseValues); entry.OriginalValues.SetValues(databaseValues); entry.CurrentValues.SetValues(mergedValues);break
;default
:throw new
ArgumentException
("Invalid option"
); } } }
Fortunately, the DbUpdateConcurrencyException
gives you access
to everything you need to know about the conflict. The exception’s
Entries
property gives you the
DbEntityEntry
for each of the
entities that had a concurrency conflict.
Because Entity Framework stops at the first exception, the
Entries
property on DbUpdateConcurrencyException
will almost
always contain just a single entry. If you have a relationship that
does not expose a foreign key property on your entity, Entity
Framework treats relationships as separate from the entity; these
relationships are known as independent
associations. SaveChanges
also treats the relationships as
separate from the entity. If a concurrency conflict occurs when saving
the relationship, the resulting exception will include the entry for
the entity on each end of the relationship. This is yet one more good
reason to always include foreign key properties in your
entities.
The ResolveConcurrencyConflicts
method iterates through each of the entries in the exception to resolve
the conflict. It lets the user know what type of entity the conflict
occurred in by checking the type of the entity in the Entity
property. Next the user is shown the
current, original, and database values using the PrintPropertyValues
method you added back in
Example 5-10. The code
then gives the user three options for resolving the conflict:
If the changes that another user has made don’t make sense
given the changes the current user is making, the user can proceed
with saving his or her values to the database. This option will
overwrite any changes that were made by other users, even if those
changes were to a property that the user is not trying to update.
For example, the drop to $400 that the other user applied might
not be applicable now that the Trip
is visiting beautiful
Vermont.
If the user selects this option, the original values are set
to the database values. This updates the RowVersion
property with the new value
from the database, so that the next save will succeed. Setting the
original values will also mark any properties that have a
different current value as Modified
. In our example, only the
Description
property was
modified. However, the current value for CostUSD
is still the value retrieved
from the database ($1000), but the database value has been updated
($400). Therefore the CostUSD
property will get marked as Modified
—to be set back to the value
when originally queried ($1000).
If the user’s changes no longer make sense, the user can
discard his or her changes and accept the new values that the
other user has saved. For example, the user might decide that
given the price reduction the other user applied, it’s better to
leave the Trip Description
as
it was.
The DbEntityEntry.Reload
method makes this option very simple to implement. Reload
will query the database again for
the database values. If you wanted to avoid this additional query,
you could also set the original and current values to the database
values. Remember that Entity Framework isn’t smart enough to move
properties back out of the Modified
state if you change the current
and original value to the same thing. Therefore, you would also
need to set the State
property
on the entry back to Unchanged
.
Calling Reload
has one small advantage; it will
always pick up the very latest version of the entity at the time
the user decides to discard his or her changes. If another user
has modified the affected entity again, after we detected the
conflict and queried the database values, then
Reload
will pick up this latest set of
changes.
The user may decide that both sets of changes make sense and
they should be merged. The user’s change to the Description
may be completely unrelated
to the drop in price. The price should remain at $400 but the
change to Description
should
also be applied.
We’ll use a custom MergeValues
method, which we are about
to add, to calculate the correct values to save. These merged
values are then set to the current values. The database values are
set to the original values to ensure the RowVersion
property has the new value
from the database, so that the next save will succeed.
The final piece of code to add is the MergeValues
method that will be used when the
user decides to merge his or her changes with the changes another user
has applied (Example 5-27).
private static
DbPropertyValues
MergeValues(DbPropertyValues
original,DbPropertyValues
current,DbPropertyValues
database) {var
result = original.Clone();foreach
(var
propertyNamein
original.PropertyNames) {if
(original[propertyName]is
DbPropertyValues
) {var
mergedComplexValues = MergeValues( (DbPropertyValues
)original[propertyName], (DbPropertyValues
)current[propertyName], (DbPropertyValues
)database[propertyName]); ((DbPropertyValues
)result[propertyName]) .SetValues(mergedComplexValues); }else
{if
(!object
.Equals( current[propertyName], original[propertyName])) { result[propertyName] = current[propertyName]; }else if
(!object
.Equals( database[propertyName], original[propertyName])) { result[propertyName] = database[propertyName]; } } }return
result; }
MergeValues
begins by using the
Clone
method to create a set of
values that will store the merged result. We are cloning from the
original values, but you could clone from any of the three sets of
values (current, original, or database). Note that the code is going to
assume that the three sets of supplied values contain exactly the same
properties—in our case, we know this is true because they come from the
same entity. The method then loops through each property to perform the
merge.
If the property value is a DbPropertyValues
, we know it represents a
complex type. For complex types, the code uses a recursive call to merge
the values in the complex type. The merged values for the complex type
are then copied into the result using the SetValues
method.
For scalar properties, the code compares the current value to the original value to see if the current user has edited the value. If the current user has edited the value, the current value is copied to the merged result. If the current user hasn’t edited the value but another user has changed it in the database, the database value is copied to the result. If nobody has edited the value, all three value collections agree and the value that was originally cloned can be left in the merged result.
If you want to test out the code, update the Main
method to call ConcurrencyDemo
and run the
application.
The change tracking information that Entity Framework maintains is
also useful if you want to log the changes that a user is making during
SaveChanges
. We’ll take a look at an
example that writes the changes to the Console
, but the techniques could be used for
any number of logging or auditing solutions.
The logging output could get annoying if we leave it on for every
example in this book, so we are going to introduce a flag that allows us
to turn it on when we want to use it. Add the following property to your
BreakAwayContext
class:
public bool
LogChangesDuringSave {get
;set
; }
You’ll need some helper methods to print out the required logging
information. Add the two methods shown in Example 5-28 to your BreakAwayContext
class. You’ll need to add a
using statement for the System
,
System.Data
, and System.Linq
namespaces.
private void
PrintPropertyValues(DbPropertyValues
values,IEnumerable
<string
> propertiesToPrint,int
indent = 1) {foreach
(var
propertyName in propertiesToPrint) {var
value = values[propertyName];if
(valueDbPropertyValues
) {Console
.WriteLine("{0}- Complex Property: {1}"
,string
.Empty.PadLeft(indent), propertyName);var
complexPropertyValues = (DbPropertyValues
)value; PrintPropertyValues( complexPropertyValues, complexPropertyValues.PropertyNames, indent + 1); }else
{Console
.WriteLine("{0}- {1}: {2}"
,string
.Empty.PadLeft(indent), propertyName, values[propertyName]); } } }private
IEnumerable
<string
> GetKeyPropertyNames(object
entity) {var
objectContext = ((IObjectContextAdapter
)this
).ObjectContext;return
objectContext .ObjectStateManager .GetObjectStateEntry(entity) .EntityKey .EntityKeyValues .Select(k => k.Key); }
The PrintPropertyValues
method
is almost the same as the PrintPropertyValues
you added to the Program
class back in Example 5-10. The only
difference is that this method accepts the names of the properties that
should be printed, rather than printing all properties. As the name
suggests, the GetKeyPropertyNames
method will give you the names of the properties that make up the key of
an entity. There is no way to get this information from the DbContext
API, so the code uses the IObjectContextAdapter
to get the underlying
ObjectContext
. You’ll learn more
about IObjectContextAdapter
in Chapter 8. The code gets the
key property names by getting the EntityKey
for the entity from the ObjectStateManager
. ObjectStateManager
is the ObjectContext
equivalent to the DbContext.ChangeTracker
.
With the helper methods in place, let’s write the logging code.
The easiest way to run additional logic during the save process is to
override the SaveChanges
method on
your derived context. The SaveChanges
method on DbContext
is virtual
(Overrideable
in Visual Basic) for exactly this
purpose. Add the overridden SaveChanges
method shown in Example 5-29 to your BreakAwayContext
class.
public override int
SaveChanges() {if
(LogChangesDuringSave) {var
entries =from
ein this
.ChangeTracker.Entries()where
e.State !=EntityState
.Unchangedselect
e;foreach
(var
entryin
entries) {switch
(entry.State) {case
EntityState
.Added:Console
.WriteLine("Adding a {0}"
, entry.Entity.GetType()); PrintPropertyValues( entry.CurrentValues, entry.CurrentValues.PropertyNames);break
;case
EntityState
.Deleted:Console
.WriteLine("Deleting a {0}"
, entry.Entity.GetType()); PrintPropertyValues( entry.OriginalValues, GetKeyPropertyNames(entry.Entity));break
;case
EntityState
.Modified:Console
.WriteLine("Modifying a {0}"
, entry.Entity.GetType());var
modifiedPropertyNames =from
nin
entry.CurrentValues.PropertyNameswhere
entry.Property(n).IsModifiedselect
n; PrintPropertyValues( entry.CurrentValues, GetKeyPropertyNames(entry.Entity) .Concat(modifiedPropertyNames));break
; } } }return base
.SaveChanges(); }
The code checks if the LogChangesDuringSave
property is set to
true
—by default the property is set
to false
. If logging is enabled, the
logging logic is executed. The code then locates the entries for all
entities that are going to be saved—that’s all entities that aren’t in
the Unchanged
state. For each of
these entities, the code identifies the change being performed and the
type of entity it is being performed on. Then it prints out the values
of some of the properties of the entity. The set of properties that gets
printed depends on the type of operation being performed:
For Added
entities, the
entire set of current values that are going to be inserted are
printed.
For Deleted
entities, just
the key properties are printed out, since this is enough information
to identify the record being deleted.
For Modified
entities, the
key properties and the properties that are being updated are printed
out.
If you want to test the code out, add the TestSaveLogging
method shown in Example 5-30.
private static void
TestSaveLogging() {using
(var
context =new
BreakAwayContext
()) {var
canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single(); context.Entry(canyon) .Collection(d => d.Lodgings) .Load(); canyon.TravelWarnings ="Take a hat!"
; context.Lodgings.Remove(canyon.Lodgings.First()); context.Destinations.Add(new
Destination
{ Name ="Seattle, WA"
}); context.LogChangesDuringSave =true
; context.SaveChanges(); } }
The code retrieves the Grand Canyon Destination
and its related Lodgings
from the database. The Grand Canyon
Destination
is then modified, one of
its Lodgings
is marked for deletion,
and a new Destination
is added to the
context. The code then enables the logging you just added and calls
SaveChanges
. Update the Main
method to call TestSaveLogging
and run the application to see
the log information in the console:
Adding a new Model.Destination - DestinationId: 0 - Name: Seattle, WA - Country: - Description: - Photo: - TravelWarnings: - ClimateInfo: Modifiying an existing System.Data.Entity.DynamicProxies.Destination_C0312EA59B82EAC711175D8C037E196179 A49BDE8FF3F0D6830DDB66725C841B - DestinationId: 1 - TravelWarnings: Take a hat! Deleting an existing Model.Lodging - LodgingId: 1
As expected, logging information is printed out for the three
entities that are affected during SaveChanges
. You’ll notice that the Lodging
we modified has a strange type name—it’s not Model.Destination
. This is because we enabled
Destination
as a change tracking
proxy. The type name displayed is the type that Entity Framework creates
at runtime that derives from the Model.Destination
type.