Objective
The purpose of this article is to create a service which accepts messages from any client and redistributes those messages to all subscribed clients, and a client that can subscribe to the service, send messages to it and receive unrelated messages from it regardless of how many it sends. The result is a client/service combination using WCF (Windows Communications Foundation).
Example Origin
The example is driven by a system I am working on, where purchase and consumption data needs to be shared in real time between different installations.
This is actually my first ever C# application, my system being written in C++/CLI. By I opted for C# in for this feature because it offers neater communications protocols in the WCF.
Introducing the Sample Application - and its Heritage
I have chosen to use the example of a simple messaging service to learn, and now illustrate how I learned duplex WCF correspondence. The service maitains a list of subscribed clients, and when any one of them sends in a message, it distributes it to all subscribed clients. Each client in turn, subscribes to the service with a 'Join' request, sends message to the service, accepts any messages the service throws at it and eventually may opt to leave the service.
Initially I wanted to use webservices in C++/CLI but there was no proper dublex communication protocol available, the best being implementing both client and server functionality on every installation. So off I went to research C# and the WCF.
My entire offering here has its origins in two excellent sources of inspiration, both of which were essential in achieving my objective.
- [TROELSEN] - Chapter 25 of "Pro C# 2010 and the .NET 4 Platform (Fifth Edition)" By Andrew Troelsen
- [BARNES] - "WCF: Duplex Operations and UI Threads" by Jeff Barnes, right here on CodeProject
There are times in the text where I may appear critical of one or the other. But the reality is that both have very different target audiences and both are excellent in how they address those audiences.
[TROELSEN] is for beginners, and without it I would not have been able to get started with WCF, but doesn't do much besides make reference to the existence of duplex activity. On the other hand [BARNES] provides an excellent article on duplex communication, but I was completely lost when I tried to use it as an introduction to WCF.
WCF - Easy as ABC
Before we go any further I recommend you read one of the many introductory excellent articles on WCF here at Codeproject, or an author such as [TROELSEN]
The only theory I will emphasize ie the importance of ABC:
- Address - the address of your new service
- Binding - the transport protocol e.g. HTTP / TCP used to carry the messages
- Contract - a description of the set of method that will handle the message exchange
The Service
The Service is the central hub of all the communications that will take place between the clients. To keep things clear, I have implemented it and the other two aspects, the host and the client each in their own solution
Create a new C# Class Library project named GPH_QuickMessageServicelib.lib (xxxxlib.lib could be considered name overkill but the host and service that follow will be similarly named so having lib at the end helps preserve uniqueness in the namespace titles).
Click OK
Close Class1.cs. Use the Solution Explorer to rename Class1.cs as GPH_QuickMessageService.cs, and answer Yes when prompted to extend the rename to all references as seen below.
Reopen GPH_QuickMessageService.cs now. Add a reference to the System.ServiceModel.dll assembly using the Solution Explorer again, right clicking on GPH_QuickMessageServiceLib seen here highlighted.
Choose Add Reference from the menu (Add Service Reference will be used later when you need to add the working service to a client)
Include a using
statement in GPH_QuickMessageService.cs so that it now looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace GPH_QuickMessageServiceLib
{
public class GPH_QuickMessageService
{
}
}
The Contract
I am going to use a single contract in this example represented by a pair of interfaces called IMessageServiceInbound
and IMessageServiceCallback
. The objective of the interfaces is to allow clients to register with the service on a Publish and Subscribe basis, where users Identify themselves to the system. The interface will
- Accept messages from the user
- Route those messages to the named registered users
- Notify the user as other users Register with and Resign from the service
Warning
The system will allow you to omit [OperationContract]
from your methods, but if you make that omission, those methods will not be exposed through the WCF Runtime [TROELSEN].
We are defining them in GPH_QuickMessageService.cs. The [ServiceContract]
attribute on the IMessageServiceInbound
interface notifies WCF that a service is to be operated. Adding CallbackContract
property attaches the call back interface (IMessageServiceCallback
) to it. This forms an association between the two interfaces, and in order to consume the service operations, the client must implement the callback contract and host the object for invocation by the service [BARNES].
IMessageServiceCallback
does not have to be given a ServiceContract
attribute because WCF considers it implied due to it being listed as the CallBackContract
. You may add it for completeness if you wish.
This is the current state of GPH_QuickMessageService.cs:
[ServiceContract(
Name = "GPH_QuickMessageService",
Namespace = "GPH_QuickMessageServiceLib",
SessionMode = SessionMode.Required,
CallbackContract = typeof(IMessageServiceCallback))]
public interface IMessageServiceInbound
{
[OperationContract]
int JoinTheConversation(string userName);
[OperationContract(IsOneWay = true)]
void ReceiveMessage(string userName,List<string> addressList, string userMessage);
[OperationContract]
int LeaveTheConversation(string userName);
}
public interface IMessageServiceCallback
{
[OperationContract(IsOneWay = true)]
void NotifyUserJoinedTheConversation(string userName);
[OperationContract(IsOneWay = true)]
void NotifyUserOfMessage(string userName, String userMessage);
[OperationContract(IsOneWay = true)]
void NotifyUserLeftTheConversation(string userName);
}
The functions you see here in the contract handle receive Join requests, Messages, Leave Requests and use three corresponding Notify functions to let the other clients know what is happening
Adding a Behavior to the Contract
A behavior in the words of [TROELSEN] allows you to qualify further the functionality of a host, service or client. The example I am using is from [BARNES] where he is using the behavior to determine concurrency: "Single is the default concurrency mode, but it never hurts to be explicit Note the single threading model still functions properly with use of one way methods for callbacks since they are marked as one way on the contract."
[ServiceBehavior(
ConcurrencyMode = ConcurrencyMode.Single,
InstanceContextMode = InstanceContextMode.PerCall)]
Interfacing the Service Class with the Contract
The inbound contract is implemented as an interface on the service class:
public class GPH_QuickMessageService : IMessageServiceInbound
{
}
This is conventional webservice communication, but the outbound contract has not been forgotten. The CallbackContract
attribute on the ServiceContract
definition ties it to the inbound contract.
Service Implementation
I will follow the [TROELSEN] example here and continue coding in GPH_QuickMessageService.cs, the [BARNES] example puts the implementation of the callback in its own .cs – but while I learn the practice of using WCF I what to see as much of the mechanics in close proximity as possible. I can modularize them to follow best practice after I become comfortable with the constructs. While [BARNES] is clearly laid out in a modular format, as a beginner I am finding it difficult to make the mental connection between the components.
[TROELSEN] only has one function to create to satisfy his contract with which his service is complete and, but my example here following the [BARNES] model has six, and critically three of them are callbacks. There is still work to do, but the [BARNES] narrative is difficult for the beginner to see what comes next.
First up, is the definition of a list to hold the call back channels:
private static List<IMessageServiceCallback> _callbackList = new List<IMessageServiceCallback>();
Of course, not forgetting a default constructor:
public GPH_QuickMessageService() { }
The service has six methods or functions three of which handle inbound messages (JoinTheConversation
,ReceiveMessage
and LeaveTheConversation
). These will be mirrored by three others on the client side (NotifyUserJoinedTheConversation
, NotifyUserOfMessage
, and NotifyUserLeftTheConversation
) who recieve those messages for each subscribed client. You have already met these at a high level when you defined your contract above. Now we are implementing that contract
JoinTheConversation
This method receives requests from clients to join the conversation. It creates a user variable of type IMessageServiceCallback
, checks to see if this user is already on the callback list (adding it if not). It finishes up by notifying all users that this new user has joined the conversation.
public int JoinTheConversation(string userName)
{
IMessageServiceCallback registeredUser =
OperationContext.Current.GetCallbackChannel<IMessageServiceCallback>();
if (!_callbackList.Contains(registeredUser))
{
_callbackList.Add(registeredUser);
}
_callbackList.ForEach(
delegate(IMessageServiceCallback callback)
{
callback.NotifyUserJoinedTheConversation(userName);
_registeredUsers++;
});
return _registeredUsers;
}
ReceiveMessage
Currently a very simple method, it broadcasts a message and its author to the list of registered users. It contains an address list variable that is unused. It has been declared for future use when the functionality is tailored to broadcast to specific users.
public void ReceiveMessage(string userName,List<string> addressList, string userMessage)
{
_callbackList.ForEach(
delegate(IMessageServiceCallback callback)
{ callback.NotifyUserOfMessage(userName, userMessage); });
}
LeaveTheConversation
This method removes a user from the callback list and notifies the remaining users that the removed users has left the conversation.
public int LeaveTheConversation(string userName)
{
IMessageServiceCallback registeredUser =
OperationContext.Current.GetCallbackChannel<IMessageServiceCallback>();
if (_callbackList.Contains(registeredUser))
{
_callbackList.Remove(registeredUser);
_registeredUsers--;
}
_callbackList.ForEach(
delegate(IMessageServiceCallback callback)
{ callback.NotifyUserLeftTheConversation(userName); });
return _registeredUsers;
}
Compiling
Your service library is now ready to compile. Remember again, the three notify methods referenced above will be implemented on the client side to receive any notifications distributed by the service.
Concurrency
A quick note on concurrency before moving on. The IsOneWay
property on the OperationContract
attribute looks out of place on a duplex example. This is a single threaded piece of code, so the thread is locked while each message is being handled. When a function returns a reply, there is a risk of causing deadlock or a timeout. The IsOneWay
property means that there are no replies sent to the originator. Any responses posted by other users are in the form of new messages. My preferred approach is a multithreaded application but I have a while to go before putting one of those in place. All OneWay functions must be declared void. My Join and Leave methods do return a response, but the client I will implement chooses to ignore them. If the client was to wait on those responses it would lock up until they arrived without putting some form of threading in place.
[BARNES]. The use of one way service operations allows the callback to occur while still using the single concurrency mode rather than Reentrant or Multiple. See the [BARNES] example for more on the Concurrency Mode including how it is used in the service to prevent locking.
Hosting the service
I will follow [TROELSEN] on this and use a new solution to host the new message service. In time I will use a windows service for this, but at the moment I am sticking with a console application as per the text I am following. This new console application will be called GPH_QuickMessageServiceHost.
To begin with, it needs the System.ServiceModel and GPH_MessageServiceLib. Do this using the Solution Explorer as above. Use the Browse tab to hunt for the GPH_MessageServiceLib.dll.
These also have to be imported into the code file:
using System.ServiceModel;
using GPH_QuickMessageServiceLib;
Endpoints (The term used to describe the ABC of WCF or Address / Binding /Contract rolled into one) etc can be defined in the source code itself, but I prefer to use an App.Config for them. Add one now using Project->Add New Item:
Click Add to complete.
Add this XML Snippet between the configuration tags in the App.Config file:
<system.serviceModel>
<services>
<service name="GPH_QuickMessageServicelib.GPH_QuickMessageService">
<endpoint address ="http://localhost:8080/GPH_QuickMessageService"
binding="basicHttpBinding"
contract="GPH_QuickMessageService.IMessageServiceInbound"/>
</service>
</services>
</system.serviceModel>
service name
has specified the DLL name followed by the implementing class name.
endpoint address
is a URL style reference to the local machine including the implementing class name. This can be any URL anywhere on the web where the service is running.
binding
Is one of the standards in this case basicHttpBinding, dictated by the use of an http address.
contract
specifies the implementing class name and its interface to the Service Contract.
All the key details are wrapped in the <system.serviceModel>
</system.serviceModel>
tags.
Code to Host the Service
This is the piece of magic in the host main method that gets the service up and running:
using (ServiceHost serviceHost = new ServiceHost(typeof(GPH_QuickMessageService)))
{
serviceHost.Open();
Console.WriteLine("The service is ready.");
Console.WriteLine("Press the Enter key to terminate service.");
Console.ReadLine();
}
This distills down to defining a variable of type ServiceHost, tied to the WCF service defined earlier and invoking its open method.
This can now be compiled and run to launch a working service. All it needs now is a client. But it doesn’t launch. A quick spin on the debugger suggested:
Clearly there is work to do before moving on to the Client.
Change the binding in App.Config to wsDualHTTPbinding.
binding="wsDualHttpBinding"
service name="GPH_QuickMessageServicelib.GPH_QuickMessageService"
and
contract="GPH_QuickMessageService.IMessageServiceInbound"
Need to become:
service name="GPH_QuickMessageServiceLib.GPH_QuickMessageService"
and
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"
This in turn may kick out another issue:
The Exe needs to run with administrator privileges. Locate it using explorer, right click on it and choose Run as administrator. Answer Yes when asked to confirm the action. This is the Console window now:
Alternatively, launch Visual Studio in administrator mode to begin with.
Enabling MEX
MEX or Metadata Exchange is used to define runtime behaviours to further tune how the service will behave. I have followed the [TROELSEN] example in implementing them here – they also feature in the [BARNES] model. In this example I am adding a new Endpoint for MEX, a WCF behaviour to allow HTTP GET access, and the behaviourConfiguration
attribute will be used match the behaviour up to the service. A Host element will define the base address for the benefit of MEX.
Rework the App.config file as follows:
<system.serviceModel>
<services>
<service name="GPH_QuickMessageServiceLib.GPH_QuickMessageService"
behaviorConfiguration = "QuickMessageServiceMEXBehavior">
<endpoint address ="service"
binding="wsDualHttpBinding"
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"/>
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress ="http://localhost:8080/GPH_QuickMessageService"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="QuickMessageServiceMEXBehavior" >
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
Compile the host again, and look at the service in a web browser: http://localhost:8080/GPH_QuickMessageService.
Building a Client
Building a client using C# is a new departure for me because this is my first time using the language.
I am going to begin by creating a new Windows Forms Application called GPH_QuickMessageServiceClient.
Click OK. Then use the solution explorer to rename From1.cs to MessageForm.cs, clicking Yes when asked:
You will need to have your service running for the next step to succeed. Staying with Solution Explorer, right click on the solution, but this time choose Add Service Reference. Paste in the URL http://localhost:8080/GPH_QuickMessageService into the address box on the Add Service Reference dialog:
Click ‘Go’, and the IDE will attempt to download the service details. This is the expected outcome:
I was happy to leave the name at ServiceReference1, but you are free to change it if you wish. According to [TROELSEN] this adds the proxy for the service and any references the WCF assemblies automatically. In practice it will create an App.config if your project does not have one, or add to an existing one if it is already there, it also creates the proxy called Reference.cs. The proxy is the representative of the service at the client site. Expand the Reference entry on the Solution Explorer to see them. Next right-click on MessageForm.cs and select View Code from the popup. There are some of Using statement needed:
using System.ServiceModel;
using System.Threading;
using System.ServiceModel;
using GPH_QuickMessageServiceClient.ServiceReference1;
You will also need to add the reference to system.ServiceModel as you did for the serivce library. We has already added its using statement.
Return to the form designer and modify MessageForm until it looks something like:
It includes four command buttons, brnJoin
, btnSend
, btnLeave
, and btnExit
along with three text boxes, txtName
, txtMessageOutbound
, and txtMessageLog
which will need the Scrollbars
property set to Vertical
. txtName
will need a Text_Changed
event on it.
We will begin with Join, Leave and Send disabled. Join will become available when a name is entered. If that name is accepted, then the name box will become readonly, Join is disabled and Send enabled.
Add form load, form closing and a text changed (on Name) events, also add click events on each of the buttons. MessageForm.cs also needs some communication information.
public partial class MessageForm : Form
{
public MessageForm()
is extended to become:
[CallbackBehavior(
ConcurrencyMode = ConcurrencyMode.Single,
UseSynchronizationContext = false)]
public partial class MessageForm : Form, ServiceReference1.GPH_QuickMessageServiceCallback
{
private SynchronizationContext _uiSyncContext = null;
private ServiceReference1.GPH_QuickMessageServiceClient _GPH_QuickMessageService = null;
public MessageForm()
form_load
The form load event needs to signal its presence to the service:
_uiSyncContext = SynchronizationContext.Current;
_GPH_QuickMessageService =
new ServiceReference1.GPH_QuickMessageServiceClient(new InstanceContext(this),
"WSDualHttpBinding_GPH_QuickMessageService");
_GPH_QuickMessageService.Open();
The name WSDualHttpBinding_GPH_QuickMessageService
comes from the auto generated app.config.
Set the initial states of the fields and buttons on the form:
this.btnJoin.Enabled = false;
this.btnSend.Enabled = false;
this.btnLeave.Enabled = false;
this.btnExit.Enabled = true;
this.txtMessageOutbound.Enabled = false;
Two event handlers need to be declared:
this.txtName.TextChanged += new EventHandler(txtName_TextChanged);
this.FormClosing += new FormClosingEventHandler(MessageForm_FormClosing);
form_closing
This event fulfills the important task of terminating the relationship with the service when the form is closing:
_GPH_QuickMessageService.Close();
btnJoin
The join click event must contact the service to indicate that it has a user about to join the conversation:
_GPH_QuickMessageService.JoinTheConversation(this.txtName.Text);
It will also change some button states:
this.btnJoin.Enabled = false;
this.btnSend.Enabled = true;
this.btnLeave.Enabled = true;
this.txtMessageOutbound.Enabled = true;
btnLeave
Let the service know that this user is leaving:
_GPH_QuickMessageService.LeaveTheConversation(this.txtName.Text);
Update the button/field states:
this.btnJoin.Enabled = true;
this.btnSend.Enabled = false;
this.btnLeave.Enabled = false;
this.txtMessageOutbound.Enabled = false;
btnSend
Very little to see here, just the message being forwarded to the service:
_GPH_QuickMessageService.ReceiveMessage(this.txtName.Text, null, this.txtMessageOutbound.Text);
btnExit
btnExit
also fires a message at the service indicating that the user is leaving the conversation, but in addition it triggers the event to close the form
_GPH_QuickMessageService.LeaveTheConversation(this.txtName.Text);
this.Close();
txtName_TextChanged
This event will only change the state of the Join button. It has no WCF aspect whatsoever.
if (this.txtName.Text != String.Empty)
{
this.btnJoin.Enabled = true;
}
WriteMessage
WriteMessage
is used to format the messages that will be displayed in txtMessageLog
. Its a personal choice with no critical WCF role.
private void WriteMessage(string message)
{
string format = this.txtMessageLog.Text.Length > 0 ? "{0}\r\n{1} {2}" : "{0}{1} {2}";
this.txtMessageLog.Text = String.Format(format, this.txtMessageLog.Text,
DateTime.Now.ToShortTimeString(), message);
this.txtMessageLog.SelectionStart = this.txtMessageLog.Text.Length - 1;
this.txtMessageLog.ScrollToCaret();
}
Inbound traffic on the client
So far all the client has done is pass messages to the service in a very standard way. Now its time for the Notify methods that we first met way back when we defined the contract in the service library. These will take messages from the service and display them in the txtMessageLog
box.
[BARNES] placed the callback methods in their own region within the form class. I have followed that practice, but based on my tests it appears to be no more than a question of style.
#region GPH_QuickMessageServiceCallback Methods
NotifyUserJoinedTheConversation
NotifyUserJoinedTheConversation
takes arg_Name
as a parameter. It receives the name of each new client joining the conversation and relays it to the current client. I defer to [BARNES] for a techincal explanation of what is going on in this method:
The UI thread won't be handling the callback, but it is the only one allowed to update the controls. So, we will dispatch the UI update back to the UI sync context.
SendOrPostCallback callback =
delegate(object state)
{
string msg_user = state.ToString();
msg_user = msg_user.ToUpper();
this.WriteMessage(String.Format("[{0}] has joined the conversation.", msg_user));
};
_uiSyncContext.Post(callback, arg_Name);
NotifyUserOfMessage
This function takes the author and content of messages from the service.
SendOrPostCallback callback =
delegate(object state)
{
this.WriteMessage(String.Format("[{0}]: {1}", arg_Name.ToUpper(), arg_Message));
};
_uiSyncContext.Post(callback, arg_Name);
NotifyUserLeftTheConversation
The final method in the client example, its role is to inform the current user when it receives word from the service that other users have left the conversation.
SendOrPostCallback callback =
delegate(object state)
{
string msg_user = state.ToString();
msg_user = msg_user.ToUpper();
this.WriteMessage(String.Format("[{0}] has left the conversation.", msg_user));
};
_uiSyncContext.Post(callback, arg_Name);
Compile Errors
After you compile everything up, there will be three or four compile errors on the auto-generated proxy in Reference.cs. I don't have an explanation for why this is occurring but there are four instances over three lines where the client namespace title GPH_QuickMessageServiceClient
has to be removed from before the reference ServiceReference1
The earliest instance reported is on line 15 at column 208
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(15,208): error CS0426:
The type name 'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
[System.ServiceModel.ServiceContractAttribute(
Namespace="GPH_QuickMessageServiceLib",
ConfigurationName="ServiceReference1.GPH_QuickMessageService",
CallbackContract=typeof(
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceCallback),
SessionMode=System.ServiceModel.SessionMode.Required)]
becomes
[System.ServiceModel.ServiceContractAttribute(
Namespace="GPH_QuickMessageServiceLib",
ConfigurationName="ServiceReference1.GPH_QuickMessageService",
CallbackContract=typeof(ServiceReference1.GPH_QuickMessageServiceCallback),
SessionMode=System.ServiceModel.SessionMode.Required)]
On line 45 at column 85
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(43,85): error CS0426:
The type name 'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
public interface GPH_QuickMessageServiceChannel :
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageService,
System.ServiceModel.IClientChannel {
becomes
public interface GPH_QuickMessageServiceChannel :
ServiceReference1.GPH_QuickMessageService, System.ServiceModel.IClientChannel {
On line 48 there are two, at column 125 and at column 199
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(48,125): error CS0426: The type name
'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(48,199): error CS0426:
The type name 'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
Change
public partial class GPH_QuickMessageServiceClient :
System.ServiceModel.DuplexClientBase<
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageService>,
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageService {
to become
public partial class GPH_QuickMessageServiceClient :
System.ServiceModel.DuplexClientBase<servicereference1.gph_quickmessageservice>,
ServiceReference1.GPH_QuickMessageService {</servicereference1.gph_quickmessageservice>
Other Potential Errors
So far you have seen me make a deliberate error only to correct it with the binding, and clear a compile error that as I write this, I have yet to eliminate without editing the auto-generated code. Here are a few others that I met along the way:
The first attempt to run the host produced this beauty in the console window:
***** Console Based WCF Host for GPH_QuickMessageService *****
Unhandled Exception: System.InvalidOperationException: Operations marked
with IsOneWay=true must not declare output parameters,
by-reference parameters or return values.
at System.ServiceModel.Description.TypeLoader.CreateOperationDescription(
ContractDescription contractDescription, MethodInfo methodInfo,
MessageDirection direction, ContractReflectionInfo reflectionInfo,
ContractDescription declaringContract)
at System.ServiceModel.Description.TypeLoader.CreateOperationDescriptions(
ContractDescription contractDescription, ContractReflectionInfo reflectionInfo,
Type contractToGetMethodsFrom, ContractDescription declaringContract,
MessageDirection direction)
at System.ServiceModel.Description.TypeLoader.CreateContractDescription(
ServiceContractAttribute contractAttr, Type contractType, Type serviceType,
ContractReflectionInfo& reflectionInfo, Object serviceImplementation)
at System.ServiceModel.Description.TypeLoader.LoadContractDescriptionHelper(
Type contractType, Type serviceType, Object serviceImplementation)
at System.ServiceModel.Description.ContractDescription.GetContract(
Type contractType, Type serviceType)
at System.ServiceModel.ServiceHost.CreateDescription(IDictionary`2& implementedContracts)
at System.ServiceModel.ServiceHostBase.InitializeDescription(
UriSchemeKeyedCollection baseAddresses)
at System.ServiceModel.ServiceHost.InitializeDescription(Type serviceType,
UriSchemeKeyedCollection baseAddresses)
at System.ServiceModel.ServiceHost..ctor(Type serviceType, Uri[] baseAddresses)
at GPH_QuickMessageServiceHost.Program.Main(String[] args) in C:\SBSB\Training -
C#\GPH_QuickMessageServiceHost\GPH_QuickMessageServiceHost\Program.cs:line 16
Press any key to continue . . .
A run on the debugger highlights a problem with the OneWay
attribute (its shown above too but not as clearly):
This was caused by my use of the IsOneWay
on all three service methods - I removed it from Join and Leave to resolve the error. It was a clash with the return codes.
Remember the advice back at the host about running it with admin privileges. This is what you will see if you forget to do that:
This occurred during my first attempt (but not subsequently) because the auto-genterated app.config had /service at the end of the address. On removing it, communication was established:
If something such as above occurs and results in a timeout, here is how it is reported:
Launch the debugging aid, WcfTestClient, from the visual studio command prompt. Include the URL to the service in the command:
wcftestclient http://localhost:8080/GPH_QuickMessageService
The operations are inaccessible. The [BARNES] service is similarly restricted. As I write, I have not learned why.
You will also find that you cannot run the service library on the debugger without having the calling host project in the same solution. I like a 1:1 relationship between projects and solutions for clarity, but I am going to have to set that aside if I need to run a service library on the debugger.
An Enhancement - Targeted Messaging
The code discussed in this section was deliberately kept out of the attached example in the interests of simplicity. However, if you fully understand what was discussed above then adding the next few lines should prove doable and interesting. Here I will show you a means of deploying a directory of subscribers which can be used to decide who gets messages. Here's how the interface would look:
There is now a check list labeled Subscribers to the right of the message box, and as soon as our third subscriber joins, that list is populated with the current subscriber list. In the example above each subscriber is only aware of subscribers joining after them. This is the code used:
GPH_QuickMessageServiceLib
These are the changes to the service library. The callback contract was changed so that NotifyUserJoinedTheConversation
and NotifyUserLeftTheConversation
now include a subscriber list.
public interface IMessageServiceCallback
{
[OperationContract(IsOneWay = true)]
void NotifyUserJoinedTheConversation(string userName, List<string> SubscriberList);
[OperationContract(IsOneWay = true)]
void NotifyUserOfMessage(string userName, String userMessage);
[OperationContract(IsOneWay = true)]
void NotifyUserLeftTheConversation(string userName,List<string> SubscriberList);
}
The service class gets two new attributes - a simple list, SubscriberList
, that will hold the subscriber names to be transmitted back to all subscribers as others come and go, and a dictionary, NotifyList
, this time holding the list of subscribers and their associated callback ID's. I could have explored passing back just the subscriber id from the NotifyList
to eliminate the need for the SubscriberList
, but I leave that for another day.
private static List<string> SubscriberList = new List<string>();
private static Dictionary<string,IMessageServiceCallback> NotifyList =
new Dictionary<string,IMessageServiceCallback>();
JoinTheConversation
needs two new lines in its 'if
' statement:
SubscriberList.Add(userName);
NotifyList.Add(userName,registeredUser);
Change the callback statement to include SubscriberList
in the parameters passed back.
callback.NotifyUserJoinedTheConversation(userName, SubscriberList);
LeaveTheConversation
needs two corresponding new lines in its 'if' statement:
NotifyList.Remove(userName);
SubscriberList.Remove(userName);
Change the callback statement to include SubscriberList
in the parameters passed back.
{ callback.NotifyUserLeftTheConversation(userName, SubscriberList); });
ReceiveMessage
is completely reworked, dispensing with the anonymous delegate technique. The address list from the client is used to look up the NotifyList
, and the callbacks stored there are used to target the message. This is the new body for the method:
foreach (string tmpAddr in addressList)
{
IMessageServiceCallback tmpCallback = NotifyList[tmpAddr];
tmpCallback.NotifyUserOfMessage(userName, userMessage);
}
GPH_QuickMessageServiceHost
GPH_QuickMessageServiceHost
does not require any amendment.
GPH_QuickMessageServiceClient
First off, we need to reload the proxy, so start the service. That done return to the client, right click on ServiceReference1 in the solution explorer and choose Update Service Reference. Add a checked list to the form as illustrated above. I called mine clstSubscriber
.
MessageForm.cs needs some work. It gets a new method to look after refreshing the checked list:
private void ShowUserList(string[] _SubscriberList)
{
clstSubscriber.Items.Clear();
clstSubscriber.Items.Clear();
foreach (string _subscriber in _SubscriberList)
clstSubscriber.Items.Add(_subscriber);
}
btnSend_Click
is completely reworked to read the checked list and pass any checked subscribers to the service.
string[] addressList = new string[clstSubscriber.CheckedItems.Count];
int i = 0;
for (int j = 0; j < clstSubscriber.CheckedItems.Count; j++ )
{
addressList[i++] = (string)clstSubscriber.CheckedItems[j];
}
_GPH_QuickMessageService.ReceiveMessage(this.txtName.Text,
addressList, this.txtMessageOutbound.Text);
NotifyUserJoinedTheConversation
and NotifyUserLeftTheConversation
both get a new If statement to populate the subscriber checked list as their first action.
if (SubscriberList.Count() > 0)
ShowUserList(SubscriberList);
That's it, compile it all up - and don't forget that you will need to remove the compile errors from Reference.cs because you updated the service reference. Your subscribers will now be able to target where their messages go.
Multi Machine Deployment
While it is all well and good sending messages between clients on the same machine, the real value of WCF comes when you have two machines exchanging messages over the internet / intranet. If you wish to use the internet, assign a static IP address to the machine that will run the service - but be very careful because this could compromise your online security.
I chose to use my domestic intranet, and used the maintenance software that came with my router to identify the IP address of two of the machines connected to it. I replaced localhost:8080 in the base address of the host App.Config as follows:
baseAddress ="http://123.456.1.6/GPH_QuickMessageService"
I made the same change to the endpoint address of the client App.Config. I put a copy of the client on another machine then started the service and a client session on the machine with IP ending .6, then ran the other copy of the client on a second machine. I got a message telling me port 80 is in use by another application, Online research suggests IIS as a likely suspect and advises checking the binding.
I added :8001
to the end of the IP address in the client's app.config and redeployed it.
But despite my best efforts I continued to get a message from the client telling me that the target machine was actively refusing a connection. I could see the service from the second machine via a browser, but just could not get my client to hook in.
This is due to security implications on http bindings. I created the same windows account (username/password) on each machine, added the host and client to the firewall exceptions and eventually even dropped the firewall. Still refused.
I read online that net.tcp binding is less strict, so I added net.tcp bindings to both host and client config files as follows:
Host
="1.0"="utf-8"
<configuration>
<system.serviceModel>
<services>
<service name="GPH_QuickMessageServiceLib.GPH_QuickMessageService"
behaviorConfiguration = "QuickMessageServiceMEXBehavior">
<endpoint
address ="service"
binding="netTcpBinding"
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"/>
<endpoint
address ="service"
binding="wsDualHttpBinding"
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"/>
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:8002/GPH_QuickMessageService/" />
<add baseAddress ="http://localhost:8080/GPH_QuickMessageService"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="QuickMessageServiceMEXBehavior" >
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Client
This was a big departure from the auto-generated config to follow the [BARNES] model whoose example I had deployed over two machines in late summer.
="1.0"="utf-8"
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.tcp://192.168.1.4:8002/GPH_QuickMessageService/service"
binding="netTcpBinding"
contract="ServiceReference1.GPH_QuickMessageService"
name="TcpBinding" >
</endpoint>
<endpoint address="http://192.168.1.4:8080/GPH_QuickMessageService/service"
binding="wsDualHttpBinding"
contract="ServiceReference1.GPH_QuickMessageService"
name="WSDualHttpBinding_GPH_QuickMessageService" >
</endpoint>
</client>
</system.serviceModel>
</configuration>
The advice is that this should be sufficient alone, but as a failsafe, I also re-complied my Host and Service.
Remember to make sure your net services are running:
That done, I started the service and a client instance on one machine, then a second client instance on another and they were able to correspond using my message form.
Conclusion
Next up, I want to explore a disconnected correspondence using queues so that the service and its clients to not have to be online simultaneously. I will also have to look at threading and how it could improve performance and to explore fault handling both in terms of how errors are relayed / error recovery.
But the main purpose of this exercise was to learn WCF to allow installations of my ticketing system to communicate effectively. It has succeeded nicely in that. I find it difficult to imagine now that when I started making these notes that I had no practical working knowledge of WCF.
History
- 2012-11-06 - V1.0 - Initial submission.