Asynchronous Operation Model and Code Generation

In the previous post I gave a brief overview of an object model that would allow one to capture the data necessary to implement an asynchronous operation. In this post, I will describe in more detail the concepts of the model.

One of the key concepts, the ExceptionModel, consists of a friendly name and possibly some CatchClause objects. These are shown below:

public class CatchClause

{

    public CatchClause()

    {

    }

    public String DelegateName { get; set; }

    public NamedType Type { get; set; }

}

public class ExceptionModel

{

    private string m_name;

    private List<CatchClause> m_clauses;

    public ExceptionModel(string name)

    {

        m_name = name;

        m_clauses = new List<CatchClause>();

    }

    public IList<CatchClause> Clauses

    {

        get

        {

            return m_clauses;

        }

    }

}

 

These objects capture the metadata about a method that we usually have to learn before writing code that uses it.  The catch clause indicates the type of exception and the name of an optional delegate to be called when that exception is caught.

The Step and AsynchronousStep objects are simple data containers.  The SynchronousStep is not shown in this simplified example.

public class Step

{

    protected Step()

    {

    }

}

public class AsynchronousStep : Step

{

    public string InvokeString { get; set; }

    public string MethodName { get; set; }

    public ExceptionModel BeginException { get; set; }

    public ExceptionModel EndException { get; set; }

}

The BeginEndMethod class helps identify which arguments are passed into the Begin method, and the name shared by the Begin and End methods so they match if different from the operation name.  The AsynchronousOperation also has some arguments that should be passed to the constructor of the class derived from AsyncResultNoResult that don't appear in the signature of the Begin method. These are usually member variables of the class the Begin method is implemented on.

The AsynchronousOperation contains GUIDs that reference the base class for the AsyncResultNoResult implementation and the class which hosts the asynchronous operation.  For this simplified example, the GUIDs reference one of the known NamedType entries in the model generation code. The output result type would be used for operations that return data. Last but not least, as expected, each operation has a list of steps to perform to complete the operation.

public class BeginEndMethod

{

    private Guid m_id;

    private List<Argument> m_arguments;

    public BeginEndMethod()

    {

        m_id = Guid.NewGuid();

        m_arguments = new List<Argument>();

    }

    // Inherited from AsynchronousOperation by default.

    public string Name

    {

        get;

        set;

    }

    public Guid Id

    {

        get

        {

            return m_id;

        }

    }

    public IList<Argument> Arguments

    {

        get

        {

            return m_arguments;

        }

    }

}

public class AsynchronousOperation

{

    private Guid m_id;

    private List<Step> m_steps;

    private List<Argument> m_arguments;

    public AsynchronousOperation()

    {

        m_id = Guid.NewGuid();

        m_steps = new List<Step>();

        m_arguments = new List<Argument>();

    }

    public Guid Id

    {

        get

        {

            return m_id;

        }

    }

    public string Name { get; set; }

    public Guid BeginEndClassId { get; set; }

    public BeginEndMethod Method { get; set; }

    // Additional arguments to constructor not passed into the Begin method.

    public IList<Argument> Arguments

    {

        get

        {

            return m_arguments;

        }

    }

    public IList<Step> Steps

    {

        get

        {

            return m_steps;

        }

    }

    // Configuration

    public NamedType OutputResultType { get; set; }

    public Guid BaseClassId { get; set; }

}

 

Generated code

The model creation code from the previous post is approximately 90 lines long and generates 190 lines of boiler plate code. Each parameter to the Begin method is placed in many places throughout the implementation. For example, look how the parameter "host" is used:

using System;

namespace MyNamespace

{

    public partial class MyClass

    {

        public IAsyncResult BeginSend(

            string host,

            int port,

            byte[] buffer,

            int offset,

            int size,

            AsyncCallback asyncCallback,

            object state)

        {

            // TODO: Argument validation

            SendAsyncResult result =

                    new SendAsyncResult(

                        host,

                        port,

                        buffer,

       offset,

                        size,

                        asyncCallback,

                        state,

                        this /*owner*/,

                        "send" /*operationId*/

                       );

            result.Process();

            return result;

        }

        public void EndSend(IAsyncResult result)

        {

            AsyncResultNoResult.End(result, this, "send");

        }

    }

    internal partial class SendAsyncResult : AsyncResultNoResult

    {

        // State for the operation

        private string m_host;

        private int m_port;

        private byte[] m_buffer;

        private int m_offset;

        private int m_size;

        public SendAsyncResult(

            string host,

            int port,

            byte[] buffer,

            int offset,

            int size,

            AsyncCallback asyncCallback,

            object state,

            object owner,

            string operationId)

