This month s column explores how you can extend the ServiceHost and ServiceHostFactory types. For more information on WCF, see "Security Practices for WCF " and " Going Mobile with WCF ."

At the heart of the WCF hosting model is the ServiceHost type. When you host services with IIS or WAS, message-based activation takes care of constructing the ServiceHost instance for each service type, on demand. For self-hosted environments (not IIS or WAS), you explicitly write the code to construct the ServiceHost and open the communication channel for each endpoint. The process is simple, and usually draws from the service model configuration section to initialize. However, at times, it is valuable to provide custom endpoint and behavior configuration at run time. This is easily done with self-hosting environments because you have direct access to the ServiceHost type. For IIS and WAS, it requires you to use ServiceHostFactory to gain access to the ServiceHost type.

In this article, I ll show you how to programmatically initialize the ServiceHost in both scenarios, teach you how to work with ServiceHostFactory, and discuss practical reasons why you might want to do this.

 

A Quick ServiceHost Review

The ServiceHost type is directly associated with a particular service type, each of its endpoints, and any related behavior configuration. As a CommunicationObject derivative, ServiceHost provides methods to control when you open and close the communication channel, access to communication channel state, and access to events triggered during the lifecycle of the ServiceHost instance.

With self-hosted services, you traditionally create the ServiceHost type by associating it with a service type, calling Open, and putting initialization details in the <system.serviceModel> section of the application configuration file. For example, the code in Figure 1 will maintain an instance of the ServiceHost type until you press Enter in a console application host.

ServiceHost hostServiceA = null;

try

{

 hostServiceA =

   new ServiceHost(typeof(BusinessServices.ServiceA));

 hostServiceA.Open();

 Console.WriteLine("Services are running...");

 Console.ReadLine();

}

finally

{

 if (hostServiceA != null)

 {

   hostServiceA.Close();

 }

}

Figure 1: Maintain an instance of the ServiceHost type until you press Enter in a console application host.

The ServiceHost instance is initialized by the application configuration shown in Listing One for the <service> type BusinessServices.ServiceA.

The same configuration settings are required when you host the service in IIS or WAS, except that the .svc endpoint indicates the service type as follows:

<%@ ServiceHost Service="BusinessServices.ServiceA" %>

In addition, there is no need for the <host> section for the service configuration section because the base address is provided by the .svc endpoint within the hosting virtual application path.

In both cases, when the ServiceHost instance is constructed and opened, a collection of channel dispatchers are created, one for each endpoint, and the service behaviors associated with the service type influence how messages are processed by each endpoint.

 

ServiceHostFactory

If all you are planning to do is load the ServiceHost from configuration as shown earlier, there is no need to change the way the .svc endpoint loads the ServiceHost with IIS and WAS hosting. The code referenced earlier related to self-hosting is taken care of for you. If you want to customize the hosting code in any way, a ServiceHostFactory is necessary for IIS and WAS.

Let s start with a simple example. Although VERY rare, it is possible that the ServiceHost can be put into a faulted state. A predictable way to simulate this is with an MSMQ service that intentionally faults a poison message which means the message is returned to the queue (never lost) and, thus, the ServiceHost instance is set to a Faulted state perpetually until the poison message is dealt with. Poorly configured services can also trigger a Faulted state. Clearly, this is something we would like to know about.

To detect that the ServiceHost has faulted, you can hook the Faulted event as shown in Figure 2.

ServiceHost hostServiceA = null;

try

{

 hostServiceA =

   new ServiceHost(typeof(BusinessServices.ServiceA));

 hostServiceA.Faulted +=

   new EventHandler(hostServiceA_Faulted);

 hostServiceA.Open();

 Console.WriteLine("Services are running...");

 Console.ReadLine();

}

finally

{

 if (hostServiceA != null && hostServiceA.State!=

      CommunicationState.Faulted)

 {

   hostServiceA.Close();

 }

}

static void hostServiceA_Faulted(object sender, EventArgs e)

{

 // Notify administrator, log exception

}

Figure 2: Hook the Faulted event to detect that the ServiceHost has faulted.

In addition to logging the fault when it takes place, to close the ServiceHost you must check first if the communication state is Faulted; otherwise, Close will throw an exception.

To make this code available to a .svc endpoint, we must use a ServiceHostFactory instance to create the ServiceHost instance. First, associate the factory with the @ServiceHost endpoint:

<%@ ServiceHost Service="BusinessServices.ServiceA"

 Factory="ServiceHostExtensions.ServiceHostFactoryEx" %>

