X
Home & Office

Cool use of anonymous delegates

I try to keep up to date with the latest changes to the programming tools I use on a regular basis. This means that I have a tendency to use less generally well-known language features.
Written by John Carroll, Contributor

I try to keep up to date with the latest changes to the programming tools I use on a regular basis. This means that I have a tendency to use less generally well-known language features. I use them, however, if I feel I have good reason to use them. Until recently, I didn't have a good reason to use anonymous methods.

Normally, when you hook an event generated by an object in .NET, you register a delegate with an event that points to a named method (static or member). The C# syntax for that would be:

obj.SomeEvent += new EventHandler(this.MyHandler);

Since the EventHandler delegate is defined as something that takes an object and an EventArgs argument, as follows:

public delegate void EventHandler(object obj, EventArgs args);

...the method used with the delegate would look like this:

private void MyHandler(
   object obj, EventArgs args)
{
   // Do something
}

I never found this syntax to be particularly onerous, and so I never had much reason to do anything but use named methods with delegates. Besides, most people understand named methods, and anonymous delegates are a rather exotic C# version 2.0 addition to the language the existence of which many are unaware. Better to use what is known, as others will end up maintaining my code someday.

Last week, however, I ran across a situation that resisted conventional solutions. I have a program that makes a number of web service calls. I wanted each web service call to repeat a configurable number of times in error cases, retrying after waiting a configurable interval, and logging each error (if any) encountered during each pass.

This was a refactoring project, where existing code that had existed for several years was being adapted for use in a library that could be used more widely than the old, standalone Windows Service in which it previously resided. In that code, some of the web service calls did what they were supposed to do, looping as indicated in the previous paragraph. Others did so incompletely (skipped the wait interval, or used a hard-coded retry count) and others skipped the retry step completely.

Worse, some of the logging steps misidentified themselves to the log, mostly because the boilerplate code (which was clearly copied from elsewhere in the program) had been incompletely updated to account for its new location.

Clearly, this logic needed to be turned into something that could be shared throughout the program. But how?

The problem was that, though the retry loop, wait interval and logging steps are the same across web service calls, the arguments passed to the web service call and its return type are not. That makes things tricky. The service method can have zero or 100 arguments. The method can return nothing, or a value that is required for additional processing later in the calling method.

Generics (a bit like C++ templates, and also called parameterized types) aren't any help, as even generics need to know the calling details of a particular method. I contemplated a byzantine reflection-based framework that would serve the same purpose, but I hate methods with untyped object arrays as arguments, and besides, it seemed like an awful lot of work just to be able to reuse boilerplate web service calling logic.

This is where anonymous methods proved useful. Anonymous methods are odd beasts, as they have access to the local variables present in the method in which they are declared. This is why the following code, when compiled, causes 100 to be printed at the end.

class Program
{
   public static void RunDelegate(
      int count,
      EventHandler handler)
   {
      for (int i = 0; i < count; i++)
      {
         handler(null, null);
      }
   }
   static void Main(string[] args)
   {
      int counter = 0;
      
      Program.RunDelegate(
         100,
         delegate(object obj, EventArgs a)
         {
            counter++;
         });
      Console.Out.WriteLine(
         "Counter: " + counter);
   }
}

This peculiarity of anonymous methods enabled me to solve my boilerplate web service method problem using common code that could be shared throughout my project. The net result was a solution that ensured that retry logic was performed in exactly the same way for every web service call in the system. More generally, the lesson learned gave me one more tool to use in my programming toolkit.

A brief explanation of the code: The repeater code is located inside the WebServiceCallRepeater class. This is a simplified version of code I'm using in my project. WebServiceCall is an example of the use of the WebServiceCallRepeater class, and can be used in place of any direct call to the Web Service method.

public class WebServiceCallRepeater
{
   public delegate void OperationToRepeat(
      WebServiceCallRepeater repeater);
   private ICallContext _context;
   public WebServiceCallRepeater(
      ICallContext context)
   {
      if (context == null)
      {
         throw new ArgumentNullException(
            "context");
      }
      this._context = context;
   }
   
   public ICallContext Context
   {
      get
      {
         return this._context;
      }
   }
   
   public static void RepeatOperation(
      ICallContext context,
      string operationName,
      OperationToRepeat operationToRepeat)
   {
      WebServiceCallRepeater repeater
         = new WebServiceCallRepeater(context);
      repeater.RepeatOperation(
         operationName,
         operationToRepeat);
   }
   
   public void RepeatOperation(
      string operationName,
      OperationToRepeat operationToRepeat)
   {
      int retries = 1;
      
      DateTime begin = DateTime.Now;
      while (true)
      {
         try
         {
            this._context.Log.LogInfo(
               String.Format(
                  "Calling {0}, Attempt #: {1}",
                  operationName,
                  retries));
            
            operationToRepeat(this);
            break;
         }
         catch (Exception e)
         {
            this._context.Log.LogError(
               String.Format(
                  "Call to {0} FAILED! Time elapsed: {1}.",
                  operationName,
                  DateTime.Now - begin),
               e);
            
            retries++;
            if (retries <= this._context.RetryCount)
            {
               this._context.Log.LogInfo(
                  String.Format(
                     "Retrying {0}, attempt #{1}",
                     operationName,
                     retries));
               System.Threading.Thread.Sleep(
                  this._context.RetryInterval);
            }
            else
            {
               this._context.Log.LogError(
                  String.Format(
                     "Retry count exceeded on " +
                     "web service call {0}, limit: {1}",
                     operationName,
                     this._context.RetryCount),
                  e);
               throw;
            }
         }
      }
      
      this._context.Log.LogInfo(
         String.Format(
            "Call to {0} Succeeded! Time elapsed: {1}.",
            operationName,
            DateTime.Now - begin));
   }
}
public static class WebServiceCall
{
   public static string CallServiceMethod(
      ICallContext context,
      string arg1,
      double arg2)
   {
      Proxies.MyProxy proxy = new Proxies.MyProxy();
      
      WebServiceCallRepeater repeater =
         new WebServiceCallRepeater(
            context);
      string retVal = null;
      repeater.RepeatOperation(
         "MyProxy.MyServiceCall",
         delegate(WebServiceCallRepeater arg)
         {
            retVal = proxy.MyServiceCall("arg1", 2);
         });
      
      return retVal;
   }
}
Editorial standards