Reputation: 5257
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
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