Beginners often ask this question - "What course of action should I take when a call to SaveChanges() fails?" The answer may vary based on a given situation and requirement. However, in many cases the following course of actions can be performed:
- Trap the exception caused by the failed call to SaveChanges().
- Find out all the errors that occurred during the SaveChanges() call.
- Find out which entities caused the errors.
- Either display the errors and entities causing errors to the user or log that information somewhere.
- Based on whether an entity was added, modified or deleted rollback its effect.
Let's see how the above steps can be performed by developing a simple console application.
Begin by creating a new project of type Console Application. Then add Models folder to it and generate ADO.NET Entity Data Model for the Customers and Employees tables of the Northwind database. The following figure shows the Customer and Employee entities:
Now add the following code in the Main() method:
static void Main(string[] args)
{
NorthwindEntities db = new NorthwindEntities();
Customer obj1 = new Customer();
obj1.CustomerID = "This is a long CustomerID";
obj1.CompanyName = "Abcd";
obj1.ContactName = "Abcd";
obj1.Country = "USA";
db.Customers.Add(obj1);
Employee obj2 = db.Employees.Find(1);
obj2.FirstName = "This is a long first name value";
db.SaveChanges();
}
The above code attempts to add a new Customer to the Customers table and also attempts to modify an existing Employee with EmployeeID of 1. Notice that the code deliberately sets CustomerID property of the Customer object and FirstName property of the Employee object to some long value - something that won't fit in the column length.
When you can SaveChanges() obviously it is going to throw an exception. The following figure shows the exception thrown by an unsuccessful call to SaveChanges().
As shown above an unsuccessful call to SaveChanges() threw DbEntityValidationException exception. This exception class can be used to obtain more information about all the errors that occurred during the SaveChanges() call.
Now modify the Main() as shown below:
try
{
db.SaveChanges();
}
catch (DbEntityValidationException ex)
{
foreach (DbEntityValidationResult item in ex.EntityValidationErrors)
{
// Get entry
DbEntityEntry entry = item.Entry;
string entityTypeName = entry.Entity.GetType().Name;
// Display or log error messages
foreach (DbValidationError subItem in item.ValidationErrors)
{
string message = string.Format("Error '{0}' occurred in {1} at {2}",
subItem.ErrorMessage, entityTypeName, subItem.PropertyName);
Console.WriteLine(message);
}
}
}
Now the call to SaveChanges() is wrapped inside a try...catch block. The catch block traps DbEntityValidationException exception. The EntityValidationErrors collection of DbEntityValidationException class contains a list of validation errors. Each item of EntityValidationErrors collection is of type DbEntityValidationResult. Instead of showing a generic message the above code forms a descriptive error messages. This is done by finding which all entities caused errors. In each iteration of the outer foreach loop, the entry causing the error is accessed using the Entry property of DbEntityValidationResult class. The Entry property is of type DbEntityEntry. The Entity property of this DbEntityEntry gives you the entity that caused the error. The above code simply obtains a fully qualified name of the entity type (for example, ProjectName.Models.Customer and ProjectName.Models.Employee).
The inner foreach loop iterates through the ValidationErrors collection of DbEntityValidationResult under consideration. Each item of ValidationErrors collection is of type DbValidationError. The PropertyName and ErrorMessage properties of DbValidationError class give you the entity property that caused the error (such as CustomerID and FirstName) and the actual error message. These properties along with the entity type name are used to form a descriptive error message. In this example the error message is displayed on the console but you can log it in some text file if you so wish. The following figure shows how the error messages are displayed on the console.
So far we displayed the descriptive error messages to the end user. Now it's time to rollback the changes so that the entities causing error are either isolated from the model or the changes are discarded (in case of modified entities). Modify the code in the catch block as shown below:
...
...
foreach (DbValidationError subItem in item.ValidationErrors)
{
string message = string.Format("Error '{0}' occurred in {1} at {2}",
subItem.ErrorMessage, entityTypeName, subItem.PropertyName);
Console.WriteLine(message + "\n\n");
}
// Rollback changes
switch (entry.State)
{
case EntityState.Added:
entry.State = EntityState.Detached;
break;
case EntityState.Modified:
entry.CurrentValues.SetValues(entry.OriginalValues);
entry.State = EntityState.Unchanged;
break;
case EntityState.Deleted:
entry.State = EntityState.Unchanged;
break;
}
The above code checks the State property of the DbEntityEntry causing the error. The State property is of enumeration type - EntityState. If the current State is Added, it is changed to Detached so that the entry won't be considered a part of the DbSet for future calls. If the current State is Modified, the modified values (the values causing the error) are flushed out by replacing them with the OriginalValues. Notice how the original values are obtained using SetValues() method and OriginalValues property. If the State is Deleted, it is changed to Unchanged so that the entity is undeleted.
Once you display the errors and rollback any changes to the entities causing the errors, you may give another chance to the user to make the changes and then call SaveChanges() again.
That's it for now! Keep coding !!