/// <summary> /// Execute the batch. /// </summary> /// <param name="batch"></param> Result IBatchHandler.Execute(Batch batch) { // Execute the transactions contained in the batch. Each transaction is committed or rejected as a unit. The commands // within the batch are specified as text strings and object arrays which are be resolved through the 'Reflection' // classes to assemblies, types, methods and parameters. A wrapper around the .NET Framework Transaction class is used // to commit or reject these collections of commands as a unit. foreach (Batch.Transaction batchTransaction in batch.Transactions) { // The 'Transaction' is used to commit or reject the pseudo-methods contained in the 'Batch.Transaction. as a // unit. The transaction will be rejected and all changes will be rejected if an exception is taken during the // execution of any of the methods contained in the 'Batch.Transaction'. If there are no exceptions taken, then all // the changes are committed to their respective data stores. Transaction transaction = new Transaction(); // Every transaction has one or more data repositories that can be read or modified. This step will create a // Resource Manager for every one of the data stores that can be referenced by this server during the processing of // a transaction. A transaction can include one or more of these databases and the internal framework will // escelate from a single phase transaction to a two-phase transaction when there is more than one durable // resource. foreach (ResourceDescription resourceDescription in BatchProcessor.resourceDescriptionList) { // This will create a resource manager for an ADO data resource based on the configuration settings. if (resourceDescription is AdoResourceDescription) { transaction.Add(new AdoResourceManager(resourceDescription.Name)); } // This will create a resource manager for SQL databases based on the configuration settings. if (resourceDescription is SqlResourceDescription) { transaction.Add(new SqlResourceManager(resourceDescription.Name)); } } try { // This variable controls whether the transaction is committed or rejected. bool hasExceptions = false; // This step will use reflection to resolve the assemblies, types and method names into objects that are // loaded in memory. Once the objects are resolved, the list of parameters is passed to the method for // execution. The beautiful part about this architecture is that the target methods themselves are written and // supported as strongly typed interfaces. The 'Reflection' class takes care of matching the variable array of // objects to the strongly typed, individual parameters. foreach (Batch.Method batchMethod in batchTransaction.Methods) { try { // The 'Reflection' classes are used to resolve the method specified in the batch to an object loaded // in memory that can be used to invoke that method. Assembly assembly = Assembly.Load(batchMethod.Type.Assembly.DisplayName); Type type = assembly.GetType(batchMethod.Type.Name); if (type == null) { throw new Exception(String.Format("Unable to locate type \"{0}\" in assembly {1}", batchMethod.Type.Name, batchMethod.Type.Assembly.DisplayName)); } MethodInfo methodInfo = type.GetMethod(batchMethod.Name); if (methodInfo == null) { throw new Exception(String.Format("The method \"{0}.{1}\" can't be located in assembly {2}.", batchMethod.Type.Name, batchMethod.Name, batchMethod.Type.Assembly.DisplayName)); } // This will invoke the method specified in the batch and match each of the variable, generic // parameters to strongly typed parameters. object result = methodInfo.Invoke(null, batchMethod.Parameters); // Like WebServices, this will return the parameters to the client as a variable, generic array. It is // up to the client to sort out the order of the returned parameters. List <Object> resultList = new List <object>(); if (methodInfo.ReturnType != typeof(void)) { resultList.Add(result); } foreach (ParameterInfo parameterInfo in methodInfo.GetParameters()) { if (parameterInfo.ParameterType.IsByRef) { resultList.Add(batchMethod.Parameters[parameterInfo.Position]); } } batchMethod.Results = resultList.ToArray(); } catch (Exception exception) { // This indicates that there was some problem with the transaction and the results should be rejected, // but only after the remaining items in the transaction have been attempted. This maximizes the // information that the caller has so the next time the batch is sent, all the errors can be fixed. If // the transaction were to stop after the first exception was discovered, there could be a sequence of // many back-and-forth attempts to run the batch before all the errors were discovered and corrected. hasExceptions = true; // This mechanism will pass the actual exception in the method back to the caller and correlate it with // the Batch.Method that caused the problem. Passing the 'TargetInvocationException' back to the // client is of no practical value, so the inner exception is extracted instead and passsed back to the // client. When the client processes the BatchException, it will appear as if the exception that // happend here on the server actually happeded local to the client machine. This is the desired // action. batchMethod.Exceptions.Add(exception is System.Reflection.TargetInvocationException ? exception.InnerException : exception); } } // If any exceptions were taken while processing the batch, reject the changes to the data model. Otherwise, // the results can be committed as a unit. if (hasExceptions) { transaction.Rollback(); } else { transaction.Commit(); } } catch (Exception exception) { // This will catch any errors that occurred while using 'Reflection' to execute the batch. batchTransaction.Exceptions.Add(exception); } finally { // Release the locks and any database connection resources. transaction.Dispose(); } } // Only the return parameters and exceptions are returned to the client. There is no need to transmit the entire batch // across the process boundary. The client will integrate the return parameters and exceptions back into the original // batch making it appear as if the bach made a round trip. return(new Result(batch)); }
/// <summary> /// Execute the batch on the Web Transaction server and return the results to the caller. /// </summary> /// <param name="batch">Contains one or more transactions that are executed on the server. The results of executing the /// batch are returned to the caller in this object.</param> Result IBatchHandler.Execute(Batch batch) { Result batchResult = null; // These streams must be shut down, even when an exception is taken. They are declared here so they can be accessed by // the 'finally' clause should something go wrong. Stream requestStream = null; WebResponse webResponse = null; Stream responseStream = null; try { // The client communicates the 'batch' data structure to the server using an HTTP channel. The channel is // configured to use SSL and Certificates to insure the privacy of the conversation. WebRequest webRequest = CreateCertificateRequest(); // The 'batch' data structure is serialized and compressed before being sent to the server. MemoryStream memoryStreamRequest = new MemoryStream(); BinaryFormatter binaryFormatter = new BinaryFormatter(); binaryFormatter.Serialize(memoryStreamRequest, batch); // This section will compress the serial stream. Note that it must be compressed into an intermediate buffer // before being sent via the HttpWebRequest class because the number of bytes must be known before the call is made // for the 'ContentLength' parameter of the HttpWebRequest object. memoryStreamRequest.Position = 0L; requestStream = webRequest.GetRequestStream(); DeflateStream requestDeflateStream = new DeflateStream(requestStream, CompressionMode.Compress, true); requestDeflateStream.Write(memoryStreamRequest.GetBuffer(), 0, (int)memoryStreamRequest.Length); requestDeflateStream.Close(); requestStream.Close(); // This is where all the work is done. Send the Request to the HTTP Server and get the response. In this // case, the response will be the results of executing the remote batch. Note that we get back only the // framework of the RPB, not all the parameters, methods, assemblies and types. This is an optimization so // that the same data isn't sent twice over the network. This 'differential' data will be merged with the // original. To the user, it appears that the Remote Batch.Method Batch was sent to the server, filled in with // result and exception data, then returned to the calling method. Actually, the batch was sent, the results // and exceptions returned, and it was all stitched back together again before the user gets it. webResponse = webRequest.GetResponse(); // Decompress the results from the HttpWebResponse stream. responseStream = webResponse.GetResponseStream(); DeflateStream responseDeflateStream = new DeflateStream(responseStream, CompressionMode.Decompress); byte[] uncompressedBuffer = CompressedStreamReader.ReadBytes(responseDeflateStream); responseDeflateStream.Close(); // Deserialize the batch results and merge it back with the original structure. The 'BatchResult' // structure has the same basic structure as the original batch, but only contains output parameters and // exceptions. MemoryStream memoryStreamResponse = new MemoryStream(uncompressedBuffer); BinaryFormatter binaryFormatterResponse = new BinaryFormatter(); batchResult = (Result)binaryFormatterResponse.Deserialize(memoryStreamResponse); memoryStreamResponse.Close(); } catch (WebException webException) { // The response in the exception usually indicates that we found the server, but the protocol failed for some // reason. When the response is empty, that usually indicates that the URL is bad or the server is down for // some reason. if (webException.Response == null) { // Log the error. EventLog.Error("Web Exception {0}, {1}", webException.Status, webException.Message); } else { // Extract the information sent back from the server and log it. webResponse = (HttpWebResponse)webException.Response; EventLog.Error(webException.Message); // Present the user with the error code. There may be a better way to do this, but this will work for now. MessageBox.Show(webException.Message, string.Format("{0} - {1}", Application.ProductName, Properties.Resources.ConnectionError)); } // Prompt the user for the URL and credentials again. HttpBatchProcessor.IsUrlPrompted = true; HttpBatchProcessor.IsCredentialPrompted = true; // This gives the caller a unified way to handle all exceptions that may have occurred while processing a batch. // The Web Exceptions are handled the same way as errors from the server. throw new BatchException(webException); } catch (UserAbortException) { // forward the exception so the caller can decide what to do. throw; } catch (Exception exception) { // This gives the caller a unified way to handle all exceptions that may have occurred while processing a batch. // The general exceptions are handled the same way as exceptions from the server. throw new BatchException(exception); } finally { // Make sure that every stream involved with the request is closed when the WebRequest is finished. if (requestStream != null) { requestStream.Close(); } if (responseStream != null) { responseStream.Close(); } if (webResponse != null) { webResponse.Close(); } } return(batchResult); }