Introduction
This article shows how to expose public methods and properties of a .NET type or a COM type with the Windows Communication Foundation (WCF) service automatically. All a user should do is the following: create a host process for the service, add to it a reference to a standard assembly described below, add the section <system.serviceModel>
defining the service parameters of his/her choice to the .config file, and actually start the service with just one line of code. This line specifies the type you would like to expose and the assembly containing this type.
A WCF service wrapper is generated anew on every start of the host process. If the public interface of an exposed type is changed, then the modified interface will be exposed to the client automatically without any modification in code. When the host process is running, then the Service Reference may be automatically (e.g., with MS Visual Studio) generated on the client side, enabling client access to all the public methods and properties of the exposed type.
Background
The technique of automatic "on-the-fly" service generation is rather straightforward. The component WcfServiceHost
loads an assembly containing the type to be exposed and reflects it over the type's public methods and properties. Based on the reflection, a
[ServiceContract]
interface and the service class are generated as a string of C# code in the memory. Each method of the service class implements the appropriate
[OperationContract]
, and actually calls the corresponding method of the exposed type. The generated C# code is built into in-memory assembly by means of a
System.CodeDom.Compiler
, and a new
System.ServiceModel.ServiceHost
object containing the service class is created and opened.
When a COM interface is exposed, its Runtime Callable Wrapper (RCW) is used for reflection.
Code Sample
The projects in the attached code sample are presented in the following table:
Folder | Projects in Folder | Project Description |
ServiceHostFolder | WcfServiceHost | A managed class library responsible for actually creating the WCF service component. It loads the assembly containing the exposed type, reflects it, generates "on-the-fly" a WCF service component assembly, and then creates the service type, constructs its System.ServiceModel. ServiceHost object, and opens it for communication with the client. |
ServiceHostContainerFolder | ServiceHostContainer | An executable console application; serves as a container for the WcfServiceHost component. It instantiates the SvcHost type and calls its CreateServiceHost() method to create a service. The App.config file of the application contains the <system.serviceModel> definition for all generated WCF services. |
ComponentNetFolder | ComponentNet | A managed class library implementing a sample of type ComponentNet.Class1 to be exposed with the WCF service. |
ComponentNetBase | These managed class libraries contain the "father" and "grandfather" of ComponentNet.Class1 . They are given to illustrate the necessity for reference to all ancestors of the exposed type in the generated code. |
ComponentNetBase2 |
ComponentComFolder | ComponentCom | COM object exposing its custom, automation-compatible interface, IClass2 . |
DummyComInterop | This is just a dummy managed console application. Its only purpose is to automatically produce and test RCW on the ComponentCom COM object. Actually, the tlbimp.exe utility is used under the hood. |
ClientFolder | ConsoleClient | A client console application. It holds Service References to all generated WCF services, and provides a very simple sample of the services consumption. |
Let's examine how the sample works. Our first task is to take a type ComponentNet.Class1
located in the assembly ComponentNet.dll and expose its public methods and properties. The type ComponentNet.Class1
is derived from the type ComponentNetBase.BaseClass1
located in ComponentNetBase.dll, and the type ComponentNetBase.BaseClass1
is derived in its turn from the type ComponentNetBase2.BaseClass2
located in ComponentNetBase2.dll:
ComponentNetBase2.BaseClass2 <-- ComponentNetBase.BaseClass1 <-- ComponentNet.Class1
The appropriate service is hosted by the ServiceHostContainer.exe. This assembly refers to the WcfServiceHost
assembly, and creates the required service with just one line of code:
new SvcHost().CreateServiceHost("ComponentNet.dll", "Class1", true);
The method CreateServiceHost()
of the SvcHost
type has three parameters, namely, path to an assembly with the type to be exposed, name of this type, and a boolean parameter indicating whether the service supports sessions or not. For simplicity, all service modules are placed in the same directory; so, the first parameter is actually reduced to just the assembly file name. The method SvcHost.CreateServiceHost()
internally calls the static method WcfGenerator.GenerateServiceType()
. The latter loads ComponentNet.dll, reflects it over, and generates C# code with the GenerateInterfaceAndClass()
static private method. This method looks ugly enough, but does the job: its output C# code contains the appropriate ServiceContract
and the class implementing it. Next, the static method WcfGenerator.GenerateServiceType()
builds the C# code to an in-memory service assembly. Optionally, by setting the method's last parameter to true
, you order to put debug information including the generated C# code into the disk files. By the way, it is possible to get in-memory service assembly directly in Intermediate Language (MSIL) using classes of the System.Reflection.Emit
namespace, without prior C# code generation. But I found it easier to generate code in a more readable language, which is also easier to change and debug. Finally, an instance of the System.ServiceModel.ServiceHost
type is created, and with its Open()
method, put to opened state.
The ServiceHost
instance is created using the App.config file (which becomes the ServiceHostContainer.exe.config file). There is only a wsHttpBinding
implemented in the sample, but endpoints with the other binding configurations may be easily added. Services describe themselves to clients with standard Metadata Exchange (mex) endpoints.
To run the attached sample without compilation, first, you need to download and unzip the demo project in some directory, then run its RunServices.cmd file (which registers a COM object, starts ServiceHostContainer.exe, and later, after it will be closed, unregisters the COM), then run the ConsoleClient.exe client application. Please wait until all services start and the message "Press any key to stop Services..." appears in the ServiceHostContainer.exe console, and then press any key in the client console to let ConsoleClient.exe proceed. In the midst of its execution, the client application "falls asleep" for some time in order to test the lifetime control feature.
Discussion
Code Sample Limitations
For simplicity, the code sample in this article has the following limitations: an instance of the exposed type should be created with the default constructor (in the case of the COM exposed type, this requirement is compulsory for creating the RCW anyway), and parameters and return values of the exposed methods should not be of user-defined types. Both limitations could be lifted with some extra generated code. For example, user-defined parameters should be either originally attributed with [DataContract]
and [DataMember]
(meaning additional requirements to the original component), or serialized with more generated code.
PerSession vs. PerCall
Service may be created with either the PerSession or the PerCall Instance Context Mode (the last parameter in the SvcHost.CreateServiceHost()
method set to true
or false
, respectively). In the PerSession case, a session between one client and an instance of an exposed type is created. The exposed type constructor is called once per session, on its start. The state of the exposed type instance is preserved during the entire session and available to the client. So, if the exposed type object state should be kept, then PerSession behavior is an option. This requirement probably presents in most of the cases. Even if the state preservation is not an issue, once created, the object on the server side exists throughout entire session. In the PerCall case, the instance of the exposed type is created on each client call (this can be seen by counting the number of constructor calls in the output of ServiceHostContainer.exe in the sample). So, the PerCall approach is suitable for those [rare] cases when a stateless exposed type is called seldom, and therefore there is no need to keep a "stand-by" instance of the exposed type in the memory all the time.
In the code sample, in the case of PerCall behaviour, a _PerCall suffix is automatically added to the generated service class name.
Exposed Type Instance Lifetime
In the PerSession mode, there is no way to explicitly destroy the exposed type instance in the code sample. Its lifetime can be controlled by defining the value of the receiveTimeout
binding parameter in the service .config file. This may be tested by playing with the values of receiveTimeout
and the argument of the Sleep()
method in the ConsoleCleint project. If sleeping time exceeds the receive timeout, then by the moment of the client call, the appropriate instance of the service object is no longer valid, and the client throws an exception.
Conclusion
A technique for automatic generation of WCF interfaces for .NET and COM types' public methods and properties is presented. The type to be exposed is reflected over, and an appropriate
[ServiceContract]
interface and the service class implementing it are generated on-the-fly. Each method of the service class calls the correspondent method of the exposed type. Each service object may be created with either the
PerSession or the
PerCall behavior. A client proxy is generated automatically using the service Metadata Exchange. The code sample illustrates the technique for .NET and COM exposed types.
Thanks
My thanks go to my respected colleagues Ronen Halili, Eran David, Barak Cohen
and Alex Katz for the useful discussion on the subject of this article.