Click here to Skip to main content
15,878,814 members
Articles / Programming Languages / C#

Generic WCF Host

Rate me:
Please Sign up or sign in to vote.
4.90/5 (14 votes)
13 Mar 2008CPOL5 min read 90K   1.1K   67   11
Generic console hoster without locking up service assemblies, speeding up WCF services development with VS2005
The main propose of this article is to develop a generic WCF host implementation, to help when developing WCF services inside Visual Studio 2005.

generic_wcf_host/ConsoleHoster.gif

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 AppDomains 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.

C#
static Dictionary<string, AppDomain> appDomains = new Dictionary<string, AppDomain>();

static void Main(string[] args)
{
    // can't find settings at *.exe.config file? look at current directory
    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);

    // sets up file system monitoring
    DelayedFileSystemWatcher dfsw = new DelayedFileSystemWatcher(dropPath, pattern);
    dfsw.Created += new FileSystemEventHandler(dfsw_CreatedOrChanged);
    dfsw.Changed += new FileSystemEventHandler(dfsw_CreatedOrChanged);
    dfsw.Deleted += new FileSystemEventHandler(dfsw_Deleted);

    // before start monitoring disk, load already existing assemblies
    foreach(string assemblyFile in Directory.GetFiles(dropPath, pattern))
    {
        Create(Path.GetFullPath(assemblyFile));
    }

    dfsw.EnableRaisingEvents = true;
    Console.ReadLine(); // stay away from ENTER key
    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 AppDomains 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.
C#
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.

C#
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.

C#
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:

C#
class MonitorBehavior : IEndpointBehavior
{
    // ... empty methods removed ... \\

    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:

generic_wcf_host/ConsoleHoster2.gif

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Brazil Brazil
MCAD, MCSD, MCDBA, MCPD, MCTS
Brazilian developer and freelancer. Currently interested in programming frameworks, code generation and development productivity.

Comments and Discussions

 
QuestionAmazing!!! Pin
Daniel Liedke18-Mar-14 6:09
professionalDaniel Liedke18-Mar-14 6:09 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey27-Mar-12 4:08
professionalManoj Kumar Choubey27-Mar-12 4:08 
Questionerror in http endpoint hosting Pin
kkrakesh9-Feb-12 20:18
kkrakesh9-Feb-12 20:18 
GeneralWCF generic Windows service host Pin
Member 386383118-Dec-08 6:22
Member 386383118-Dec-08 6:22 
GeneralRe: WCF generic Windows service host Pin
RubensFarias22-Dec-08 23:28
RubensFarias22-Dec-08 23:28 
GeneralProblem with hosting in same port Pin
Igor.Laptiev10-Jun-08 19:33
Igor.Laptiev10-Jun-08 19:33 
GeneralRe: Problem with hosting in same port Pin
S. Kolic26-Nov-10 6:49
S. Kolic26-Nov-10 6:49 
GeneralRe: Problem with hosting in same port Pin
kkrakesh9-Feb-12 20:17
kkrakesh9-Feb-12 20:17 
Generalnice Pin
meetsenthilbtech6-May-08 0:52
meetsenthilbtech6-May-08 0:52 
Generalty Pin
RubensFarias9-May-08 14:15
RubensFarias9-May-08 14:15 
GeneralRe: nice Pin
Shane A Macaulay2-Feb-09 15:19
Shane A Macaulay2-Feb-09 15:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.