The implementation for ServiceHostFactoryEx is very simple. Derive a type from ServiceHostFactory and construct the ServiceHost type of your choice when requests arrive to the factory. Requests sent to the .svc endpoint will forward to ServiceHostFactoryEx sending the service type name along with it. ServiceHostFactoryEx is shown in Figure 3.

public class ServiceHostFactoryEx : ServiceHostFactory

{

 public ServiceHostFactoryEx()

 {

 }

 public override System.ServiceModel.ServiceHostBase

  CreateServiceHost(string constructorString, Uri[]

   baseAddresses)

 {

   Type t = Type.GetType(string.Format("{0}, {1}",

     constructorString, constructorString.Substring(0,

     constructorString.IndexOf("."))));

      ServiceHost host = new ServiceHost(t, baseAddresses);

      host.Faulted += new EventHandler(host_Faulted);

      return host;

 }

 protected override System.ServiceModel.ServiceHost

   CreateServiceHost(Type serviceType, Uri[] baseAddresses)

 {

      ServiceHost host = new ServiceHost(serviceType,

        baseAddresses);

      host.Faulted += new EventHandler(host_Faulted);

      return host;

 }

}

Figure 3: ServiceHostFactoryEx implementation.

The first constructor is always called from IIS and WAS, as it cannot be a singleton service in this hosting environment. The only trick here is that you have to make assumptions about the assembly name that owns the service type, as you are only provided with the fully qualified namespace and type name. What I usually do is make sure my assemblies are named for the namespace; thus, I can make that assumption in this code:

Type t = Type.GetType(string.Format("{0}, {1}",

 constructorString, constructorString.Substring(0,

 constructorString.IndexOf("."))));

The constructorString parameter is the service type name; I extract the namespace from there to build the fully qualified type name. With this I can construct the ServiceHost type, hook the Faulted event, and return the new instance to the runtime:

ServiceHost host = new ServiceHost(t, baseAddresses);

host.Faulted += new EventHandler(host_Faulted);

return host;

If you want more control over the ServiceHost construction process, you should also extend the ServiceHost type.

 

Controlling Service Behaviors at Run Time

If you look at the <behaviors> section from Listing One, you ll see that some service behaviors have been explicitly associated with the service. Behaviors affect how authentication and authorization are executed, as well as debugging and metadata support, throttling requests to the service type, and a number of other features. In configuration, you associate behaviors with a service using the behaviorConfiguration property of the <service> type:

<service behaviorConfiguration="ServiceBehavior"

 name="BusinessServices.ServiceA">

You also can add them programmatically to the ServiceHost at run time. Service behaviors are represented at run time as types that implement IServiceBehavior. Common behaviors include:

  • ServiceCredentials
  • ServiceAuthorizationBehavior
  • ServiceDebugBehavior
  • ServiceMetadataBehavior
  • ServiceThrottlingBehavior
  • ServiceSecurityAuditBehavior
  • AspNetCompatibilityRequirementsAttribute

Some new behaviors have also been added to .NET 3.5 (to be discussed in future articles):

  • DurableServiceAttribute
  • PersistenceProviderBehavior
  • WorkflowRuntimeBehavior

The point is, you can construct these behaviors at run time, then initialize the ServiceHost instance with the required settings.

Why set behaviors at run time? Perhaps for any of the following reasons:

  • To ensure certain fixed behavior settings are always reflected at run time, without leaving them readily accessible in the configuration file.
  • To provide good defaults when no configuration settings are present, and use configuration settings if present.
  • To set behavior settings from a global configuration store, perhaps a configuration service or database, so services hosted on different machines or in different hosting environments can share common settings, and to control configuration settings for all services from a central location.

The behavior configuration for ServiceA shown in Listing One includes the following settings:

  • Exceptions are not reported as faults.
  • Metadata exchange is enabled, browsing is disabled.
  • UserName credentials are authenticated and authorized by the ASP.NET provider model.
  • Throttling settings support 30 concurrent requests (instead of 16) and 1,000 concurrent sessions (instead of 10).

To handle this at run time, initialize the ServiceHost instance before you call Open. Error handlers, another type of behavior that can be set only at run time, must be initialized after the channel dispatchers have been created (in other words, after you call Open). Initialization would look something like this:

ConfigureServiceBehaviors(hostServiceA);

hostServiceA.Open();

AddErrorHandler(hostServiceA);

