Custom Config file for a WCF Service hosted in IIS

I am constantly developing new WCF services to try out various techniques, ideas, scenarios.

Many times for these quickie WCF applications I will just use a text editor to write the code.  As you know there are multiple options for hosting your WCF services.  For these quick apps, I will typically write a simple custom console host for the WCF service, what is sometimes called "self-hosting" the WCF service.  You've seen some form of this boilerplate code many times:

         public static void Main()
        {
            string addressRoot= "https://localhost:5555/";
            string endpointSuffix= "MyWcfService";
            string endpointAddress= addressRoot + endpointSuffix;

            // get the type we are hosting 
            System.Type t1 = typeof(Ionic.Samples.Webservices.MyWcfService);

            var host = 
                new System.ServiceModel.ServiceHost(t1,
                                                    new System.Uri(endpointAddress));

            // Create the binding
            var basicBinding = new System.ServiceModel.BasicHttpBinding();
            basicBinding.Namespace = "urn:Ionic.Samples";

            host.AddServiceEndpoint
                (typeof(IService),
                 basicBinding, 
                 "");

            // add the "get metadata" behavior 
            // This will allow the service to emit WSDL when tickled at the proper HTTP endpoint
            var smb= new System.ServiceModel.Description.ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            smb.HttpGetUrl = new System.Uri(endpointAddress);
            host.Description.Behaviors.Add(smb);

            System.Console.WriteLine();

            // Open the ServiceHost to create listeners and start listening for messages
            host.Open();
            System.Console.WriteLine("Service implementation: " + t1.ToString());
            System.Console.WriteLine("Service Address: {0}", endpointAddress);
            System.Console.WriteLine("The service is ready.");
            System.Console.WriteLine();
            System.Console.WriteLine("Press <ENTER> to terminate the service.");
            System.Console.WriteLine();
            System.Console.ReadLine();

            // Close to shutdown the service
            host.Close();
        }

 

But sometimes I need the WCF service to run for a longer period of time, maybe permanently.  This is where the IIS host makes sense - for security and lifecycle purposes.  And the ASP.NET deployment stuff works nicely for WCF services - I copy the Interface (IService.cs) and the Service Implementation code to the App_Code directory of IIS, and then copy the .svc file over, and I'm nearly there.  The only thing the IIS-hosted service lacks now is the service configuration. 

Deploying to IIS, I don't get to use my boilerplate ServiceHost code as above.  Which means I don't have the programmatic control over the endpoint and the ServiceMetadataBehavior, and I don't get to specify an endpoint and any behaviors.  So of course I have to embed the relevant configuration information in the .config file, which in the case of IIS hosting, is the web.config file.  

A problem comes in when hosting multiple WCF services in a single IIS virtual directory:  Each Service gets its own individual .svc file, but all services share a single web.config file.  If I update a service, then I need to modify that single file.  If I add a service, I need to edit the file.  If I remove a service, I need to edit the file.  And in each case the edits and changes for one service could affect the other services, if the edits are not done properly. 

Ideally I'd like the IIS-hosted WCF service to have its own configuration, independent of any other services running from that same vdir.  The .svc file is completely independent.  Why can't the configuration also be independent?  How can I do this?

I looked around and found the ServiceHostBase.ApplyConfiguration method.  If I construct a custom service host, then I can override the ApplyConfiguration method and load my service configuration from... anywhere.   I decided to establish a simple convention - I will look for a file in the server directory called <ServiceName>.config.  This is not <AssemblyName>.config, which is the .NET default.  In this case the assembly is likely going to be dynamically generated and named.  So using the assembly name to find the config file is probably not right.  The next option is to use the Service Name; this is something you specify in code in the [ServiceBehavior] attribute.  Assuming you select a unique name for each service, that name ought to be useful for uniquifying the configuration file.  

Then, it is a simple matter of loading in the configuration file, within the custom ServiceHost.  This bit of black magic does the trick for me: 

         protected override void ApplyConfiguration()
        {

            // generate the name of the custom configFile, from the service name:
            string configFilename = System.IO.Path.Combine ( physicalPath,
                                                             String.Format("{0}.config", this.Description.Name));
            
            if (string.IsNullOrEmpty(configFilename) || !System.IO.File.Exists(configFilename))
                base.ApplyConfiguration();
            else
                LoadConfigFromCustomLocation(configFilename);

        } 

 

Then there is some supporting logic, to get the physical path and to actually load the configuration:

         private string _physicalPath = null;
        private string physicalPath
        {
            get 
            {
                if (_physicalPath == null)
                {
                    // if hosted in IIS
                    _physicalPath = System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath;
                    if (String.IsNullOrEmpty(_physicalPath))
                    {
                        // for hosting outside of IIS
                        _physicalPath= System.IO.Directory.GetCurrentDirectory();
                    }
                }
                return _physicalPath;
            }
        }

        private void LoadConfigFromCustomLocation(string configFilename)
        {
            var filemap = new System.Configuration.ExeConfigurationFileMap();
            filemap.ExeConfigFilename = configFilename;
 
            System.Configuration.Configuration config = 
                System.Configuration.ConfigurationManager.OpenMappedExeConfiguration
                (filemap, 
                 System.Configuration.ConfigurationUserLevel.None);
 
            var serviceModel = System.ServiceModel.Configuration.ServiceModelSectionGroup.GetSectionGroup(config);
 
            bool loaded= false;
            foreach (System.ServiceModel.Configuration.ServiceElement se in serviceModel.Services.Services)
            {
                if(!loaded)
                    if (se.Name == this.Description.ConfigurationName)
                    {
                        base.LoadConfigurationSection(se);
                        loaded= true;
                    }
            }
            if (!loaded)
                throw new ArgumentException("ServiceElement doesn't exist");         
        }

 

And of course to make this work, you will need to specify a custom ServiceHostFactory in the .svc file.  The custom service host approach is useful for lots of things; in the past I used it for WSDL-Flattening or for modifying namespaces on SOAP payloads. Of course you can have a single custom service host that combines all those capabilities, if you like. For this post I am focusing only on the config file.

 <%@ ServiceHost
    Language="C#" 
    Debug="true" 
    Service="Ionic.Samples.Webservices.Sep20.CustomConfigService" 

    Factory="Ionic.ServiceModel.ServiceHostFactory"

%>

 

For now, the logic that reads in a unique file per service will do, but in the general case I'd like to be able to read the configuration from a remote centralized repository, like a network share or a database.  That is something left for the future.

I've attached all the code here in a zipfile.

CustomConfigFile.zip