Showing posts with label Controller. Show all posts
Showing posts with label Controller. Show all posts

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()
    };
}

Wednesday, November 26, 2008

Controller Simple Save Entity (Signature)

I want to discuss the thought behind the signature of the Save methods in the Controllers. In later posts, we'll discuss some of the inner-workings of a Save, but for now we will focus on how you should expect to interact with it in your Unit Tests, and in the Presentation Layer.
First, you will notice that there are no Insert and Update methods, just a Save. This was done to simplify the experience of using this framework, as well as move the concern of whether or not this entity previously existed into the Controller Layer and not in the Presentation Layer, which is appropriate since the Presentation should not have business rules in it.
Second...you might be asking yourself how errors are are handled within Save methods. We decided to go with a custom ProTransControllerException. *(An understanding of .Net Exception handling is needed to really grasp what we are doing here, but that is a different discussion...also, we weighed the pros and cons of using a relatively "expensive" technique like throwing Exceptions for validation errors, but for ProTrans the cost was worth the simplicity and standardization that comes with simply throwing the Exception)* The TeachingExample_ControllerSave_Email Unit Test, which exists in the EnterpriseControllerTest in the ControllersTest_CSharp project, has a straight-forward example of how you catch the custom exception, and handle the messages *(I have pasted a sample of the code from this Unit Test below...and again, we are discussing the signature and how to interact with the save methods right now, we will discuss what to do with these messages in your web application at a different time)*.
This save signature is what you will see 80% - 90% of the time, the only other major technique for saving, is advanced saving when you have validation warnings that may or may not be skipped. For the sake of keeping these posts as short and to the point as possible, this technique will be discussed in a seperate post.
[TestMethod]
public void TeachingExample_ControllerSave_Email()
{
    Email emailEntity = new Email();
    try
    {
        //This should fail validation, because the required fields were never set.
        ControllerEnterprise.EmailSave(emailEntity);
    }
    catch (ProTransControllerException ptException)
    {
        //These are the messages that should have been thrown.
        string[] expectedMessages = new string[] {
            "Recipients is required, but was not supplied."
            , "Subject is required, but was not supplied."
            , "Message is required, but was not supplied."};

        //The exception should have the same number of messages
        Assert.AreEqual(expectedMessages.Length, ptException.Messages.Length);

        //Every message should be found in the Messages property of the exception.
        foreach (string curString in expectedMessages)
        {
            Assert.IsTrue(ptException.Messages.Contains(curString));
        }
    }
    catch (Exception ex)
    {
        Assert.Fail(ex.Message);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31