ConfigureServiceBehaviors and AddErrorHandler are shown in Figure 5. The ServiceHost type provides direct access to ServiceAuthorizationBehavior and ServiceCredentials via its Authorization and Credentials properties, thus setting values for those behaviors is a simple property initialization instruction. ServiceThrottlingBehavior, ServiceDebugBehavior, and ServiceMetadataBehavior must be accessed via the Find method of the Behaviors collection. In Figure 4, the ServiceThrottlingBehavior is only initialized with values if there is not a configuration behavior already present. The other two behaviors are always added to the ServiceHost, but the values for IncludeExceptionDetailsInFaults and HttpGetEnabled are initialized to true or false depending if the code is compiled for debug or not.

private static void ConfigureServiceBehaviors(ServiceHost host)

{

 host.Authorization.ImpersonateCallerForAllOperations = false;

 host.Authorization.PrincipalPermissionMode =

   PrincipalPermissionMode.UseAspNetRoles;

 host.Credentials.UserNameAuthentication.

  UserNamePasswordValidationMode =

   UserNamePasswordValidationMode.MembershipProvider;

 host.Credentials.ServiceCertificate.SetCertificate(

  StoreLocation.LocalMachine, StoreName.My,

   X509FindType.FindBySubjectName, "RPKey");

 ServiceThrottlingBehavior throttleBehavior = host.Description.Behaviors.Find<ServiceThrottlingBehavior>();

 if (throttleBehavior == null)

 {

      throttleBehavior = new ServiceThrottlingBehavior();

     throttleBehavior.MaxConcurrentCalls = 30;

     throttleBehavior.MaxConcurrentSessions = 1000;

     host.Description.Behaviors.Add(throttleBehavior);

 }

   bool debugMode =

#if DEBUG

true;

#else

false;

#endif

 ServiceDebugBehavior debugBehavior =

  host.Description.Behaviors.Find<ServiceDebugBehavior>();

 if (debugBehavior == null)

 {

     debugBehavior = new ServiceDebugBehavior();

     host.Description.Behaviors.Add(debugBehavior);

 }

   debugBehavior.IncludeExceptionDetailInFaults = debugMode;

 ServiceMetadataBehavior mexBehavior = host.

  Description.Behaviors.Find<ServiceMetadataBehavior>();

 if (mexBehavior == null)

 {

     mexBehavior = new ServiceMetadataBehavior();

     host.Description.Behaviors.Add(mexBehavior);

 }

 mexBehavior.HttpGetEnabled = debugMode;

}

private static void AddErrorHandler(ServiceHost host)

{

 foreach (ChannelDispatcher dispatcher in

   host.ChannelDispatchers)

 {

     foreach (EndpointDispatcher epDispatcher in

       dispatcher.Endpoints)

     {

         if ((epDispatcher.ContractName !=

           "IMetadataExchange" && epDispatcher.ContractName

          != "IHttpGetHelpPageAndMetadataContract"))

             dispatcher.ErrorHandlers.Add(

             new ConvertToFaultErrorHandler());

     }

 }

}

Figure 4: Code to initialize behaviors for the ServiceHost instance.

Error handlers are types that implement IErrorHandler, and must be added to each endpoint exposed by the ServiceHost instance after the host is open. You can add error handlers using a custom attribute that you apply directly to the service type:

[ConvertToFaultErrorHandler]

public class ServiceA : IServiceA

If you want to add the same error handler to all services, and prefer to control that at the host, Figure 4 illustrates how to do this after the host is opened. In fact, this listing also illustrates how to add the error handler to non-metadata endpoints only.

To summarize, the configuration in Figure 4 adds some value to the configuration file results with the following results:

  • Exceptions are reported as faults only when the code is compiled in debug.
  • Metadata exchange is always enabled, but browsing is enabled only when compiled for debug.
  • UserName credentials are always authenticated and authorized by the ASP.NET provider model.
  • Throttling settings default to the configuration settings unless the configuration section is present, which can provide overrides.
  • An error handler to convert non-CommunicationException exceptions to faults is always added to non-metadata endpoints.

Adding Service Endpoints at Run Time

Aside from working with service behaviors at run time, for these reasons you may also want to configure endpoints at run time:

  • To initialize endpoints from a global configuration store, perhaps a configuration service or database. Run time configuration services can be useful for dynamic discovery and to provide a central place to view and control service configuration.
  • To hard-code relative endpoint configuration that is always fixed, while allowing base addresses to be changed in configuration for flexibility. This can be useful if you deploy services to customers and don t want them to have access to non-required configuration settings.

