Alex
Alex

Reputation: 5257

Passing an interface method as a parameter

Note: This could very well be very C# specific language question, unrelated to WCF or web services at all.

There is a 3-party ASMX web service, which shall be used for data retrieval. I have created a generalized method called ExecuteCommand() which is used for every request against the web service. The purpose of this method is to handle cookie session/exceptions and other common logic. For each request a new channel shall be used, in order to simplify the disposal of unused resources.

The problem is that to use the ExecuteCommand() method - I have to initialize a channel each time, in order to be able to pass the method to be executed as an argument. Sorry if it sounds too complicated. Here is a usage example:

string color = "blue";
var channel = _strategyFactory.CreateChannel<CarServiceSoapChannel>();
var cars = WcfHelper.ExecuteCommand(channel, () => channel.GetCars(color));
// channel is null here. Channel was closed/aborted, depending on Exception type.

After ExecuteCommand() is called - channel is already disposed of. The reason why channel object is needed at all, is to be able to provide a method to be executed as a parameter! i.e.() => channel.GetCars(). To further support these words, here is the WcfHelper class internals:

public static class WcfHelper
{
    public static Cookie Cookie { get; set; }

    public static T ExecuteCommand<T>(IClientChannel channel, Expression<Func<T>> method)
    {
        T result = default(T);

        try
        {
            // init operation context
            using (new OperationContextScope(channel))
            {
                // set the session cookie to header
                if (Cookie != null) {
                    HttpRequestMessageProperty request = new HttpRequestMessageProperty();
                    request.Headers["Cookie"] = cookie.ToString();
                    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = request;  
                }

                // execute method
                var compiledMethod = method.Compile();
                result = compiledMethod.Invoke();
            }
        }
        // do different logic for FaultException, CommunicationException, TimeoutException
        catch (Exception)
        {
            throw;
        }
        finally
        {
            CloseOrAbortServiceChannel(channel);
            channel = null;
        }

        return result;
    }

    private static void CloseOrAbortServiceChannel(ICommunicationObject communicationObject)
    {
        bool isClosed = false;

        if (communicationObject == null || communicationObject.State == CommunicationState.Closed)
            return;

        try
        {
            if (communicationObject.State != CommunicationState.Faulted)
            {
                communicationObject.Close();
                isClosed = true;
            }
        }
        catch (Exception)
        {
            throw;
        }
        finally
        {
            if (!isClosed)
                AbortServiceChannel(communicationObject);
        }
    }

    private static void AbortServiceChannel(ICommunicationObject communicationObject)
    {
        try
        {
            communicationObject.Abort();
        }
        catch (Exception)
        {
            throw;
        }
    }
}

So the short question - it it possible to initialize a channel variable inside the ExecuteCommand method itself, while having a possibility to define, which method shall be executed inside ExecuteCommand for a given channel?

I am trying to accomplish something like this:

string color = "blue";
var cars = WcfHelper.ExecuteCommand<Car[], CarServiceSoapChannel>(channel => channel.GetCars(color));

or even

string color = "blue";
var cars = WcfHelper.ExecuteCommand<CarServiceSoapChannel>(channel => channel.GetCars(color));

Any other code improvement suggestions are welcomed but not mandatory, of course.

P.S. ASMX is added as a Service reference in Visual Studio. Therefore, there were some entities that automatically generated for the "CarService", such as - CarServiceSoapChannel interface, CarServiceSoapClient class and of course CarService interface containing methods of a web service. In the example above a ChannelFactory is used to create a channel for the CarServiceSoapChannel interface, hence, here is where the question name is coming from: Passing an interface method as a parameter. This could be a bit misleading, but I hope it's clear what I trying to accomplish from the description itself.

Update 25.05.2018 I followed the advice of @nvoigt and was able to achieve the result I wanted:

public static TResult ExecuteCommand<TInterface, TResult>(Func<TInterface, TResult> method)
    where TInterface : IClientChannel
{
    TResult result = default(TResult);
    IClientChannel channel = null;

    try
    {
        channel = StrategyFactory.CreateChannel<TInterface>();

        // init operation context
        using (new OperationContextScope(channel))
        {
            // set the session cookie to header
            if (Cookie != null)
                Cookie.SetCookieForSession();

            // execute method
            result = method((TInterface)channel);
        }
    }
    catch (Exception)
    {
        throw;
    }
    finally
    {
        CloseOrAbortServiceChannel(channel);
        channel = null;
    }

    return result;
}

This already achieves the initial goal. There is, however, one issue with this approach. In order to call the method - you have to explicitly specify the method's return parameter.

So to say, if you want to call the method - writing this is not enough:

var result = WcfHelper.ExecuteCommand<CarServiceSoapChannel>(channel => channel.IsBlue())

You will have to specifically specify, that the return type shall be boolean

var result = WcfHelper.ExecuteCommand<CarServiceSoapChannel, bool>(channel => channel.IsBlue())

I personally don't mind writing an extra bit of code, as it is still a big advantage over my initial method implementation, however, I am wondering in the new version could be improved?

For example, When I had just 1 generic TResult type in the method - the return type <TResult> could be omitted. This is no longer the case with 2 generics. In any case, thank you @nvoigt!

Upvotes: 3

Views: 3371

Answers (1)

nvoigt
nvoigt

Reputation: 77364

Your method signature should be:

public static TResult ExecuteCommand<TInterface>(Func<TInterface, TResult> method)

Then in your WcfHelper (which should probably not be static anymore because it needs a member _strategyFactory) you create a channel as before:

{
    var channel = _strategyFactory.CreateChannel<CarServiceSoapChannel>();

    return method(channel);
}

Obviously, you need to add all the fancy try/finally stuff again.


As you should have instances anyway now with the factory in the class as a member, you could put the service contract generic into your class to make it easier for users:

public class ConnectionToService<TInterface> : where TInterface : class
{
    public TResult ExecuteCommand<TResult>(Func<TInterface, TResult> method)
    {
        var channel = _strategyFactory.CreateChannel<CarServiceSoapChannel>();

        return method(channel);
    }
}

Usage:

var service = new ConnectionToService<ICarService>();

var color = service.ExecuteCommand(s => s.GetColor());

Upvotes: 2

Related Questions