Click here to Skip to main content
15,886,518 members
Articles / Programming Languages / C#

Finding Called Operation and Changing Return Value in WCF Service

Rate me:
Please Sign up or sign in to vote.
4.11/5 (6 votes)
16 Feb 2011CPOL5 min read 25.3K   233   11   2
How to recreate a return object of an operation from a WCF behavior/error handler

Introduction

I'm currently working on the infrastructure of a SOA project which provides service for multiple platforms (classic website - JSON+SOAP, mobile application - JSON, kiosks - MSMQ).
I believe that the biggest issue we're had so far is providing a uniform API for all platforms, to be honest as uniform as we can. We especially had problems with creating a general error handler that will return the same custom result object for all types of endpoints.

JSON, actually JavaScript in particular is weakly typed so basically anything can be returned even if the data doesn't apply to the declared contract as long as the client knows how to handle the data we're good. Keep in mind that it's not that trivial to implement.

SOAP is a different story, more precisely as long as the client actually checks whether the reply he received followed exactly what was expected. Just to be clear .NET SOAP clients do check the received data (as far as I know).

* In general, WCF can handle all types of data as long as the server and client know how to handle it, some protocols are stricter than others and this is the main reason for difficulties in providing a uniform API.

Background

As I mentioned before, my goal was to implement a general error handler that will handle all kinds of endpoints. WCF provides an interface called IErrorHandler which provides entry points for handling exception:

  • C#
    bool HandleError(Exception)

    which indicates whether the IErrorHandler handled the exception.

  • C#
    void ProvideFault(Exception, MessageVersion, ref Message)

    creates a message to be return when an exception is thrown.

This is perfect for what I'm trying to do because it allows me to catch the error and convert it to an object that I told the client I'll provide, even if the operation failed I don't want the client to handle my exception instead I will just let him know that the call failed.

There are several problems that needed to be solved to implement such a thing:

  • The first issue was to provide an object that will describe the error (exception) for all operations.
    The solution for this is quite trivial I just make all the API return types inherit from a common basic object called Result which holds some properties that can describe an exception and some internal stuff in a friendly and serializable manner.
  • The second issue was returning the Result object with the exception details.
    Here comes the tricky part, the first (and naive) approach was to create a Result object when an exception is caught and creating a Message object with Message.CreateMessage.
    This actually works, well for clients that don't check what they got like (as mentioned in the introduction) but when a strict client sees the response, he'll just throw an exception that mostly means – you don't follow the contract.

    I then thought about trying to "fake" a message, so I used the DataContractSerializer with a BodyWriter to create the Message and it worked as long as I know what the original return object is and I hardcoded some stuff to imitate WCF serialization.
    I knew that I'm starting to follow the right path but I realized I have few more things to solve before I can continue.  
    First I need a serializer because I can't estimate how WCF serializes the data for every object on every call, I knew this exists somewhere because WCF is doing this serialization on every call so I took a look over this diagram and I noticed the Formatter block. I did some research and I found out it implements IDispatchMessageFormatter, but sadly there's very little info about it, all attempts to try and implement it failed, I even tried inheriting from WebHttpBehavior.

    I tried to inherit from WebHttpBehavior in order to use its formatter by calling GetReplyDispatchFormatter, it still failed. I then realized I don't really need to create a formatter, I already have one from WCF. I just need to understand how to get to it.
    I need to find the current operation and use the associated formatter. Doing some research on the VS watch window led me to realize that the easiest way to find the current operation is just look for it by the action which can be found in OperationContext.Current.IncomingMessageHeaders.Action, now we just need to go over the OperationContext.Current.EndpointDispatcher.DispatchRuntime.Operations array and look for the operation that handles this action.
    Now that we have the current operation, we can take the associated formatter (from the Formatter property), this is also important for further use for when we'll recreate the original return parameter.

  • Finding the original return parameter.
    I had to dig deep into the guts of WCF and maybe some would say abuse it with reflection to actually find out which type is returned by the operation that was called. It really surprised me because, and I hope that other people feel the same, this is a trivial and legitimate ability that is expected to be provided.
    The operation also has a property called Invoker which is implemented by the SyncMethodInvoker, this invoker has a private property called Method (MethodInfo) which in turn has a property called ReturnType (Type) that holds the return type of the operation.

