The main propose of this article is to develop a generic WCF host implementation, to help when developing WCF services inside Visual Studio 2005.
Background
When you need to develop a WCF service, you need to host it somewhere. So far, you have three options: IIS 6.0 (HTTP only), IIS7/WAS (Windows 2008/Vista) or self host (desktop application, windows service, etc.).
A quick look at several development sites shows developers mostly end up coding their own WCF host using a desktop application. The main reasons are habitual: it's cheap, it's easy, it's fun. Create a new console application, add a reference to WCF project, five lines of code and you are ready to go.
Inside a few other sites, you can see people writing windows services to act as host. But you can easily note some drawbacks of this approach: errors are written to Event Viewer, assemblies exclusively locked (so you can't rebuild that assembly) and some additional steps to stop/recompile/restart the host (mav wrote a tip about that).
To help with this scenarios, I wrote a generic console host which receive notifications when those assemblies are rebuilt, dropping their AppDomain
s and starting a new one. To avoid a boring, almost static black screen, I also display some information about the messages received and sent through this host, and exceptions that can be thrown.
If you are using Visual Studio 2008, there is two utilities that do the same thing: WCF Service Host and WCF Test Client. They can be found inside your %ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE directory.
Using the Code
Watching the File System
We start configuring the DelayedFileSystemWatcher
object and looking for all assemblies which already reside inside configured directory.
static Dictionary<string, AppDomain> appDomains = new Dictionary<string, AppDomain>();
static void Main(string[] args)
{
const string pattern = "*.dll";
string dropPath = Path.GetFullPath(
args.Length == 0 ?
ConfigurationManager.AppSettings["DropPath"] ??
Environment.CurrentDirectory : args[0]);
Console.Title = dropPath + Path.DirectorySeparatorChar + pattern;
if (!Directory.Exists(dropPath))
throw new DirectoryNotFoundException(dropPath);
DelayedFileSystemWatcher dfsw = new DelayedFileSystemWatcher(dropPath, pattern);
dfsw.Created += new FileSystemEventHandler(dfsw_CreatedOrChanged);
dfsw.Changed += new FileSystemEventHandler(dfsw_CreatedOrChanged);
dfsw.Deleted += new FileSystemEventHandler(dfsw_Deleted);
foreach(string assemblyFile in Directory.GetFiles(dropPath, pattern))
{
Create(Path.GetFullPath(assemblyFile));
}
dfsw.EnableRaisingEvents = true;
Console.ReadLine();
Console.ResetColor();
}
Note I'm using DelayedFileSystemWatcher
, written by Adrian Hamza. His class encapsulates regular FileSystemWatcher
, adding a time pool. If multiple events triggers inside a time interval (one second in this case), it queues them, removing the duplicates ones.
The events I signed up for let me maintain the AppDomains
list. If an assembly was Deleted
, I need to drop its AppDomain
; If Created
or Changed
, I need to drop and recreate it, since I can't unload a single assembly from an AppDomain
. Basically, I keep references to AppDomain
s I create inside a generic Dictionary<string, AppDomain>
, using assembly full path as key, and catch all Exceptions who can arise, writing them to Console.
Since the whole point isn't there, I'll move into AppDomain
creation and skip this details. You can find the complete and runnable code inside the attachment of this article.
Creating the New AppDomain
In this step, I had to create a class who inherits from MarshalByRefObject
class. I'll need to instantiate this class inside the foreign AppDomain
and there to start the WCF services host. But there are a few gotchas:
- You need to inform the correct configuration file for your service assembly prior to create the
AppDomain
. - You need to enable shadow copy assemblies (using
ShadowCopyFiles
property), or your DLL file will be locked up. Put it simple, the framework copies it to another location, keeping the original one free to be changed. Note it's a string
property; - I had tried to inspect the service assembly for WCF services prior to create
AppDomains
, using Assembly.ReflectionOnlyLoadFrom(assemblyFile)
, but that approach also locks the assembly file. This technique is explained in an Kader Yildirim article, in French. - When you call
CreateInstanceAndUnwrap
with proxy class full name, the proxy instance will be created inside the new AppDomain
. Here is where the magic resides.
class RemoteProxy : MarshalByRefObject
{
static public AppDomain Start(string assemblyFile)
{
AppDomain domain = null;
try
{
AppDomainSetup info = new AppDomainSetup();
info.ShadowCopyFiles = "true";
info.ConfigurationFile = assemblyFile + ".config";
AppDomain appDomain =
AppDomain.CreateDomain(
assemblyFile, null, info);
RemoteProxy proxy = (RemoteProxy)appDomain
.CreateInstanceAndUnwrap(
Assembly.GetExecutingAssembly().FullName,
typeof(RemoteProxy).FullName);
if (!proxy.LoadServices(assemblyFile))
AppDomain.Unload(appDomain);
else
domain = appDomain;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
}
return domain;
}
}
In order to create our ServiceHost
and start listening our services, we need to locate the classes whose have the [ServiceContract]
attribute. That attribute can be defined in our class in an inherited interface. That recursive code can also be found in the attached zip file.
Hosting WCF Services
For now, let's load the service assembly and look for WCF service implementations. You need to create a AssemblyName
to inform the assembly codebase, the path where other dependencies can be found. Once those service implementations are found, finally we will create our ServiceHost
.
bool hasServices = false;
public bool LoadServices(string assemblyFile)
{
try
{
AssemblyName assemblyRef = new AssemblyName();
assemblyRef.CodeBase = assemblyFile;
Assembly assembly = Assembly.Load(assemblyRef);
Type[] serviceTypes = LocateServices(assembly.GetTypes());
foreach (Type serviceType in serviceTypes)
{
try
{
Create(serviceType);
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
}
}
}
catch(Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
}
return hasServices;
}
So far, we're inside a new AppDomain
, pointing to our service assembly configuration file, so we have all information about endpoints we need. For each loaded endpoint, we'll to add a server endpoint behavior to inspect the received messages, for debugging proposes.
private void Create(Type serviceType)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Starting {0}", serviceType.FullName);
try
{
ServiceHost host = new ServiceHost(serviceType, new Uri[0]);
foreach (ServiceEndpoint endpoint in host.Description.Endpoints)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(" {0}", endpoint.Address);
endpoint.Behaviors.Add(new MonitorBehavior());
}
host.Open();
hasServices = true;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(ex.Message);
}
}
"Yield the Information You Conceal"
To finish it, let's create a behavior to collect informations about the inbound/outbound messages. Starting with MonitorBehavior
class, which implements the IEndpointBehavior
interface. All methods implementations are empty in this case, except ApplyDispatchBehavior
, in charge for attach our MonitorDispatcher
, as follows:
class MonitorBehavior : IEndpointBehavior
{
public void ApplyDispatchBehavior(
ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors
.Add(new MonitorDispatcher());
}
class MonitorDispatcher : IDispatchMessageInspector
{
public object AfterReceiveRequest(
ref Message request,
IClientChannel channel,
InstanceContext instanceContext)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(
"{0:HH:mm:ss.ffff}\t{1}\n\t\t{2} ({3} bytes)\n\t\t{4}",
DateTime.Now, request.Headers.MessageId,
request.Headers.Action, request.ToString().Length,
request.Headers.To);
return null;
}
public void BeforeSendReply(
ref Message reply,
object correlationState)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(
"{0:HH:mm:ss.ffff}\t{1}\n\t\t{2} ({3} bytes)",
DateTime.Now, reply.Headers.RelatesTo,
reply.Headers.Action, reply.ToString().Length);
}
}
}
The code above dumps at console the message id, timestamp, length and the action requested for inbound and outbound messages. That can be useful to look for services returning too much information (for instance, a big collection) or just to look at message XML content (calling ToString()
).
Running the Code
You will see a screen like this:
The article attachment contains two projects:
ConsoleHoster
: The host server application. You should start it first. You can change the directory to be monitored inside the app.config file, which is displayed at window title bar. Pressing ENTER will quit this application. ServiceImplementation
: A sample implementation (service and client). The client part simply calls the service, which will be hosted at server part. Running it, press any key to call the service again or ESC to quit. Note the events fired at server host when you recompile this application.
Points of Interest
While I don't have an extensive experience using AppDomain
, this implementation runs pretty smooth in my environment and I could successfully drop a Windows service built to hold service hosts.
For further reading, I want to recommend Suzanne Cook and Bart De Smet blogs, where I could find lots of information about AppDomains
and ShadowCopy
.
History
- 16th March, 2008: 1.0: First version