VITA Tutorial

Part 4. Exceptions and entity validation

Outline

We discuss a basic classification of exceptions in VITA, using exception sub-classes for handling validation failures and soft errors in the database.

Exception classification VITA

Exception handling is important. VITA framework takes it seriously - it implements a robust sub-system for handling and throwing exceptions. At the core of the system is the following exception classification.
An application flow might be interrupted by a number of different events. In .NET application this interruption results in exception, which cancels the code flow and unwinds the call stack looking for a catch block to handle the exception. The exception causes might be quite different in nature, but at the most basic level they fall into one of two groups:
  1. Fatal application fault caused either by a bug, mis-configuration, or some communication failure (server crush). Whatever is the exact reason for the failure, one thing is common for all of them - they require the attention of the developer or system administrator. So the exception information should be logged, and proper support person should be notified - by email or other means.
  2. Non-fatal or soft errors caused by a user mistake. For example, a user enters an invalid value in some field in the input form and submits it. (Yes, sometimes this might be prevented by using immediate validation in the input control, but this is not always possible - we might detect the mistake when we are already deep in the call stack on the server - we talk about general case like this). What is different for this soft error is that application is functioning OK, no bug, no need to ring the alarm, log any errors or wake up programmers in the middle of the night. What we need to do is cancel the operation, unwind the stack, and deliver the error message to the end user, clearly explaining what is wrong and maybe how to fix it.
To distinguish these two situations and to make it possible to easily handle non-fatal errors, VITA defines a special kind of exception: NonFatalException. This exception and its sub-classes are used for situations when system is OK, but user did something 'wrong' and can do something to fix it.
I expect some readers here are ready to object: " Wait! Exceptions for user errors? exceptions are for "unexpected" things only, and user mistake is an expected thing!". See my response here.

Handling exceptions

The very basic schema of exception handling in the application code is the following:

  try {
    ...
    DoStuffWithEntities();
    ...
  } catch(NonFatalException nfx) {
    ShowMessage(nfx.Message); 
  } catch(Exception ex) {
    LogError(ex);
    NotifyProgrammer(ex);
    ShowMessage("Sorry, app failed.");
  }

This is a very generic schema, just to give you an idea. In the real code, you will be probably catching the sub-classes of the NonFatalException, and we are about to show how to do this.

ValidationException

ValidationException a sub-class of NonFatalException. It is thrown by the session.SaveChanges() method when the engine encounters errors in the entities submitted to the database. ValidationException.Errors property contains one or more errors (ValidationError objects), each describing a particular error, including entity type, key, error message, invalid value, etc. You can use this information, when in the UI code to properly display the error in the UI.
When the application code submits an entity to the database, there is a number of validation checks that the framework executes automatically. For example, when a property value is required (no Nullable attribute), the system checks that the value is in fact provided. For a string property with limited size it would check that the length of the value does not exceed the limit.
In addition to default automatic checks you can define a custom method to perform the validation:

  [Validate(typeof(BookExtensions), "ValidateBook")]  
  public interface IBook {
    ...
  }
  . . . 
   public static void ValidateBook(EntityValidator validator, IBook book) {
      validator.Check(book.Price > 0.01, book.Price, 
           "Book price must be greater than 1 cent.", null, "PriceNegative", "Price", book);
   }

Let's try to create and submit two entities with a few violations. We will catch the validation exception and print out the errors it contains:

      var invalidAuthor = session.NewAuthor(null, 
         "VeryLooooooooooooooooooongLaaaaaaaaaaaaastNaaaaaaaaaaaaaaame");
      var invalidBook = session.NewBook(BookEdition.EBook, BookCategory.Fiction, 
          "Not valid book", "Some invalid book", null, DateTime.Now, -5.0);
      try {
        session.SaveChanges();
      } catch (ValidationException vex) {
        foreach (var err in vex.Errors)
          Console.WriteLine(" Error: " + err.Message);   
      }

If we run this code, we get the following output:

tut_part4_valErrors.jpg

Three out of the four errors are added by the built-in validation; the price error is detected by the custom validation method we defined.

Unique index violation

Another sub-class of the NonFatalException is UniqueIndexViolationException. VITA treats violation of unique index in the database as non-fatal exception - something caused by user input, and therefore something that may be fixed by the user. We think that this is the most common case. There are cases when violation of a unique index is a result of a code bug, but in cases like these the developer can add some code that detects the situation and re-throws the exception as fatal.
Let's look at an example. We have a Unique attribute on property IPublisher.Name in our entity model; the effect of this attribute is a unique index in the database in the target table:

    // in IPublisher entity
    [Unique(KeyName = "PubName")]
    string Name { get; set; }
  }

The key name is optional, but it lets you easily identify which index is violated when you have more than one. Assuming the publisher already exists, the following code throws an exception that we catch and print the message:

      session = entityStore.OpenSession(); 
      var msPub2 = session.NewPublisher("MS Publishing");
      try {
        session.SaveChanges();
      } catch (UniqueIndexViolationException ex) {
        if (ex.KeyName == "PubName")
          Console.WriteLine("Duplicate publisher.");
      }

The default message that VITA provides for this exception is somewhat technical, so you probably need to replace it with customized message - a sentence that does not mention "index" but says something like "Duplicate object".
VITA provides quite valuable service here for application code. Index violations are detected in the database, and pop-up to the calling c# code as database exceptions (SqlClientException for MS SQL). They manifest themselves just like other fatal errors in database, caused by program bug, invalid SQL or mis-configuration. The task of separating of 'soft' errors (like unique index violation) from more serious errors is quite challenging - you have to look inside exception object and find some special clues programmatically. Good news is that VITA is doing it for you - it does all hard work, detects uniqeness violations and rethrows the error as UniqueIndexViolationException, with some extra helpful information like key name, making it much easier to handle the 'duplicates' in the application code.

Optimistic concurrency and ConcurrentUpdateException

VITA framework provides automatic support for what is known as 'optimistic concurrency'. This is the pattern of handling conflicting updates when you allow the updates to start (you don't block users from starting to edit the same record), but you detect the conflict when changes are submitted - the later update is rejected.
VITA supports automatic handling of RowVersion/Timestamp columns. If an entity has a property marked with the RowVersion attribute, it adds an extra code to the generated CRUD SQLs: the update throws an error if the submitted value of the row version column does not match the current value in the table row. This error is translated into a ConcurrentUpdateException that is thrown into a calling application code. This exception is a subclass of the NonFatalException - so the concurrency conflict is treated as 'soft' error. The application code should catch the exception and inform the user that somebody else updated the record, so he/she has to resubmit the changes.
See unit tests project for an example (unit test) of using the row-versioned entities and ConcurrentUpdateException.

Conclusion

The following image shows the output of the sample code execution for this section:

tut_part4_output.jpg


Tutorial Home

Last edited Mar 29, 2013 at 9:52 PM by rivantsov, version 45

Comments

No comments yet.