Finally, we have all the stuff we need, so all that's left to do now is to:

  • Create the return type.
  • Fill it up with the exception details.
  • Serialize the created object into a message with the operation formatter.

Using the Code

I'm writing a simplified implementation of IErrorHandler.

This is a basic IErrorHandler:

C#
public class CustomErrorHandler : IServiceBehavior, IErrorHandler 
{
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers)
        {
            chDisp.ErrorHandlers.Add(this);
        } 
    }
    
    public void Validate(ServiceDescription serviceDescription, 
		ServiceHostBase serviceHostBase)
    {
    
    }
    
    public void AddBindingParameters(ServiceDescription serviceDescription, 
	ServiceHostBase serviceHostBase, 
        	Collection<serviceendpoint /> 
		endpoints, BindingParameterCollection bindingParameters)
    {
    
    }
    
    public bool HandleError(Exception error)
    {
        return true;
    }
    
    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
    }
}

We'll modify the ProvideFault to recreate the original return object:

C#
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
    try
    {
        // find operation invoker
        DispatchOperation operation = OperationByAction(CurrentAction);
        Type returnType = GetOperationReturnType(operation);

        // create result type
        object value = Activator.CreateInstance(returnType, error);

        try
        {
            // serialize the created object
            fault = operation.Formatter.SerializeReply(version, new object[] { }, value);
        }
        catch (Exception ex)
        {
            throw new Exception("failed to create error result");
        }
    }
    catch (Exception ex)
    {
        fault = Message.CreateMessage(
            version,
            MessageFault.CreateFault(FaultCode.CreateSenderFaultCode
		("error handler", ""), "failed to create custom error Result object\n" 
		+ ex.Message),
            OperationContext.Current.IncomingMessageHeaders.Action
        );
    }
}

Throw in a few helper methods to make the code more readable:

C#
private static string CurrentAction
{
    get { return OperationContext.Current.IncomingMessageHeaders.Action; }
}

private static DispatchRuntime CurrentDispatchRuntime
{
    get { return OperationContext.Current.EndpointDispatcher.DispatchRuntime; }
}

private static string CurrentEndpoint
{
    get { return OperationContext.Current.
    	EndpointDispatcher.EndpointAddress.Uri.AbsoluteUri; }
}

private Type GetOperationReturnType(DispatchOperation operation)
{
    string key = CurrentEndpoint + CurrentAction;
    Type returnType;

    if (typeCache.ContainsKey(key))
    {
        returnType = typeCache[key];
    }
    else
    {
        // find original return type
        object method = GetPropertyValue(operation.Invoker, "Method");
        returnType = GetPropertyValue(method, "ReturnType") as Type;

        typeCache.Add(key, returnType);
    }

    return returnType;
}

private object GetPropertyValue(object obj, string property)
{
    PropertyDescriptor pd = TypeDescriptor.GetProperties(obj)[property];

    return pd.GetValue(obj);
}

private DispatchOperation OperationByAction(string action)
{
    SynchronizedKeyedCollection<string, /> operations = CurrentDispatchRuntime.Operations;

    for (int i = 0; i < operations.Count; i++)
        if (operations[i].Action == action)
                return operations[i];

    throw new Exception("operation not found in DispatchRuntime operations");
}

That's all. The attached file contains the complete implementation.

Points of Interest

If you use this code, keep in mind that the reflection part is not guaranteed to work on future releases of WCF.

History

  • 9th February, 2011: Initial version
  • 12th February, 2011: Code optimization

License

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


Written By
Team Leader
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionUpdates for REST Site Pin
Mikel Looper17-Jan-15 20:00
professionalMikel Looper17-Jan-15 20:00 
GeneralMy vote of 5 Pin
Patrick Kalkman10-Feb-11 19:23
Patrick Kalkman10-Feb-11 19:23 

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.