Wednesday, December 16, 2009

Making Complex Operations Easy

The Controllers namespace is the usual place you would look for performing save operations on our entities. These classes make it easy to keep all of our main business logic in one place. There are times, however, when the task at hand simply becomes too complicated to keep all of the logic in separate helper methods.

Introducing complex operations (specifically save operations).

By following this practice your operation will be much more readable, easier to debug and maintain because tasks will be isolated, and easier to test because you can call individual components separately if you like.

Step 1: Create a folder for your operation
This folder will house your save operation class as well as any other extensions that you decide to create. In our example we're creating a class named "AdvancedShippingNotificationSave" and we have a few extensions that we'll be creating. "LegacyTasks" is a class that isolates actions that need to be performed after an ASN has been saved. "PartValidation" runs validation before an ASN whenever the user is validating the ASN. And finally, "StatusChangeValidator" runs a few validation steps under certain circumstances.

Step 2: Inherit from SaveBase
Our "AdvancedShippingNotificationSave" class needs to inherit from SaveBase -- you'll be required to specify the type of the entity and the primary controller to use when you do this. In our case, AdvancedShippingNotification and ShipmentController. In the simplest case, all you need to do is call the base constructor with these values: the name of the service to fetch entities, the name of the service to insert entities, the name of the service to update entities, and finally a list of values that should be used when creating an entity from scratch.

Step 3: Override specific methods as needed
"NormalizeEntity": Takes a moment to ensure the entity is structurally sound. This is a good time to ensure entity properties are properly filled out.

"GetExistingVersion": Responsible for gathering the existing version of the entity in the database. This will work automatically if you specified the 'get' service name in the constructor.

"CompareToExistingVersion": Perform any adjustments on the given entity using the existing version of the entity as a point of reference.

"GetCoreWarnings": Checks the basic warnings that the user must acknowledge before this entity is allowed to be saved.

"GetCoreErrors": Gathers a list of basic errors that indicate that this entity is not valid for saving.

"OnBeforeSave": Fired as a last-chance moment to affect the save operation.

"SaveEntity": Performs the actual creation or editing of this entity. This will work automatically if you specified the aforementioned values of the base constructor.

"OnAutomaticSave": Called with the result of the save operation on this entity (but only in cases where you do not override the 'SaveEntity' method). In most cases, this method can be ignored.

"OnAfterSave": Fired after a successful save operation.

"OnError": Fired after a failed save operation. This is not a point of recovery, but rather just a moment to perform any logic necessary for rollback-type actions.

Step 4: Implement extensions as needed
You can create any number of classes that will act as extensions to your save operation. This is really powerful because of separation of concerns: each separate type of business rule that you must implement can be written in isolation so that it has minimal effect on other components.

Simply create new classes that implement either "IProTransSaveValidator" and/or "IProTransSaveEventListener." The first interface provides your class with a chance to perform 'warning' or 'error' validation on your operation. Example: ensure that the values of your entity is valid based on its status. The second interface provides your extension with a chance to perform 'pre' and 'post' actions in response to your complex operation. Example: send an email every time a shipment is updated.

The final step you must take is to register your extensions by overriding the "GetWarningValidators", "GetErrorValidators", and/or "GetEventListeners" methods of your main class.

Step 5: Update the controller method
The last step is to make sure that there is a controller method to perform your save operation and that it properly calls your class:
// Use the ASN complex save operation entity
AdvancedShippingNotificationSaveOperation operation = new AdvancedShippingNotificationSaveOperation();
string[] errors = operation.Save(advancedShippingNotification, true);
if (errors.Length > 0)
    throw new ProTransControllerException(errors);


As an example, here is the entire code (minus extension classes) necessary to perform a complex save of ASN data:
/// 
/// Sets up the base information of the service names for getting, inserting, and updating.
/// Also sets up the fields that should be used when new ASNs are created.
/// 
public AdvancedShippingNotificationSaveOperation() 
    : base("ASNHeaderNewGetEntityByASNId", "AddASNHeader", "ASNHeaderNewUpdateEntityByPrimaryKey",
        "int_ASNId", 0,
        "sdt_CreationDateTime", DateTime.Now,
        "sdt_DateEntered", DateTime.Now,
        "sdt_ShipDateTime", DateTime.Now,
        "vc_ShipmentWghtUnit", MeasurementUnit.Pounds,
        "vc_SCACCode", "PNII",
        "ti_HowReceived", 0,
        "ti_AsnStatus", 6,
        "int_Verified", 0,
        "sdt_ModifiedDate", DateTime.Now,
        "chr_PurposeCode", 00)
{
}

/// 
/// Take this time to ensure that a proper ASN ID is specified.
/// 
protected override void NormalizeEntity()
{
    // If missing, we still need to specify a value for the sproc to work.
    if (!Entity.AdvancedShippingNotificationID.HasValue)
        Entity.AdvancedShippingNotificationID = 0;
}

/// 
/// Take this time to apply audit information if available.
/// 
protected override void OnBeforeSave()
{
    if (CurrentUser != null)
    {
        Entity.ModifiedByDate = DateTime.Now;
        Entity.ModifiedByUsername = CurrentUser.UserName;
    }
}

/// 
/// Set up extension points for doing error validation. We could have just override 
/// GetCoreErrors, but doing it this way makes the code clean, easy to read, and separated
/// by responsibility.
/// 
protected override IProTransSaveValidator[] GetErrorValidators()
{
    return new IProTransSaveValidator[]{
        new StatusChangeValidator(),
        new PartValidation()
    };
}

/// 
/// Set up extension points for reacting in some way to the operation.
/// 
protected override IProTransSaveEventListener[] GetEventListeners()
{
    return new IProTransSaveEventListener[]{
        new LegacyTasks()
    };
}

No comments: