Wednesday, April 27, 2005

Error Eventing Pattern

C# suffers from a lack of error handling patters for systems with separated layers. Often domain objects contain the business logic needed to identify errors. When an error is identified it often needs to be bubbled to the layer sending messages to the domain objects. This can be done with exceptions; however, the first exception will break the execution and you can only return one error at a time. This can cause the users a great bit of pain, especially if you are developing a web app. Additionally, collection of data for a domain object can span multiple views; therefore, you need to be able to validate only the data you are concerned with and not data that has not yet been entered.

Using events as error notification can solve these issues.

How it works:
A domain object contains an inner class ErrorNotifier. ErrorNotifier contains all the events that correspond to errors concerning the domain object. When the validate method is called on the domain object the business rules are checked, the appropriate error events are fired if errors are found, and any object subscribing to the error events will be notified.

When to use it:
You should use Error Eventing when you are not using a remote domain layer. Additionally, it is helpful when you need to validate only specific rules in a domain object.

Example: Booking a hotel room
Create a Reservation domain object in the domain layer.

public class Reservation
{
public const string GUESTS_INVALID = "Guests Invalid";
public const string CHECK_IN_INVALID = "Check In Invalid";
public const string HOTEL_NAME_INVALID = "Hotel Name Invalid";
private ReservationErrorNotifier _errorNotifier = new ReservationErrorNotifier();
private int _guests;
private DateTime _checkIn;
private string _hotelName;

public ReservationErrorNotifier ErrorNotifier
{
get
{
return _errorNotifier;
}
}

public int Guests
{
get { return _guests; }
set { _guests = value; }
}

public DateTime CheckIn
{
get { return _checkIn; }
set { _checkIn = value; }
}

public string HotelName
{
get { return _hotelName; }
set { _hotelName = value; }
}

public void Validate()
{
_errorNotifier.Validate(this);
}

public class ReservationErrorNotifier
{
public event ErrorEventHandler GuestsInvalid;
public event ErrorEventHandler CheckInInvalid;
public event ErrorEventHandler HotelNameInvalid;

public void Validate(Reservation reservation)
{
if (reservation.Guests <>Page 1 of your search is only going to collect check in date and number of guests. Therefore, the service layer contains a Search command that only accepts and validates check in date and guests
public class SearchCommand : Command
{
private int _guests;
private DateTime _checkIn;

public SearchCommand(int guests, DateTime checkIn)
{
_guests = guests;
_checkIn = checkIn;
}

public void Execute()
{
Reservation reservation = new Reservation();
reservation.ErrorNotifier.GuestsInvalid += delegate { this.ErrorList.Add(Reservation.GUESTS_INVALID); };
reservation.ErrorNotifier.CheckInInvalid += delegate { this.ErrorList.Add(Reservation.CHECK_IN_INVALID); };
reservation.Guests = _guests;
reservation.CheckIn = _checkIn;
reservation.Validate();
this.Result = reservation;
}
}
Luckily, our reservation system is so simple the hotel will be selected on the next page and that will complete the reservation process. Therefore, we need to validate the existing reservation after adding the hotel to it. This is done with the Complete command located in the service layer.

public class CompleteCommand : Command
{
private Reservation _reservation;
private string _hotelName;

public CompleteCommand(Reservation existingReservation, string hotelName)
{
_reservation = existingReservation;
_hotelName = hotelName;
}

public void Execute()
{
Reservation reservation = new Reservation();
reservation.ErrorNotifier.GuestsInvalid += delegate { this.ErrorList.Add(Reservation.GUESTS_INVALID); };
reservation.ErrorNotifier.CheckInInvalid += delegate { this.ErrorList.Add(Reservation.CHECK_IN_INVALID); };
reservation.ErrorNotifier.HotelNameInvalid += delegate { this.ErrorList.Add(Reservation.HOTEL_NAME_INVALID); };
reservation.HotelName = _hotelName;
reservation.Validate();
}
}
Both SearchCommand and CompleteCommand inherit their Result and ErrorList properties from Command

public abstract class Command
{
private List _errorList = new List();
private object _result;

public List ErrorList
{
get
{
return _errorList;
}
}

public object Result
{
get { return _result; }
set { _result = value; }
}
}
Finally, the command objects can be used in the presentation layer to display the errors after execution.

Lastly, the unit tests used to drive this development.

[TestFixture]
public class ReservationTests
{
[Test]
public void NumberOfGuestsMustBeGreaterThanZero()
{
bool methodCalled = false;
Reservation reservation = new Reservation();
reservation.Guests = 0;
reservation.ErrorNotifier.GuestsInvalid += delegate { methodCalled = true; };
reservation.Validate();
Assert.IsTrue(methodCalled);
}

[Test]
public void ReservationCheckInDateFallAfterToday()
{
bool methodCalled = false;
Reservation reservation = new Reservation();
reservation.CheckIn = DateTime.MinValue;
reservation.ErrorNotifier.CheckInInvalid += delegate { methodCalled = true; };
reservation.Validate();
Assert.IsTrue(methodCalled);
}

[Test]
public void ReservationHotelCannotBeNull()
{
bool methodCalled = false;
Reservation reservation = new Reservation();
reservation.HotelName = null;
reservation.ErrorNotifier.HotelNameInvalid += delegate { methodCalled = true; };
reservation.Validate();
Assert.IsTrue(methodCalled);
}
}

[TestFixture]
public class SearchCommandTests
{
[Test]
public void InvalidGuestsAndCheckInErrorsAreAdded()
{
SearchCommand cmd = new SearchCommand(0, DateTime.MinValue);
cmd.Execute();
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.GUESTS_INVALID));
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.CHECK_IN_INVALID));
}
}

[TestFixture]
public class CompleteCommandTests
{
[Test]
public void InvalidGuestsAndCheckInErrorsAreAdded()
{
CompleteCommand cmd = new CompleteCommand(new Reservation(),null);
cmd.Execute();
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.GUESTS_INVALID));
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.CHECK_IN_INVALID));
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.HOTEL_NAME_INVALID));
}
}


Source available here (Visual Studio 2005 Beta 2 April release).
Post a Comment