Hands-On Lab
Reliable, Transacted and Instrumented Messaging with the Windows Communication Foundation
This lab is designed for use with the WinFx February 2005 CTP.
Information in this document is subject to change without notice. The example companies, organizations, products, people, and events depicted herein are fictitious. No association with any real company, organization, product, person or event is intended or should be inferred. Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation.
Microsoft may have patents, patent applications, trademarked, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property.
© 2005 Microsoft Corporation. All rights reserved.
Microsoft, MS-DOS, MS, Windows, Windows NT, MSDN, Active Directory, BizTalk, SQL Server, SharePoint, Outlook, PowerPoint, FrontPage, Visual Basic, Visual C++, Visual J++, Visual InterDev, Visual SourceSafe, Visual C#, Visual J#, and Visual Studio are either registered trademarks or trademarks of Microsoft Corporation in the U.S.A. and/or other countries.
Other product and company names herein may be the trademarks of their respective owners.
Contents
Reliable, Transacted and Instrumented Messaging with the Windows Communication Foundation
Exercise 1 – Define a data contract for use in a derivatives trading service
Task 1 – Create the Derivatives Trading Service Project
Task 2 – Add a Reference to the Windows Communication Foundation to the TradingService Project
Task 3 – Define the Contract that the Trading Service will Expose
Task 4 – Implement the Contract that the Trading Service Exposes
Task 5 – Host the Derivatives Trading Service in a .NET Executable
Task 6 – Configure the Trading Service
Task 7 – Deploy the Derivatives Trading Service
Task 8– Build a Client for the Derivatives Trading Service
Task 9 – Use the Derivatives Trading Service
Exercise 2 – Manage state within a service
Task 1 – Understand the Problem
Task 2 – Control the Sequence in which the Operations of the Trading Service are Invoked
Task 3 – Separate the Execution of Operations on behalf of Different Clients
Task 4 – Keep Track of Data between the Invocations of Operations on behalf of a Single Client
Task 5 – Make the Client Compatible with the Modified Derivatives Trading Service
Task 6 – Test the Enhanced Solution
Exercise 3 – Experiment with Reliable Messaging
Task 1 – Understand the Problem
Task 2 – Turn Reliable Messaging On
Task 3 – Test the Enhanced Solution
Task 4 – Introduce Uncertainty for which Reliable Messaging can Compensate
Exercise 4 – Build a queued messaging service for recording derivatives trades
Task 1 – Install MSMQ if Necessary
Task 2 – Add the Trade Recording Service to the Solution
Task 3 – Provide a Host for the Trade Recording Service
Task 4 – Configure the Trade Recording Service
Task 5 – Deploy the Trade Recording Service
Task 6 – Use the Trade Recording Service
Task 7 – Witness the Superior Availability of the Trade Recording Service
Exercise 5 – Wrap a set of messages in a transaction
Task 2 – Enhance the Derivatives Trading Service
Task 3 – Modify the Client to Conform to the Service
Task 4 – See the Composition of Multiple Distributed Operations into a Single Unit of Work
Exercise 6 – Add performance counters to a service
Task 1 – Define a Custom Trading Volume Performance Counter
Task 2 – Update the Value of the Custom Trading Volume Performance Counter
Task 3 – Monitor the Custom Trading Volume Performance Counter
Estimated time to complete this lab: 120 minutes
This lab demonstrates the Windows Communication Foundation’s facilities for managing state within a service, and how you can use it to enable reliable, queued, and transacted messaging in your application.
Scenario
You’ve developed an application that calculates the value of derivatives, a financial entity whose value is based on the performance of another entity (for instance, a stock option is a derivative that depends on the value of the stock) and you’ve allowed other software systems to access your application using Windows Communication Foundation. But now you want to manage state in that application so that the calculations take place in the right context. You also need to guard against common problems of distributed applications, such as dropped messages, unavailable services, and transactions accidentally processed separately. You may even want to measure the performance of the application. You’ll see how to do all those things with Windows Communication Foundation.
· Exercise 1 – Define a data contract for use in a derivatives trading service · Exercise 2 – Manage state within a service · Exercise 3 – Experiment with Reliable Messaging · Exercise 4 – Build a queued messaging service for recording derivatives trades · Exercise 5 – Wrap a set of messages in a transaction · Exercise 6 – Add performance counters to a service |
In this exercise, you will build a Windows Communication Foundation service that uses a data contract to define the format of data in the messages it exchanges. The service is for trading in derivatives.
The service that you will construct in this exercise is one that allows a client to define a derivative trade, identify any number of quantitative analytical functions to be used in estimating the value of the derivative to be traded, and, optionally, make the trade.
Of course, while actually writing software to calculate the value of derivatives is beyond the scope of this lab, one can pretend to do so by following these steps:
1. Open Visual Studio 2005, and create a new blank solution called, TradingService, in c:\Windows Communication Foundation\Labs, as shown in figure 1.1, below.
Figure 1.1 Creating a blank Visual Studio solution
2. Add a C# Class Library project called, TradingService, to the solution, as shown in figure 1.2, below.
Figure 1.1 Adding a Class Library project to the solution
3. In the properties for the project, set the default namespace to Fabrikam
3. Add references to the System.ServiceModel and System.Runtime.Serialization .NET assemblies, to the TradingService project, as shown in figure 1.3, below.
Figure 1.1 Adding references to the Windows Communication Foundation assemblies
1. Rename the class file, Class1.cs, in the TradingService project to ITradingService.cs, and modify the content thereof to read as follows:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
[ServiceContract]
public interface ITradingService
{
[OperationContract]
string BeginDeal();
[OperationContract]
void AddTrade(Trade trade);
[OperationContract]
void AddFunction(string function);
[OperationContract]
decimal Calculate();
[OperationContract]
void Purchase();
[OperationContract]
void EndDeal();
}
}
2. Thus, you have defined an interface, ITradingService, with a method, AddTrade(), that accepts an instance of a class named Trade as a parameter. Furthermore, the AddTrade() method has the [OperationContract] attribute, which implies that parameters may be passed to it from a remote client. Therefore, not only must a Trade class be defined, but a data contract must also be defined for that class to specify the format in which instances of the class will be transmitted to the AddTrade() method from remote clients. So, add a class named, Trade.cs, to the project, and modify its contents to look like this:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
namespace Fabrikam
{
[DataContract(Namespace="Fabrikam",Name="Trade")]
public class Trade
{
[DataMember]
public string Symbol;
[DataMember]
public long? Count;
[DataMember]
public DateTime? Date;
public override string ToString()
{
string symbol = (this.Symbol != null)?((this.Symbol != string.Empty)?this.Symbol:"?"):"?";
string date = (this.Date != null)?this.Date.Value.ToShortDateString():"?";
string count = (this.Count != null)?this.Count.ToString():"?";
return string.Format("{0}x{1} on {2}",count,symbol,date);
}
}
}
1. Add a class named, TradingSystem.cs, to the project, and modify its contents in this way:
using System;
using System.Collections.Generic;
using System.Text;
namespace Fabrikam
{
public class TradingSystem: ITradingService
{
#region ITradingService Members
string ITradingService.BeginDeal()
{
string dealIdentifier = Guid.NewGuid().ToString();
return dealIdentifier;
}
void ITradingService.AddTrade(Trade trade)
{
Console.WriteLine("Added trade for {0}",trade);
}
void ITradingService.AddFunction(string function)
{
Console.WriteLine("Added function {0}",function);
}
decimal ITradingService.Calculate()
{
Decimal value = DateTime.Now.Millisecond/10;
Console.WriteLine("Calculated value as {0}",value);
return value;
}
void ITradingService.Purchase()
{
Console.WriteLine("Purchased!");
}
void ITradingService.EndDeal()
{
Console.WriteLine("Completed deal.");
}
#endregion
}
}
2. Compile the TradingService project to confirm that there are no syntax errors.
1. Add a C# Console Application project called, TradingServiceHost, to the TradingService solution.
2. Add a reference to the System.ServiceModel .NET assembly, to the Host project.
3. Add a reference to the TradingService project to the TradingServiceHost project, as shown in figure 1.4, below.
Figure 1.4 Adding a reference to the TradingService project
4. Modify the Program.cs class in the TradingServiceHost project to read as follows:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
public class Program
{
public static void Main(string[] args)
{
using(ServiceHost host = new ServiceHost(typeof(TradingSystem)))
{
host.Open();
Console.WriteLine("The trading service is available.");
Console.ReadKey();
}
}
}
}
5. Compile the TradingServiceHost project to ensure that there are no syntax errors.
1. Add an application configuration file named, app.config, to the TradingServiceHost project in the TradingService solution.
2. Modify the contents of that file thusly:
address="http://localhost:8000/TradingService"
binding="wsHttpBinding"
contract="Fabrikam.ITradingService"/>
1. Build the TradingService solution.
2. Start a new instance of the TradingServiceHost project.
3. When you see a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window to terminate it.
1. Add a C# Console Application project called, Client, to the TradingService solution.
2. Add references to the System.ServiceModel and System.Runtime.Serialization .NET assemblies, to the Client project.
3. Add a class module to the Client project, name it ITradingService.cs, and modify its contents to look like this:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Client
{
[ServiceContract]
public interface ITradingService
{
[OperationContract]
string BeginDeal();
[OperationContract]
void AddTrade(Trade trade);
[OperationContract]
void AddFunction(string function);
[OperationContract]
Decimal Calculate();
[OperationContract]
void Purchase();
[OperationContract]
void EndDeal();
}
}
4. Add another class module to the client project, name it Trade.cs, and alter its contents in this way:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
namespace Client
{
[DataContract(Namespace="Fabrikam",Name="Trade")]
public class Trade
{
[DataMember]
public string Symbol;
[DataMember]
public long? Count;
[DataMember]
public DateTime? Date;
}
}
By so doing, you have defined a data contract in the client application that is compatible with the data contract defined in the derivatives trading service, although the class used to define the data contract in the client application is different from the class used to define the data contract in the service.
5. Add an application configuration file named, app.config, to the Client project and change its contents to look like this:
address="http://localhost:8000/TradingService"
binding="wsHttpBinding"
contract="Client.ITradingService"/>
6. Modify the code in the Program.cs module of the Client project thusly:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
namespace Client
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Press any key when the service is ready.");
Console.ReadKey();
ITradingService dealProxy = new ChannelFactory
dealProxy.BeginDeal();
Trade trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
dealProxy.AddTrade(trade);
dealProxy.AddFunction("InterestRateEstimation");
dealProxy.AddFunction("TechnologyStockEstimation");
decimal dealValue = dealProxy.Calculate();
Console.WriteLine("Deal value estimated at ${0}",dealValue);
dealProxy.Purchase();
dealProxy.EndDeal();
((IChannel)dealProxy).Close();
Console.WriteLine();
Console.WriteLine("Done.");
Console.ReadKey();
}
}
}
7. Build the Client project to confirm the absence of any syntax errors.
1. Modify the Startup Project properties of the TradingService solution as shown in figure 1.5, below.
Figure 1.5 The Startup Project Properties of the TradingService Solution
2. Choose Debug and then Start Debugging from the Visual Studio menus.
3. When you see a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application window of the TradingServiceHost should be similar to the output shown in figure 1.6, below, while the output in the console application window of the Client should be similar to the output in figure 1.7. The numbers shown in your console application windows may vary slightly from the numbers shown in the figures, due to variations in the prevailing market conditions over time.
Figure 1.6 Output from the Trading Service
Figure 1.7 Output from the Trading Service Client
4. Enter a keystroke into the console application window of the Client executable to terminate it, and enter a keystroke into the console application window of the TradingServiceHost executable to terminate the trading service.
In this exercise, you will use the Windows Communication Foundation’s facilities for managing state within a service.
1. Examine the code of the TradingSystem class in the TradingSystem.cs module of the TradingService project of the TradingService solution constructed in the previous exercise:
using System;
using System.Collections.Generic;
using System.Text;
namespace Fabrikam
{
public class TradingSystem: ITradingService
{
#region ITradingService Members
string ITradingService.BeginDeal()
{
string dealIdentifier = Guid.NewGuid().ToString();
return dealIdentifier;
}
void ITradingService.AddTrade(Trade trade)
{
Console.WriteLine(string.Format("Added trade for {0}",trade));
}
void ITradingService.AddFunction(string function)
{
Console.WriteLine(string.Format("Added function {0}",function));
}
decimal ITradingService.Calculate()
{
Decimal value = DateTime.Now.Millisecond/10;
Console.WriteLine(string.Format("Calculated value as {0}",value));
return value;
}
void ITradingService.Purchase()
{
Console.WriteLine("Purchased!");
}
void ITradingService.EndDeal()
{
Console.WriteLine("Completed deal.");
}
#endregion
}
}
By default, a new instance of the TradingSystem class will be created every time any of the class’ methods are invoked via the Windows Communication Foundation. Therefore, by default, if the AddTrade() method is invoked one or more times, and then the Purchase() method, none of the data received from the invocations of the AddTrade() method will be available in the execution of the Purchase() method.
Now, suppose that problem is overcome, and when the Purchase() method is invoked, all of the data received from preceding invocations of the AddTrade() method are available. An additional problem is that the client on behalf of which the Purchase() method is being invoked may not be the same client on behalf of whom all of the calls to the AddTrade() method had been made, so one client’s activities might inadvertently result in the execution of trades defined by another client.
Furthermore, nothing in the code of the TradingSystem class ensures that the methods of the class are invoked in a valid sequence. Thus, not only is it possible for the Purchase() method to be invoked before the AddTrade() method has ever been invoked, but it is also possible for the EndDeal() method to be invoked before the BeginDeal() method has ever been invoked.
You will solve all of these problems quickly in the next few tasks. They will be tackled in reverse order.
1. Alter the code in the ITradingService.cs module of the TradingService project as shown here:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
[ServiceContract(Session=true)]
public interface ITradingService
{
[OperationContract(IsInitiating=true,IsTerminating=false)]
string BeginDeal();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddTrade(Trade trade);
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddFunction(string function);
[OperationContract(IsInitiating=false,IsTerminating=false)]
decimal Calculate();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=false)]
void Purchase();
[OperationContract(IsInitiating=false,IsTerminating=true,IsOneWay=true)]
void EndDeal();
}
}
1. Include the System.ServiceModel namespace in the TradingSystem.cs module of the TradingService project.
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
2. Alter the code in the TradingSystem.cs module of the TradingService project in this way:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class TradingSystem: ITradingService
1. Add this DealData class to the Fabrikam namespace in the TradingSystem.cs module of the TradingService project:
internal class DealData: IExtension
{
private string dealIdentifier = null;
private List
public DealData(string dealIdentifier)
{
this.dealIdentifier = dealIdentifier;
this.trades = new List
}
public string DealIdentifier
{
get
{
return this.dealIdentifier;
}
}
public void AddTrade(Trade trade)
{
trades.Add(trade);
}
public Trade[] Trades
{
get
{
return this.trades.ToArray();
}
}
#region IExtension
void IExtension
{
}
void IExtension
{
}
#endregion
}
The DealData class defines the structure of the state information that will be stored between the invocations of the operations of the Derivatives Trading Service.
2. Alter the BeginDeal() method of the TradingService class in this way, to keep track of the identifier for a deal, by storing it in a DealData object within the extensions to the Windows Communication Foundation’s InstanceContext:
string ITradingService.BeginDeal()
{
string dealIdentifier = Guid.NewGuid().ToString();
OperationContext.Current.InstanceContext.Extensions.Add(new DealData(dealIdentifier));
Console.WriteLine("Started deal {0}",dealIdentifier);
return dealIdentifier;
}
3. Modify the AddTrade() method of the TradingService class to keep track of trades added to deals:
void ITradingService.AddTrade(Trade trade)
{
DealData dealData = OperationContext.Current.InstanceContext.Extensions.Find
dealData.AddTrade(trade);
Console.WriteLine("Added trade for {0}",trade);
}
4. Change the Purchase() method of the TradingService class so that it operates on the trades that have been added to a deal:
void ITradingService.Purchase()
{
DealData dealData = OperationContext.Current.InstanceContext.Extensions.Find
foreach(Trade trade in dealData.Trades)
{
Console.WriteLine("Purchased {0}",trade);
}
}
5. Alter the EndDeal() method to use the deal identifier that was stored by the BeginDeal() method.
void ITradingService.EndDeal()
{
DealData dealData = OperationContext.Current.InstanceContext.Extensions.Find
Console.WriteLine("Completed deal: {0}",dealData.DealIdentifier);
}
1. Alter the interface, ITradingService in the ITradingService.cs module of the Client project in the TradingService solution, in this way:
[ServiceContract(Session=true)]
public interface ITradingService
{
[OperationContract(IsInitiating=true,IsTerminating=false)]
string BeginDeal();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddTrade(Trade trade);
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddFunction(string function);
[OperationContract(IsInitiating=false,IsTerminating=false)]
decimal Calculate();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=false)]
void Purchase();
[OperationContract(IsInitiating=false,IsTerminating=true,IsOneWay=true)]
void EndDeal();
}
1. Choose Debug and then Start Debugging from the Visual Studio menus.
2. When you see a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application window of the TradingServiceHost should be similar to the output shown in figure 1.6, above, while the output in the console application window of the Client should be similar to the output in figure 1.7. The numbers shown in your console application windows may vary slightly from the numbers shown in the figures, due to variations in the prevailing market conditions over time.
3. Enter a keystroke into the console application window of the Client executable to terminate it, and enter a keystroke into the console application window of the TradingServiceHost executable to terminate the trading service.
In this exercise, you will witness the Windows Communication Foundation’s reliable messaging capabilities.
1. Examine the ITradingService interface in the TradingService project.
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
[ServiceContract(Session=true)]
public interface ITradingService
{
[OperationContract(IsInitiating=true,IsTerminating=false)]
string BeginDeal();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddTrade(Trade trade);
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddFunction(string function);
[OperationContract(IsInitiating=false,IsTerminating=false)]
decimal Calculate();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=false)]
void Purchase();
[OperationContract(IsInitiating=false,IsTerminating=true,IsOneWay=true)]
void EndDeal();
}
}
The IsInitiating and IsTerminating parameters that we have provided for the [OperationContract] attribute ensure that any sequence of invocations of the operations defined by the interface begin with an invocation of the BeginDeal() method, and end with the invocation of the EndDeal() method. However, if a client invokes the BeginDeal() method, and then the AddTrade() method twice, followed by the Purchase() method and then the EndDeal() method, but one of the invocations of the AddTrade() method never reaches the service, the operations of the service will still be invoked in a valid sequence, and the missing AddTrade() invocation will go unnoticed. Similarly, if the invocations of the AddTrade() method both arrive, but one is delayed and arrives after the invocation of the Purchase() method, then, once again, the operations of the service will still be invoked in a valid sequence, but not the sequence that the client intended. Fortunately, with the Windows Communication Foundation, guarding against these problems is very easy. In the next task, you will configure reliable messaging to ensure that no messages are lost.
1. Alter the definition of the ITradingService interface in the TradingService solution in this way:
[ServiceContract(Session=true)]
[DeliveryRequirements(RequireOrderedDelivery=true)]
public interface ITradingService
2. Adjust the definition of the ITradingService interface in the Client solution in the same way:
[ServiceContract(Session=true)]
[DeliveryRequirements(RequireOrderedDelivery=true)]
public interface ITradingService
3. Modify the app.config file in the TradingServiceHost project of the TradingService solution, configuring the binding of the Derivatives Trading service to incorporate the Windows Communication Foundation’s implementation of the WS-ReliableMessaging protocol:
address="http://localhost:8000/TradingService"
binding="wsHttpBinding"
bindingConfiguration="ReliableHttpBinding"
contract="Fabrikam.ITradingService,TradingService"/>
4. Make corresponding modifications to the app.config file in the Client project:
address="http://localhost:8000/TradingService"
binding="wsHttpBinding"
bindingConfiguration="ReliableHttpBinding"
contract="Client.ITradingService,Client"/>
1. Choose Debug and then Start Debugging from the Visual Studio menus.
2. When you see a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application window of the TradingServiceHost should be similar to the output shown in figure 1.6, above, while the output in the console application window of the Client should be similar to the output in figure 1.7. The numbers shown in your console application windows may vary slightly from the numbers shown in the figures, due to variations in the prevailing market conditions over time.
3. Enter a keystroke into the console application window of the Client executable to terminate it, and enter a keystroke into the console application window of the TradingServiceHost executable to terminate the trading service.
By completing the previous task, you will have seen that the Derivatives Trading Service and its client can communicate with one another without error via a reliable messaging protocol. However, because you were not able to see transmissions between the client and the service going astray, you were also not able to see for yourself how the reliable messaging protocol would compensate for such mishaps. Now you will add some logic to intercept transmissions from the client and randomly and visibly discard some of them.
1. Add a new class module called InterceptorBinding.cs to the Client project, and replace the default contents of that class with this code:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.Text;
using System.Threading;
using System.Xml;
namespace Client
{
public class MessageInterceptor : IMessageInterceptor
{
private static Random randomizer = new Random(DateTime.Now.Millisecond);
public void ProcessSend(ref Message message)
{
string action = message.Headers.Action;
Console.WriteLine(string.Format("Voting on message {0} ...", action));
if (!(action.Contains("ITradingService")))
{
return;
}
int randomNumber = MessageInterceptor.randomizer.Next(10);
if (randomNumber > 6)
{
Console.WriteLine("Dropping message.");
message = null;
return;
}
Console.WriteLine("Forwarding message.");
}
public void ProcessReceive(ref Message message)
{
}
}
public interface IMessageInterceptor
{
void ProcessReceive(ref Message message);
void ProcessSend(ref Message message);
}
public class InterceptorBindingElement : BindingElement
{
IMessageInterceptor interceptor = null;
public InterceptorBindingElement()
{
this.interceptor = new MessageInterceptor();
}
protected InterceptorBindingElement(InterceptorBindingElement other)
: base(other)
{
this.interceptor = other.interceptor;
}
public override T GetProperty
{
throw new Exception("The method or operation is not implemented.");
}
public override IChannelFactory
{
InterceptorChannelFactory
result.InnerChannelFactory = context.BuildInnerChannelFactory
return result;
}
public override bool CanBuildChannelFactory
{
return base.CanBuildChannelFactory
}
public override BindingElement Clone()
{
return new InterceptorBindingElement(this);
}
}
class NullMessageInterceptor : IMessageInterceptor
{
public void ProcessSend(ref Message message)
{
}
public void ProcessReceive(ref Message message)
{
}
}
public class InterceptorChannelFactory
{
IMessageInterceptor interceptor;
public InterceptorChannelFactory(IMessageInterceptor interceptor, IDefaultCommunicationTimeouts timeouts)
: base(timeouts)
{
if (interceptor == null)
{
this.interceptor = new NullMessageInterceptor();
}
else
{
this.interceptor = interceptor;
}
}
public IMessageInterceptor Interceptor
{
get
{
return interceptor;
}
}
public override MessageVersion MessageVersion
{
get
{
return InnerChannelFactory.MessageVersion;
}
}
public override string Scheme
{
get
{
return InnerChannelFactory.Scheme;
}
}
protected override Type GetCommunicationObjectType()
{
return typeof(TChannel);
}
protected override TChannel OnCreateChannel(EndpointAddress remoteAddress, Uri via)
{
InnerChannelFactory.Open();
TChannel innerChannel = InnerChannelFactory.CreateChannel(remoteAddress, via);
if (typeof(TChannel) == typeof(IOutputChannel))
{
return (TChannel)(object)new InterceptorOutputChannel(this, (IOutputChannel)innerChannel);
}
else if (typeof(TChannel) == typeof(IRequestChannel))
{
return (TChannel)(object)new InterceptorRequestChannel(this, (IRequestChannel)innerChannel);
}
else if (typeof(TChannel) == typeof(IOutputSessionChannel))
{
return (TChannel)(object)new InterceptorOutputSessionChannel(this, (IOutputSessionChannel)innerChannel);
}
else if (typeof(TChannel) == typeof(IRequestSessionChannel))
{
return (TChannel)(object)new InterceptorRequestSessionChannel(this,
(IRequestSessionChannel)innerChannel);
}
throw new ArgumentException("Channel type is not supported.", "TChannel");
}
protected override void OnOpen(TimeSpan timeout)
{
ThrowIfInnerFactoryNotSet();
base.OnOpen(timeout);
}
void ThrowIfInnerFactoryNotSet()
{
if (this.InnerChannelFactory == null)
{
throw new InvalidOperationException("Inner channel factory is not set.");
}
}
IChannelFactory
public IChannelFactory
{
get { return innerChannelFactory; }
set { innerChannelFactory = value; }
}
class InterceptorOutputChannel : InterceptorChannelBase
{
InterceptorChannelFactory
public InterceptorOutputChannel(InterceptorChannelFactory
: base(factory, innerChannel, factory.interceptor)
{
this.factory = factory;
}
public EndpointAddress RemoteAddress
{
get { return this.InnerChannel.RemoteAddress; }
}
public Uri Via
{
get { return this.InnerChannel.Via; }
}
public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state)
{
return this.BeginSend(message, factory.DefaultSendTimeout, callback, state);
}
public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state)
{
ThrowIfDisposedOrNotOpen();
return new SendAsyncResult
}
public void EndSend(IAsyncResult result)
{
SendAsyncResult
}
public void Send(Message message)
{
this.Send(message, factory.DefaultSendTimeout);
}
public void Send(Message message, TimeSpan timeout)
{
ThrowIfDisposedOrNotOpen();
Interceptor.ProcessSend(ref message);
if (message == null)
{
OnDropMessage();
}
else
{
this.InnerChannel.Send(message, timeout);
}
}
}
class InterceptorRequestChannel : InterceptorChannelBase
{
InterceptorChannelFactory
public InterceptorRequestChannel(InterceptorChannelFactory
: base(factory, innerChannel, factory.interceptor)
{
this.factory = factory;
}
public EndpointAddress RemoteAddress
{
get { return this.InnerChannel.RemoteAddress; }
}
public Uri Via
{
get { return this.InnerChannel.Via; }
}
public IAsyncResult BeginRequest(Message message, AsyncCallback callback, object state)
{
return this.BeginRequest(message, factory.DefaultSendTimeout, callback, state);
}
public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state)
{
ThrowIfDisposedOrNotOpen();
return new RequestAsyncResult(this, message, timeout, callback, state);
}
public Message EndRequest(IAsyncResult result)
{
return RequestAsyncResult.End(result);
}
public Message Request(Message message)
{
return this.Request(message, factory.DefaultSendTimeout);
}
public Message Request(Message message, TimeSpan timeout)
{
ThrowIfDisposedOrNotOpen();
Interceptor.ProcessSend(ref message);
if (message == null)
{
return null;
//OnDropMessage();
//return Message.CreateMessage(new DroppedMessageFault("Request Message dropped by interceptor."));
}
else
{
Message reply = this.InnerChannel.Request(message, timeout);
Interceptor.ProcessReceive(ref reply);
if (reply == null)
{
OnDropMessage();
return Message.CreateMessage(MessageVersion.Soap11WSAddressing10, new DroppedMessageFault("Reply Message dropped by the interceptor."), null);
}
else
{
return reply;
}
}
}
}
class InterceptorOutputSessionChannel : InterceptorOutputChannel, IOutputSessionChannel
{
IOutputSessionChannel innerSessionChannel;
public InterceptorOutputSessionChannel(InterceptorChannelFactory
: base(factory, innerChannel)
{
this.innerSessionChannel = innerChannel;
}
public IOutputSession Session
{
get { return innerSessionChannel.Session; }
}
internal override void OnDropMessage()
{
Fault();
innerSessionChannel.Abort();
}
}
class InterceptorRequestSessionChannel : InterceptorRequestChannel, IRequestSessionChannel
{
IRequestSessionChannel innerSessionChannel;
public InterceptorRequestSessionChannel(InterceptorChannelFactory
IRequestSessionChannel innerChannel)
: base(factory, innerChannel)
{
this.innerSessionChannel = innerChannel;
}
public IOutputSession Session
{
get { return innerSessionChannel.Session; }
}
internal override void OnDropMessage()
{
Fault();
innerSessionChannel.Abort();
}
}
}
public class InterceptorChannelBase
{
TChannel innerChannel;
IMessageInterceptor interceptor;
protected InterceptorChannelBase(ChannelManagerBase manager, TChannel innerChannel, IMessageInterceptor interceptor)
: base(manager)
{
this.innerChannel = innerChannel;
this.interceptor = interceptor;
}
internal TChannel InnerChannel
{
get { return this.innerChannel; }
}
internal IMessageInterceptor Interceptor
{
get { return this.interceptor; }
}
protected override void OnAbort()
{
this.innerChannel.Abort();
}
protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
{
return this.innerChannel.BeginClose(timeout, callback, state);
}
protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
{
return this.innerChannel.BeginOpen(timeout, callback, state);
}
protected override void OnClose(TimeSpan timeout)
{
this.innerChannel.Close(timeout);
}
protected override void OnEndClose(IAsyncResult result)
{
this.innerChannel.EndClose(result);
}
protected override void OnEndOpen(IAsyncResult result)
{
this.innerChannel.EndOpen(result);
}
protected override void OnOpen(TimeSpan timeout)
{
this.innerChannel.Open(timeout);
}
internal virtual void OnDropMessage() { }
}
public class SendAsyncResult
{
Message message;
TimeSpan timeout;
InterceptorChannelBase
public SendAsyncResult(InterceptorChannelBase
: base(callback, state)
{
this.message = message;
this.timeout = timeout;
this.channel = channel;
channel.Interceptor.ProcessSend(ref this.message);
if (this.message == null)
{
channel.OnDropMessage();
Complete(true);
}
else
{
channel.InnerChannel.BeginSend(this.message, timeout, new AsyncCallback(HandleCallback), null);
}
}
void HandleCallback(IAsyncResult asyncResult)
{
try
{
channel.InnerChannel.EndSend(asyncResult);
Complete(false);
}
catch (Exception e)
{
Complete(false, e);
}
}
public new WaitHandle AsyncWaitHandle
{
get { return base.AsyncWaitHandle; }
}
public new bool IsCompleted
{
get { return base.IsCompleted; }
}
public new bool CompletedSynchronously
{
get { return base.CompletedSynchronously; }
}
public static void End(IAsyncResult result)
{
if (result == null)
{
throw new ArgumentNullException("result");
}
SendAsyncResult
if (outputResult == null)
{
throw new ArgumentException("Invalid AsyncResult", "result");
}
AsyncResult.End(outputResult);
}
}
public abstract class AsyncResult : IAsyncResult
{
AsyncCallback callback;
object state;
bool completedSynchronously;
bool endCalled;
Exception exception;
bool isCompleted;
ManualResetEvent manualResetEvent;
protected AsyncResult(AsyncCallback callback, object state)
{
this.callback = callback;
this.state = state;
}
public object AsyncState
{
get { return state; }
}
public WaitHandle AsyncWaitHandle
{
get
{
if (manualResetEvent != null)
{
return manualResetEvent;
}
lock (ThisLock)
{
if (manualResetEvent == null)
{
manualResetEvent = new ManualResetEvent(isCompleted);
}
}
return manualResetEvent;
}
}
public bool CompletedSynchronously
{
get { return completedSynchronously; }
}
public bool IsCompleted
{
get { return isCompleted; }
}
object ThisLock
{
get { return this; }
}
protected void Complete(bool completedSynchronously)
{
if (isCompleted)
{
throw new InvalidOperationException("AsyncResults can only be completed once.");
}
ManualResetEvent manualResetEvent = null;
this.completedSynchronously = completedSynchronously;
if (completedSynchronously)
{
// If we completedSynchronously, then there's no chance that the manualResetEvent was created so
// we don't need to worry about a race
this.isCompleted = true;
if (this.manualResetEvent != null)
{
throw new InvalidOperationException("No ManualResetEvent should be created for a synchronous AsyncResult.");
}
}
else
{
lock (ThisLock)
{
this.isCompleted = true;
manualResetEvent = this.manualResetEvent;
}
}
try
{
if (callback != null)
{
callback(this);
}
if (manualResetEvent != null)
{
manualResetEvent.Set();
}
}
catch (Exception unhandledException)
{
// The callback raising an exception is equivalent to Main raising an exception w/out a catch.
// Queue it onto another thread and throw it there to ensure that there's no other handler in
// place and the default unhandled exception behavior occurs.
// Because the stack trace gets lost on a rethrow, we're wrapping it in a generic exception
// so the stack trace is preserved.
unhandledException = new Exception("AsyncCallbackException", unhandledException);
ThreadPool.UnsafeQueueUserWorkItem(new WaitCallback(RaiseUnhandledException), unhandledException);
}
}
protected void Complete(bool completedSynchronously, Exception exception)
{
this.exception = exception;
Complete(completedSynchronously);
}
protected static void End(AsyncResult asyncResult)
{
if (asyncResult == null)
{
throw new ArgumentNullException("asyncResult");
}
if (asyncResult.endCalled)
{
throw new InvalidOperationException("Async object already ended.");
}
asyncResult.endCalled = true;
if (!asyncResult.isCompleted)
{
using (WaitHandle waitHandle = asyncResult.AsyncWaitHandle)
{
waitHandle.WaitOne();
}
}
if (asyncResult.exception != null)
{
throw asyncResult.exception;
}
}
void RaiseUnhandledException(object o)
{
Exception exception = (Exception)o;
throw exception;
}
}
public class RequestAsyncResult : AsyncResult
{
Message message;
TimeSpan timeout;
InterceptorChannelBase
public RequestAsyncResult(InterceptorChannelBase
: base(callback, state)
{
this.message = message;
this.timeout = timeout;
this.channel = channel;
channel.Interceptor.ProcessSend(ref this.message);
if (this.message == null)
{
channel.OnDropMessage();
//this.message = Message.CreateMessage(new DroppedMessageFault("Request Message dropped by interceptor."));
Complete(true);
}
else
{
channel.InnerChannel.BeginRequest(this.message, timeout, new AsyncCallback(HandleCallback), null);
}
}
void HandleCallback(IAsyncResult asyncResult)
{
try
{
Message reply = channel.InnerChannel.EndRequest(asyncResult);
channel.Interceptor.ProcessReceive(ref reply);
if (reply == null)
{
channel.OnDropMessage();
this.message = Message.CreateMessage(MessageVersion.Soap11WSAddressing10,new DroppedMessageFault("Reply Message dropped by interceptor."),null);
}
else
{
this.message = reply;
}
Complete(false);
}
catch (Exception e)
{
Complete(false, e);
}
}
public new WaitHandle AsyncWaitHandle
{
get { return base.AsyncWaitHandle; }
}
public new bool IsCompleted
{
get { return base.IsCompleted; }
}
public new bool CompletedSynchronously
{
get { return base.CompletedSynchronously; }
}
public static Message End(IAsyncResult result)
{
if (result == null)
{
throw new ArgumentNullException("result");
}
RequestAsyncResult requestResult = result as RequestAsyncResult;
if (requestResult == null)
{
throw new ArgumentException("Invalid AsyncResult", "result");
}
AsyncResult.End(requestResult);
return requestResult.message;
}
}
public class ReplyAsyncResult : AsyncResult
{
Message message;
TimeSpan timeout;
IRequestContext innerContext;
InterceptorChannelBase
public ReplyAsyncResult(InterceptorChannelBase
TimeSpan timeout, IRequestContext innerContext, AsyncCallback callback, object state)
: base(callback, state)
{
this.message = message;
this.timeout = timeout;
this.innerContext = innerContext;
this.channel = channel;
channel.Interceptor.ProcessSend(ref this.message);
if (this.message == null)
{
channel.OnDropMessage();
this.message = Message.CreateMessage(MessageVersion.Soap11WSAddressing10,new DroppedMessageFault("Reply Message dropped by interceptor."), null);
Complete(true);
}
else
{
innerContext.BeginReply(this.message, new AsyncCallback(HandleCallback), null);
}
}
void HandleCallback(IAsyncResult asyncResult)
{
try
{
innerContext.EndReply(asyncResult);
Complete(false);
}
catch (Exception e)
{
Complete(false, e);
}
}
public new WaitHandle AsyncWaitHandle
{
get { return base.AsyncWaitHandle; }
}
public new bool IsCompleted
{
get { return base.IsCompleted; }
}
public new bool CompletedSynchronously
{
get { return base.CompletedSynchronously; }
}
public static void End(IAsyncResult result)
{
if (result == null)
{
throw new ArgumentNullException("result");
}
ReplyAsyncResult requestResult = result as ReplyAsyncResult;
if (requestResult == null)
{
throw new ArgumentException("Invalid AsyncResult", "result");
}
AsyncResult.End(requestResult);
}
}
public class DroppedMessageFault : MessageFault
{
FaultReason faultReason;
FaultCode faultCode;
public DroppedMessageFault(string reason)
{
faultReason = new FaultReason(reason);
faultCode = new FaultCode("DroppedMessageFault");
}
public override FaultCode Code
{
get { return faultCode; }
}
public override bool HasDetail
{
get { return false; }
}
public override FaultReason Reason
{
get { return faultReason; }
}
protected override void OnWriteDetail(XmlDictionaryWriter writer, EnvelopeVersion version)
{
throw new NotImplementedException("The method or operation is not implemented.");
}
protected override void OnWriteDetailContents(XmlDictionaryWriter writer)
{
throw new Exception("The method or operation is not implemented.");
}
}
}
2. Examine the ProcessSend() method of the MessageInterceptor class that you have added to the InterceptorBindingElement module.
public void ProcessSend(ref Message message)
{
string action = message.Headers.Action;
Console.WriteLine("Voting on message {0} ...",action);
if(!(action.Contains("ITradingService")))
{
return;
}
int randomNumber = MessageInterceptor.randomizer.Next(10);
if(randomNumber > 6)
{
Console.WriteLine("Dropping message.");
message = null;
return;
}
Console.WriteLine("Forwarding message.");
}
This method receives messages and randomly annihilates about thirty percent of them. You will now have the Windows Communication Foundation send all messages outbound from the client to the Derivatives Trading Service through this method directly after they have been processed by the reliable messaging system within the client. By doing so, you will simulate the effect of messages getting lost in transmission between the client and the server, and you will see the Windows Communication Foundation’s reliable messaging facilities compensate for the loss.
3. Add a reference to the System.Configuration assembly to the Client project of the Trading Service solution.
4. Add a new class module called ReliableHttpBinding.cs to the Client project, and alter the code therein in this way to define a custom binding that incorporates your message interceptor:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
namespace Client
{
public class ReliableHttpBinding: CustomBinding, IBindingDeliveryCapabilities
{
public ReliableHttpBinding()
{
this.Elements.Add(new ReliableSessionBindingElement(true));
this.Elements.Add(new InterceptorBindingElement());
this.Elements.Add(new HttpTransportBindingElement());
}
#region IBindingCapabilities Members
bool IBindingDeliveryCapabilities.AssuresOrderedDelivery
{
get
{
return true;
}
}
bool IBindingDeliveryCapabilities.IsTransactionAware
{
get
{
return true;
}
}
bool IBindingDeliveryCapabilities.QueuedDelivery
{
get
{
return false;
}
}
#endregion
}
}
5. Copy the ReliableHttpBinding.cs class in the Client project, and paste into the TradingServiceHost project, so that there is a copy of that class module in both of those two projects.
6. Modify the copy in the TradingServiceHost project to use the Fabrikam namespace and to omit the message interceptor by commenting out the line of code which adds it to the binding elements. It will suffice for messages to be lost on the way from the client:
namespace Fabrikam
{
public ReliableHttpBinding()
{
this.Elements.Add(new ReliableSessionBindingElement(true));
//this.Elements.Add(new InterceptorBindingElement());
this.Elements.Add(new HttpTransportBindingElement());
}
7. Modify the code in the Program.cs module of the Client project to use the new custom binding when transmitting to the Derivatives Trading Service:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Client
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press any key when the service is ready.");
Console.ReadKey();
do
{
ITradingService dealProxy = new ChannelFactory
dealProxy.BeginDeal();
Trade trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
dealProxy.AddTrade(trade);
dealProxy.AddFunction("InterestRateEstimation");
dealProxy.AddFunction("TechnologyStockEstimation");
decimal dealValue = dealProxy.Calculate();
Console.WriteLine(string.Format("Deal value estimated at ${0}",dealValue));
dealProxy.Purchase();
dealProxy.EndDeal();
((IChannel)dealProxy).Close();
Console.WriteLine("Press q to quit or any other key to continue.");
}
while(Console.ReadKey().KeyChar != 'q');
Console.WriteLine();
Console.WriteLine("Done.");
Console.ReadKey();
}
}
}
8. Change the code in the Program.cs module of the TradingServiceHost project to add an endpoint to the Derivatives Trading Service that uses the new custom binding as well:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
public class Program
{
public static void Main(string[] args)
{
using(ServiceHost host = new ServiceHost(typeof(TradingSystem)))
{
host.AddServiceEndpoint(typeof(ITradingService),new ReliableHttpBinding(),"http://localhost:8001/TradingService");
host.Open();
Console.WriteLine("The trading service is available.");
Console.ReadKey();
}
}
}
}
1. Choose Debug and then Start Debugging from the Visual Studio menus.
2. When you see a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application window of the TradingServiceHost should be similar to the output shown in figure 1.6, above, almost unchanged from how it has been hitherto. However, the output in the console application window of the Client executable should be similar to the output in figure 3.1, below, where one can see that a message has been destroyed at random and automatically retried.
Figure 3.1 Automatic Compensation for Lost Messages
3. Choose Debug and Stop Debugging from the Visual Studio menus.
4. You will not need the service endpoint that you configured to use the custom binding again, so remove it from the code in the Program.cs module of the TradingServiceHost project. You will be adding an endpoint in the next task. Note that your application will not run again until you have completed Exercise 4.
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
public class Program
{
public static void Main(string[] args)
{
using(ServiceHost host = new ServiceHost(typeof(TradingSystem)))
{
//host.AddServiceEndpoint(typeof(ITradingService),new ReliableHttpBinding(),"http://localhost:8001/TradingService");
host.Open();
Console.WriteLine("The trading service is available.");
Console.ReadKey();
}
}
}
}
In this exercise, you will build a Windows Communication Foundation service that receives messages via an MSMQ message queue. The service will be used for recording trades of derivatives.
1. Choose Add or Remove Programs from the Windows Control panel.
2. Click Add/Remove Windows Components.
3. Select Application Server in the Windows Components Wizard window as shown in figure 4.1, and click on the Details button.
Figure 4.1 The Windows Components Wizard
4. If Message Queuing is not checked in the Application Server window, as shown in figure 4.2, below, then check it, and click on the button labeled, OK, and then on the button labeled, Next, in the Windows Components Wizard, and follow the instructions on the subsequent screens to install MSMQ. Otherwise, click on the button labeled, Cancel, in the Application Server window and the Windows Components Wizard. Close the Add or Remove Programs window.
Figure 4.2 The Application Server Window of the Windows Components Wizard
1. Add a C# class library project called, TradeRecordingService to the TradingService solution.
2. Add references to the System.ServiceModel and System.Runtime.Serialization assemblies to the project.
3. In the TradeRecordingService project, rename the Class1.cs class module, ITradeRecorder.cs, and modify its code to look like this:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
[ServiceContract]
[DeliveryRequirements(QueuedDeliveryRequirements = QueuedDeliveryRequirementsMode.Required)]
public interface ITradeRecorder
{
[OperationContract(IsOneWay=true)]
void RecordTrades(Trade[] trades);
}
}
4. Copy the class module, Trade.cs, from the TradingService project and paste into to the TradeRecordingService project so that both projects have a copy of the module.
5. Add a class module called TradeRecorder.cs to the TradeRecordingService project, and modify its code to look like this:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class TradeRecorder: ITradeRecorder
{
public TradeRecorder()
{
}
#region ITradeRecorder Members
void ITradeRecorder.RecordTrades(Trade[] trades)
{
Console.WriteLine("Recording trade ...");
foreach(Trade trade in trades)
{
Console.WriteLine(string.Format("Recorded trade for {0}",trade));
}
}
#endregion
}
}
6. Compile the TradeRecordingService project to ensure that there are no syntax errors.
1. Add a C# console application project called, TradeRecordingServiceHost, to the TradingService solution.
2. Add references to the System.Messaging and System.ServiceModel assemblies to the project.
3. Add a reference to the TradeRecordingService project to the TradeRecordingServiceHost project.
4. In the TradeRecordingServiceHost project, modify the code of the Program.cs module to look like this:
using System;
using System.Collections.Generic;
using System.Messaging;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
public class Program
{
private const string queueName = @".\private$\traderecording";
public static void Main(string[] args)
{
if(!(MessageQueue.Exists(queueName)))
{
MessageQueue.Create(queueName,true);
}
TradeRecorder tradeRecorder = new TradeRecorder();
using(ServiceHost host = new ServiceHost(tradeRecorder))
{
host.Open();
Console.WriteLine("The trade recording service is available.");
Console.ReadKey();
}
}
}
}
1. Add an application configuration file called, app.config to the TradingRecordingServiceHost project, and alter its contents to look like this:
address="net.msmq://localhost/private/TradeRecording"
binding="netMsmqBinding"
bindingConfiguration="QueuedBinding"
contract="Fabrikam.ITradeRecorder"/>
1. Build the TradingService solution.
2. Start a new instance of the TradeRecordingServiceHost project.
3. When you see a message in the console application window of the TradeRecordingServiceHost executable confirming that the trade recording service is available, enter a keystroke into the console application window to terminate it.
1. Copy the module, ITradeRecorder.cs, from the TradingRecordingService project and paste into to the TradingService project so that both projects have a copy of the module.
2. Modify code of the Purchase() method of the TradingSystem class in the TradingService project so that the Trading Service uses the Trade Recording Service to record derivatives trades:
void ITradingService.Purchase()
{
DealData dealData = OperationContext.Current.InstanceContext.Extensions.Find
ITradeRecorder proxy = new ChannelFactory
proxy.RecordTrades(dealData.Trades);
foreach(Trade trade in dealData.Trades)
{
Console.WriteLine(string.Format("Purchased {0}",trade));
}
}
3. Modify the app.config file of the TradingServiceHost project to specify how the Trading Service is to communicate with the Trade Recording Service:
address="http://localhost:8000/TradingService"
binding="wsHttpBinding"
bindingConfiguration="ReliableHttpBinding"
contract="Fabrikam.ITradingService,TradingService"/>
address="net.msmq://localhost/private/TradeRecording"
binding="netMsmqBinding"
bindingConfiguration="QueuedBinding"
contract="Fabrikam.ITradeRecorder"/>
4. Modify the code in the Program.cs module of the Client project of the TradingService solution so as to circumvent the mechanism for discarding messages that you used in the last exercise:
do
{
ITradingService dealProxy = new ChannelFactory
dealProxy.BeginDeal();
Trade trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
dealProxy.AddTrade(trade);
dealProxy.AddFunction("InterestRateEstimation");
dealProxy.AddFunction("TechnologyStockEstimation");
decimal dealValue = dealProxy.Calculate();
Console.WriteLine(string.Format("Deal value estimated at ${0}",dealValue));
dealProxy.Purchase();
dealProxy.EndDeal();
((IChannel)dealProxy).Close();
Console.WriteLine("Press q to quit or any other key to continue.");
}
while(Console.ReadKey().KeyChar != 'q');
5. Set the Startup Project properties of the TradingService solution as shown in figure 4.3, below.
Figure 4.3 The Startup Project Properties of the TradingService Solution
6. Choose Debug and then Start Debugging from the Visual Studio menus.
7. When you see a message in the console application window of the TradeRecordingServiceHost confirming that the Trade Recording Service is available, and a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application window of the TradingServiceHost should be similar to the output shown in figure 1.6, above. The output in the console application window of the Client should be similar to the output in figure 1.7, above. The output in the console application window of the TradeRecordingServiceHost should be similar to the output shown in figure 4.4, below.
Figure 4.4 Output from the Trade Recording Service
8. Choose Debug and Stop Debugging from the Visual Studio menus.
9. Choose Debug and then Start Debugging from the Visual Studio menus.
1. Choose Debug and then Start Debugging from the Visual Studio menus.
2. Close the console application window of the TradeRecordingServiceHost.
3. When you see a message in the console application window of the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application window of the TradingServiceHost should be similar to the output shown in figure 1.6, above. The output in the console application window of the Client should be similar to the output in figure 1.7, above.
4. Choose Debug and Stop Debugging from the Visual Studio menus.
5. Start a new instance of the TradeRecordingServiceHost.
6. The output in the console application window of the TradeRecordingServiceHost should be similar to the output shown in figure 4.4, above, thereby proving that the Trade Recording Service can process messages that were sent to it when it was not available.
7. Choose Debug and Stop Debugging from the Visual Studio menus.
In this exercise, you will witness the Windows Communication Foundation’s ability to compose multiple distributed operations into a single unit of work, or transaction. We will assume that a client using the Derivatives Trading Service that we have been constructing will want to estimate the price of two deals: a primary deal, and another one that is intended as a hedge against the possibility of losses in the first. The client will either commit to both deals together, or to neither one. The client will commit to both deals together if the difference in the estimated value of the two deals differs within a certain range.
1. Begin by modifying the client to conform to the preceding assumptions, altering the code in the Program.cs class of the Client project in this way:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
using System.Transactions;
namespace Client
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press any key when the service is ready.");
Console.ReadKey();
do
{
Console.WriteLine();
using(TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
decimal? dealValue = null;
decimal? hedgeValue = null;
ITradingService dealProxy = null;
ITradingService hedgeProxy = null;
try
{
dealProxy = new ChannelFactory
dealProxy.BeginDeal();
Trade trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
dealProxy.AddTrade(trade);
dealProxy.AddFunction("InterestRateEstimation");
dealProxy.AddFunction("TechnologyStockEstimation");
dealValue = dealProxy.Calculate();
dealProxy.Purchase();
dealProxy.EndDeal();
hedgeProxy = new ChannelFactory
hedgeProxy.BeginDeal();
trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
hedgeProxy.AddTrade(trade);
hedgeProxy.AddFunction("InterestRateEstimation");
hedgeProxy.AddFunction("TechnologyStockEstimation");
hedgeValue = hedgeProxy.Calculate();
hedgeProxy.Purchase();
hedgeProxy.EndDeal();
if((dealValue != null)&&(hedgeValue != null))
{
Console.WriteLine("Deal value is ${0}, and hedge value is ${1}",dealValue.Value,hedgeValue.Value);
if((dealValue.Value - hedgeValue.Value) > 20m)
{
Console.WriteLine("Voting to complete trade!");
scope.Complete();
}
else
{
Console.WriteLine("Voting NOT to complete
trade! Transaction will not complete. Keep
trying until a transaction commits.");
}
}
}
catch(Exception exception)
{
Console.WriteLine(exception.Message);
}
finally
{
((IChannel)dealProxy).Close();
((IChannel)hedgeProxy).Close();
}
}
Console.WriteLine("Press q to quit or any other key to continue.");
}
while(Console.ReadKey().KeyChar != 'q');
Console.WriteLine();
Console.WriteLine("Done.");
Console.ReadKey();
}
}
}
2. Add a reference to the System.Transactions assembly to the Client project of the TradingService solution.
1. Now enhance the Derivatives Trading Service so that it will only record any single trade if the client commits to all of the trades that it is considering together. To do so, add an attribute to the Purchase() method of the TradingSystem class in the TradingSystem.cs module of the TradingService project.
[OperationBehavior(TransactionScopeRequired=true,TransactionAutoComplete=true)]
void ITradingService.Purchase()
{
DealData dealData = OperationContext.Current.InstanceContext.Extensions.Find
ITradeRecorder proxy = new ChannelFactory
proxy.RecordTrades(dealData.Trades);
Console.WriteLine("Purchased!");
}
2. Add an attribute indicating that the Purchase() operation can be composed into a transaction in the declaration of the operation in the ITradingService.cs module of the TradingService project:
public interface ITradingService
{
[OperationContract(IsInitiating=true,IsTerminating=false)]
string BeginDeal();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddTrade(Trade trade);
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddFunction(string function);
[OperationContract(IsInitiating=false,IsTerminating=false)]
decimal Calculate();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=false)]
[TransactionFlow(TransactionFlowOption.Allowed)]
void Purchase();
[OperationContract(IsInitiating=false,IsTerminating=true,IsOneWay=true)]
void EndDeal();
}
3. Modify the app.config file of the TradingServiceHost project to specify that information about any transaction in progress is to be passed to the Derivatives Trading Service:
transactionFlow="true" >
1. In the Client project of the TradingService solution, alter the definition of the ITradingService interface to conform to the new definition in the TradingService project:
public interface ITradingService
{
[OperationContract(IsInitiating=true,IsTerminating=false)]
string BeginDeal();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddTrade(Trade trade);
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=true)]
void AddFunction(string function);
[OperationContract(IsInitiating=false,IsTerminating=false)]
decimal Calculate();
[OperationContract(IsInitiating=false,IsTerminating=false,IsOneWay=false)]
[TransactionFlow(TransactionFlowOption.Allowed)]
void Purchase();
[OperationContract(IsInitiating=false,IsTerminating=true,IsOneWay=true)]
void EndDeal();
}
2. Change the configuration of the binding in the app.config file of the Client to match the definition of the binding in the configuration of the service:
transactionFlow="true" >
1. In the TradingService solution in Visual Studio, choose Debug and then Start Debugging from the menus.
2. When you see a message in the console application window of the TradeRecordingServiceHost confirming that the Trade Recording Service is available, and a message in the TradingServiceHost executable confirming that the derivatives trading service is available, enter a keystroke into the console application window of the Client executable. The output in the console application windows of the Client executable, the TradingServiceHost executable, and the Trade RecordingHost executable should be similar to the output in figures 5.1, 5.2, and 5.3, below. There we see that the client has sent four purchase methods to the service, but only committed to the first pair, and, consequently, only two trades show up in the output of the Trade Recording Service.
Figure 5.1 Output from the Trading Service Client
Figure 5.2 Output from the Trading Service
Figure 5.3 Output from the Trade Recording Service
3. Choose Debug and Stop Debugging from the Visual Studio menus.
In this exercise, you will add custom performance counters to enhance the built-in instrumentation of the Trade Recording Service.
1. To the TradeRecorder.cs module in the TradeRecordingService project, add code to create a custom performance counter category called, TradeRecording, and code to add a custom performance counter called, Trade Volume, to that category:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class TradeRecorder: ITradeRecorder
{
private const string CounterCategoryName = "TradeRecording";
private const string VolumeCounterName = "Trade Volume";
private PerformanceCounterCategory counterCategory = null;
public TradeRecorder()
{
if (PerformanceCounterCategory.Exists(TradeRecorder.CounterCategoryName))
{
PerformanceCounterCategory.Delete(TradeRecorder.CounterCategoryName);
}
CounterCreationData volumeCounter = new CounterCreationData(TradeRecorder.VolumeCounterName, "Volume of trading.",PerformanceCounterType.NumberOfItemsHEX32);
CounterCreationDataCollection counterCollection = new CounterCreationDataCollection(new CounterCreationData[] { volumeCounter });
this.counterCategory = PerformanceCounterCategory.Create(TradeRecorder.CounterCategoryName,"Trade Recording Data",PerformanceCounterCategoryType.MultiInstance,counterCollection);
}
#region ITradeRecorder Members
void ITradeRecorder.RecordTrades(Trade[] trades)
{
Console.WriteLine("Recording trade ...");
foreach(Trade trade in trades)
{
Console.WriteLine(string.Format("Recorded trade for {0}",trade));
}
}
#endregion
}
}
1. For each instance of the Trade Recording Service to be able to update the value of the newly-defined trading volume performance counter for trades communicated across its own receiving queue, it must access its own instance of the performance counter. That can be accomplished using the facilities of Windows Management Instrumentation, so add a reference to the System.Management assembly to the TradeRecordingService project in the TradingService solution.
2. Include the System.Management, System.Management.Instrumentation, System.ServiceModel.Description, and System.Runtime.InteropServices namespaces in the TradeRecorder.cs module of the TradeRecordingService project:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management;
using System.Management.Instrumentation;
using System.Runtime.InteropServices;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Text;
3. To the TradeRecorder class in the TradeRecorder.cs module, add the InitializeCounters() method for retrieving the instance of the trading volume performance counter associated with the current instance of the Trade Recording Service:
private PerformanceCounter volumeCounter = null;
public void InitializeCounters(ServiceEndpointCollection endpoints)
{
List
foreach (ServiceEndpoint endpoint in endpoints)
{
names.Add(string.Format("{0}@{1}", this.GetType().Name, endpoint.Address.ToString()));
}
while (true)
{
try
{
foreach (string name in names)
{
string condition = string.Format("SELECT * FROM Service WHERE Name=\"{0}\"", name);
SelectQuery query = new SelectQuery(condition);
ManagementScope managementScope = new ManagementScope(@"\\.\root\ServiceModel", new ConnectionOptions());
ManagementObjectSearcher searcher = new ManagementObjectSearcher(managementScope, query);
ManagementObjectCollection instances = searcher.Get();
foreach (ManagementBaseObject instance in instances)
{
PropertyData data = instance.Properties["CounterInstanceName"];
this.volumeCounter = new PerformanceCounter(TradeRecorder.CounterCategoryName, TradeRecorder.VolumeCounterName, data.Value.ToString());
this.volumeCounter.ReadOnly = false;
this.volumeCounter.RawValue = 0;
break;
}
}
break;
}
catch(COMException)
{
}
}
if(this.volumeCounter != null)
{
Console.WriteLine("Volume counter initialized.");
}
Console.WriteLine("Counters initialized.");
}
4. Include the System.Threading namespaces in the TradeRecorder.cs module of the TradeRecordingService project:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management;
using System.Management.Instrumentation;
using System.Runtime.InteropServices;
using System.ServiceModel;
using System.Text;
using System.Threading;
5. Modify the RecordTrades() method of the TradeRecorder class in the TradeRecordingService.cs module to update the value of the trading volume performance counter:
private long tradeCount = 0;
void ITradeRecorder.RecordTrades(Trade[] trades)
{
Console.WriteLine("Recording trade ...");
lock (this)
{
while (this.volumeCounter == null)
{
Thread.Sleep(100);
}
}
foreach(Trade trade in trades)
{
this.tradeCount+=((trade.Count != null)?trade.Count.Value:0);
this.volumeCounter.RawValue = this.tradeCount;
Console.WriteLine(string.Format("Recorded trade for {0}",trade));
}
}
6. Modify the app.config file of the TradeRecordingServiceHost project to expose data via performance counters and via Windows Management Instrumentation :
address="net.msmq://localhost/private/TradeRecording"
binding="netMsmqBinding"
bindingConfiguration="QueuedBinding"
contract="Fabrikam.ITradeRecorder"/>
7. Alter the code in the Program.cs module of the TradeRecordingServiceHost project to call the InitializeCounter() method that you added to the TradeRecordingService:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
namespace Fabrikam
{
class Program
{
string queueName = “.\\private$\\TradeRecording”;
static void Main(string[] args)
{
if(!(MessageQueue.Exists(queueName)))
{
MessageQueue.Create(queueName,true);
}
TradeRecorder tradeRecorder = new TradeRecorder();
using(ServiceHost host = new ServiceHost(tradeRecorder))
{
host.Open();
tradeRecorder.InitializeCounters(host.Description.Endpoints);
Console.WriteLine("The trade recording service is available.");
Console.ReadKey();
}
}
}
}
1. Modify the code in the Program.cs file of the Client project so as to generate a large number of trades:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
using System.Transactions;
namespace Client
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press any key when the service is ready.");
Console.ReadKey();
//do
for(int i = 0;i < 500;i++)
{
Console.WriteLine();
using(TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
decimal? dealValue = null;
decimal? hedgeValue = null;
ITradingService dealProxy = null;
ITradingService hedgeProxy = null;
try
{
dealProxy = new ChannelFactory
dealProxy.BeginDeal();
Trade trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
dealProxy.AddTrade(trade);
dealProxy.AddFunction("InterestRateEstimation");
dealProxy.AddFunction("TechnologyStockEstimation");
dealValue = dealProxy.Calculate();
dealProxy.Purchase();
dealProxy.EndDeal();
hedgeProxy = new ChannelFactory
hedgeProxy.BeginDeal();
trade = new Trade();
trade.Count = 10;
trade.Symbol = "MSFT";
trade.Date = DateTime.Now.AddMonths(2);
hedgeProxy.AddTrade(trade);
hedgeProxy.AddFunction("InterestRateEstimation");
hedgeProxy.AddFunction("TechnologyStockEstimation");
hedgeValue = hedgeProxy.Calculate();
hedgeProxy.Purchase();
hedgeProxy.EndDeal();
if((dealValue != null)&&(hedgeValue != null))
{
Console.WriteLine("Deal value is ${0}, and hedge value is ${1}",dealValue.Value,hedgeValue.Value);
if((dealValue.Value - hedgeValue.Value) > 20m)
{
Console.WriteLine("Voting to complete trade!");
scope.Complete();
}
}
}
catch(Exception exception)
{
Console.WriteLine(exception.Message);
}
finally
{
((IChannel)dealProxy).Close();
((IChannel)hedgeProxy).Close();
}
}
//Console.WriteLine("Press q to quit or any other key to continue.");
}
//while(Console.ReadKey().KeyChar != 'q');
Console.WriteLine();
Console.WriteLine("Done.");
Console.ReadKey();
}
}
}
2. Choose Debug and then Start Debugging from the Visual Studio menus.
3. When you see a message in the console application window of the TradeRecordingServiceHost confirming that the Trade Recording Service is available, and a message in the TradingServiceHost executable confirming that the derivatives trading service is available, choose Run from the Windows Start menu, enter,
PerfMon
in the Run dialog box and click on the OK button.
4. In the Performance window, click the Add button, and then scroll through the list of performance counter categories in the list labeled Performance Object, as shown in figure 6.1, below. Examine the counters in the built-in categories of Windows Communication Foundation performance counters. Those categories are called, ServiceModelService, ServiceModelEndpoint, and ServiceModelOperation, and provide counters for monitoring Windows Communication Foundation applications at the level of the service, the endpoints of a service, and the individual operations of a service endpoint.
Figure 6.1 Output from the Trade Recording Service
5. Select the custom TradeRecording category, and click on the Add button, and then close the dialog.
6. In the console application window of the Client executable, enter a keystroke.
7. Observe how, as soon as the client starts committing to trades of derivatives, the graph of the value of the custom Trade Volume counter begins to show activity in the Performance window, as depicted in figure 6.2, below.
8. The value of the Trade Volume counter will quickly exceed the default scale of the graph, so right-click on the entry for the Trade Volume counter in the list in the bottom right-hand corner of the Performance window, as show in figure 6.3, below, and alter the scale to 0.01, as shown in figure 6.4, below.
Figure 6.2 Monitoring the Custom Trade Volume Performance Counter
Figure 6.3 Accessing the Performance Counter Display Properties
Figure 6.3 Adjusting the Scale of the Display of a Performance Counter
Lab Summary
Building a distributed application for the real world is not an easy task—in addition to building the software resource, you’ve got to guard against dropped messages due to network outages or congested networks, failures in the services you’re communicating with, and messages being processed in an unexpected order. But as we’ve seen, the Windows Communication Foundation helps you build applications that are resilient to these problems, so your application can support your business even when problems arise.