.NET - ADO.NET Entity Framework & LINQ to Entities - deel 3
- Datum:
- Auteur: Stefan Cruysberghs
- Deze pagina is enkel in het Engels beschikbaar
In previous parts of this series of articles about the Entity Framework I already covered the following topics :
- The Entity Framework
- Installation
- Generate an Entity Data Model
- The Entity Data Model (EDM)
- Entity SQL
- ObjectQuery queries which return entity types
- ObjectQuery queries with parameters
- ObjectQuery queries which return primitive types
- ObjectQuery queries which return anonymous types
- EntityCommand queries which return entity types
- EntityCommand queries which return anonymous types
- EntityCommand queries with parameters
- LINQ to Entities
- View SQL statements
- Tools
In this article I will demonstrate how to modify data, use change tracking and handle concurrency conflicts.
Add, update and delete entities
Creating, modifying and deleting objects and applying these changes to the database is quite easy. Object Services will track all changes made to objects and generate and execute T-SQL statements that will perform insert, update and delete statements against the persisted data store.
Applying the changes to the database can be done by calling the SaveChanges() method of the ObjectContext. This is the equivalent of the SubmitChanges() method of the LINQ to SQL DataContext.
Update (modify) an entity
NorthwindEntities context = new NorthwindEntities();
Employee firstEmployee = context.Employees.First(e => e.EmployeeID == 1);
if (firstEmployee != null)
{
firstEmployee.City = "San Francisco";
firstEmployee.Notes = "New notes for employee 1";
int affected = context.SaveChanges();
}
1) Retrieve one employee. In LINQ to Entities you need to use the First() extension method because the SingleOrDefault() method will raise a NotSupportedException (The 'Single' operator is not supported by LINQ to Entities. Consider using 'First' instead.)
2) Change some properties.
3) Call the SaveChanges() method of the object context. This function will return the number of objects which are added, modified or deleted.
In this case the SaveChanges() will execute following T-SQL statement :
exec sp_executesql N'update [dbo].[Products]
set [CategoryID] = null
where (([ProductID] = @0) and ([CategoryID] = @1))
',N'@0 int,@1 int',@0=85,@1=12
Add (create) an entity
Employee newEmployee = new Employee();
newEmployee.FirstName = "Jan";
newEmployee.LastName = "Jansen";
context.AddToEmployees(newEmployee);
context.SaveChanges();
1) Create a new Employee object and set some properties.
When the partial entity classes are generated, each class is generated with a static create factory method. So you can also use this CreateEmployee() method to instantiate an Employee and all its required (not null) properties.
If you look closely you will notice that the EmployeeID is also a required field. Of course this is not really true because it is an AutoIncrement field which will be filled by the SQL Server database. In LINQ to SQL there is an Auto Generated option for each property member. The Entity Framework does not have this. Probably because it is designed to work with all kinds of databases which may not support an AutoIncrement datatype. Therefore just pass a dummy value.
Employee newEmployee = Employee.CreateEmployee(0, "Jansen", "Jan");
2) Call the AddToEmployees() method of the ObjectContext object. This differs from LINQ to SQL where you could use the InsertOnSubmit() method of the Employees collection. In the Entity Framework specialized AddTo methods will be generated for each EntityType.
You could also use the AddObject() method of the object context. The first parameter is the name of the entity set. Personally I prefer the specialized AddTo methods.
context.AddObject("Employees", newEmployee);
3) Call the SaveChanges() method of the object context.
Add (create) entity with associated entities
Category newCategory = new Category();
newCategory.CategoryName = "Software";
newCategory.Description = "Software products";
newCategory.Products.Add(new Product() { ProductName = "Microsoft Visual Studio 2008" });
newCategory.Products.Add(new Product() { ProductName = "Microsoft Office 2007" });
context.AddToCategories(newCategory);
context.SaveChanges();
1) Create a new Category object and set some properties.
2) When you create a new entity that is associated to another entity by a navigation property, you can call the Add() method of this collection. Create 2 new Product objects and add them to the Products collection of the Category.
3) Call the AddToCategories() method of the object context.
4) Call the SaveChanges() method of the object context. Three T-SQL statements will be executed :
exec sp_executesql N'insert [dbo].[Categories]([CategoryName], [Description], [Picture])
values (@0, @1, null)
select [CategoryID]
from [dbo].[Categories]
where @@ROWCOUNT > 0 and [CategoryID] = scope_identity()',N'@0 nvarchar(8),@1 nvarchar(17)',@0=N'Software',@1=N'Software products'
exec sp_executesql N'insert [dbo].[Products]([ProductName], [SupplierID], [CategoryID], [QuantityPerUnit], [UnitPrice],
[UnitsInStock], [UnitsOnOrder], [ReorderLevel], [Discontinued])
values (@0, null, @1, null, null, null, null, null, @2)
select [ProductID]
from [dbo].[Products]
where @@ROWCOUNT > 0 and [ProductID] = scope_identity()',N'@0 nvarchar(28),@1 int,@2 bit',@0=N'Microsoft Visual Studio 2008',@1=12,@2=0
exec sp_executesql N'insert [dbo].[Products]([ProductName], [SupplierID], [CategoryID], [QuantityPerUnit], [UnitPrice],
[UnitsInStock], [UnitsOnOrder], [ReorderLevel], [Discontinued])
values (@0, null, @1, null, null, null, null, null, @2)
select [ProductID]
from [dbo].[Products]
where @@ROWCOUNT > 0 and [ProductID] = scope_identity()',N'@0 nvarchar(21),@1 int,@2 bit',@0=N'Microsoft Office 2007',@1=12,@2=0
Delete an entity
Category cat = context.Categories.First(c => c.CategoryName == "Software");
context.DeleteObject(cat);
context.SaveChanges();
1) Retrieve one Category object
2) Call the DeleteObject() method of the object context and pass the Category object. It would be practical if there was an overloaded method which would only need an EntityKey.
3) Call the SaveChanges() method of the object context.
In a default Northwind database this example will throw an exception : "The DELETE statement conflicted with the REFERENCE constraint "FK_Products_Categories". The conflict occurred in database "Northwind", table "dbo.Products", column "CategoryID". The statement has been terminated."
If you want to delete the Software category and both its products, you need to change the Delete Rule of the FK_Products_Categories foreign key in your SQL Server database. Set it to Cascade.
Afterwards you have to update the Entity Data Model. In most cases updating this will fail! If this happens you will need to modify the XML of the EDMX file yourself. Just look of assocation in the conceptual schema (CSDL) and make sure the OnDelete Action is set to "Cascade".
<Association Name="FK_Products_Categories">
<End ...>
<OnDelete Action="Cascade" />
</End>
</Association>
Concurrency handling
Set ConcurrencyMode
The Entity Framework implements an optimistic concurrency model. By default changes will be committed to the database without checking for concurrency. For properties that might experience a high degree of concurrency, you need to set the Concurrency Mode property to Fixed.
These properties will be added in the WHERE clause of the executed T-SQL statement to compare the client values with the values in the database.
exec sp_executesql N'update [dbo].[Employees]
set [LastName] = @0, [City] = @1
where ((((([EmployeeID] = @2) and ([LastName] = @3)) and ([FirstName] = @4)) and ([Title] = @5)) and ([City] = @6))
',N'@0 nvarchar(5),@1 nvarchar(11),@2 int,@3 nvarchar(7),@4 nvarchar(5),@5 nvarchar(20),@6 nvarchar(7)',
@0=N'Smith',@1=N'Los Angeles',@2=1,@3=N'Davolio',@4=N'Nancy',@5=N'Sales Representative',@6=N'Seattle'
Resolve concurrency conflicts
The following example will demonstrate one way how to resolve concurrency conflicts. When a concurrency conflict occurs, an OptimisticConcurrencyException will be raised. You can solve this conflict by calling the Refresh() method of the object context with the RefreshMode.ClientWins (or StoreWins) option. This will re-execute the LINQ to Entities query and swap the original values with the values retrieved from the database. Afterwards you have to call the SaveChanges() method again.
// Create DataContext, retrieve employee 1 and change some data
NorthwindEntities context1 = new NorthwindEntities();
Employee firstEmployee1 = context1.Employees.First(e => e.EmployeeID == 1);
firstEmployee1.LastName = "Smith";
firstEmployee1.City = "Los Angeles";
// Create second DataContext, retrieve employee 1 and change some data
NorthwindEntities context2 = new NorthwindEntities();
Employee firstEmployee2 = context2.Employees.First(e => e.EmployeeID == 1);
firstEmployee2.LastName = "Paxton";
firstEmployee2.City = "New York";
// Save changes of first DataContext
context1.SaveChanges();
try
{
// Save changes of second DataContext
// This should raise an exception because the data in the database
// has been changed after retrieving it
int affected = context2.SaveChanges();
}
catch (OptimisticConcurrencyException)
{
// The concurrently conflict can be resolved by refreshing the DataContext
context2.Refresh(RefreshMode.ClientWins, firstEmployee2);
// And saving the changes again
context2.SaveChanges();
}
This preceding example did not work correctly on my PC with beta 3 of the Entity Framework and CPT 2 of the tools. Danny Simmons from Microsoft helped me tracing this bug. It seems that the ConcurrencyMode="Fixed" option which is correct in the EDMX file (and in the designer) is not propagated to the CSDL file. Deleting the CSDL file does not help. It will be recreated again but without the ConcurrencyMode option. So you have to modify this CSDL file manually ! Danny assured me that this bug will be solved in the next version.
Update 20/10/2008 : In Visual Studio 2008 SP1 this bug is solved.
Change tracking
ObjectStateManager
We have seen how we can add, modify and delete entities and apply these changes to the database. In the Entity Framework change tracking is done by an ObjectStateManager object which is controlled by the object context. The ObjectStateManager will track all changes made to objects and generate and execute T-SQL statements when the SaveChanges() method is being called.
The ObjectStateManager is more advanced than the change tracking features of the DataContext in LINQ to SQL. So now we will take a closer look on how to display some useful change tracking information.
The ObjectStateManager has 2 interesting methods :
- GetObjectStateEntries() : returns a collection of ObjectStateEntry objects for the given state
- GetObjectStateEntry() : returns an ObjectStateEntry object for the given entity
An ObjectStateEntry object contains some useful data which differs depending on the state (Added, Modified, Deleted) :
Added | Modified | Deleted | |
CurrentValues | X |
X |
|
Entity | X |
X |
X |
EntityKey | X |
X |
|
OriginalValues | X |
||
GetModifiedProperties() | X |
The next example will retrieve the first employee, modify two properties, call the GetObjectStateEntries(EntityState.Modified) method to get a list of all modified entities and finally run through the ObjectStateEntry collection to display entity name, key values and original and current values.
NorthwindEntities context = new NorthwindEntities();
Employee firstEmployee = context.Employees.First(e => e.EmployeeID == 1);
if (firstEmployee != null)
{
firstEmployee.City = "San Francisco";
firstEmployee.Notes = "New notes for employee 1";
}
var objectStateEntries =
context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
foreach (var entry in objectStateEntries)
{
Console.WriteLine("{0} - {1} - {2}",
entry.EntityKey.EntityContainerName,
entry.EntityKey.EntitySetName.ToString(),
entry.EntityKey.EntityKeyValues.First().Key + "=" + entry.EntityKey.EntityKeyValues.First().Value);
for (int i = 0; i < entry.OriginalValues.FieldCount; i++)
{
Console.WriteLine("\t {0} -> {1}", entry.OriginalValues[i], entry.CurrentValues[i]);
}
}
This will be the result :
NorthwindEntities - Employees - EmployeeID=1
1 -> 1
Davolio -> Davolio
Nancy -> Nancy
Sales Representative -> Sales Representative
Ms. -> Ms.
8/12/1948 0:00:00 -> 8/12/1948 0:00:00
1/05/1992 0:00:00 -> 1/05/1992 0:00:00
507 - 20th Ave. E. Apt. 2A -> 507 - 20th Ave. E. Apt. 2A
Seattle -> San Francisco
WA -> WA
98122 -> 98122
USA -> USA
(206) 555-9857 -> (206) 555-9857
5467 -> 5467
System.Byte[] -> System.Byte[]
New notes for employee 1 -> New notes for employee 1
http://accweb/emmployees/davolio.bmp -> http://accweb/emmployees/davolio.bmp
We have LINQ to Objects at our disposal so you can query the entries in the ObjectStateManager before displaying the tracking information :
var objectStateEntries = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(e => e.Entity is Product);
foreach (var entry in objectStateEntries)
{
Console.WriteLine("{0} - {1}", entry.EntityKey.EntityContainerName, entry.EntityKey.EntitySetName.ToString());
for (int i = 0; i < entry.CurrentValues.FieldCount; i++)
{
Console.WriteLine("\t {0}", entry.CurrentValues[i]);
}
}
Dump() & DumpAsHtml() extension methods
After putting all this together I created a Dump() extension method for the ObjectStateManager class. This method has several overloaded versions and can be used to display the change tracking info of all entries, of the given collection of entries and for a given entity.
Just copy this source and add it somewhere in your project :
// Stefan Cruysberghs, February 2008, http://www.scip.be
public static class EntityFrameworkExtensionMethods
{
/// <summary>
/// Convert an ObjectStateEntry object to a string representation
/// </summary>
/// <param name="entry">The given ObjectStateEntry</param>
/// <returns>The string representation</returns>
private static string ObjectStateEntryToString(ObjectStateEntry entry)
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat("\n- <b>{0} ", entry.State.ToString());
if (entry.EntityKey == null)
{
if (entry.EntitySet == null)
builder.Append("Entity : null </b>[null]");
else
builder.AppendFormat("EntitySet : {0}</b>", entry.EntitySet.Name);
}
else
{
builder.AppendFormat("Entity : {0} </b>", entry.EntityKey.EntitySetName);
if (entry.EntityKey.IsTemporary)
{
builder.Append("[Temporary]");
}
else
{
foreach (var key in entry.EntityKey.EntityKeyValues)
{
builder.AppendFormat("[{0} = {1}]", key.Key, ObjectToString(key.Value));
}
}
}
return (builder.ToString());
}
/// <summary>
/// Convert an object to a string representation
/// </summary>
/// <param name="obj">The given object</param>
/// <returns>The string representation</returns>
private static string ObjectToString(Object obj)
{
if (obj.GetType().Name == "String")
return String.Format("\"{0}\"", obj.ToString());
else if (obj.ToString() == "")
return "null";
else
return obj.ToString();
}
/// <summary>
/// Private extension method for ObjectStateManager class
/// Dump all tracking info to a string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <param name="objectStateEntries">Collection of ObjectStateEntries. If null, then all entities will be displayed</param>
/// <param name="entityKey">EntityKey of given entity. If null, then all entities will be displayed</param>
/// <param name="asHtml">Output string as HTML</param>
/// <returns>String with tracking info about entries</returns>
private static string Dump(
this ObjectStateManager manager,
IEnumerable<ObjectStateEntry> objectStateEntries,
EntityKey entityKey,
bool asHtml)
{
StringBuilder dump = new StringBuilder();
if (entityKey != null)
{
objectStateEntries = new List<ObjectStateEntry>();
(objectStateEntries as List<ObjectStateEntry>).Add(manager.GetObjectStateEntry(entityKey));
}
else if (objectStateEntries == null)
{
objectStateEntries =
manager.GetObjectStateEntries(EntityState.Added)
.Union(manager.GetObjectStateEntries(EntityState.Modified)
.Union(manager.GetObjectStateEntries(EntityState.Deleted)));
}
dump.AppendFormat("ObjectStateManager entries : # {0}\n", objectStateEntries.Count());
foreach (var entry in objectStateEntries)
{
dump.Append(ObjectStateEntryToString(entry));
if (entry.State == EntityState.Added)
{
for (int i = 0; i < entry.CurrentValues.FieldCount; i++)
{
dump.AppendFormat("\n\t- {0} = {1}",
entry.CurrentValues.GetName(i),
ObjectToString(entry.CurrentValues[i]));
}
}
else if (entry.State == EntityState.Modified)
{
foreach (string prop in entry.GetModifiedProperties())
{
dump.AppendFormat("\n\t- {0} : {1} -> {2}",
prop,
ObjectToString(entry.OriginalValues[prop]),
ObjectToString(entry.CurrentValues[prop]));
}
}
}
if (asHtml)
{
dump.Replace("\n", "<br />");
dump.Replace("\t", " ");
}
else
{
dump.Replace("<b>", "");
dump.Replace("</b>", "");
}
return dump.ToString();
}
/// <summary>
/// Extension method for ObjectStateManager class
/// Dump all tracking info about the entries in the ObjectStateManager to a string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <returns>String with tracking info about entries</returns>
public static string Dump(this ObjectStateManager manager)
{
return Dump(manager, null, null, false);
}
/// <summary>
/// Extension method for ObjectStateManager class
/// Dump all tracking info about the given ObjectStateEntries to a string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <param name="objectStateEntries">Collection of ObjectStateEntries. If null, then all entities will be displayed</param>
/// <returns>String with tracking info about entries</returns>
public static string Dump(this ObjectStateManager manager, IEnumerable<ObjectStateEntry> objectStateEntries)
{
return Dump(manager, objectStateEntries, null, false);
}
/// <summary>
/// Extension method for ObjectStateManager class
/// Dump all tracking info about the given Entity in the ObjectStateManager to a string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <param name="entityKey">Entity key of given entity. If null, then all entities will be displayed</param>
/// <returns>String with tracking info about entry</returns>
public static string Dump(this ObjectStateManager manager, EntityKey entityKey)
{
return Dump(manager, null, entityKey, false);
}
/// <summary>
/// Extension method for ObjectStateManager class
/// Dump all tracking info about the entries in the ObjectStateManager to a HTML string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <returns>HTML string with tracking info about entries</returns>
public static string DumpAsHtml(this ObjectStateManager manager)
{
return Dump(manager, null, null, true);
}
/// <summary>
/// Extension method for ObjectStateManager class
/// Dump all info about the given ObjectStateEntries to a HTML string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <param name="objectStateEntries">Collection of ObjectStateEntries</param>
/// <returns>HTML string with tracking info about entries</returns>
public static string DumpAsHtml(this ObjectStateManager manager, IEnumerable<ObjectStateEntry> objectStateEntries)
{
return Dump(manager, objectStateEntries, null, true);
}
/// <summary>
/// Extension method for ObjectStateManager class
/// Dump all tracking info about the given Entity in the ObjectStateManager to a HTML string
/// </summary>
/// <param name="manager">ObjectStateManager</param>
/// <param name="entityKey">Entity key of given entity</param>
/// <returns>HTML string with tracking info about entry</returns>
public static string DumpAsHtml(this ObjectStateManager manager, EntityKey entityKey)
{
return Dump(manager, null, entityKey, true);
}
}
This example will show you how to utilize the Dump() method. We will modify one entity, delete one and add 3 new ones :
NorthwindEntities context = new NorthwindEntities();
// Modify Employee
Employee firstEmployee = context.Employees.First(e => e.EmployeeID == 1);
if (firstEmployee != null)
{
firstEmployee.City = "San Francisco";
firstEmployee.Notes = "New notes for employee 1";
}
// Delete Employee
context.DeleteObject(context.Employees.First(e => e.EmployeeID == 2));
// Add Employee
context.AddToEmployees(new Employee() { EmployeeID = 1000, FirstName = "Jan", LastName = "Jansen" });
// Add Employee
context.AddToEmployees(new Employee() { EmployeeID = 1001, FirstName = "Piet", LastName = "Pieters" });
// Add Product
context.AddToProducts(new Product() { ProductID = 1000, ProductName = "Visual Studio 2008" });
// Dump all tracking info
Console.WriteLine(context.ObjectStateManager.Dump());
Console.WriteLine("------------------------------------------");
// Dump tracking info of first employee
Console.WriteLine(context.ObjectStateManager.Dump(firstEmployee.EntityKey));
Console.WriteLine("------------------------------------------");
// Dump tracking info of all added products
IEnumerable<ObjectStateEntry> objectStateEntries =
context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(e => e.Entity is Product);
Console.WriteLine(context.ObjectStateManager.Dump(objectStateEntries));
The result will be a detailed list with all change tracking information :
----------------------------------------------------------------------------------------
ObjectStateManager entries : # 6
- Added Entity : Employees [Temporary]
- EmployeeID = 1000
- LastName = "Jansen"
- FirstName = "Jan"
- Title = null
- TitleOfCourtesy = null
- BirthDate = null
- HireDate = null
- Address = null
- City = null
- Region = null
- PostalCode = null
- Country = null
- HomePhone = null
- Extension = null
- Photo = null
- Notes = null
- PhotoPath = null
- Added Entity : Employees [Temporary]
- EmployeeID = 1001
- LastName = "Pieters"
- FirstName = "Piet"
- Title = null
- TitleOfCourtesy = null
- BirthDate = null
- HireDate = null
- Address = null
- City = null
- Region = null
- PostalCode = null
- Country = null
- HomePhone = null
- Extension = null
- Photo = null
- Notes = null
- PhotoPath = null
- Added Entity : Products [Temporary]
- ProductID = 1000
- ProductName = "Visual Studio 2008"
- QuantityPerUnit = null
- UnitPrice = null
- UnitsInStock = null
- UnitsOnOrder = null
- ReorderLevel = null
- Discontinued = False
- Modified Entity : Employees [EmployeeID = 1]
- City : "New York" -> "San Francisco"
- Notes : "Education includes a BA in psychology from Colorado State University in 1970" -> "New notes for employee 1"
- Deleted EntitySet : FK_Employees_Employees
- Deleted Entity : Employees [EmployeeID = 2]
----------------------------------------------------------------------------------------
ObjectStateManager entries : # 1
- Modified Entity : Employees [EmployeeID = 1]
- City : "New York" -> "San Francisco"
- Notes : "Education includes a BA in psychology from Colorado State University in 1970" -> "New notes for employee 1"
----------------------------------------------------------------------------------------
ObjectStateManager entries : # 1
- Added Entity : Products [Temporary]
- ProductID = 1000
- ProductName = "Visual Studio 2008"
- QuantityPerUnit = null
- UnitPrice = null
- UnitsInStock = null
- UnitsOnOrder = null
- ReorderLevel = null
- Discontinued = False
----------------------------------------------------------------------------------------
I have also implemented a DumpAsHtml() extension method which can be practical when you want to display the result in the HTML Visualizer while debugging :
NoTracking option
If you want to retrieve data which will not be modified, then you can disable change tracking by using the MergeOption.NoTracking. The ObjectStateManager will not be used and this will reduce the amount of time needed to execute the query. All retrieved entities will be in detached state. NoTracking is a good option in an ASP.NET web application or when displaying data in read-only WinForms or WPF grids/treelists/...
When using Object Services and Entity SQL, you have to call one of the overloaded constructors of the ObjectQuery. The third parameter is a MergeOption enumeration. The default behavior is AppendOnly. You have to change this to NoTracking.
NorthwindEntities context = new NorthwindEntities();
var sql = "SELECT VALUE emp FROM NorthwindEntities.Employees AS emp";
var query = new ObjectQuery<Employee>(sql, context, MergeOption.NoTracking);
foreach (var emp in query)
Console.WriteLine("{0} {1} {2} {3}", emp.EmployeeID, emp.FirstName, emp.LastName, emp.Country);
When using LINQ to Entities or the CreateQuery<T>() factory method with Entity SQL, you can not pass this option to a specific query. In these cases you have to set the MergeOption for the whole EntitySet.
NorthwindEntities context = new NorthwindEntities();
context.Employees.MergeOption = MergeOption.NoTracking;
var query = context.Employees;
foreach (var emp in query)
Console.WriteLine("{0} {1} {2} {3}", emp.EmployeeID, emp.FirstName, emp.LastName, emp.Country);
I hope that this walkthrough provides a good overview on how to add, modify and delete data, how to resolve concurrency conflicts and how to take a look at the change tracking information or disable it. The Dump() extension methods can be useful for everyone who is using the Entity Framework and of course new suggestions or comments are welcome.