none
practical WCFproxy wrapper RRS feed

  • Question

  • Hi guru-type folk out there...

    I've cobbled this code together from various sources, and while it works I'm concerened about three things:

    1) its overkill - and example of of the REAL code that i see tends to follow these lines (litterally)

    DI creates a channel (clientbase/channelfactory)/ClientProxy (svcutil) and then just treats it as a local function:

    try {var result1 = svc1.DoSomething(param1, param2);} catch (exception e){log.Write("bad happened {0}", ex);}

    I' don't GROK how the SERVICE side handles this but if it fails doesn't this leave the connection in ABORTED state consuming resources on the server until it timesout --> ie poor scaleability?

    2) its "wrong" in some way that I wont figure out until further down the track and/or its too late.
    I'm not "super happy" about the "nice message" thing but not sure what would be a better way to do this... or if perhaps some form of logging is good enough and limit the "nice message" type reposes to a handfull of errors... or maybe rethrowing NEW exceptions within these that have such "client specific" message and display those tot he users?

    3)doing this in a wrapper is no different to the example i laid out in #1 other than centralising the error handling, and it would be better to have some kind of circuitbreaker/ping test here that had some kind of "fast fail logic" rather than all the try catch stuff around the different types of exceptions (ie: at the moment the different exceptions "do" nothing, but I imagine that the "timeout" catch could conceivably trigger some kind of "try again 2-3 times", but would that cause/compound "responsiveness" issues on the client if things timed out for some legitimate reason making the system grind to a halt? (making the issue worse or moving the issue to a differnt area)

    Anyway the proxy class has two "ideas" Use and Exec/Request

    The "USE(x=>x)" is more flexible/generic but the syntax feels a bit awkward... maybe,...

    Exec() and REquest() are supposed to be a kind of "pair" and an alternative to the "Use()" funtion... just for separation... was gonna get rid of one "style"...

    This is how your supposed to use it (or how I had envisioned using it).

    A) Use()

    var p1 = new WCFProxy<IService1>();
                
                p1.Use(x => n = x.GetItem(1231)); 

    B) Exec()/REquest()

    var p2 = new WCFProxy<IService2>();
    string msg, r;
     
    p2.Exec(x=>x.DoOtherStuff((int)234, (string)"options?"), out msg);
    r = p1.Request(x => x.GetItem(234), out msg);
    

    Anyway here's my code so far, can you take a look and let me know if I've missed something/thoughts impressions etc...? (thanks in advance)

    [Also havn't gone far with logging, but also trying to keep it simple... perhaps its too simple?]

        public interface ILog
        {
            void Log(string message, params object[] args);
            void Log(System.Diagnostics.TraceLevel level, string message, params object[] args);
            void Log(Exception exception);
        }
    
        public class NullLogger : ILog
        {
            public void Log(string message, params object[] args)
            {
                //nothing
            }
    
            public void Log(TraceLevel level, string message, params object[] args)
            {
                //nothing
            }
            public void Log(Exception exception)
            {
                //nothing
            }
        }
    
        public class DebugLogger : ILog
        {
            public void Log(string message, params object[] args)
            {
                Log(TraceLevel.Info, message, args);
            }
    
            public void Log(TraceLevel level, string message, params object[] args)
            {
                switch (level)
                {
                    case TraceLevel.Verbose:
                        Debug.WriteLine(message, args); // or always?
                        Trace.WriteLine(string.Format(message, args), "classname");
                        break;
                    case TraceLevel.Info:
                        Trace.TraceInformation(message, args);
                        break;
                    case TraceLevel.Warning:
                        Trace.TraceWarning(message, args);
                        break;
                    case TraceLevel.Error:
                        Trace.TraceError(message, args);
                        break;
                    default:
                        //whatevers...
                        break;
                }
            }
            public void Log(Exception exception)
            {
                Log(TraceLevel.Error, exception.ToString());
            }
    
        }
    
    
        public class WCFProxy<TService>
        {
            private ILog logger = new DebugLogger();
    
            public WCFProxy(ILog logger = null)
            {
                if (logger != null)
                    this.logger = logger;
            }
    
            // each type "T" will have its own static instance of this factory, but being a static ensures that the expense of creating the ChannelFactory
            // is only done once per service contract.
            private static ChannelFactory<TService> _channelFactory = new ChannelFactory<TService>("*");//("WSHttpBinding_" + typeof(TService).Name);
            
            public  TResult Use<TResult>(Func<TService, TResult> serviceAction)
            {
                TResult result = default(TResult);
                using (var proxy = (IClientChannel)_channelFactory.CreateChannel())
                {
    
                    // Because we are passing in a Func<TService, TResult> the name will always be <GetProductByID>_b123 so just strip the unwanted characters
                    // for logging purposes
                    string operationName = serviceAction.Method.Name;
                    if (operationName.Contains('<') && operationName.Contains('>'))
                        operationName = operationName.Substring(1, operationName.IndexOf('>') - 1);
    
                    try
                    {
                        result = serviceAction((TService)proxy);
                        proxy.Close();
                    }
                    catch (CommunicationException e)
                    {
                        logger.Log(TraceLevel.Error, "Service Communication Exception - Service: {0} - Operation: {1} - Details: {2}",
                            typeof(TService).Name, operationName, e.ToString());
                        proxy.Abort();
                        //throw;
                    }
                    catch (TimeoutException e)
                    {
                        logger.Log(TraceLevel.Error, "Service Timeout Exception - Service: {0} - Operation: {1} - Details: {2}",
                            typeof(TService).Name, operationName, e.ToString());
                        proxy.Abort();
                        //throw;
                    }
                    catch (Exception e)
                    {
                        logger.Log(TraceLevel.Error, "Service General Exception - Service: {0} - Operation: {1} - Details: {2}",
                            typeof(TService).Name, operationName, e.ToString());
                        proxy.Abort();
                        //throw;
                    }
                }
                return result;
            }
    
    
            public TResult Request<TResult>(Func<TService, TResult> serviceAction, out string niceMessage)
            {
                TResult result = default(TResult);
                using (var proxy = (IClientChannel)_channelFactory.CreateChannel())
                {
                    try
                    {
                        niceMessage = "service call executing...";
                        result = serviceAction((TService)proxy);
                        proxy.Close();
                        niceMessage = "service call completed...";
                    }
                    catch (FaultException faultEx)
                    {
                        switch (faultEx.Code.Name)
                        {
                            case "ConnectionFault":
                                niceMessage = "Connection problem:" + faultEx.Message + "\n\n Try again later.";
                                break;
                            case "DataReaderFault":
                                niceMessage = "Data problem:" + faultEx.Message + "\n\n Contact the administrator.";
                                break;
                            default:
                                niceMessage = "Unknown problem:" + faultEx.Message + "\n\n Contact the administrator.";
                                break;
                        }
                        logger.Log(faultEx);
                        proxy.Abort();
                    }
                    catch (CommunicationException exception)
                    {
                        niceMessage = "failure to communicate...";
                        logger.Log(exception);
                        proxy.Abort();
                    }
                    catch (TimeoutException exception)
                    {
                        niceMessage = "timeout...";
                        logger.Log(exception);
                        proxy.Abort();
                    }
                    catch (Exception e)
                    {
                        niceMessage = "unknown Service error...";
                        logger.Log(e);
                        proxy.Abort();
                        throw;
                    }
                }
    
                return result;
            }
    
            // doesn't return a value
            public void Exec(Action<TService> serviceAction, out string niceMessage)
            {
                using (var proxy = (IClientChannel)_channelFactory.CreateChannel())
                {
                    try
                    {
                        niceMessage = "service call executing...";
                        serviceAction((TService)proxy);
                        proxy.Close();
                        niceMessage = "service call completed...";
                    }
                    catch (FaultException faultEx)
                    {
                        switch (faultEx.Code.Name)
                        {
                            case "ConnectionFault":
                                niceMessage = "Connection problem:" + faultEx.Message + "\n\n Try again later.";
                                break;
                            case "DataReaderFault":
                                niceMessage = "Data problem:" + faultEx.Message + "\n\n Contact the administrator.";
                                break;
                            default:
                                niceMessage = "Unknown problem:" + faultEx.Message + "\n\n Contact the administrator.";
                                break;
                        }
                        logger.Log(faultEx);
                        proxy.Abort();
                    }
                    catch (CommunicationException exception)
                    {
                        niceMessage = "failure to communicate...";
                        logger.Log(exception);
                        proxy.Abort();
                    }
                    catch (TimeoutException exception)
                    {
                        niceMessage = "timeout...";
                        logger.Log(exception);
                        proxy.Abort();
                    }
                    catch (Exception e)
                    {
                        niceMessage = "unknown Service error...";
                        logger.Log(e);
                        proxy.Abort();
                        throw;
                    }
                }
            }



    - sure I'm noJedi but that's no reason to stop trying to make stuff levitate! -

    Tuesday, October 25, 2016 10:40 PM

All replies

  • Hello noJedi,

    I understand what the proxy is trying to achieve; mainly a consistent approach to logging and error handling.  To me the proxy really is not taking advantage of the WCF stack and all its extension points.  For example, I would not have the logging as part of the proxy class (in the constructor) but done via an attribute and/or customer endpoint behaviors.

    An example of this is in the MSDN project: WCF ServiceFramework.  This project is looking at addressing Auditing and Registration so is a bit more enterprise than logging per se but I like the approach a bit more as it extends WCF in behaviors.  For example:

    ...
    <behaviors> 
          <endpointBehaviors> 
            <behavior name="clientAuditRegistration"> 
              <frameworkclient> 
                <audit includePayloads="true" includeCorrelation="true" propagateCorrelation="true" includeEvent="true" propagateEvent="true" traceSource="clienttrace" /> 
                <registration clientName="UnitTests" clientVersion="urn:spike:serviceframework:unittests:v1" /> 
              </frameworkclient> 
            </behavior> 
            <behavior name="clientAuditNoCorr"> 
              <frameworkclient> 
                <audit includePayloads="true" includeCorrelation="false" traceSource="clienttrace" /> 
              </frameworkclient> 
            </behavior> 
    ...


    Cheers, Jeff

    Wednesday, October 26, 2016 1:49 AM
  • While my example did highlight logging, it wasn't the focus of our issue.

    We are trying to simplify the client code while ensuring scalable practices in volume of calls.

    [[eg: one issue we are having in a production system is that (it looks like) the connections/memory 'runs out' on our server (not the whole server, just the IIS process for the webservice) and then we get timeouts (because the default #threads per processor, yadda) because the client isn't calling "Close()" and so the calls are (I think) waiting for the connection timeout (00:05:00? 30/60/90 seconds?) before another connection becomes available, the solution for our devOps guys at the moment is reccyle the app pool]]

    we think its because of code like this in the client:

    var svc = new ServiceClient();
    var restult = svc.DoSomething();//implicit OPEN, never manually closed/aborted - reliance on some custom but mostly global error handling for most failures
    //other things (like returning the result or getting other stuff from other services)

    Client devs don't like the 100s of "try{ restult = svc.DoSomething(); svc.Close();} catch{... svc.Abort();}" and see the bolded text as something that should be "automatic", so we are tryiing to help them out.

    I really like what you did there, but you are right in that its a lot more "enterprisy" than what I'm looking for.

    I found this, which has given me some more food for thought - http://megakemp.com/2009/07/02/isolating-wcf-through-dependency-injection-part-ii/

    Since your post I've found more "wcf framework" type things that have all tried to encapsulate the "create/open", "Method", "Close/Abort" steps in one way or another...

    I like your approach to adding elements via the entry points, as is obviously been designed for...

    Thanks heaps for your guidance... I'm going to have to evaluate a handful of these options to try to figure out what works for us - I liked the simplicity of what I had come up with, but its clear from seeing the various aspects to yours and others code, mine is probably overly simplistic.

    :(

    Thanks


    - sure I'm noJedi but that's no reason to stop trying to make stuff levitate! -



    Wednesday, November 2, 2016 1:05 PM
  • Thanks for the follow up post noJedi.

    Cheers, Jeff

    Wednesday, November 2, 2016 8:11 PM