- Introduction
- Getting to the Controller
- System.Messaging Namespace
- RequestQueMonitor Project
- RequestsProcessor Discussion
- Summary
Getting to the Controller
I like to follow this basic design tenet when designing web services:
Move as much processing logic out of the web service as possible.
In other words, when an HTTP request comes into the web service, do as little processing in the web service itself as possible, and farm out all remaining logic to classes in "the engine." In this case, the engine begins with a class called Controller. Aside from basic XML structural validation and user/session authorization, the vast bulk of the processing occurs in or below this Controller class.
The reason for this approach was born out of experience. In the early days of the .NET web service classes, I (and others) had trouble putting complex logic in the web service classes. To be fair, I also ran into problems with the Java Servlet components. Various combinations of "stuff" caused the process to hang. In other cases, the results were not what I had expected, and the debugging task was difficult. As a matter of habit, then, I have come to move the more interesting logic out of the web service itself and to call helper classes instead.
The Controller component is an example of a "point man" classa class acting as the lead in an entire chain of other classesused to kick off the bulk of the actual processing. This tends to make the code easier to read anyway and greatly reduces the debugging complexity, because you then can write standalone tester programs that call this class.
The issues I noted previously with the web service classes likely have been fixed in the .NET Framework. However, I will continue to use this approach of moving logic out, because doing so brings clarity and easier debugging.
Figure 7-1 is a rather high-level sequence diagram of a request flow. It shows the calling sequence from the initiation of a request through the calling of the Controller component. This should put into perspective the context of the remainder of this section, in which we discuss the Controller in more detail.
Figure 7-1 Getting to the Controller.
The Controller Structure
Surprisingly, the Controller class is named Controller. It takes in as its sole parameter an XmlNode object and returns an EAIResponse object. By the time the Controller gets the go-ahead to start processing the input via its process method, you should know a few things:
-
The input request was a well-formed XML message.
-
If the request was a new login message, and the username and password were valid.
-
If the request was submitted for an existing session, that session was valid.
NOTE
The Controller doesn't actually see login requests. They are handled by the web service itself.
Because we know each of these pieces of information here, it's full steam ahead as we enter the process method. The XmlNode object is simply a representation of the request string the originator submitted. We expect this to be in the following format:
<EAIRequest> <SessionID>xyz-123-abc-987</SessionID> <Requests> <Request Name="DoThis"> <Needed>info</Needed> </Request> </Requests> </EAIRequest>
The Controller class has a default, blank constructor. It then has a single public method, named process.We look at a couple of private helper methods in a moment.
From a high level, the Controller component is really responsible for only a few, albeit important tasks:
-
Converting an XmlNode object into an EAIRequest object
-
Determining whether the request is synchronous or asynchronous
-
Sending an EAIRequest object to MessageQueue for asynchronous requests
-
Creating a RequestsProcessor object for synchronous requests
-
Firing off the actual Requests to the RequestsProcessor
-
Compiling status info and creating and returning an EAIResponse object
Figure 7-2 is a sequence diagram of a simple synchronous request coming into the Controller.
Figure 7-2 Controller synchronous request processing.
The Controller first creates a new EAIResponse object that will be returned from the process call. It then converts the incoming XmlNode object, representing the original request, into an EAIRequest object. This is a helper object that contains various information and objects that make it much easier to process the request. Among the EAIRequest members is a Requests object that contains Request objects. These Request objects each represent a single <Request> block from the incoming request.
After the EAIRequest object has been created, Controller checks to see if the request is synchronous or asynchronous. For our purposes, a synchronous request is specified in the incoming message by the originator, and it means that the caller expects the request to be handled while he waits. In other words, the caller expects a response to the request that contains status information about all Request block processing for requests sent in. For example, a request specified for asynchronous processing would have the following structure:
<EAIRequest> <SessionID>123-xyz-987-abc</SessionID> <Requests Asynch="true"> <Request Name="First"> ... </Request> ... <Request Name="Last"> ... </Request> </Requests> </EAIRequest>
To make matters a bit more confusing (but flexible), the caller can also specify that he wants processing to either continue on in the face of Requests returning an error or to FailOnFirstError. Failing on first error means that processing stops the first time a Request block returns an error. This is accomplished by the optional XML attribute FailOnFirstError on the Requests element. If the attribute is not present, FailOnFirstError is set to false. For example, if you were submitting a bunch of Request blocks in a Requests block and you wanted the processing to halt the first time any Request returned an error, you would send in something like the following:
<EAIRequest> <SessionID>123-xyz-987-abc</SessionID> <Requests FailOnFirstError="true"> <Request Name="First"> ... </Request> ... <Request Name="Last"> ... </Request> </Requests> </EAIRequest>
Therefore, you have four ways in which to combine the preceding two attributes. They are shown in Table 7-1.
Table 7-1 FailOnFirstError and Asynch request attributes
|
FailOnFirstError = TRUE |
FailOnFirstError = FALSE |
Synchronous |
Processing starts from Controller and returns when the first error is encountered processing a Request block. Therefore, not all requests might even have processing attempted. |
Processing starts from Controller and continues on with each Request block being processed, even if one or more Request blocks return an error. |
Asynchronous |
Processing starts from the Microsoft Message Queuing (MSMQ) and returns when the first error is encountered processing a Request block. Therefore, not all requests might even have processing attempted. |
Processing starts from the MSMQ and continues with each Request block being processed, even if one or more Request blocks return an error. |
Listing 7-1: Controller.cs DiscussionController.cs
using System; using System.Xml; using System.Messaging; using System.Collections; using EAIFramework.Messages; using EAIFramework.Util; using EAIFramework.Handler; namespace EAIFramework.Controller { /// <summary> /// EAIFramework.Controller.Controller /// This class is responsible for taking in an XmlNode object /// and converting it to an EAIRequest object. Then each /// Request block in the incoming EAIRequest message is /// processed in either a synchronous or asynchronous /// fashion, as specified in the incoming message. /// </summary> public class Controller { public Controller(){}//end constructor
Listing 7-2 shows the process method that will do the bulk of the work.
Listing 7-2: Controller.cs process Method
public EAIResponse process(XmlNode req) { Logger.log(Logger.INFO,"-------------------------"); Logger.log(Logger.INFO,"Entering Controller "); Logger.log(Logger.INFO,"with str: " +req.OuterXml); Logger.log(Logger.INFO,"-------------------------"); TrnxLogProxy tlp = new TrnxLogProxy(); EAIFramework.Messages.EAIResponse resp = new EAIFramework.Messages.EAIResponse(); resp.OverallStatusCode=StatusCodes.OK; resp.OverallStatus = StatusCodes.Descriptions( StatusCodes.OK); // Create the EAIRequest object Logger.log(Logger.INFO, "Controller: Going to create new EAIRequest"); EAIRequest eaiReq = CreateRequest(req); resp.TransactionID=eaiReq.TransactionID; resp.SessionID=eaiReq.SessionID; resp.OriginalXML=req.OuterXml; if( ! eaiReq.Synchronous) { Logger.log(Logger.INFO, "THIS IS AN ASYNCHRONOUS TRNX!"); resp = this.processAsynchRequests(eaiReq); // Now save and return the response to the caller int nRows = tlp.SaveResponse(resp.ToString(), resp.TransactionID); return resp; }//end Asynch request. Logger.log(Logger.INFO, "THIS IS A SYNCHRONOUS TRNX!"); //-------------------------------------------------- // The rest of this method is handling // a SYNCHRONOUS request //-------------------------------------------------- // Set up a place to compile the results of // the <Request> processing. There will be // one Component (in Components) for each // Request (in Requests). This will come back in the // ArrayList rreqs, below. RequestsProcessor processor = new RequestsProcessor( eaiReq); ArrayList rreqs = processor.process(); if( !(eaiReq.SessionID.Equals("")) ) resp.RequestingUsername = eaiReq.SessionID; else resp.RequestingUsername = eaiReq.RequestingUsername; // Populate the EAIResponse object with // the results of the processing // Obscure the Password, if it's there try{req.SelectSingleNode( "/EAIRequest/RequestingPassword"). InnerText = "*****";} catch(Exception exc){} resp.RequestResponses = (RequestResponse[]) rreqs.ToArray( new RequestResponse().GetType()); tlp.UpdateTransaction(resp); int nRespRows = tlp.SaveResponse(resp.ToString(), resp.TransactionID); return resp; }//end process()
The process method performs, or at least kicks off, the heavy lifting of Request processing. It starts off by doing some diagnostic logging and then creates a TrnxLogProxy object. This is used later to update the transaction in the database.
Next, a new EAIResponse object, named resp, is created that you use to return to the caller all the status information gathered from the various steps needed to process each Request block. By default, you set the response object as showing success (OK).
The first of the private helper methods, CreateRequest(), is called. It takes in the XmlNode object and returns a populated EAIRequest object. This object can then be used to easily gain access to the various parts of the input request. We take a look at CreateRequest() in a moment.
After CreateRequest() is called, the main branching for synchronous or asynchronous processing takes place. If the request is not to be handled in a synchronous manner (that is, if it's asynchronous), you call the private helper method processAsynchRequests(). This also is discussed in a moment. This basically sends the request to a message queue that watches for asynchronous requests. Before you leave, you save off the response to the database.
On the other hand, if this request is to be handled in a synchronous manner, a new RequestsProcessor object is created. The RequestsProcessor class, discussed later in this chapter, encapsulates all the functionality necessary to actually process a bunch of requests. You could have written the code for this processing here in the Controller class. However, because the request is sent off to a message queue for asynchronous processing, you can call the same RequestsProcessor class from both the Controller class and the message queue. This leaves you with a single chunk of code, making maintenance and enhancements easier and more reliable.
The process method of the RequestsProcessor class is called next, and you get back an ArrayList of RequestResponse objects. There should be one for each processed Request. When FailOnFirstError is false, you should see a RequestResponse object for each Request. If FailOnFirstError is true, you might see fewer RequestResponse objects than Requests, if one of the Requests fails.
When the RequestsProcessor returns its results, Controller updates the EAIResponse object that will be sent back. You'll recall that the T_TransactionLog table has a column to hold either SubmittingUser or SessionID, because both will never come in at the same time. Controller sets the RequestingUsername member to the value of either a SubmittingUsername or a SessionID, whichever is present. It then sets the Password field to a series of asterisks, if password has a value. This just masks the password in the response message. It's true that, for now, you should never see requests with a username and password hit the Controller. However, to be prepared for a time when you need to send login requests to the Controller, the information will be presented back to the user as he would expect it to be. Finally, the response object is populated with the remainder of the interesting information, including the TransactionID, SessionID, OriginalXML string, and RequestResponse status objects returned by the RequestsProcessor.
To finish out the Controller.process method, the TrnxLogProxy object that was created near the start of the method is called on two different methods. First, the UpdateTransaction() method is called. This takes in the newly-created response object and does an UPDATE command on the row in the T_TransactionLog table that has the TransactionID held in the response object. It at least changes the Status and StatusCode columns.
All that's left to do is return the EAIResponse object to the caller. For now, the only component calling the Controller class is the web service. It then returns to the original caller a string representation of the response object that you just sent back.
Private Helper Methods
Now let's take a look at the private helper methods in the Controller class that the process method called. The longest of these methods is the CreateRequest() method, shown in Listing 7-3. It takes in an XmlNode object and returns an EAIRequest object.
Listing 7-3: The CreateRequest() Method
/// <summary> /// This is a helper method for the /// process() method that hides all /// the logic to build the EAIRequest /// object from the input XmlNode /// object /// </summary> /// <param name="req">The input XmlNode object</param> /// <returns>EAIRequest object</returns> private EAIRequest CreateRequest(XmlNode req) { EAIRequest rRet = new EAIRequest(); rRet.OriginalXML = req.OuterXml; try { rRet.RequestingUsername = req.SelectSingleNode( "/EAIRequest/RequestingUsername") .InnerText; } catch (Exception exc) { Logger.log(Logger.INFO, "CreateReq: Couldn't read RequestingUsername: " + exc.Message); rRet.RequestingUsername=""; } try{ rRet.TransactionID = Int32.Parse( req.SelectSingleNode( "/EAIRequest/TrnxID").InnerText); } catch (Exception exc) { Logger.log(Logger.INFO, "CreateReq: Couldn't read TrnxID: " + exc.Message); rRet.TransactionID = -1; } try{ rRet.OverallStatus = req.SelectSingleNode( "/EAIRequest/OverallStatus") .InnerText; } catch (Exception exc) { Logger.log(Logger.INFO, "CreateReq: Couldn't read OverallStatus: " + exc.Message); } try { Logger.log(Logger.INFO, "CreateReq: Going to get StatusCode now..."); string sSC = req.SelectSingleNode( "/EAIRequest/OverallStatusCode").InnerText; if( (sSC==null) || sSC.Equals("")) sSC="0"; Logger.log(Logger.INFO, "CreateReq: Just got StatusCode: " + sSC); int nSC = Int32.Parse( sSC ); Logger.log(Logger.INFO, "CreateReq: Just converted int to: " + nSC.ToString()); rRet.OverallStatusCode = nSC; Logger.log(Logger.INFO, "CreateReq: Stuck in OverallStatusCode."); } catch(Exception exc) { Logger.log(Logger.INFO, "CreateReq: Couldn't read OverallStatusCode: " + exc.Message); }//end catch try { Logger.log(Logger.INFO, "CreateReq: Going to get SessionID now..."); string ssn = req.SelectSingleNode( "/EAIRequest/SessionID") .InnerText; rRet.SessionID = ssn; Logger.log(Logger.INFO, "CreateReq: Stuck in SessionID " + ssn); } catch(Exception exc) { Logger.log(Logger.INFO, "CreateReq: Couldn't read SessionID: " + exc.Message); rRet.SessionID = ""; }//end catch //rRet.Started = // new DateTime( req.SelectSingleNode( // "/EAIRequest/Started").InnerText); XmlNode reqs = req.SelectSingleNode( "/EAIRequest/Requests"); XmlAttribute async = reqs.Attributes["Asynch"]; if(async != null) { if(async.InnerText.Equals("true")) rRet.Synchronous = false; else rRet.Synchronous = true; }//End if Asynch != null else rRet.Synchronous = true; XmlAttribute fof = reqs. Attributes["FailOnFirstError"]; if(fof != null) { if(fof.InnerText.Equals("true")) rRet.FailOnFirstError = true; else rRet.FailOnFirstError = false; }//End if FailOnFirstError != null else rRet.FailOnFirstError = false; // Now build the Requests object XmlNodeList xnl = req.SelectNodes( "/EAIRequest/Requests/Request"); Requests r = new Requests(); if(xnl.Count>0) { int nReqNum = 0; foreach(XmlNode xn in xnl) { Request rr = new Request(xn.OuterXml); rr.TrnxID = rRet.TransactionID; rr.Iteration = nReqNum++; r.add(rr); }//end foreach rRet.Requests = r; }//end if there are any requests rRet.Requests = r; return rRet; }//end CreateRequest()
The CreateRequest() method begins by instantiating a new EAIRequest that will be returned to the caller. It then steps through, member by member; pulls out the information from the XmlNode object sent in; and populates the appropriate member of the request object. Working with the XML is the same as done previously, with the possible exception of pulling in an attribute. You will want to check for a couple of different attributes, including FailOnFirstError and Asynch.
I will now walk through pulling out a particular XML element, to make sure you understand what is going on in the code. You'll notice that very similar code repeats several times, getting different XML elements. I will then walk through pulling out an XML attribute. It also repeats for subsequent attributes later in the method.
First, let's look at the code to pull out the RequestingUsername element (see Listing 7-4). The statement is placed in a try/catch block, because the SelectSingleNode() method can throw an exception. The code populates the RequestingUsername member of the rRet (EAIRequest) object, which is a string. It is populated with the InnerTextin other words, the value between the XML element tagsfor the element matching the XPath search string of /EAIRequest/RequestingUsername. Let's take a look at the code that actually does the XPath search now.
Listing 7-4: XPath Example
try{ // First get the XmlNode object for the XPath search // of "/EAIRequest/RequestingUsername". // This XPath search should find the XML Element at the // following spot: // // <EAIRequest> // <RequestingUsername>theUser</RequestingUsername> // ...other interesting XML goodies here... // </EAIRequest> System.XML.XmlNode xnReqUser = req.SelectSingleNode( "/EAIRequest/RequestingUsername"); // Now that we have the XmlNode for the RequestingUsername, // get the value for that element. In this case, strUser // would be set to equal "theUser". string strUser = xnReqUser.InnerText; // Finally, populate the RequestingUsername member with the // value we got for strUser. rRet.RequestingUsername = strUser; } catch( Exception exc) { // error processing here...
}
NOTE
As you might notice with most of the code in this project, I like to split most operations into separate statements. Over the years, I have found that it's much quicker to debug problems if the statements are separated than if you have many methods chained together in a single statement. I also tend to break that rule if I have become particularly comfortable with a series of calls. Crunching through an XmlNode is just one of those occasions. I will also concede that code looks "cooler" if it's all smushed into one line, but I long ago (and quite happily, I might add) traded worrying about looking cool for simplifying my life. Splitting multiple calls into separate lines is one such simplification.
The catch block traps any errors, including the case in which the element doesn't exist. If this is the case, the response RequestingUsername is set to a blank string. The same holds true for several other bits of information, including the TransactionID, OverallStatus, OverallStatusCode, and so on. Each of these works the same way.
Midway through this method is the code to pull out the Request XML nodes. Here is where you hit the first of the XML attributes, Asynch. You tell the system to process this transaction asynchronously by sending in the attribute Asynch with a value of true on the Requests XML element. For example, the tag would look like this:
... <Requests Asynch="true">...</Requests>...
The default is to handle requests synchronously so that if the attribute is left off or is set to a value of false, you set the EAIRequest member Synchronous to true. Otherwise, you set the Synchronous member to false, meaning that you want the incoming transaction to be processed in an asynchronous fashion.
To get the Asynch attribute, you first get the XmlNode for the Requests block by using the statement SelectSingleNode("/EAIRequest/Requests"), exactly as you did earlier for other pieces of information. The next statement, however, is where you create an XmlAttribute object. This is accomplished with the following statement:
XmlAttribute async = reqs.Attributes["Asynch"];
This is another case in which the .NET Framework enables you to specify an array item by name rather than by an index number. This is a very handy little trick that saves many lines of code by hiding the logic necessary to search through the array of items for a particular entry.
All you need to do now to get the value of the attribute is to look at the InnerText member, just like you did for XmlNode objects. Check to see if the InnerText equals true for the Asynch attribute. If so, set the rRet.Synchronous member to false, indicating that this transaction should be processed asynchronously. If the value sent in was not true for the Asynch attribute, set the rRet.Synchronous member to true. The other possibility for the Asynch attribute is that it wasn't sent in at all. The else block traps the situation in which the async (XmlAttribute) object is null. This means that the attribute wasn't sent in, so you set the rRet.Synchronous member to true, because synchronous processing is the default.
At this point, you immediately step into another section of code that checks for the FailOnFirstError attribute. It is processed in exactly the same manner as the Async attribute, but it sets the rRet.FailOnFirstError member. This member is used in the RequestsProcessor object to see whether to continue processing if a particular request returns something other than true.
The processAsynchRequests() Method
We end the discussion of the Controller class by taking a look at the processAsynchRequests() method. Until now, we have essentially ignored the asynchronous processing of transactions. Well, no more!
Asynchronous transaction processing occurs as follows:
-
An EAIRequest object is created and sent to the processAsynchRequests() method.
-
A Message object is created, along with a MessageQueue object.
-
The Message is sent to the MessageQueue.
-
If no errors are caught, meaning that the message was sent, a success EAIResponse object is returned to the caller.
-
Otherwise, an error EAIResponse object is returned.
-
Special code monitoring the MessageQueue takes the incoming request object and processes the individual Request blocks.
Figure 7-3 shows a sequence diagram of a successful asynchronous transaction flowing through the system. It is very similar to the synchronous transaction diagram in Figure 7-2, but instead of creating and calling an instance of RequestsProcessor, it uses a private helper method to send a Message object to a MessageQueue.
Figure 7-3 Asynchronous request flow sequence diagram.
To keep it all together, we first look at the processAysnchRequests() method and then talk about using the message queues. You'll notice that creating a Message object and sending it to a message queue is quite easy and straightforward with .NET. You accomplish what previously was fairly complex processing with just a couple lines of code (see Listing 7-5).
Listing 7-5: processAsynchRequests() Method
/// <summary> /// This method takes in an EAIRequest object and /// fires off the request to the RequestsQueueName /// MSMQ listener. It will then immediately return /// </summary> /// <param name="req"></param> /// <returns>EAIResponse</returns> protected EAIResponse processAsynchRequests( EAIRequest req) { EAIResponse resp = new EAIResponse(); resp.SessionID = req.SessionID; resp.TransactionID = req.TransactionID; ConfigData cdata = new ConfigData(); string strQueName = cdata.getConfigSetting( "RequestsQueueName"); MessageQueue mq; try { Logger.log(Logger.INFO, "Controller: Going to check que: " + strQueName); if( !MessageQueue.Exists(strQueName)){ MessageQueue.Create(strQueName); }//end if } catch(Exception exc) { Logger.log(Logger.ERROR, "Controller: ERROR calling MessageQueue." + "Exists(): " + exc.Message); }//end catch mq = new System.Messaging.MessageQueue(strQueName); mq.DefaultPropertiesToSend.Recoverable = true; XmlMessageFormatter xmf = (XmlMessageFormatter)mq.Formatter; xmf.TargetTypes = new Type[]{typeof(EAIRequest)}; mq.Formatter = xmf; System.Messaging.Message msg = new System.Messaging.Message( req ); msg.Formatter = new XmlMessageFormatter( new Type[]{typeof(EAIRequest)}); try { mq.Send(msg); resp.OverallStatusCode = 1; resp.OverallStatus = StatusCodes.Descriptions( resp.OverallStatusCode); resp.Description = "Requests submitted to " + strQueName; }//end try catch(Exception exc) { resp.OverallStatusCode = 50; resp.OverallStatus = StatusCodes.Descriptions( resp.OverallStatusCode); resp.Description = exc.Message; }//end catch return resp; }//end processAsynchRequests() }//end class }//end namespace
Here's how that code breaks down. processAsynchRequests() takes in an EAIRequest object that was created earlier in the Controller. It returns an EAIResponse object, so the first thing it does is create an instance of this object and populate a few of the members.
The name of the message queue that will accept transaction requeststhat is, the overall incoming request represented by the EAIRequest objectis identified in the config file by RequestsQueueName. Therefore, you create a ConfigData object, get the value for RequestsQueueName, and store it in a member called strQueueName.
As the first of the real message queue processing steps, you check to see if the particular message queue exists. If it doesn't, it is created. You do the check by calling the static method Exists() on the MessageQueue class.
The MessageQueue and Message classes are in the System.Messaging namespace. This namespace is kept in a library that must be added to your Visual Studio .NET project. In the Solution Explorer window in your project, right-click the References item and select Add Reference. On the .NET tab of the dialog box that pops up, scroll down to find the System.Messaging.dll entry. Select this and click Select. You'll see the System.Messaging entry appear in the Selected Components box at the bottom of the dialog box; click OK. You should then be put back in the VS.NET project. System.Messaging should be listed in the References section of the Solution Explorer.
Figure 7-4 shows the Add Reference dialog box, with the .NET library for System.Messaging selected. You can now include the Messaging namespace in your various components in the project with the following line:
using System.Messaging;
Figure 7-4 Add Reference dialog box.
Now you create a MessageQueue object for the queue and set the Formatter to handle our EAIRequest object (more on this in a moment). Finally, the message, which contains the EAIRequest object, is sent to the message queue for processing. If you don't get an error sending the message, the response object to be sent back to the caller is set with an OK status and is returned. If an error was encountered sending the message, you update the response object appropriately in the catch block and return the response object. It's important to keep in mind that the status, either OK or an error, is not reflective of the status of the request processing, in this case. It merely indicates whether the submission to the message queue was successful. The caller gets back the generated TransactionID in the response, which can be used later to query about the status of the actual processing.
Thus ends the asynch processingwell, okay, the sending of the request. The code to pick up the message and to process the requests is discussed later in this chapter. However, this is another area in which the .NET Framework has made very powerful functionality extremely easy to use. This is great for software developers, because they really don't have much code to write. It's not so great news for book authors hoping to make their code look impressive.