Passing data from one controller
What to do when SaveChanges() fails?
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 !!