            : base(asyncCallback, state, owner, operationId)

        {

            m_host = host;

            m_port = port;

            m_buffer = buffer;

            m_offset = offset;

            m_size = size;

        }

        internal override void Process()

        {

            this.StartConnect();

        }

        private void _ConnectCompleted(IAsyncResult result)

        {

            bool unhandled = true;

            Exception caughtException = null;

            try

            {

                m_socket.EndConnect(result);

                this.StartSend();

                unhandled = false;

            }

            catch (SocketException exception)

            {

                unhandled = false;

                caughtException = exception;

            }

          catch (InvalidOperationException exception)

            {

                unhandled = false;

                caughtException = exception;

            }

            finally

            {

                if (unhandled)

                {

                   caughtException =

                        new Exception("Unhandled exception detected.");

                }

                if (caughtException != null)

                {

                    this.Complete(

                        caughtException,

                        false /*completedSynchronously*/);

                }

            }

        }

        private void _SendCompleted(IAsyncResult result)

        {

            bool unhandled = true;

            Exception caughtException = null;

            try

            {

                m_socket.EndSend(result);

                this.StartDisconnect();

                unhandled = false;

            }

            catch (SocketException exception)

            {

                unhandled = false;

                caughtException = exception;

            }

            finally

            {

                if (unhandled)

                {

                    caughtException =

                        new Exception("Unhandled exception detected.");

    }

                if (caughtException != null)

                {

                    this.Complete(

                        caughtException,

                        false /*completedSynchronously*/);

                }

            }

        }

        private void _DisconnectCompleted(IAsyncResult result)

        {

            bool unhandled = true;

            Exception caughtException = null;

            try

            {

                m_socket.EndDisconnect(result);

                unhandled = false;

            }

            catch (SocketException exception)

            {

                unhandled = false;

                caughtException = exception;

            }

            finally

            {

                if (unhandled)

                {

                    caughtException =

                        new Exception("Unhandled exception detected.");

                }

                if (caughtException != null)

                {

                    this.Complete(

           caughtException,

                        false /*completedSynchronously*/);

                }

                else

                {

                    // Success

                    this.Complete(null);

                }

            }

      }

    }

}

 

Custom Code

Most of the code can be generated, but there is still some code that the programmer needs to supply. In this case it is the implementations of the Start methods:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Net;

using System.Net.Sockets;

namespace MyNamespace

{

    internal partial class SendAsyncResult

    {

        private Socket m_socket;

        public void StartConnect()

        {

            m_socket = new Socket(

     AddressFamily.InterNetwork,

                SocketType.Stream,

                ProtocolType.Tcp);

            m_socket.BeginConnect(

                m_host,

                m_port,

                this._ConnectCompleted,

                null /*state*/);

        }

        public void StartSend()

        {

            m_socket.BeginSend(

                m_buffer,

                m_offset,

                m_size,

                SocketFlags.None,

                this._SendCompleted,

                null /*state*/);

        }

        public void StartDisconnect()

        {

            m_socket.BeginDisconnect(

                true /*reuse*/,

                this._DisconnectCompleted,

                null /*state*/);

        }

    }

}

 

These 48 lines of code are the lines the developer really needs to write to customize the operation correctly. If there was some UI to enter the metadata about the operation, then this could be quite a time savings for the developer. The developer would not have to keep track of all the metadata and the pattern in his head once it was stored in a database. The generated code and the custom code would be merged by the compiler since both parts are declared as partial portions of the SendAsyncResult class.

Limitations

I mentioned before that this was a simplified model. It has some limitations that would need to be addressed such as:

  • The using statements in each file should be automatically determined based on the referenced classes
  • Our code model concepts are "borrowed" from a code model that doesn't exist. An appropriate code model should be reused for these concepts.
  • No support for parameter validation
  • No support for serialization of the model
  • Getting data into the SendAsyncResult can be more complicated.
  • No support for generic arguments (i.e. AsyncResult derived operations)
  • No support for End methods returning data. OutputResultType exists, but there is no support to use this information.
  • Ordering and removal of duplicate catch clauses
  • Support for synchronous steps
  • Additional support for catch clause customization (for example, ignoring and or logging the exception)
  • Support for multiple parallel operations in a step, and customization for partial success

Despite the limitations, it is impressive what the simple model can do.

Summary

In this post, I showed how a simple model can be created to allow developers to focus on only the code that needs to be custom written.  By having the conceptual model in code, the developer doesn't have to keep track of it in his mind.  

Series

Start of series previous