To initialize service endpoints at run time, simply call AddServiceEndpoint on the ServiceHost instance, as shown in Figure 5.

private void ConfigureServiceEndpoints(ServiceHost host)

{

 XmlDictionaryReaderQuotas readerQuotas =

  new XmlDictionaryReaderQuotas();

 readerQuotas.MaxArrayLength = 200000;

 readerQuotas.MaxStringContentLength = 200000;

 BasicHttpBinding basicHttp = new BasicHttpBinding();

 basicHttp.MaxReceivedMessageSize=200000;

 basicHttp.MaxBufferSize = 200000;

 basicHttp.ReaderQuotas = readerQuotas;

 WSHttpBinding wsHttpCommon = new WSHttpBinding();

 wsHttpCommon.MaxReceivedMessageSize=200000;

 wsHttpCommon.ReaderQuotas = readerQuotas;

 wsHttpCommon.Security.Message.ClientCredentialType

    = MessageCredentialType.UserName;

 wsHttpCommon.Security.Message.EstablishSecurityContext

   = false;

 wsHttpCommon.Security.Message.NegotiateServiceCredential

   = false;

 WSHttpBinding wsHttpSession =

   new WSHttpBinding(SecurityMode.Message, true);

 wsHttpSession.MaxReceivedMessageSize = 200000;

 wsHttpSession.ReaderQuotas = readerQuotas;

 wsHttpSession.Security.Message.ClientCredentialType

   = MessageCredentialType.UserName;

 wsHttpSession.Security.Message.EstablishSecurityContext

   = true;

 wsHttpSession.Security.Message.NegotiateServiceCredential

   = false;

 host.AddServiceEndpoint(typeof(BusinessServices.IServiceA),

   basicHttp, "ServiceA/Soap11");

 host.AddServiceEndpoint(typeof(BusinessServices.IServiceA),

   wsHttpCommon, "ServiceA/Soap12");

 host.AddServiceEndpoint(typeof(BusinessServices.IServiceA),

   wsHttpSession, "ServiceA/Soap12Session");

}

Extending the ServiceHost

Figure 5: Adding service endpoints at run time.

As explained earlier, when you host with IIS and WAS, the .svc endpoint will invoke a ServiceHostFactory of your choice if you need to control ServiceHost initialization. You can hook the Faulted event, initialize service behaviors, and initialize endpoints before returning the ServiceHost type to the runtime. You can t add the error handler, however, until after the ServiceHost is opened. To do this in a central place, you would have to customize the ServiceHost type. Furthermore, you might want to encapsulate some of the common behavior and endpoint configuration requirements in a ServiceHost type rather than cluttering the factory.

Figure 7 illustrates a customized ServiceHost type with the following overrides:

  • ApplyConfiguration supplies code to configure the service behaviors before applying the final configuration settings for the service.
  • InitializeRuntime supplies code to add endpoints before initializing the runtime, and code to add the error handler after the runtime is initialized.
  • OnFaulted is overridden to handle the rare event of a Faulted ServiceHost.
public class ServiceHostEx: ServiceHost

{

 private Type m_serviceType;

 public ServiceHostEx(object singletonInstance, params Uri[]

   baseAddresses): base(singletonInstance, baseAddresses)

 {

 }

 public ServiceHostEx(Type serviceType, params Uri[]

   baseAddresses) : base(serviceType, baseAddresses)

 {

   m_serviceType = serviceType;

 }

 protected override void ApplyConfiguration()

 {

   ConfigureServiceBehaviors();

   base.ApplyConfiguration();

 }

 protected override void InitializeRuntime()

 {

   ConfigureMetadataEndpoints();

   ConfigureServiceEndpoints();

   base.InitializeRuntime();

   this.AddErrorHandler<ConvertToFaultErrorHandler>(false);

 }

 protected override void OnFaulted()

 {

   base.OnFaulted();

 // TODO: notify administrator

 }

 private void ConfigureServiceBehaviors()

 {...}

 private void ConfigureMetadataEndpoints()

 {...}

 private void ConfigureServiceEndpoints()

 {...}

}

Figure 7: Implementation of ServiceHostEx.

You can construct ServiceHostEx directly if you are self-hosting, but for IIS and WAS you would use the ServiceHostFactory to construct the ServiceHostEx instance. Now, all of your customizations of the ServiceHost initialization process are encapsulated into the custom type and are available to any hosting environment.

 

Conclusion

