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/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube