To start with, then, let’s add the basics.
Database Modifications
There isn’t much to change in the database at this point, but you do need to add a new table, Audit, and associated stored procedures.
The Audit Table
During order processing, one of the most important functions of the pipeline is to maintain an up-to-date audit trail. The implementation of this involves adding records to a new database table called Audit. You need to create this table with fields as shown in Table 14-1.
Table 14-1. The Audit Table
Column Name Column Type Description
AuditID int Primary key, also set as table identity
OrderID int The ID of the order that the audit entry applies to DateStamp datetime The date and time that the audit entry was created,
default value GETDATE() Message varchar(512) The text of the audit entry
MessageNumber int An identifying number for the audit entry type
C H A P T E R 1 4 ■ O R D E R P I P E L I N E 523
Entries will be added by OrderProcessor and by individual pipeline stages to indicate successes and failures. These can then be examined to see what has happened to an order, an important function when it comes to error checking.
The MessageNumber column is interesting because it allows you to associate specific messages with an identifying number. It would be possible to have another database table allowing you to match these message numbers with descriptions, although this isn’t really necessary, because the scheme used for numbering (as you’ll see later in the chapter) is descriptive. In addition, the Message column already provides human-readable information.
The CreateAudit Stored Procedure
Now that you have the new Audit table, you need a way to add entries. You’ll do this with the following stored procedure:
CREATE PROCEDURE CreateAudit (@OrderID int,
@Message nvarchar(512), @MessageNumber int) AS
INSERT INTO Audit (OrderID, Message, MessageNumber) VALUES (@OrderID, @Message, @MessageNumber)
Business Tier Modifications
There are several new classes to consider here, as well as a new method to add to the CommerceLibAccess class you created earlier. The new classes we’ll look at in this section are
• CommerceLibException: A standard exception to be used by the order processor.
• OrderProcessorMailer: Utility class allowing the order processor to send emails with simple syntax.
• IPipelineSection: Standard interface for pipeline sections.
• OrderProcessor: Controlling class for order processing.
• PSDummy: Test pipeline section.
You’ll also need to modify the BalloonShopConfiguration class to include additional order processor configuration properties.
The CreateAudit Method
This method is a wrapper around the CreateAudit stored procedure added earlier and uses standard code. Add the following code to the CommerceLibAccess class:
public static void CreateAudit(int orderID, string message, int messageNumber)
{
// get a configured DbCommand object
DbCommand comm = GenericDataAccess.CreateCommand();
// set the stored procedure name comm.CommandText = "CreateAudit";
// create a new parameter
DbParameter param = comm.CreateParameter();
param.ParameterName = "@OrderID";
param.Value = orderID;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// create a new parameter param = comm.CreateParameter();
param.ParameterName = "@Message";
param.Value = message;
param.DbType = DbType.String;
param.Size = 512;
comm.Parameters.Add(param);
// create a new parameter param = comm.CreateParameter();
param.ParameterName = "@MessageNumber";
param.Value = messageNumber;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// execute the stored procedure
GenericDataAccess.ExecuteNonQuery(comm);
}
You’ll see more details about the messageNumber parameter used for auditing later in the chapter, when we analyze the order-processor functionality in an exercise.
The OrderProcessorException Class
This is the first new class that you’ll add to the CommerceLib library. If you haven’t already done so, you need to add a subdirectory called CommerceLib to the BalloonShop App_Code directory.
Add a new class to this directory called CommerceLibException with the following code:
namespace CommerceLib {
/// <summary>
/// Standard exception for order processor /// </summary>
public class OrderProcessorException : ApplicationException {
private int sourceStage;
public OrderProcessorException(string message, int exceptionSourceStage) : base(message) {
sourceStage = exceptionSourceStage;
}
8213592a117456a340854d18cee57603
C H A P T E R 1 4 ■ O R D E R P I P E L I N E 525
public int SourceStage {
get {
return sourceStage;
} } } }
This code extends the base exception class ApplicationException, adding an integer prop- erty called SourceStage. This property allows you to identify the pipeline section (if any) that is responsible for throwing the exception.
Note that we use ApplicationException instead of Exception as the base class for this new exception class. This is recommended because the base Exception class is the base class for all exceptions; that is, both application and runtime exceptions. ApplicationException derives directly from Exception and should be used for exceptions thrown from application code.
Another class, SystemException, which also derives directly from Exception, is used by runtime code. This distinction gives you a simple way to see roughly what code is generating exceptions even before examining any exceptions thrown. The Microsoft recommendation is to neither catch nor throw SystemException derived classes in your code. In practice, however, we often do, because often try...catch blocks have a default catch block to catch all exceptions.
BalloonShop Configuration Modifications
The BalloonShopConfiguration class already includes a number of configuration properties for the site, or, to be more specific, allows access to properties stored in web.config. The OrderProcessor class (which you’ll add shortly) needs various additional pieces of information in order to function. The new code to add to this class is as follows:
public static class BalloonShopConfiguration {
...
// Returns the email address for customers to contact the site public static string CustomerServiceEmail
{ get { return
ConfigurationManager.AppSettings["CustomerServiceEmail"];
} }
// The "from" address for auto-generated order processor emails public static string OrderProcessorEmail
{ get
{ return
ConfigurationManager.AppSettings["OrderProcessorEmail"];
} }
// The email address to use to contact the supplier public static string SupplierEmail
{ get {
return ConfigurationManager.AppSettings["SupplierEmail"];
} } }
The new settings exposed by this class are as follows:
• CustomerServiceEmail: The email address that customers can use to send in queries about orders.
• OrderProcessorEmail: An email address used as the “from” address for emails sent from the order processor to the administrator.
• SupplierEmail: The email address of the product supplier, so that the order processor can send order notifications ready for picking/shipping.
For testing purposes, you can set all of these settings to your email address. Later you will probably want to change some or all of these appropriately.
■ Note Depending on the size of your enterprise, you may have multiple suppliers, in which case you’ll probably want to store supplier information in the BalloonShop database and associate each product or product range with a different supplier. To keep things simple, however, the code in this book assumes that you only have one supplier—which may well use the same email address as the site administrator.
You also need to add the relevant properties to the <appSettings> section of web.config:
<appSettings>
...
<add key="CustomerServiceEmail" value="<yourEmail>" />
<add key="OrderProcessorEmail" value="<yourEmail>" />
<add key="SupplierEmail" value="<yourEmail>" />
</appSettings>
Obviously, you need to replace the values <yourEmail> and <yourEmail> with values appro- priate to your system.
C H A P T E R 1 4 ■ O R D E R P I P E L I N E 527
The OrderProcessorMailer Class
This class enables code to send emails during order processing. Add a new class to the App_Code/CommerceLib directory with code as follows:
namespace CommerceLib {
/// <summary>
/// Mailing utilities for OrderProcessor /// </summary>
public static class OrderProcessorMailer {
public static void MailAdmin(int orderID, string subject, string message, int sourceStage)
{
// Send mail to administrator
string to = BalloonShopConfiguration.ErrorLogEmail;
string from = BalloonShopConfiguration.OrderProcessorEmail;
string body = "Message: " + message + "\nSource: " + sourceStage.ToString() + "\nOrder ID: " + orderID.ToString();
Utilities.SendMail(from, to, subject, body);
} } }
The only method of this class, MailAdmin, uses the Utilities.SendMail method to send an email to the site administrator by using settings from BalloonShopConfiguration. Later, when we need to send mails to customers and suppliers, we’ll add more code to this class.
The IPipelineSection Interface
This IPipelineSection interface is implemented by all pipeline section classes, so that OrderProcessor can use them in a standard way. Add the following interface definition in a file called IPipelineSection.cs in the App_Code/CommerceLib directory:
namespace CommerceLib {
/// <summary>
/// Standard interface for pipeline sections /// </summary>
public interface IPipelineSection {
void Process(OrderProcessor processor);
} }
This interface exposes a single method, Process, that OrderProcessor will use to process an order through the pipeline stage in question. This method includes a reference to the calling
class, so that pipeline sections will have access to order information and utility methods exposed by the OrderProcessor class.
The OrderProcessor Class
As is probably apparent now, the OrderProcessor class (which is the class responsible for moving an order through the pipeline) is a little more complicated than the classes you’ve seen so far in this chapter. However, you can start simply and build up additional functionality as needed. To start with, you’ll create a version of the OrderProcessor class with the following functionality:
• Dynamically selects a pipeline section supporting IPipelineSection
• Adds basic auditing data
• Gives access to the current order and customer details
• Gives access to administrator mailing
• Mails the administrator in case of error
The code for this class, which you should also add to the App_Code/CommerceLib directory, is as follows:
namespace CommerceLib {
/// <summary>
/// Main class, used to obtain order information, /// run pipeline sections, audit orders, etc.
/// </summary>
public class OrderProcessor {
internal IPipelineSection CurrentPipelineSection;
internal bool ContinueNow;
internal CommerceLibOrderInfo Order;
public OrderProcessor(string orderID) {
// get order
Order = CommerceLibAccess.GetOrder(orderID);
}
public OrderProcessor(CommerceLibOrderInfo orderToProcess) {
// get order
Order = orderToProcess;
}
public void Process() {
C H A P T E R 1 4 ■ O R D E R P I P E L I N E 529
// configure processor ContinueNow = true;
// log start of execution
CreateAudit("Order Processor started.", 10000);
// process pipeline section try
{
while (ContinueNow) {
ContinueNow = false;
GetCurrentPipelineSection();
CurrentPipelineSection.Process(this);
} }
catch (OrderProcessorException ex) {
MailAdmin("Order Processing error occurred.", ex.Message, ex.SourceStage);
CreateAudit("Order Processing error occurred.", 10002);
throw new OrderProcessorException(
"Error occurred, order aborted. "
+ "Details mailed to administrator.", 100);
}
catch (Exception ex) {
MailAdmin("Order Processing error occurred.", ex.Message, 100);
CreateAudit("Order Processing error occurred.", 10002);
throw new OrderProcessorException(
"Unknown error, order aborted. "
+ "Details mailed to administrator.", 100);
} finally {
CommerceLibAccess.CreateAudit(Order.OrderID, "Order Processor finished.", 10001);
} }
public void CreateAudit(string message, int messageNumber) {
CommerceLibAccess.CreateAudit(Order.OrderID, message, messageNumber);
}
public void MailAdmin(string subject, string message, int sourceStage)
{
OrderProcessorMailer.MailAdmin(Order.OrderID, subject, message, sourceStage);
}
private void GetCurrentPipelineSection() {
// select pipeline section to execute based on order status // for now just provide a dummy
CurrentPipelineSection = new PSDummy();
} } }
This class includes two constructors, which are used to initialize the order processor with order information by either using the ID of an order or by simply using a CommerceLibOrderInfo instance. The class also includes its own versions of the CommerceLibAccess.CreateAudit and OrderProcessorMailer.MailAdmin methods, both of which are time savers that enable you to call these methods with the order ID parameter filled in automatically.
We’ll walk through the rest of the code here shortly. Suffice to say for now that the only pipeline section used is PSDummy, which you’ll add next.
The PSDummy Class
The PSDummy class is a dummy pipeline section that you’ll use in your basic pipeline implemen- tation to check that things are working correctly. Add this class to the App_Code/CommerceLib directory:
namespace CommerceLib {
/// <summary>
/// Summary description for PSDummy /// </summary>
public class PSDummy : IPipelineSection {
public void Process(OrderProcessor processor) {
processor.CreateAudit("PSDoNothing started.", 99999);
processor.CreateAudit("Customer: "
+ processor.Order.Customer.UserName, 99999);
processor.CreateAudit("First item in order: "
+ processor.Order.OrderDetails[0].ItemAsString, 99999);
processor.MailAdmin("Test.", "Test mail from PSDummy.", 99999);
processor.CreateAudit("PSDoNothing finished.", 99999);
} } }
C H A P T E R 1 4 ■ O R D E R P I P E L I N E 531
The code here uses the AddAudit and MailAdmin methods of OrderProcessor to show that the code has executed correctly. Again, we’ll look at this code in more detail shortly.
Presentation Tier Modifications
All you need to do now for the order processor to process an order is to add some very simple code to the checkout page.
Checkout Page Modifications
Modify the code in placeOrderButton_Click in Checkout.aspx.cs as follows:
using CommerceLib;
...
protected void placeOrderButton_Click(object sender, EventArgs e) {
...
// Create the order and store the order ID
string orderId = ShoppingCartAccess.CreateCommerceLibOrder(
shippingId, taxId);
// Process order
OrderProcessor processor = new OrderProcessor(orderId);
processor.Process();
// Redirect to the conformation page Response.Redirect("OrderPlaced.aspx");
}