Customizing the initialization of the ServiceHost at run time can be useful to your production deployments. Although rare, it is probably always a good idea to hook the Faulted event of the ServiceHost instance so you can report this exception to administrators and react accordingly. More likely, you ll benefit from the ability to enforce certain behavior policies and possibly configure service endpoints at run time. The sample code for this article includes examples related to this discussion for self-hosting and IIS/WAS. Enjoy!

 

Download the samples for this article at http://www.dasblonde.net/downloads/aspprofeb08.zip.

 

Michele Leroux Bustamante is Chief Architect of IDesign Inc., Microsoft Regional Director for San Diego, and Microsoft MVP for Connected Systems. At IDesign, Michele provides training, mentoring, and high-end architecture consulting services focusing on Web services, scalable and secure architecture design for .NET, federated security scenarios, Web services, interoperability, and globalization architecture. She is a member of the International .NET Speakers Association (INETA), a frequent conference presenter, conference chair for SD West, and is frequently published in several major technology journals. Michele also is on the board of directors for IASA (International Association of Software Architects), and a Program Advisor to UCSD Extension. Her latest book is Learning WCF (O Reilly, 2007); see her book blog at http://www.thatindigogirl.com. Reach her at mailto:mlb@idesign.net, or visit http://www.idesign.net and her main blog at http://www.dasblonde.net.

Begin Listing One

<configuration>

 <connectionStrings>

   <remove name="LocalSqlServer"/>

   <add name="LocalSqlServer" connectionString="data

     source=localhost;Initial Catalog=aspnetdb;Integrated

     Security=True; " providerName="System.Data.SqlClient" />

 </connectionStrings>

 <system.web>

   <roleManager enabled="true"/>

 </system.web>

 <system.serviceModel>

   <services>

     <service behaviorConfiguration="ServiceBehavior"

       name="BusinessServices.ServiceA">

       <endpoint address="ServiceA/Soap11"

         binding="basicHttpBinding" contract=

        "BusinessServices.IServiceA"

        bindingConfiguration="basicHttp"/>

       <endpoint address="ServiceA/Soap12"

        binding="wsHttpBinding" contract=

        "BusinessServices.IServiceA"

        bindingConfiguration="wsHttpCommon"/>

       <endpoint address="ServiceA/Soap12Session"

         binding="wsHttpBinding" contract=

        "BusinessServices.IServiceA"

         bindingConfiguration="wsHttpSession"/>

       <endpoint address="mex" binding="mexHttpBinding"

         contract="IMetadataExchange"/>

       <host>

         <baseAddresses>

           <add baseAddress="http://localhost:8000"/>

         </baseAddresses>

       </host>

     </service>

   </services>

   <bindings>

     <basicHttpBinding>

       <binding name="basicHttp" maxBufferSize="200000"

         maxReceivedMessageSize="200000" >

         <readerQuotas maxArrayLength="200000"

           maxStringContentLength="200000" />

       </binding>

     </basicHttpBinding>

     <wsHttpBinding>

       <binding name="wsHttpCommon"

         maxReceivedMessageSize="200000"  >

         <readerQuotas maxArrayLength="200000"

          maxStringContentLength="200000" />

         <security mode="Message">

           <message clientCredentialType="UserName"

            establishSecurityContext="false"

            negotiateServiceCredential="false"/>

         </security>

       </binding>

       <binding name="wsHttpSession"

         maxReceivedMessageSize="200000" >

         <readerQuotas maxArrayLength="200000"

           maxStringContentLength="200000" />

         <security mode="Message">

           <message clientCredentialType="UserName"

             establishSecurityContext="true"

             negotiateServiceCredential="false"/>

         </security>

       </binding>

     </wsHttpBinding>

   </bindings>

   <behaviors>

     <serviceBehaviors>

       <behavior name="ServiceBehavior">

         <serviceDebug includeExceptionDetailInFaults=

          "false"/>

         <serviceMetadata httpGetEnabled="false"/>

         <serviceCredentials>

           <userNameAuthentication

            userNamePasswordValidationMode=

            "MembershipProvider"/>

           <serviceCertificate findValue="RPKey"

             storeLocation="LocalMachine" storeName="My"

             x509FindType="FindBySubjectName"/>

         </serviceCredentials>

         <serviceAuthorization

           principalPermissionMode="UseAspNetRoles" />

         <serviceThrottling maxConcurrentCalls="30"

          maxConcurrentSessions="1000"/>

       </behavior>

     </serviceBehaviors>

   </behaviors>

 </system.serviceModel>

</configuration>