Practical .NET
Checking Up on Your Entity Framework Objects with DbEntityEntry
The Entity Framework DbEntityEntry object lets you do all sorts of things you probably didn't think were possible, including getting the latest data from the database (without losing your current data) and invoking the .NET Framework validation subsystem.
In an earlier column I showed how to use the DbCollectionEntry object to speed up loading the objects in navigation properties. However, on my way to the DbCollectionEntry object, I blew right past the DbEntityEntry object, which is also pretty cool.
You may have seen this object used implicitly in other discussions of Entity Framework (EF). For example, I've often seen examples of code where developers have triggered database updates by using the DbEntityEntry object's State property to arbitrarily change an object's status in Entity Framework. Typical code to trigger a delete for a Customer object that hasn't been retrieved from the database looks something like this:
db.Customers.Attach(New Customer With {.Id = "A123"})
db.Entry(cust).State = System.Data.Entity.EntityState.Deleted
However, that just scratches the surface of what you can do with the DbEntityEntry object (and, by the way, I think there's a better way to delete rows than using the State property). The DbEntityEntry will, for example, let you provide your users with a "refresh from the database" option on an object-by-object basis, will let you validate an object's data against its original values, and let you invoke the .NET Framework validation subsystem to tell you if there are any problems with your entity object.
Accessing the DbEntityEntry Class
To use the DbEntityEntry class, you first need to retrieve or create the object with which you want to work. This code retrieves the Customer object with its Id property set to A123:
Dim db As New CustomerOrdersContext
Dim custs = From c In db.Customers
Where c.Id = "A123"
Select c
This code creates a new Customer object:
Dim cust As Customer
cust = New Customer()
To get the DbEntityEntry object, you call the DbContext object's Entry method, passing the entity object you've retrieved or created. That's just what this code does:
Dim custEntity As DbEntityEntry(Of Customer)
custEntity = db.Entry(cust)
Getting the Database Values
Now that you have a DbEntityEntry object, you can use its Reload method to retrieve the current values for this object, back at the database. While you can do this for all of your currently retrieved objects by calling the DbContext object's Refresh method, the DbEntityEntry object lets you to do it on an object-by-object method. Unlike the Refresh method, the Reload method always overwrites the values in your "in-memory" version of the object with the data from the row in the database (but I have a fix for that later in this section).
This code is all you need to reload your object from the database (if you use this code with a newly created object you'll get an error):
custEntity.Reload()
Of course, making a call to the database can be time-consuming, so you might prefer to use the ReloadAsync method, which allows you to do other processing while waiting for the data to arrive. Code that uses ReloadAsync might look something like this:
Dim asyncValues As Task
asyncValues = custEntity.ReloadAsync()
'...other work...
'...wait for task to complete if it hasn't already
asyncValues.Wait()
'...work with reloaded values
If you want to get the current database values without losing the current values in your "in-memory" version, then you can use the GetDatabaseValues method. That method returns a DbPropertyValues object with a GetValue method that allows you to retrieve the individual value for any property just by passing in the property name. This code, for example, retrieves the database values for my Customer entity and then gets the value for the CreditStatus property, all without losing the values currently in the entity:
Dim values As DbPropertyValues
values = custEntity.CurrentValues
Dim creditStatus As CreditStatusEnum
creditStatus = values.GetValue(Of CreditStatusEnum)("CreditStatus");
The DbPropertyValues also has a collection of property names. You can use this to, among other things, scan objects for particular properties. This code checks to see if an entity class has a Log property set to True and uses that to trigger some processing:
if (vals.PropertyNames.Contains("Log"))
{
if (vals.GetValue<bool>("Log"))
{
'...write to log file...
}
}
Validating with Current and Original Values
Similarly, the CurrentValues property will let you retrieve the current values from the entity object by name. The OriginalValues property works the same way but lets you retrieve the values the entity had when it was first retrieved.
Those two properties let you perform validation checks that are based around changes from one value to another. Here's some code that compares a property's current value to its original values and won't let a Customer go directly from an Unacceptable status to an Excellent status:
If custEntity.OriginalValues.GetValue(Of CreditStatusEnum)("CustCreditStatus") == CreditStatusEnum.Unacceptable AndAlso
custEntity.CurrentValues.GetValue(Of CreditStatusEnum) ("CustCreditStatus") == CreditStatusEnum.Excellent )
{
'...error...
}
If you've gone to the trouble of decorating your entity class with validation properties like Required and StringLength, you might like to find out if the data in your entity class is valid -- especially if you've just created that object or updated it. The DbEntityEntry's GetValidationResult method will do that for you, returning a DbEntityValidationResult object. That DbEntityValidationResult has two useful properties: An IsValid property that will tell you if your object has problems and a ValidationResults property with a collection of error messages (specifically, a set of DbValidationError objects).
Using these objects, this code checks to see if a Customer object has the right values in its properties and then displays the error messages in the Debug window:
Dim validResult as DbEntityValidationResult
validResult = custEntity.GetValidationResult()
If Not validResult.IsValid Then
For Each ve As DbValidationError In validResult.ValidationErrors
Debug.WriteLine(ve.ErrorMessage)
Next
End If
I fully recognize that you might live a long and happy life without needing the DbEntityEntry object -- these are all niche cases. But for all those niche cases, DbEntityEntry will make your code (and your life) considerably simpler.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.