Pass interface methods as parameters
Note : This is most likely a very C#
specific language issue that has nothing to do with it WCF
or web services
not at all.
There is a 3rd party ASMX
web service which should be used for data retrieval. I created a generic method called ExecuteCommand()
, which is used for every request made to the web service. The purpose of this method is to handle cookie sessions/exceptions and other common logic. For each request, a new channel should be used to simplify disposal of unused resources.
The problem is with that ExecuteCommand()
method - I have to initialize a channel each time to be able to pass the method to be executed as a parameter. Sorry 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 that ExecuteCommand()
is called - channel
has been disposed of. The reason an channel
object is needed at all is to be able to provide a method that can be executed as a parameter! ie () => channel.GetCars()
. To further support these words, here is WcfHelper
the inside of the class:
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, short question - is it possible to initialize variables inside channel
the ExecuteCommand
method itself , while at the same time it is possible to define which method should 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));
even
string color = "blue";
var cars = WcfHelper.ExecuteCommand<CarServiceSoapChannel>(channel => channel.GetCars(color));
Of course, any other code improvement suggestions are welcome, but not mandatory.
PS is added ASMX
as Service reference
in Visual Studio
. So there are some entities that are auto-generated for "CarService" like - CarServiceSoapChannel
interfaces, CarServiceSoapClient
classes, and of course interfaces that CarService
contain web service methods. In the above example, a ChannelFactory
is used to create a channel for the CarServiceSoapChannel
interface , so here the question name comes from: Passing an interface method as a parameter
. This can be misleading, but I hope it's pretty clear what's being accomplished from the description itself.
Update 25.05.2018 I followed @nvoigt 's suggestion and was able to achieve my desired result:
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 has achieved the original goal. However, there is a problem with this approach. In order to call the method - you have to explicitly specify the return parameter of the method.
If you're going to call the method, so to speak - it's not enough to write code:
var result = WcfHelper.ExecuteCommand<CarServiceSoapChannel>(channel => channel.IsBlue())
You will have to specify the return type asboolean
var result = WcfHelper.ExecuteCommand<CarServiceSoapChannel, bool>(channel => channel.IsBlue())
I personally don't mind writing extra code as it still has a big advantage over my initial method implementation, however, I'm wondering if the new version can improve this?
For example, when I have only 1 generic type TResult
in the method - the return type <TResult>
can be omitted. This is no longer the case with 2 generics. Anyway, thanks @nvoigt !
Your method signature should be:
public static TResult ExecuteCommand<TInterface>(Func<TInterface, TResult> method)
Then, in WcfHelper (probably no longer should because static
it requires 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.
Since you should now have the factory in your class as an instance of the member, you can put a generic service contract into your class to make it easier for the user:
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());