Exception Shielding #5: Exception Shielding Implementation

Now that we're getting close to the end of this series about Exception Shielding, it's time to explain the final version of the sample source code.

Attached to this post you'll find the complete sample C# solution. Note that, to run this solution, you will need the following pieces:

  • Microsoft Enterprise Library 2.0 January 2006 CTP (Required)
  • Web Service Software Factory December 2006 CTP (Optional)

Now let's take a look at the parts of this solution that actually implement Exception Shielding.

CustomersService Internal Implementation

There are a number of components, running in the application server, that perform the CustomersService.Create operation. Understanding the role of each of these components is relevant to understand how I implemented the exception shielding mechanism.

CustomersService Implementation

The service layer is responsible for servicing the request, handling exceptions (as we'll see) and pass the request on to the business logic manager. This layer is divided into two components: CustomersService implements the service contract and creates a new CustomersServiceAdapter to service each request. The adapter also implements the service contract but is responsible for actually "knowing" which business component can process it. It enforces, if you will, the independence between the service contract and the service implementation.

The business logic layer (CreateCustomer) is responsible for validating the customer data and passing it on to the data layer for persistence.

Finally, the data access layer (CustomersRepository) knows how to store a customer in the database. It uses the Enterprise Library Data Access Application Block to perform "communication" with the service's database.

Handling Exceptions in the Data Layer

In the context of our exception shielding sample, the data layer is responsible for catching exceptions raised by the database and passing them on to the upper layer in the stack (the business logic layer).

Since we're only interested in processing the "Customer already exists" exception, when it founds the SQL Server error code 2627 it throws a CustomersRepositoryException. All other SQL exceptions are re-thrown as CustomersRepositoryFailureException.

Here's the code do to exactly that:

/// <summary>
/// Stores a new customer.
/// </summary>
/// <param name="customer">Customer data.</param>
public void Add(Customer customer)
{
CustomerInsertFactory insertFactory =
new CustomerInsertFactory();
try
{
base.Add(insertFactory, customer);
}
catch (SqlException ex)
{
HandleSqlException(ex);
}
}
/// <summary>
/// Generic exceptions handler.
/// </summary>
/// <param name="ex">Last exception raised.</param>
private static void HandleSqlException(SqlException ex)
{
switch (ex.Number)
{
case ErrorCodes.DuplicateKey:
throw new CustomerRepositoryException(
Properties.Resources.RES_CustomerAlreadyExists, ex);
default:
throw new CustomerRepositoryFailureException(ex);
}
}

Handling Exceptions in the Business Logic Layer

The business layer component (CreateCustomer), validates the data received (the Customer business entity) and raises a CreateCustomerException if any of the required fields (CustomerId and Name) is missing. Any CustomerRepositoryException is also directly "translated" to CreateCustomerException. Any other exception is ignored.

/// <summary>
/// Creates a new customer.
/// </summary>
/// <param name="customer">Customer to store.</param>
[SuppressMessage("Microsoft.Performance", "CA1822")]
public void CreateCustomerAction(Customer customer)
{
try
{
// Validate customer

ValidateCustomer(customer);

// Save in the database

CustomerRepository repository =
new CustomerRepository("CustomersServiceDB");
repository.Add(customer);
}
catch (CustomerRepositoryException ex)
{
throw new CreateCustomerException(-1000, ex.Message, ex);
}
}
/// <summary>
/// Validates the customer data.
/// </summary>
/// <param name="customer">Customer data.</param>
private static void ValidateCustomer(Customer customer)
{
StringBuilder errors = new StringBuilder();
if ((customer.CustomerId == null) ||
(customer.CustomerId.Length == 0))
{
errors.AppendLine(Properties.Resources.RES_CustomerIdIsRequired);
}
if ((customer.Name == null) ||
(customer.Name.Length == 0))
{
errors.AppendLine(Properties.Resources.RES_NameIsRequired);
}
if (errors.Length > 0)
{
errors.Insert(0,
Properties.Resources.RES_MissingData + Environment.NewLine);
throw new CreateCustomerException(-2000, errors.ToString());
}
}

Handling Exceptions in the Service Layer

The CustomersServiceAdapter component is the core of the service implementation and the core of our Exception Shielding design.

This component receives the request from the service client, translates the data contract into a Customer business entity and passes it to the CreateCustomer business logic component (CreateCustomerAction).

/// <summary>
/// Create a new customer.
/// </summary>
/// <param name="request">Customer data.</param>
[SuppressMessage("Microsoft.Design", "CA1031")]
public void Create(Customer request)
{
try
{
// Translate data contract to a business entity

Translator translator = new Translator();
BusinessEntities.Customer customer =
translator.TranslateDataContract(request);

// Execute operation

CreateCustomer manager = new CreateCustomer();
manager.CreateCustomerAction(customer);
}
catch (CreateCustomerException ex)
{
HandleException(ex, ex.ErrorCode, ex.Message);
}
catch (Exception ex)
{
HandleException(ex, -3000,
Properties.Resources.RES_GenericErrorMessage);
}
}
private static void HandleException(Exception ex,
int errorId, string errorMessage)
{
// Log exception

ExceptionsHandler.LogException(ex);

// Raise fault

CustomersServiceFault fault = new CustomersServiceFault();
fault.ErrorId = errorId;
fault.ErrorMessage = errorMessage;
throw new FaultException<CustomersServiceFault>
(fault, new FaultReason(new FaultReasonText(
fault.ErrorMessage, CultureInfo.CurrentCulture)));
}

Exception Shielding Implementation Summary

As you can see in the previous code piece the actual exception shielding is performed by the HandleException method.

This method raises a fault exception (the way WCF provides to return faults to the service client) containing the error id and the error message (notice that the fault reason is equal to the error message).

All exceptions of type CreateCustomerException (see Create) are "directly translated" to fault exceptions with the same error message. These are the two errors that can be raised by the lower layers (business and data):

  • If the customer data is missing one of the required attributes.
  • When the customer already exists.

All other exceptions are translated into a generic error message (RES_GenericErrorMessage).

Thus, exception shielding occurs at two levels:

  • First, only a subset of the possible exceptions are returned to the client.
  • Second, in any case (exception), no implementation details (like the stack trace for example) are disclosed to the caller.

This may seem a very simple code to implement the design pattern. In fact it is but that's the beauty of it. :)

Next time: Exception Logging Implementation

Published 03 May 07 10:37 by hgr
Filed under: , , ,

Attachment(s): CustomersService.zip

Comments

No Comments
Anonymous comments are disabled