Introduction
A key feature that makes WCF successful is its easy-to-use programming model. Service developers can use WCF to implement their services after mastering the ABC concepts of WCF which are quite easy to acquire. For example, if you need the security feature, you can simply apply some attributes to your services or services method. Everything is quite easy.
However, have you ever thought about what would happen at the runtime of WCF Service when you decorate your method with OperationContractAttribute, and what would happen when you set some properties of SeriveBehaviorAttribute? The simple programming model shields you from the complex internal implementation of WCF, but it also resulted in our lost the control. People may want to know what happens in the black box, this article is dedicated for them.
Do you know what a service will do once it is opened? How can a service get the message from the transport layer and then dispatch it to the property service instance? Everything taking place in this process is hidden from us. All we need to do is to implement the service contract and open the service host; the rest will be handled by the WCF framework. Here I want to delve into the runtime of WCF service to show you how WCF framework fulfills its job.
There are four steps for a service to handle a request from a client. I will explain them in details in the following sections.
- Initialization
- Open
- AcceptConnection
- HandleRequest
To have a good understanding of what I am going to talk about, you are expected to have already worked on WCF for a while. Now we begin our story.
Initialization
In this step, all stuffs we used for describing a service, such as code, configuration files, will be converted to an instance of ServiceDescription type. This instance will gather all the information of a particular service. It will be used to generate the WSDL of the service, and more importantly, WCF framework will use it to build the runtime pipeline on the service side, through this pipeline the WCF will eventually find the property service instance and operation to server for client’s request.
ServiceHost is used for creating the ServiceDescription. The information of ServiceDescription mainly has two recourses: Code and Config file. So we have two methods for each of them in ServiceHost.
In CreateDescription() method, reflection is applied to find out the necessary information implied in the code especially through some attributes, such as OperationContractAttribute, SeriveBehaviorAttribute. This information usually will be used to create Behavior, ContractDescription, OperationDescription as displayed in the following diagram:
In ApplyConfiguration() method, some other information such as Address, Binding, Endpoint Can be read out, as well as some Behaviors from config file, if we add some custom behavior sections in the file.
After creating a ServiceHost, you can still modify the service description before opening it. It is demonstrated as follows:
Finally, all the information about the service has been prepared in the ServiceDescription. Now we can use them to build the service side pipeline through which the client request walks, and it’s also the time to open the ServiceHost.
Open
Many of us have seen the above picture of WCF runtime. The proxy call made by client will go through the pipeline on both sides and eventually be processed by the service instance on the server side. From this picture, we know that the message will be processed by all the components in the pipeline; however, we don’t know how these components are created or how messages are dispatched to them. Now I will explain how they are created when the ServiceHost is opened.
In Step 1, ServiceHost will build a collection of ChannelListeners which are the factories of Channels and will be used to create channel stack on service side in future.
In Step 2, ChannelDispatcher and EndpointDispatcher will be built; both of them play important roles in the message dispatcher.
In Step 3, behaviors defined in the ServiceDescription will be applied, usually they will influence the runtime components in WCF framework, when WCF framework goes through these components, its behaviors will be changed. For example, you can provide a custom InstanceProvider in a custom behavior, during the runtime when WCF framework asks for a service instance through IInstanceProvider interface, it will use the one provided by yourself.
In Step 4, ChannelDispatcher will open all the ChannelListener built by ServiceHost, and then get the instance of type ImmutableDispatchRuntime which is a core component used to dispatcher message. When message is fetched from channel, ImmutableDispatchRuntime will get a service instance and then select an operation on it to invoke.
Finally, ChannelDispatcher will create a ListenerHandle and use the ChannelPump method as a message loop for accepting incoming connections.
As we can see, we are still doing some preparation jobs in the Open phase. We have set up the channel factory stack, but we haven’t built the channel stack. We’ve built all kinds of dispatcher, but they will not start to work until some messages arrive. Now we just need a message to knock on the door.
AcceptConnection
Once a new connection is received by the ListenerHandler, it will call Accept method on ChannelListeners which will build the channel stack. The ListenerHandler creates a ChannelHandler object and associates the channel with that handler. The ChannelHandler object needs to register. After that, the ChannelHandler will try to read messages from that channel using MessagePump method. In other words, it issues a pending Receive on that channel.
HandleRequest
When the client sends a message, ChannelHandler is notified of that message via a callback. First, it will inform all channels to receive it. At this time all channels especially protocol channels will have a chance to process the message to implement their protocols, such as Security, Transaction etc.
The handler then determines the EndpointDispatcher which the message is addressed to. Then it uses the DispatchRuntime of that EndpointDispatcher to determine the Operation that the runtime should invoke. By default, WCF will choose the Operation based on the Action property of the incoming message. During the Open phase, WCF would build a DispatchOperationRuntime dictionary, the key of which is the Action property (operation name by default) of the Operation. So if we pass the Action property of the incoming message to the dictionary, we can get the proper Operation which will be invoked. If you don’t want to follow the Action matching rule, you can use a custom OperationSelector.
After we get the DispatchOperationRuntime, ChannelHandler can’t invoke it, since we haven’t got a service instance yet. So ChannelHandler will dispatch the message to ImmutableDispatchRuntime While ImmutableDispatchRuntime will try to get an instance context. First it will check whether we have an existing context, if not it will try to use IInstanceContextProvider to get that existing context. If we use PerSession or Singleton as our InstanceContext Mode, maybe we can get an existing instance context, thus when we try to get service instance later, we will get the same one in the context. Otherwise, if we use PerCall as InstanceContextMode mode, we will always create a new service instance context here. After preparing the instance context, ImmutableDispatchRuntime will call IDispatchMessageInspector to inspect the message. Here you can write a custom Inspector to do something around the message for you. After inspecting the message, ImmutableDispatchRuntime will call the instance context to get a service instance. And instance context will subsequently call InstanceProvider to get the instance. Now you can image what we can do here. Since we have got service instance, now we can invoke the selected operation on it. So we call InvokeBegin method on DispatchOperationRuntime object which is retrived in step①.
Now we begin to invoke the operation, first we need to desterilize the message to get the method parameters in CLR type. So DispatchOperationRuntime will call IDispatchMessageFormatter to do this. After desterilizing, DispatchOperationRuntime will call IParameterInspector to inspect the parameters. Here you get a chance to do your magic around those parameters, such as validation. At last, we use IOperationInvoker to invoke the method.
Now we have gone out from the maze. We know what have been done by WCF runtime. So what can we get from this knowledge? Personally, I have learnt two things from it.
- How the easy-to-use programming model works?
I have learnt what will happen if I decorate my service or methods with some WCF Attributes, and what will happen if I change its value.
- How to extend WCF runtime.
I have learnt a lot of extension points in WCF runtime and now I know when they will be called and how they will be called so I am very confident to do some customization around them.
Finally, I want to show you a call tree of WCF runtime. I think it’s a variety of collaboration diagram, the reason I don’t use collaboration diagram here is that the diagram may be too huge to present in a word document. And I think the call tree works better for me, so I hope it will be useful for you as well. The color in the tree represents the class to which the method belongs, and the @ symbol represents the logical method name I invented. There may be some mistakes in the call tree, please let me know if you notice any.
ServiceHost DispatcherBuilder ChannelDispatchers DispatcherRuntime ListenerHandle ChannelHandle ImmutableDispatchRuntime InstanceContext InstanceBehavior InstanceProvider DispatchOperationRuntime IInstanceContextProvider(PerCall,PerSession,Singleton)