In the previous chapter you learned how to add new entities and change
or delete existing entities. All the examples we looked at involved making
changes one at a time to entities that are tracked by the context. Each of
the changes affected a single entity or relationship. You saw that you can
perform multiple of these single entity operations and then call SaveChanges
to push all the changes to the
database in a single transaction. In this chapter we will look at making
changes to entities that are not being tracked by a context. Entities that
are not being tracked by a context are known as disconnected
entities.
For most single-tier applications, where the user interface and database access layers run in the same application process, you will probably just be performing operations on entities that are being tracked by a context. Operations on disconnected entities are much more common in N-Tier applications. N-Tier applications involve fetching some data on a server and returning it, over the network, to a client machine. The client application then manipulates this data before returning it to the server to be persisted.
The N-Tier pattern makes data access more complex because there is no longer a context tracking changes that are made to each entity. The data is fetched using one context, and returned to the client where there is no context to track changes. The data is then sent back to the server and must be persisted back to the database using a new instance of the context.
While the majority of content in this chapter is aimed at developers writing N-Tier applications, it’s useful information for anyone working with Entity Framework and will give you a deeper understanding of how Entity Framework behaves.
When it comes time to persist the data on the server, you are typically working with a graph of entities. A graph of entities is simply a number of entities that reference each other. We’ve already worked with graphs of entities that are attached to the context. In the last chapter we looked at adding a relationship using a navigation property, which is enough to create a graph, because one entity now references another. In N-Tier scenarios this graph of entities is usually disconnected from the context, though, meaning the context isn’t yet tracking any of the entities in the graph.
When it comes time to start performing operations on this disconnected graph, there are some additional behaviors in Entity Framework that you need to be aware of. The entity that you perform the operation on is known as the root of the graph. Performing an operation on the root of disconnected graph can have side effects on the rest of the graph, too.
Before we delve into the complexities of N-Tier scenarios, let’s
take a quick look at an example of the side effects of performing an
operation on the root of a disconnected graph. In the previous chapter we
saw that DbSet.Add
can be used to
register a new entity to be inserted when SaveChanges
is called.
You’ll see the term register used throughout this chapter. When an entity is registered with the context it means that the context becomes aware of the entity and starts tracking it.
So far the entities we’ve passed to the Add
method have been standalone instances with
no references to other entities. Now let’s see what happens when we pass
the root of a newly created graph of entities that isn’t yet tracked by
the context. Add the AddSimpleGraph
method that is shown in Example 4-1.
private static void
AddSimpleGraph() {var
essex =new
Destination
{ Name ="Essex, Vermont"
, Lodgings =new
List
<Lodging
> {new
Lodging
{ Name ="Big Essex Hotel"
},new
Lodging
{ Name ="Essex Junction B&B"
}, } };using
(var
context =new
BreakAwayContext
()) { context.Destinations.Add(essex);Console
.WriteLine("Essex Destination: {0}"
, context.Entry(essex).State);foreach
(var
lodgingin
essex.Lodgings) {Console
.WriteLine("{0}: {1}"
, lodging.Name, context.Entry(lodging).State); } context.SaveChanges(); } }
The code constructs a new Destination
instance, which also references two
new Lodging
instances in its Lodgings
property. Then the new Destination
is added to a context using the
Add
method. Once the Destination
is added, the code uses the DbContext.Entry
method to get access to the
change tracking information that Entity Framework has about the new
Destination
. From this change tracking
information the State
property is used
to write out the current state of the entity. This process is then
repeated for each of the newly created Lodgings
that are referenced from the new
Destination
. If you modify the Main
method to call AddSimpleGraph
and run the application you will
see the following output:
Essex Destination: Added Big Essex Hotel: Added Essex Junction B&B: Added
It’s no surprise that Entity Framework has the new Destination
registered as an Added
entity because we used the Add
method to add it to the context. What may be
a little less obvious is that Entity Framework looked in the navigation
properties of the Destination
instance
and saw that it referenced two Lodging
instances that the context wasn’t already tracking. Entity Framework also
registers these entities as Added
and
will insert them into the database when SaveChanges
is called. The process of finding
related entities is recursive, so if one of the new Lodging
instances referenced a new Person
instance, the Person
would also get added to the context.
Figure 4-1 attempts to
visualize how calling Add
on a
disconnected Destination
will also add
other disconnected entities that are reachable from the Destination
.
If a reference is found to an entity that is already tracked by the
context, the entity that is already tracked is left in its current state.
For example, if one of our new Lodging
instances referenced an existing Person
that had been queried for using the context, the existing Person
would not be marked as Added
. The existing Person
would remain in the Unchanged
state and the Lodging
would be inserted with its foreign key
pointing to the existing Person
. Figure 4-2 attempts to
visualize how adding a disconnected Destination
will
also add other disconnected entities, but if an entity that is being
tracked by the context is found, it is left in its current state.
Now that you’ve seen how Entity Framework behaves when it is asked
to add the root of a graph of entities to the context, we’re going to take
a look at how this affects N-Tier applications. Remember that in an N-Tier
application the data is queried for in one context, but the changes need
to be persisted using another context. For example, on the server side of
your application you could expose a GetDestinationAndLodgings
method that will
return a Destination
with all of its
Lodgings
:
public
Destination
GetDestinationAndLodgings(int
destinationId)
The client side of the application could then fetch a Destination
and make some changes to it. These
changes could include changing the Description
of the Destination
and adding a new Lodging
to its Lodgings
collection. The server side of the
application could then expose a SaveDestinationAndLodgings
method to push all
these changes back to the database:
public void
SaveDestinationAndLodgings(Destination
destination)
When it comes time to implement the SaveDestinationAndLodgings
method, things get a
little more complicated. Like the simple graph from Example 4-1, the Destination
that gets passed in isn’t tracked by
a context. In fact, because it’s been serialized over the network on its
way to and from the client, it’s not even the same instance that was
queried for using Entity Framework. The SaveDestinationAndLodgings
method needs to let
Entity Framework know if it’s an existing Destination
or a new Destination
that needs to be added. If it’s an
existing Destination
, some of the
properties may have been modified and therefore need to be updated in the
database. The Lodgings
property may
also contain instances of Lodging
that
can also be existing or new. Since Entity Framework hasn’t been tracking
these entities, you need to let it know all of this information.
Your server-side logic could be exposed through a service or perhaps a class that follows the repository pattern. Either way, you need to overcome the disconnected nature of the objects that are returned from the client application.
There are a number of different approaches to solving the challenges associated with building N-Tier applications. Covering the details of each approach is well beyond the scope of this book, but we’ll take a quick look at the different options that are available to you and how they relate to using Entity Framework.
In a lot of cases you can save yourself the headache of dealing with the intricacies of N-Tier data access by using a framework that takes care of tracking changes that are made to data on the client and applying those changes on the server. Entity Framework is tightly integrated with WCF Data Services, which is one such framework. WCF Data Services is Microsoft’s solution to the N-Tier challenge. There are frameworks available from other vendors as well.
WCF Data Services allows you to choose what data from your model is exposed from the server and what permission clients have for the data (read, append, update, and so on). WCF Data Services also includes a client component that takes care of tracking changes you make to data on the client, pushing those changes back to the database, and saving them using your Entity Framework model. WCF Data Services uses the OData protocol (http://odata.org) for exposing data from the server; this allows your service to be accessed by clients other than WCF Data Services, including non .NET Framework platforms.
The WCF Data Services client has a similar “look and feel” to
performing data access directly against DbContext
. Using WCF Data Services is arguably
the simplest approach to N-Tier data access with Entity Framework, and
it is the approach the Entity Framework team recommends. You can learn
more about using WCF Data Services with DbContext
in this article: http://msdn.microsoft.com/en-us/data/hh272554.
WCF Data Services is a good option for building N-Tier applications, but there are some reasons it may not be the right tool for your job. WCF Data Services gives you quite a bit of control over how requests are processed on the server, but it is a framework and therefore it is somewhat scoped in what it can handle. Depending on your application, you may have some advanced requirements that are not supported.
You may also be in a situation where the shape of the server operations and/or the protocol to be used for client-server communication are outside of your control. In these situations, authoring your own web services may be the only viable option.
Another way to avoid the complexity of determining the changes to
be made on the server is to expose very granular operations that require
the client to identify the exact change to be made. For example, rather
than the SaveDestinationAndLodgings
method we saw earlier in this chapter, you could expose AddDestination
and UpdateDestination
methods. These methods would
only operate on a standalone Destination
instance rather than a graph of
entities. You would expose separate methods for adding and updating
Locations
. Entity Framework makes it
simple to implement these methods and you’ll find everything you need
for this approach in the Understanding How DbContext Responds to Setting the State of a
Single Entity section of this
chapter.
While this option makes the server component of your application much easier, it potentially makes the client component more complex. You are also likely to end up with a large number of operations exposed from the server. If you can’t use an existing framework, such as WCF Data Services, you will need to weigh up the benefits of this approach and the approach covered in the next section. Granular operations will typically give you a higher quantity of code, but that code will be much simpler to write, test, and debug. Next we’ll take a look at a more generalized solution that will probably involve less code, but is inherently more complex.
Another option is to have a more generalized server operation that accepts a graph of entities and then lets Entity Framework know what state each entity in that graph should be in. This is often referred to as replaying the changes on the server, because you are walking through each entity and letting Entity Framework know what happened on the client. The process of iterating through each entity and setting its state is also known as painting the state.
There are many different ways to implement this logic. You could write code that is strongly tied to the classes in your model and each server operation knows how to navigate the graph that is passed in, look at each entity, and set its state appropriately. You can also come up with a generalized approach that can replay the changes for any graph given any root entity. The generalized approach typically uses a base class or an interface that exposes information to allow your server-side code to work out what state each entity is in. You’ll see examples of both of these in the next section of this chapter.
Chapter 18 of Programming
Entity Framework, 2e, demonstrated this pattern by
providing a base class with a State
property. The
State
property could be set on the client side and
then read on the server side to determine how to register the class
with the context.
Now that we’ve taken a look at the N-Tier challenges and the high-level options for addressing them, it’s time to see how to implement those techniques using Entity Framework. We’ll start by taking a step back and looking at how to set the state for a single entity. Then we’ll take those techniques and use them to set the state for each entity in a graph.
Building an actual N-Tier application is beyond the scope of this book, so we’re going to keep working in the console application. We’ll implement methods that you could potentially expose from a web service for your client application to consume. Rather than serializing entities, you’ll use a temporary context to fetch any data from the database and then manipulate the data to mimic client-side changes. You’ll then pass these objects into the pseudo-server-side operations that will use a new context, with no previous knowledge of the entity instances, to persist the changes to the database.
Entity Framework has a list of states that the change tracker uses to record the status of each entity. In this section, you’ll see how to move an entity into each of these states. The states that Entity Framework uses when change tracking are the following:
The entity is being tracked by the context but does not exist
in the database. During SaveChanges
an INSERT
statement will be used to add this
entity to the database.
The entity already exists in the database and has not been
modified since it was retrieved from the database. SaveChanges
does not need to process the
entity.
The entity already exists in the database and has been
modified in memory. During SaveChanges
an UPDATE
statement will be sent to the
database to apply the changes. A list of the properties that have
been modified is also kept to determine which columns should be set
in the UPDATE
statement.
The entity already exists in the database and has been marked
for deletion. During SaveChanges
a DELETE
statement will be used
to remove the entity from the database.
The entity is not being tracked by the context.
Setting an entity to the Detached
state used to be important before the
Entity Framework supported POCO objects. Prior to POCO support, your
entities would have references to the context that was tracking them.
These references would cause issues when trying to attach an entity to a
second context. Setting an entity to the Detached
state clears out all the references
to the context and also clears the navigation properties of the
entity—so that it no longer references any entities being tracked by the
context. Now that you can use POCO objects that don’t contain references
to the context that is tracking them, there is rarely a need to move an
entity to the Detached
state. We will
not be covering the Detached
state in
the rest of this chapter.
Arguably the simplest operation is to take an entity and mark it
as Added
; in fact, you saw how to do
that in Chapter 3. You
can use the DbSet.Add
method to tell
Entity Framework that an entity is added. Add the AddDestination
and TestAddDestination
methods shown in Example 4-2.
private static void
TestAddDestination() {var
jacksonHole =new
Destination
{ Name ="Jackson Hole, Wyoming"
, Description ="Get your skis on."
}; AddDestination(jacksonHole); }private static void
AddDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Destinations.Add(destination); context.SaveChanges(); } }
The TestAddDestination
method
mimics a client application creating a new Destination
and passing it to the AddDestination
method on the server. The
AddDestination
method adds the new
Destination
to a context and then
saves it to the database. If you update the Main
method to call TestAddDestination
and run the application,
you will see that the Jackson Hole Destination
is added to the database.
There is also another way to write this same method. Earlier in
this chapter we saw that we could use DbContext.Entry
to get access to the change
tracking information for an entity. We used the State
property on the change tracking
information to read the state of an entity, but we can also set this
property, too. Update the AddDestination
method to use the State
property rather than DbSet.Add
(Example 4-3). You’ll need to
add a using for the System.Data
namespace to use the EntityState
enum.
private static void
AddDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Entry(destination).State =EntityState
.Added; context.SaveChanges(); } }
Calling DbSet.Add
and setting
the State
to Added
both achieve exactly the same thing. If
the entity is not tracked by the context, it will start being tracked by
the context in the Added
state. Both
DbSet.Add
and setting the State
to Added
are graph operations—meaning that any
other entities that are not being tracked by the context and are
reachable from the root entity will also be marked as Added. If the
entity is already tracked by the context, it will be moved to the
Added
state. So far we’ve only added
entities that aren’t tracked by the context, but a little later in this
chapter you’ll see that being able to set the state of an entity that is
already tracked to Added
is important.
Whether you choose DbSet.Add
or
setting the State
property is simply
a matter of which is more convenient in the code you are writing. For
this simple scenario, the code is arguably easier to understand if you
stick with DbSet.Add
. Later in the
chapter you’ll see that setting the State
property is cleaner in generalized
scenarios where your code calculates the state you are setting the
entity to.
While DbSet.Add
is used to tell
Entity Framework about new entities, DbSet.Attach
is used to tell Entity Framework
about existing entities. The Attach
method will mark an entity in the Unchanged
state. Add the AttachDestination
and TestAttachDestination
methods shown in Example 4-4.
private static void
TestAttachDestination() {Destination
canyon;using
(var
context =new
BreakAwayContext
()) { canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single(); } AttachDestination(canyon); }private static void
AttachDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Destinations.Attach(destination); context.SaveChanges(); } }
The TestAttachDestination
method fetches an existing Destination
from the database and passes it to
the AttachDestination
method, which
uses DbSet.Attach
to register the
existing Destination
with a context
and save the changes. You can also write this method by setting the
State property for the entity to Unchanged
(Example 4-5).
private static void
AttachDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Entry(destination).State =EntityState
.Unchanged; context.SaveChanges(); } }
If you update the Main
method to call TestAttachDestination
and run your
application, you’ll discover the AttachDestination
is quite pointless because
it doesn’t do anything. That’s because we told Entity Framework that the
Destination
was Unchanged
; therefore SaveChanges
just ignores the entity. While
this is a bit silly if it’s all we do in the method, it will be very
useful when we have a graph of entities, some of which may not need any
changes pushed to the database.
DbSet.Attach
and setting the
State
property to Unchanged
have the same effect. If the entity
isn’t tracked by the context, it will begin being tracked in the
Unchanged
state. If it is already
tracked, it will be moved to the Unchanged
state.
You’ve seen how to mark an existing entity as
Unchanged
; now let’s look at existing entities that
have some changes that need to be pushed to the database. There are a
few options that range from marking every property as modified to
telling Entity Framework what the original values were for each property
and letting it calculate the modifications. For the moment we’ll just
focus on getting the changes into the database by marking the whole
entity as modified. You’ll learn about setting individual properties as
modified in Tracking Individually Modified Properties.
When you tell Entity Framework that an entire entity is modified,
it will send an update statement to the database that sets every column
to the values currently stored in the properties of the entity. There
isn’t an AttachAsModified
method on
DbSet
, although there are plenty of
people asking for one, so don’t be surprised if it turns up in the
future. For the moment, you need to set the State
property to Modified
. Add the UpdateDestination
and TestUpdateDestination
methods shown in Example 4-6.
private static void
TestUpdateDestination() {Destination
canyon;using
(var
context =new
BreakAwayContext
()) { canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single(); } canyon.TravelWarnings ="Don't fall in!"
; UpdateDestination(canyon); }private static void
UpdateDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Entry(destination).State =EntityState
.Modified; context.SaveChanges(); } }
The TestUpdateDestination
simulates a client application that queries for the Grand Canyon
Destination
from the server, modifies
the TravelWarnings
property, and
passes the updated Destination
to the
UpdateDestination
method on the
server. The UpdateDestination
method
marks the incoming Destination
as
Modified
and saves the changes to the
database. If you update the Main
method to call TestModifyDestination
and run the application, an update statement is sent to the
database:
exec sp_executesql N' update [baga].[Locations] set [LocationName] = @0, [Country] = @1, [Description] = null, [Photo] = null, [TravelWarnings] = @2, [ClimateInfo] = null where ([LocationID] = @3) ',N'@0 nvarchar(200),@1 nvarchar(max) ,@2 nvarchar(max) ,@3 int', @0=N'Grand Canyon',@1=N'USA',@2=N'Don''t fall in!',@3=1
Notice that even though we only updated the TravelWarnings
property, Entity Framework is
updating every column. TravelWarnings
gets updated to the new value we set; all the other columns get
“updated” to the same values they had when we retrieved the Destination
from the database. This is because
Entity Framework doesn’t know which properties were updated. We just
specified that the entity was Modified
, so Entity Framework is updating all
the columns to match the current property values.
In the previous chapter you saw that DbSet.Remove
can be used to delete existing
entities. You also learned that calling Remove
on an entity in the Added
state will cancel the addition and cause
the context to stop tracking the entity. Calling Remove
on an entity that isn’t tracked by the
context will cause an InvalidOperationException
to be thrown. The
Entity Framework throws this exception because it isn’t clear whether
the entity you are trying to remove is an existing entity that should be
marked for deletion or a new entity that should just be ignored. For
this reason, we can’t use just Remove
to mark a disconnected entity as Deleted; we need to Attach
it first. Add the DeleteDestination
and TestDeleteDestination
methods shows in Example 4-7.
private static void
TestDeleteDestination() {Destination
canyon;using
(var
context =new
BreakAwayContext
()) { canyon = (from
din
context.Destinationswhere
d.Name =="Grand Canyon"
select
d).Single(); } DeleteDestination(canyon); }private static void
DeleteDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Destinations.Attach(destination); context.Destinations.Remove(destination); context.SaveChanges(); } }
The TestDeleteDestination
method simulates a client application fetching an existing Destination
from the server and then passing
it to the DeleteDestination
method on
the server. The DeleteDestination
method uses the Attach
method to let
the context know that it’s an existing Destination
.
Then the Remove
method is used to
register the existing Destination
for
deletion.
Having to attach the entity and then delete it is a little
confusing and it’s not immediately clear what the code is trying to
achieve when we look at it. Fortunately we can also set the State
property of the entity to Deleted
. Because Remove
is used in attached scenarios, its
behavior is different when used on added, unchanged, or disconnected
entities. However, changing the State
property is only used for explicitly setting state of an entity, so
Entity Framework assumes that setting the state to deleted means you
want an existing entity marked for deletion. We can rewrite the DeleteDestination
method to use this approach
and the intent of the code becomes a lot clearer (Example 4-8).
private static void
DeleteDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Entry(destination).State =EntityState
.Deleted; context.SaveChanges(); } }
Entity Framework only needs the key value(s) of an entity to be
able to construct a DELETE
statement for the entity. Therefore, you can reduce the amount of data
that gets sent between the server and client by only sending back the
key value of entities that need to be deleted. Add another overload of
DeleteDestination
that just accepts
the key of the Destination
to be
deleted (Example 4-9).
private static void
DeleteDestination(int
destinationId) {using
(var
context =new
BreakAwayContext
()) {var
destination =new
Destination
{ DestinationId = destinationId }; context.Entry(destination).State =EntityState
.Deleted; context.SaveChanges(); } }
The code constructs a new Destination
instance with only the key property set—that’s DestinationId
. This entity with only the key
value set is known as a stub entity. The code
then sets the State
property for
this new entity to Deleted
,
indicating that it is an existing entity to be marked for deletion.
Because Entity Framework will only access the DestinationId
property when it constructs
the DELETE
statement, it doesn’t
matter that the other properties are not populated.
If your entity contains any concurrency tokens, these
properties are also used to construct the DELETE
statement. You can still use the stub entity approach, but you will
need to set values for the concurrency token properties as
well.
If there is one thing you can do to make your life easier in N-Tier scenarios, it’s to expose foreign key properties for the relationships in your model. Relationships that include a foreign key property are called foreign key associations, and unless you have a very good reason not to expose the foreign key properties you will save yourself a lot of pain by including them.
More detailed information on including foreign keys in your Code First model is available in Chapter 4 of Programming Entity Framework: Code First. For Database First and Model First, see Chapter 19 of Programming Entity Framework, 2e.
The good news is that if you include foreign key properties in
your model you already know everything you need to know to work with
relationships. If you mark the entity as added, it will get inserted
with the value currently assigned to its foreign key property. If you
mark an entity as modified, the foreign key property will get updated
along with all the other properties. To see this in action, add the
UpdateLodging
and TestUpdateLodging
methods shown in Example 4-10.
private static void
TestUpdateLodging() {int
reefId;Lodging
davesDump;using
(var
context =new
BreakAwayContext
()) { reefId = (from
din
context.Destinationswhere
d.Name =="Great Barrier Reef"
select
d.DestinationId).Single(); davesDump = (from
lin
context.Lodgingswhere
l.Name =="Dave's Dump"
select
l).Single(); } davesDump.DestinationId = reefId; UpdateLodging(davesDump); }private static void
UpdateLodging(Lodging
lodging) {using
(var
context =new
BreakAwayContext
()) { context.Entry(lodging).State =EntityState
.Modified; context.SaveChanges(); } }
The TestUpdateLodging
method
simulates a client application that fetches a Lodging
and changes its DestinationId
property. Remember that
DestinationId
is the foreign key to
the Destination
that the Lodging
belongs to. This time it looks like
Dave’s reputation has become national, so he is moving his business
around the world from Hawaii, USA, to Queensland, Australia. Once the
changes are made, the Lodging
is
passed to the UpdateLodging
method
on the server. This method looks very much like the UpdateDestination
method you wrote back in
Example 4-6. As you
can see, there is nothing special required to deal with foreign key
relationships.
You aren’t restricted to using the foreign key property to change relationships. You can still use the navigation properties, and you’ll see this in action a little later in this chapter.
If you chose not to include a foreign key property, you are using independent associations. They are called independent associations because Entity Framework reasons about the relationship independently of the entities that the relationship belongs to, and this makes things difficult when it comes to disconnected entities. In fact, foreign keys are so vital in N-Tier scenarios that the Entity Framework team chose not to expose the methods for changing the state of independent relationships on the DbContext API. To work with them, you will need to drop down to the ObjectContext API.
Delving into the complexities of independent associations in N-Tier scenarios is well beyond the scope of this book. You can find a detailed look at this topic in Chapter 19 of Programming Entity Framework, 2e.
The problem you’ll encounter is that change tracking only tracks
scalar properties. When you change a foreign key property, such as
Lodging.DestinationId
, the context
is aware of that property. But when you change a navigation property,
there’s nothing to track. Even if you mark the Lodging
as Modified
, the context is only aware of the
scalar properties. When you use an independent association, the
context actually keeps track of the relationship itself. It has an
object that contains the keys of the two related instances and this is
what the context uses to track relationship modifications and update
them in the database and the state of the relationship. When your
entity is not connected to the context, the context is unable to do
the work of modifying these relationship objects. When you reconnect
the entities to a context, you need to manually dig down into the context, find
those relationship objects, and modify them. It’s pretty complex and
very confusing. This is one of the reasons developers convinced the
Entity Framework team that we really needed to have foreign key
properties available to us after struggling with independent
associations in Entity Framework 1.0.
So, given that you simply might find yourself in this same
situation, let’s take a look at a quick example to give you an idea of
the difference in resolving this problem in a disconnected scenario to
the simplicity of working with foreign key associations, and hopefully
convince you to stay away from them. Let’s assume for a moment that we
hadn’t included the DestinationId
property on Lodging
and used an
independent association instead. The code for UpdateLodging
would need to look something
like Example 4-11.
private static void
UpdateLodging(Lodging
lodging,Destination
previousDestination) {using
(var
context =new
BreakAwayContext
()) { context.Entry(lodging).State =EntityState
.Modified; context.Entry(lodging.Destination).State =EntityState
.Unchanged;if
(lodging.Destination.DestinationId != previousDestination.DestinationId) { context.Entry(previousDestination).State =EntityState
.Unchanged; ((IObjectContextAdapter
)context).ObjectContext .ObjectStateManager .ChangeRelationshipState( lodging, lodging.Destination, l => l.Destination,EntityState
.Added); ((IObjectContextAdapter
)context).ObjectContext .ObjectStateManager .ChangeRelationshipState( lodging, previousDestination, l => l.Destination,EntityState
.Deleted); } context.SaveChanges(); } }
The first thing you’ll notice is that we now require the
Destination
instance that the
Lodging
used to belong to. This is
because changing an independent association requires that the context
have an added relationship to the new entity and a deleted
relationship to the previous entity. This is a complicated side effect
of the way that Entity Framework handles concurrency checks when
updating independent associations. The code starts by marking the
lodging as Modified
, to take care
of updating any properties that aren’t involved in the relationship.
The current Destination
is also
marked as an existing entity. The code then checks to see if this call
is changing the Destination
this
Lodging
is assigned to, by
comparing the current Destination
and the previous Destination
. If
the Destination
does need to be
changed, the previous Destination
is also marked as an existing entity. The code then uses ObjectContext.ObjectStateManager.ChangeRelationshipState
to mark the relationship to the current Destination
as Added
and the previous Destination
as Deleted
. With all that taken care of, it’s
time to call SaveChanges
and push
the changes to the database.
Many-to-many relationships are always independent
associations. If you have many-to-many relationships in your model,
you will need to use ChangeRelationshipState
to mark references
as Added
, Unchanged
, or Deleted
when processing changes on the
server.
Now that you know the fundamental building blocks, it’s time to plug them together to determine and set the state of each entity in a graph. When a disconnected entity graph arrives on the server side, the server will not know the state of the entities. You need to provide a way for the state to be discovered so that the context can be made aware of each entity’s state. This section will demonstrate how you can coerce the context to infer and then apply entity state.
The first step is to get the graph into the context. You do that by performing an operation that will cause the context to start tracking the root of the graph. Once that is done, you can set the state for each entity in the graph.
Back in Example 4-1 you
saw that adding the root of a graph will cause every entity in the graph
to be registered with the context as a new entity. This behavior is the
same if you use DbSet.Add
or change
the State
property for an entity to
Added
. Once all the entities are
tracked by the state manager, you can then work your way around the
graph, specifying the correct state for each entity. It is possible to
start by calling an operation that will register the root as an existing
entity. This includes DbSet.Attach
or
setting the State
property to
Unchanged
, Modified
, or Deleted
. However, this approach isn’t
recommended because you run the risk of exceptions due to duplicate key
values if you have added entities in your graph. If you register the
root as an existing entity, every entity in the graph will get
registered as an existing entity. Because existing entities should all
have unique primary keys, Entity Framework will ensure that you don’t
register two existing entities of the same type with the same key. If
you have a new entity instance that will have its primary key value
generated by the database, you probably won’t bother assigning a value
to the primary key; you’ll just leave it set to the default value. This
means if your graph contains multiple new entities of the same type,
they will have the same key value. If you attach the root, Entity
Framework will attempt to mark every entity as Unchanged
, which will fail because you would
have two existing entities with the same key.
For example, assume you have an existing Destination
that includes two new instances of
the Lodging
class in its Lodgings
property. The key of Lodging
is the integer property LodgingId
, which is generated by the database
when you save. This means the two new Lodging
s both have zero assigned to their
LodgingId
property. If you attempt to
register the root as an existing entity, the two
Lodging
instances will also be registered as existing
entities. This will fail because that would mean there are two existing
Lodging
s with a key of zero.
There may be some cases where you have multiple graphs and/or
individual entities that need to be registered with the context. This
occurs when not all the entities you need to reason about are reachable
from one root. For example, we are going to be writing a method that
will save a Destination
and the
Lodgings
that it references. Each of
these entities will either be a new entity to be added or an existing
entity to be updated. The method will also accept a separate list of
Lodgings
that should be deleted.
Because these Lodgings
are to be
deleted, the client will probably have removed them from the Lodgings
collection on the root Destination
. Therefore registering the root
Destination
won’t be enough to
register the deleted Lodgings
; we’ll
need to register them separately.
Table 4-1 summarizes the options along with the pros and cons that you’ve read in this section.
Method | Result | Warnings |
Add Root | Every entity in graph will be change tracked and marked with Added state | SaveChanges will attempt to insert data that may already exist in database |
Attach Root | Every entity in graph will be change tracked and marked with Unchanged state | New entities will not get inserted into database and have a conflict with matching keys |
Add or Attach Root, then paint state throughout graph | Entities will have correct state when painting is complete | It is recommended that you Add the root rather than attaching it to avoid key conflicts for new entities. More information is provided at the start of this section. |
We’re going to start by looking at an example where we iterate
through the graph using our knowledge of the model and set the state for
each entity throughout the graph, or painting the
state. In the next section you’ll see how you can generalize
this solution so that you don’t have to manually navigate the graph set
the state of each entity. We’re going to write a method that will save a
Destination
and its related Lodgings
. Deleted entities are tricky in
disconnected scenarios. If you delete the entity on the client side,
there’s nothing to send to the server so that it knows to delete that
data in the database as well. This example demonstrates one pattern for
overcoming the problem. Add the SaveDestinationAndLodgings
method shown in
Example 4-12.
private static void
SaveDestinationAndLodgings(Destination
destination,List
<Lodging
> deletedLodgings) {// TODO: Ensure only Destinations & Lodgings are passed in
using
(var
context =new
BreakAwayContext
()) { context.Destinations.Add(destination);if
(destination.DestinationId > 0) { context.Entry(destination).State =EntityState
.Modified; }foreach
(var
lodgingin
destination.Lodgings) {if
(lodging.LodgingId > 0) { context.Entry(lodging).State =EntityState
.Modified; } }foreach
(var
lodgingin
deletedLodgings) { context.Entry(lodging).State =EntityState
.Deleted; } context.SaveChanges(); } }
The new method accepts the Destination
to be saved. This Destination
may also have Lodgings
related to it. The method also
accepts a list of Lodgings
to be
deleted. These Lodgings
may or may
not be in the Lodgings
collection of
the Destination
that is being saved.
You’ll also notice a TODO to ensure that the client calling the method
only supplied Destinations
and
Lodgings
, because that is all that
our method is expecting. If the caller were to reference an unexpected
InternetSpecial
from one of the
Lodgings
, we wouldn’t process this
with our state setting logic. Validating input is good practice and
isn’t related to the topic at hand, so we’ve left it out for
clarity.
The code then adds the root Destination
to the context, which will cause
any related Lodgings
to also be
added. Next we are using a check on the key property to determine if
this is a new or existing Destination
. If the key is set to zero, it’s
assumed it’s a new Destination
and
it’s left in the added state; if it has a value, it’s marked as a
modified entity to be updated in the database. The same process is then
repeated for each of the Lodgings
that is referenced from the Destination
.
Finally the Lodgings
that are
to be deleted are registered in the Deleted
state. If these Lodgings
are still referenced from the
Destination
, they are already in the
state manager in the added state. If they were not referenced by the
Destination
, the context isn’t yet
aware of them. Either way, changing the state to Deleted
will register them for deletion. With
the state appropriately set for every entity in the graph, it’s time to
call SaveChanges
.
To see the SaveDestinationAndLodgings
method in action,
add the TestSaveDestinationAndLodgings
method shown in
Example 4-13.
private static void
TestSaveDestinationAndLodgings() {Destination
canyon;using
(var
context =new
BreakAwayContext
()) { canyon = (from
din
context.Destinations.Include(d => d.Lodgings)where
d.Name =="Grand Canyon"
select
d).Single(); } canyon.TravelWarnings ="Carry enough water!"
; canyon.Lodgings.Add(new
Lodging
{ Name ="Big Canyon Lodge"
});var
firstLodging = canyon.Lodgings.ElementAt(0); firstLodging.Name ="New Name Holiday Park"
;var
secondLodging = canyon.Lodgings.ElementAt(1);var
deletedLodgings =new
List
<Lodging
>(); canyon.Lodgings.Remove(secondLodging); deletedLodgings.Add(secondLodging); SaveDestinationAndLodgings(canyon, deletedLodgings); }
This method retrieves the Grand Canyon Destination
from the database, using eager
loading to ensure that the related Lodgings
are also in memory. Next it changes
the TravelWarnings
property of the
canyon
. Then one of the Lodgings
is modified and another is removed
from the Lodgings
property of the
canyon
and added to a list of
Lodgings
to be deleted. A new
Lodging
is also added to the canyon
. Finally the canyon
and the list of Lodgings
to be deleted are passed to the
SaveDestinationAndLodgings
method. If
you update the Main
method to call
TestSaveDestinationAndLodgings
and
run the application, a series of SQL statements will be sent to the
database (Figure 4-3).
The first update
is for the
existing Grand Canyon Destination
that we updated the TravelWarnings
property on. Next is the update
for
the Lodging
we changed the name of.
Then comes the delete
for the
Lodging
we added to the list of
Lodging
s to be deleted. Finally, we
see the insert
for the new Lodging
we created and added to the Lodgings
collection of the Grand Canyon
Destination
.
The SaveDestination
method we
implemented in Example 4-12
isn’t overly complex, but if we expose methods to save various parts of
our model, we would be repeating the state setting code over and over
again in each method. So let’s take a look at a more generalized approach
to applying changes on the server.
You may recognize this pattern from Programming Entity Framework, 2e. It was introduced in Chapter 18 when demonstrating the user of POCOs in WCF Services.
This approach relies on having a consistent way to determine the
state of any entity in your model. The easiest way to achieve that is to
have an interface or abstract
base
class that every entity in your model will implement.
For this example we are going to use an IObjectWithState
interface that will tell us the
current state of the entity. It will have no dependencies on Entity
Framework and will be implemented by your domain classes, so it can go in
the Model
project. Go ahead and add the
IObjectWithState
interface to the
Model
project (Example 4-14). Later in this
section you’ll add this interface to some of your classes.
namespace
Model {public interface
IObjectWithState
{State
State {get
;set
; } }public enum
State
{ Added, Unchanged, Modified, Deleted } }
Note that we’ve opted for a new enum to represent state rather than
reusing the EntityState
enum from
Entity Framework. This ensures our domain model doesn’t have any
dependencies on types from our data access technology.
Before we get to applying state, it would be useful if any entities
we retrieve from the database would have their state automatically set to
Unchanged
. Otherwise the server needs
to manually do this before returning the objects to the client. The
easiest way to do this is to listen to the ObjectMaterialized
event on the underlying
ObjectContext
. Add the constructor in
Example 4-15 to the
BreakAwayContext
class. You’ll need to
add a using statement for the
System.Data.Entity.Infrastructure
namespace to get
access to the IObjectContextAdapter
interface.
public
BreakAwayContext() { ((IObjectContextAdapter
)this
).ObjectContext .ObjectMaterialized += (sender, args) => {var
entity = args.Entityas
IObjectWithState
;if
(entity !=null
) { entity.State =State
.Unchanged; } }; }
The code uses IObjectContextAdapter
to get access to the
underlying ObjectContext
. It then wires
up a new handler to the ObjectMaterialized
event, which will fire
whenever an entity is returned from a query to the database. Because all
objects that come from the database are existing objects, we take this
opportunity to mark them as Unchanged
if they implement our state tracking interface.
In a real-world scenario you would need to implement the change
tracking interface on every class in your model. But for the sake of
simplicity, we will just use Destination
and Lodging
for this demonstration. Go ahead and
edit the Lodging
and Destination
classes to implement the IObjectWithState
interface:
public class
Destination
:IObjectWithState
public class
Lodging
:IObjectWithState
You’ll also need to add a State
property into both of these classes to satisfy the IObjectWithState
interface that you just
added:
public
State
State {get
;set
; }
Now it’s time to write a method that uses all this information to take a disconnected graph and apply the client-side changes to a context by setting the correct state for each entity in the graph.
One important thing to remember is that this approach is dependent on the client application honoring the contract of setting the correct state. If the client doesn’t set the correct state, the save process will not behave correctly.
Add the SaveDestinationGraph
and
ConvertState
methods shown in Example 4-16.
public static void
SaveDestinationGraph(Destination
destination) {using
(var
context =new
BreakAwayContext
()) { context.Destinations.Add(destination);foreach
(var
entryin
context.ChangeTracker .Entries<IObjectWithState
>()) {IObjectWithState
stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State); } context.SaveChanges(); } }public static
EntityState
ConvertState(State
state) {switch
(state) {case
State
.Added:return
EntityState
.Added;case
State
.Modified:return
EntityState
.Modified;case
State
.Deleted:return
EntityState
.Deleted;default
:return
EntityState
.Unchanged; } }
The code uses DbSet.Add
on the
root Destination
to get the contents of
the graph into the context in the Added
state. Next it uses the ChangeTracker.Entries<TEntity>
method to
find the entries for all entities that are tracked by the context and
implement IObjectWithState
.
The Entries
method will give
you access to the same object that you would get by calling DbContext.Entry
on each entity. There is a
nongeneric overload of Entries
that
will give you an entry for every entity that is tracked by the context.
The generic overload, which we are using, will filter the entries to
those that are of the specified type, derived from the specified type,
or implement the specified interface. If you use the generic overload,
the Entity
property of each entry
object will be strongly typed as the type you specified. You’ll learn
more about the Entries
method in
Chapter 5.
For each entry, the code converts the state from the entities
State
property to Entity Framework’s
EntityState
and sets it to the State
property for the change tracker entry.
Once all the states have been set, it’s time to use SaveChanges
to push the changes to the database.
Now that we have our generalized solution, let’s write some code to test
it out. We’re going to apply the same changes we did back in Example 4-13, but this time using
our new method of applying changes. Add the TestSaveDestinationGraph
method shown in Example 4-17.
private static void
TestSaveDestinationGraph() {Destination
canyon;using
(var
context =new
BreakAwayContext
()) { canyon = (from
din
context.Destinations.Include(d => d.Lodgings)where
d.Name =="Grand Canyon"
select
d).Single(); } canyon.TravelWarnings ="Carry enough water!"
; canyon.State =State
.Modified;var
firstLodging = canyon.Lodgings.First(); firstLodging.Name ="New Name Holiday Park"
; firstLodging.State =State
.Modified;var
secondLodging = canyon.Lodgings.Last(); secondLodging.State =State
.Deleted; canyon.Lodgings.Add(new
Lodging
{ Name ="Big Canyon Lodge"
, State =State
.Added }); SaveDestinationGraph(canyon); }
The code simulates a client application that queries for an existing
Destination
and its related Lodgings
. The Destination
is updated and marked as Modified
. The first Lodging
is also updated and marked as Modified
. The second Lodging
is marked for deletion by setting its
State
property to Deleted
. Finally, a new Lodging
is put into the Lodgings
collection with its State
set to Added
. The graph is then passed to the SaveDestinationGraph
method. If you update the
Main
method to call TestSaveDestinationGraph
and run your
application, the same SQL statements from Figure 4-3 will be run against
the database.
With some simple tweaks to the SaveDestinationGraph
method we wrote in Example 4-16, we can create a
method that can work on any root in our model, not just Destinations
. Add the ApplyChanges
method shown in Example 4-18.
The generic method demonstrated in this section is specifically
designed for use with disconnected scenarios where you have a
short-lived context. Notice that the context is instantiated in the
ApplyChanges
method.
private static void
ApplyChanges<TEntity>(TEntity root)where
TEntity :class
,IObjectWithState
{using
(var
context =new
BreakAwayContext
()) { context.Set<TEntity>().Add(root);foreach
(var
entryin
context.ChangeTracker .Entries<IObjectWithState
>()) {IObjectWithState
stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State); } context.SaveChanges(); } }
This new method accepts any root that implements IObjectWithState
. Because we don’t know the
type of the root until runtime, we don’t know which DbSet
to add it to. Fortunately there is a
Set<T>
method on DbContext
that can be used to create a set of
a type that will be resolved at runtime. We use that to get a set and
then add the root. Next we set the state for each entity in the graph
and then push the changes to the database. If you want to test this new
method out, change the last line of your TestSaveDestinationGraph
method to call
ApplyChanges
rather than SaveDestinationGraph
:
ApplyChanges(canyon);
Running the application will result in the same SQL statements from Figure 4-3 being run again.
There is one potential issue with our ApplyChanges
method—at the moment it blindly
assumes that every entity in the graph implements IObjectWithState
. If an entity that doesn’t
implement the interface is present, it will just be left in the Added
state and Entity Framework will attempt
to insert it. Update the ApplyChanges
method as shown in Example 4-19.
private static void
ApplyChanges<TEntity>(TEntity root)where
TEntity :class
,IObjectWithState
{using
(var
context =new
BreakAwayContext
()) { context.Set<TEntity>().Add(root); CheckForEntitiesWithoutStateInterface(context);foreach
(var
entryin
context.ChangeTracker .Entries<IObjectWithState
>()) {IObjectWithState
stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State); } context.SaveChanges(); } }private static void
CheckForEntitiesWithoutStateInterface(BreakAwayContext
context) {var
entitiesWithoutState =from
ein
context.ChangeTracker.Entries()where
!(e.Entityis
IObjectWithState
)select
e;if
(entitiesWithoutState.Any()) {throw new
NotSupportedException
("All entities must implement IObjectWithState"
); } }
The method now calls a CheckForEntitiesWithoutStateInterface
helper
method that uses the nongeneric overload of ChangeTracker.Entries
to get all entities that
have been added to the context. It uses a LINQ query to find any of
these that don’t implement IObjectWithState
. If any entities that don’t
implement IObjectWithState
are found,
an exception is thrown.
This approach works well with timestamp-style concurrency tokens,
where the property that is used as a concurrency token will not be
updated on the client. For existing entities, the value of the timestamp
property will be sent to the client and then back to the server. The
entity will then be registered as Unchanged
,
Modified
, or Deleted
with the same
value in the concurrency token property as when it was originally
queried from the database.
If you need to have concurrency checking in your N-Tier application, timestamp properties are arguably the easiest way to implement this. More information on timestamp properties in Code First models is available in Chapter 3 of Programming Entity Framework: Code First. For Database First and Model First, see Chapter 23 of Programming Entity Framework, 2e.
If a property that can be updated on the client is used as a concurrency token, this approach will not suffice. Because this approach does not track the original value of properties, the concurrency token will only have the updated value for the concurrency check. This value will be checked against the database value during save, and a concurrency exception will be thrown because it will not match the value in the database. To overcome this limitation you will need to use the approach described in Recording Original Values.
So far, the methods you’ve seen have focused on changing the state of an entity. For a lot of applications, it’s enough to simply track the state at the entity level and update entire objects just as you would when relying on stored procedures. However, you may find yourself wanting to be more granular with the way modified properties are tracked. Rather than marking the entire entity as modified, you might want only the properties that have actually changed to be marked as modified. This is how change tracking works when properties of a tracked entity are modified. It ensures that only the properties that have actually been changed would be included in the update statement. In this section we’ll take a quick look at some common approaches to achieving granular property tracking when the entities are disconnected from the context:
Keeping track of modified properties on the client side and passing that list to the server along with the entities
Storing the original properties into the entities when they are retrieved from the database before passing them onto the client
Requerying the database when the entities have been returned to the server from the client
The samples provided in this section will provide you with a closer look at the Change Tracker API that is tied to the DbContext API. The Entity Framework team worked hard to make our lives easier when dealing with disconnected scenarios. You’ve already seen some of the benefits we developers can reap from their work, and you’ll see more as you read through to the end of this chapter.
This first approach is very similar to tracking state at the
entity level. In addition to marking an entity as modified, the client
is also responsible for recording which properties have been modified.
One way to do this would be to add a list of modified property names to
the state tracking interface. Update the IObjectWithState
interface to include a
ModifiedProperties
property, as shown
in Example 4-20.
using
System.Collections.Generic;namespace
Model {public interface
IObjectWithState
{State
State {get
;set
; }List
<string
> ModifiedProperties {get
;set
; } }public enum
State
{ Added, Unchanged, Modified, Deleted } }
You’ll also need to add this property to the Destination
and Lodging
classes to satisfy this new addition
to the IObjectWithState
interface:
public
List
<string
> ModifiedProperties {get
;set
; }
Now that we have a place to record the modified properties, let’s
update the ApplyChanges
method to
make use of it, as shown in Example 4-21.
private static void
ApplyChanges<TEntity>(TEntity root)where
TEntity :class
,IObjectWithState
{using
(var
context =new
BreakAwayContext
()) { context.Set<TEntity>().Add(root); CheckForEntitiesWithoutStateInterface(context);foreach
(var
entryin
context.ChangeTracker .Entries<IObjectWithState
>()) {IObjectWithState
stateInfo = entry.Entity;if
(stateInfo.State ==State
.Modified) { entry.State =EntityState
.Unchanged;foreach
(var
propertyin
stateInfo.ModifiedProperties) { entry.Property(property).IsModified =true
; } }else
{ entry.State = ConvertState(stateInfo.State); } } context.SaveChanges(); } }
The changes to the method are inside the foreach
loop where we apply the state. If we
find a modified entity in the graph, we mark it as Unchanged
, rather than Modified
. Once the entity is marked as
Unchanged
, we loop through the
modified properties and mark each of them as modified. We do this using
the Property
method on the entry to
get the change tracking information for the given property and then
setting the IsModified
property to
true
. As soon as we mark one of the
properties as modified, the state of the entity will also move to
Modified
. But only the properties we
marked as modified will be updated when we save.
There are two overloads of Property
, one that accepts the name of the
property as a string and the other that accepts a lambda expression
representing the property. We are using the string property because we
don’t know the names of the properties we want to access until
runtime. If you know the name of the property you want to access, you
can use the lambda version so that you get compile-time checking of
the supplied value (for example, context.Entry(destination).Property(d =>
d.Name).IsModified = true
).
You’ll also need to update the
TestSaveDestinationGraph
method to populate
ModifiedProperties
for the
Destination
and Lodging
that are
modified:
canyon.TravelWarnings ="Carry enough water!"
; canyon.State =State
.Modified; canyon.ModifiedProperties =new
List
<string
> {"TravelWarnings"
};var
firstLodging = canyon.Lodgings.First(); firstLodging.Name ="New Name Holiday Park"
; firstLodging.State =State
.Modified; firstLodging.ModifiedProperties =new
List
<string
> {"Name"
};
If you run the application again, you will see a set of SQL
statements similar to the ones in Figure 4-3. This time,
however, the update
statements only
set the properties we marked as modified. Here is the SQL from the
update statement for the Destination
:
exec sp_executesql N'update [baga].[Locations] set [TravelWarnings] = @0 where ([LocationID] = @1) ',N'@0 nvarchar(max) ,@1 int', @0=N'Carry enough water!',@1=1
This approach has the same implications for concurrency as the generic approach you saw in the previous section. Timestamp concurrency properties will work well, but concurrency properties that can be modified on the client will cause issues.
An alternative to asking the client to record the properties that
were modified is to keep track of the original values for existing
entities. One of the big advantages of this approach is that you are no
longer relying on the client to tell you which properties were modified.
This makes the code on the client side much simpler and less error
prone. Entity Framework can then check for changes between the original
and current values to determine if anything is modified. Let’s change
the IObjectWithState
interface to
record original values rather than modified properties. Because we are
going to calculate whether an entity is modified or not, we no longer
need the Modified
option in the
State
enum, so let’s remove that,
too. These changes are shown in Example 4-22.
using
System.Collections.Generic;namespace
Model {public interface
IObjectWithState
{State
State {get
;set
; }Dictionary
<string
,object
> OriginalValues {get
;set
; } }public enum
State
{ Added, Unchanged, Deleted } }
You’ll also need to remove the ModifiedProperties
property from Destination
and Lodging
and add in the new OriginalValues
property:
public
Dictionary
<string
,object
> OriginalValues {get
;set
; }
We want Entity Framework to automatically populate the OriginalValues
property when an entity is
retrieved from the database, so let’s update the event handler we added
to the constructor of our context (Example 4-23). You’ll need to add
a using statement for the System.Collections.Generic
namespace.
public
BreakAwayContext() { ((IObjectContextAdapter
)this
).ObjectContext .ObjectMaterialized += (sender, args) => {var
entity = args.Entityas
IObjectWithState
;if
(entity !=null
) { entity.State =State
.Unchanged; entity.OriginalValues = BuildOriginalValues(this
.Entry(entity).OriginalValues); } }; }private static
Dictionary
<string
,object
> BuildOriginalValues(DbPropertyValues
originalValues) {var
result =new
Dictionary
<string
,object
>();foreach
(var
propertyNamein
originalValues.PropertyNames) {var
value = originalValues[propertyName];if
(valueis
DbPropertyValues
) { result[propertyName] = BuildOriginalValues((DbPropertyValues
)value); }else
{ result[propertyName] = value; } }return
result; }
In addition to marking the entity as Unchanged
, this updated code will populate the
OriginalValues
property. It does this
by getting the original values from the change tracking entry for the
entity and using the BuildOriginalValue
helper method to convert
them to the required dictionary format. The helper method loops through
each of the properties that we have original values for. If the value is
just a normal scalar property, it copies the value of the property to
into the resulting dictionary. If the value is a DbPropertyValues
, this indicates that it is a
complex property and the code uses a recursive call to build a nested
dictionary of the values in the complex property. More information on
nested DbPropertyValues
for complex
properties is available in Working with Complex Properties.
Because the entity has just been returned from the database, the
current and original values are the same, so we could have also used the
CurrentValues
property to get the
values. Now we can update the ApplyChanges
method to make use of this
property (Example 4-24).
private static void
ApplyChanges<TEntity>(TEntity root)where
TEntity :class
,IObjectWithState
{using
(var
context =new
BreakAwayContext
()) { context.Set<TEntity>().Add(root); CheckForEntitiesWithoutStateInterface(context);foreach
(var
entryin
context.ChangeTracker .Entries<IObjectWithState
>()) {IObjectWithState
stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State);if
(stateInfo.State ==State
.Unchanged) { ApplyPropertyChanges(entry.OriginalValues, stateInfo.OriginalValues); } } context.SaveChanges(); } }private static void
ApplyPropertyChanges(DbPropertyValues
values,Dictionary
<string
,object
> originalValues) {foreach
(var
originalValuein
originalValues) {if
(originalValue.Valueis
Dictionary
<string
,object
>) { ApplyPropertyChanges( (DbPropertyValues
)values[originalValue.Key], (Dictionary
<string
,object
>)originalValue.Value); }else
{ values[originalValue.Key] = originalValue.Value; } } }
After painting the state of entities throughout the graph, the
code now checks to see if it’s an existing entity. For existing entities
the code uses the ApplyPropertyChanges
helper method to set the
original values for the entity. The helper method loops through the
OriginalValues
that were captured
when the entity was retrieved from the database. If the value is a
nested dictionary, indicating a complex property, then it uses a
recursive call to apply the changes for the individual properties inside
the complex property. If the value is just a scalar value, it updates
the original value being stored by the context. Entity Framework will
detect if any of the values differ from the values currently assigned to
the properties of the entity. If a difference is detected, the property,
and therefore the entity, will be marked as modified. We also need to
update the ConvertState
method
because we no longer have a Modified
option in the State
enum (Example 4-25).
public static
EntityState
ConvertState(State
state) {switch
(state) {case
State
.Added:return
EntityState
.Added;case
State
.Deleted:return
EntityState
.Deleted;default
:return
EntityState
.Unchanged; } }
To test out the new logic, you can update the TestSaveDestinationGraph
method to no longer
mark entities as Modified
when it
changes properties (Example 4-26). This is no
longer required because the ApplyChanges
method will calculate this for
you.
private static void
TestSaveDestinationGraph() {Destination
canyon;using
(var
context =new
BreakAwayContext
()) { canyon = (from
din
context.Destinations.Include(d => d.Lodgings)where
d.Name =="Grand Canyon"
select
d).Single(); } canyon.TravelWarnings ="Carry enough water!"
;var
firstLodging = canyon.Lodgings.First(); firstLodging.Name ="New Name Holiday Park"
;var
secondLodging = canyon.Lodgings.Last(); secondLodging.State =State
.Deleted; canyon.Lodgings.Add(new
Lodging
{ Name ="Big Canyon Lodge"
, State =State
.Added }); ApplyChanges(canyon); }
If you run the application, you will get the familiar set of SQL statements from Figure 4-3. The update statements that are generated will only set properties that were actually modified.
This approach offers the best concurrency support because it records the same information that is stored by the change tracker when modifying entities that are attached to a context. Timestamp concurrency properties will work because the value retrieved from the database is sent to the client and then back to the server, to be used when updating existing data. Concurrency properties that can be modified will also work because the original value, which was assigned to the property when it was retrieved from the database, is recorded. Because this value is set as the original value for the property, Entity Framework will use the original value when performing concurrency checks.
Another approach that developers sometimes try is to calculate the modified properties by querying the database to get the current entity and then copying the values from the incoming entity. Because this approach requires one query to get the entity from the database and often a second query to update the data, it’s usually slower than just marking the entity as modified and updating every column. That said, sending a lot of unnecessary updates to the database isn’t ideal. If one of the other techniques in this chapter doesn’t work for you, this may be worth looking at.
Entity Framework makes it easy to copy the values from one object
to another. Putting graphs aside for a moment, we could implement an
UpdateDestination
method as follows:
public static void
UpdateDestination(Destination
destination) {using
(var
context =new
BreakAwayContext
()) {if
(destination.DestinationId > 0) {var
existingDestiantion = context.Destinations .Find(destination.DestinationId); context.Entry(existingDestiantion) .CurrentValues .SetValues(destination); }else
{ context.Destinations.Add(destination); } context.SaveChanges(); } }
If the Destination
has a key
value assigned, it’s assumed to be an existing Destination
. The Find
method is used to load the Destination
from the database. The SetValues
method is used on CurrentValues
to copy values from the incoming
Destination
to the existing Destination
from the database. Entity
Framework will automatically detect if any of the property values are
different. If there are differences, the appropriate properties will be
marked as modified.
This approach falls down when you start working with graphs,
though. Let’s assume the incoming Destination
references a new Lodging
. We can’t query for this Lodging
from the database, since it’s new, so
we need to register this Lodging
for
addition. The problem is if we try and add the Lodging
to the context, it will also try and
add any other entities that it references. If the Lodging
references an existing entity, it’s
going to end up in the context in the added state. This gets very
complicated, because we now want to try and take this existing entity
back out of the context so that we can query for the entity from the
database and copy its values over.
While it is technically possible to make this work, the code gets
very complicated. Fortunately, there is a better alternative. Rather
than getting the existing entity from the database and copying the
values to it, we can attach the incoming entity and then set its
original values to the values from the database. Update the ApplyChanges
method as shown in Example 4-27.
private static void
ApplyChanges<TEntity>(TEntity root)where
TEntity :class
,IObjectWithState
{using
(var
context =new
BreakAwayContext
()) { context.Set<TEntity>().Add(root); CheckForEntitiesWithoutStateInterface(context);foreach
(var
entryin
context.ChangeTracker .Entries<IObjectWithState
>()) {IObjectWithState
stateInfo = entry.Entity; entry.State = ConvertState(stateInfo.State);if
(stateInfo.State ==State
.Unchanged) {var
databaseValues = entry.GetDatabaseValues(); entry.OriginalValues.SetValues(databaseValues); } } context.SaveChanges(); } }
Rather than getting the original values that were recorded when
the entity was retrieved from the database, the code now gets the
current original values from the database. In fact, the OriginalValues
property on IObjectWithState
is now no longer required.
The database values are retrieved using the GetDatabaseValues
method. This method returns
DbPropertyValues
, which is the same
type returned from the CurrentValues
and OriginalValues
property on an
entity. The SetValues
method on
DbPropertyValues
will copy the values
from any other DbPropertyValues
instance into itself. We use this functionality to copy the database
values into the original values for each entity. If you run the
application, SQL statements similar to those from Figure 4-3 will be executed
against the database. However, this time the update
statements will only set the properties
that were actually changed.
This approach bypasses concurrency checks because it requeries for the database values just before saving any changes. These new database values are used in place of the values that were originally retrieved from the database when sending data to the client. If you are using concurrency tokens, this approach to replaying changes on the server is not suitable.