Pitfalls of (Mis)Using C# Iterators to Implement Coroutines

LBushkin picture LBushkin · Dec 14, 2009 · Viewed 7k times · Source

I am writing refactoring a Silverlight program to consumes a portion of its existing business logic from a WCF service. In doing so, I've run into the restriction in Silverlight 3 that only allows asynchronous calls to WCF services to avoid cases where long-running or non-responsive service calls block the UI thread (SL has an interesting queuing model for invoking WCF services on the UI thread).

As a consequence, writing what once was straightforward, is becoming rapidly more complex (see the code examples at the end of my question).

Ideally, I would use coroutines to simplify the implementation, but sadly, C# does not currently support coroutines as a native language facility. However, C# does have the concept of generators (iterators) using the yield return syntax. My idea is to re-purpose the yield keyword to allow me to build a simple coroutine model for the same logic.

I am reluctant to do this, however, because I am worried that there may be some hidden (technical) pitfalls that I'm not anticipating (given my relative inexperience with Silverlight and WCF). I am also worried that the implementation mechanism may not be clear to future developers and may hinder rather than simplify their efforts to maintain or extend the code in the future. I've seen this question on SO about re-purposing iterators to build state machines: implementing a state machine using the "yield" keyword, and while it's not exactly the same thing I'm doing, it does make me pause.

However, I need to do something to hide the complexity of the service calls and manage the effort and potential risk of defects in this type of change. I am open to other ideas or approaches I can use to solve this problem.

The original non-WCF version of the code looks something like this:

void Button_Clicked( object sender, EventArgs e ) {
   using( var bizLogic = new BusinessLogicLayer() ) {
       try  {
           var resultFoo = bizLogic.Foo();
           // ... do something with resultFoo and the UI
           var resultBar = bizLogic.Bar(resultFoo);
           // ... do something with resultBar and the UI
           var resultBaz = bizLogic.Baz(resultBar);
           // ... do something with resultFoo, resultBar, resultBaz
       }
   }
}

The re-factored WCF version becomes quite a bit more involved (even without exception handling and pre/post condition testing):

// fields needed to manage distributed/async state
private FooResponse m_ResultFoo;  
private BarResponse m_ResultBar;
private BazResponse m_ResultBaz;
private SomeServiceClient m_Service;

void Button_Clicked( object sender, EventArgs e ) {
    this.IsEnabled = false; // disable the UI while processing async WECF call chain
    m_Service = new SomeServiceClient();
    m_Service.FooCompleted += OnFooCompleted;
    m_Service.BeginFoo();
}

// called asynchronously by SL when service responds
void OnFooCompleted( FooResponse fr ) {
    m_ResultFoo = fr.Response;
    // do some UI processing with resultFoo
    m_Service.BarCompleted += OnBarCompleted;
    m_Service.BeginBar();
}

void OnBarCompleted( BarResponse br ) {
    m_ResultBar = br.Response;
    // do some processing with resultBar
    m_Service.BazCompleted += OnBazCompleted;
    m_Service.BeginBaz();
} 

void OnBazCompleted( BazResponse bz ) {
    m_ResultBaz = bz.Response;
    // ... do some processing with Foo/Bar/Baz results
    m_Service.Dispose();
}

The above code is obviously a simplification, in that it omits exception handling, nullity checks, and other practices that would be necessary in production code. Nonetheless, I think it demonstrates the rapid increase in complexity that begins to occur with the asynchronous WCF programming model in Silverlight. Re-factoring the original implementation (which didn't use a service layer, but rather had its logic embedded in the SL client) is rapidly looking to be a daunting task. And one that is likely to be quite error prone.

The co-routine version of the code would look something like this (I have not tested this yet):

void Button_Clicked( object sender, EventArgs e ) {
    PerformSteps( ButtonClickCoRoutine );
}

private IEnumerable<Action> ButtonClickCoRoutine() {
    using( var service = new SomeServiceClient() ) {
        FooResponse resultFoo;
        BarResponse resultBar;
        BazResponse resultBaz;

        yield return () => {
            service.FooCompleted = r => NextStep( r, out resultFoo );
            service.BeginFoo();
        };
        yield return () => {
            // do some UI stuff with resultFoo
            service.BarCompleted = r => NextStep( r, out resultBar );
            service.BeginBar();
        };
        yield return () => {
            // do some UI stuff with resultBar
            service.BazCompleted = r => NextStep( r, out resultBaz );
            service.BeginBaz();
        };
        yield return () => {
            // do some processing with resultFoo, resultBar, resultBaz
        }
    }
}

private void NextStep<T>( T result, out T store ) { 
    store = result;
    PerformSteps();  // continues iterating steps
}

private IEnumerable<Action> m_StepsToPerform;
private void PerformSteps( IEnumerable<Action> steps ) {
   m_StepsToPerform = steps;
   PerformSteps();        
}

private void PerformSteps() {
   if( m_StepsToPerform == null ) 
       return; // nothing to do

   m_StepsToPerform.MoveNext();
   var nextStep = m_StepsToPerform.Current;
   if( nextStep == null ) {
       m_StepsToPerform.Dispose();
       m_StepsToPerform = null;
       return; // end of steps
   }
   nextStep();
}

There are all sorts of things that need to be improved in the above code. But the basic premise is to factor out the continuation pattern (creating an interception point for exception handling and various checks) while allowing the event-based async model of WCF to drive when each step is performed - basically when the last async WCF call completes. While on the surface this looks like more code, it's worth mentioning that PerformSteps() and NextStep() are reusable, only the implementation in ButtonClickCoRoutine() would change with each different implementation site.

I'm not entirely sure I like this model, and I wouldn't be surprised if a simpler way existed to implement it. But I haven't been able to find one on the "interwebs" or MSDN, or anywhere else. Thanks in advance for the help.

Answer

Jon Skeet picture Jon Skeet · Dec 14, 2009

You should definitely look at the Concurrency and Coordination Runtime. It uses iterators for exactly this purpose.

On the other hand, you should also look at Parallel Extensions and its approach to continuations. Parallel Extensions is part of .NET 4.0, whereas the CCR requires separate licensing. I would advise you to go with a framework written by people who eat, breathe and sleep this stuff though. It's just too easy to get details wrong on your own.