Model overview for asynchronous programming
In a previous post, I presented some code to implement the IAsyncResult pattern. In this post, I will present a simplified model representing the concepts needed to implement an asynchronous operation based on the pattern. If designed correctly, the model can be used to automatically generate code that implements most of the pattern.
Because there are often no tools to help, these models are usually built in our minds when we learn the pattern and have applied it a few times. These models may also be passed on to us when we join a new team as coding conventions. If we are lucky, these models are documented well and the documentation is updated as things change so that it is current. In reality, tracking the model over its lifetime happens usually in hallway conversations, and information gets lost when people leave the team, which is a disadvantage. Modeling the concepts in code has the advantage that you can use the model to generate code and the model serves as the documentation. The model is always current and a quick way for another person to understand the pattern.
Code Model Concepts
Because we are generating code there are some things that need to be specified in the model that are code related. These concepts are not really part of the model for asynchronous operations, but are borrowed from a model that should exist for representing code in a program. For the asynchronous programming model, we need the following basic code concepts:
public class Argument
{
private Guid m_id;
public Argument()
{
m_id = Guid.NewGuid();
}
public Guid Id
{
get
{
return m_id;
}
}
public NamedType Type { get; set; }
public string Name { get; set; }
}
public class NamedType
{
private Guid m_id;
public NamedType()
{
m_id = Guid.NewGuid();
}
public Guid Id
{
get
{
return m_id;
}
}
public string Name { get; set; }
public Namespace Namespace { get; set; }
}
public class Namespace
{
private Guid m_id;
public Namespace()
{
m_id = Guid.NewGuid();
}
public string Name { get; set; }
public Guid Id
{
get
{
return m_id;
}
}
public override string ToString()
{
return this.Name;
}
}
The Namespace, NamedType, and Argument classes each have a unique identifier so the object can be referenced by the model when needed. References are better than duplicating the entire representation of the concept in some cases. These classes have the minimum implementation required for our model.
One code model concept that is not represented above is the expression used to reference an object on which a method is to be called. Object instances can come from a variety of places including member variables, the current object "this", or the return value of a method call. In the model I simplified this concept to be a string that is rendered in the template. The property InvokeString from the model is usually set to "m_socket" in this post and would be used to render method calls such as "m_socket.EndConnect(result)" in the output.
Model Concepts
The first concept to understand in building asynchronous operations is that they are composed of sequence of steps, or other operations, that are combined to implement the operation. The steps can be synchronous or asynchronous and take input from arguments which need to be specified. Steps may have return values or other output that may need to be passed to the next step in the sequence.
The second concept is that of an exception model. Each method called to implement the operation has some exceptions that it can throw which need to be handled. Exception information is metadata about a method that usually has to be retrieved from the documentation or just by experience. For steps that use asynchronous operations to implement their functionality, both the Begin and End methods may have their own exception patterns. To generate the right exception handling code, the model needs to have a way to specify which exceptions can be expected from which methods. We can leverage the fact that groups of methods on a class are designed to have a consistent behavior. This allows us to define fewer separate exception patterns and assign them to methods that behave similarly.
The third concept is the asynchronous operation itself. It has properties such as Name, base class, and an implementation class that are used to integrate the operation into the code.
Building the model
With an overview of the model, we can now look at the code needed to generate the model for an asynchronous operation. The operation connects to a remote host, sends some data, and disconnects. The code is shown below:
class Program
{
static Namespace[] s_namespaces = new Namespace[] {
new Namespace { Name = "MyNamespace" },
new Namespace { Name = "System" },
new Namespace { Name = "AsyncResultModel" },
new Namespace { Name = "System.Net.Sockets" }
};
static NamedType[] s_types = new NamedType[] {
new NamedType { Name = "int", Namespace=s_namespaces[1] }, // 0
new NamedType { Name = "string", Namespace=s_namespaces[1] }, // 1
new NamedType { Name = "byte[]", Namespace=s_namespaces[1] }, // 2
new NamedType { Name="MyClass", Namespace= s_namespaces[0] }, // 3
new NamedType { Name="AsyncResultNoResult", Namespace=s_namespaces[2]}, // 4
new NamedType { Name="AsyncResult", Namespace=s_namespaces[2] }, // 5
new NamedType { Name="InvalidOperationException", Namespace=s_namespaces[2] }, // 6
new NamedType { Name="SocketException", Namespace=s_namespaces[3] } // 7
};
static AsynchronousOperation BuildModel()
{
// Add input arguments
Argument[] arguments = new Argument[] {
new Argument { Name = "host", Type = s_types[1] },
new Argument { Name = "port", Type = s_types[0] },
new Argument { Name = "buffer", Type = s_types[2] },
new Argument { Name = "offset", Type = s_types[0] },
new Argument { Name = "size", Type = s_types[0] }
// AsyncCallback and object are standard arguments and will be supplied automatically
};
ExceptionModel ivoModel = new ExceptionModel("IvoModel");
ivoModel.Clauses.Add(new CatchClause { Type = s_types[6] }); // InvalidOperationException
ExceptionModel endMethodModel = new ExceptionModel("EndMethodModel");
endMethodModel.Clauses.Add(new CatchClause { Type = s_types[7] }); // SocketException
// Create the asynchronous operation
var operation = new AsynchronousOperation
{
Name = "Send",
BeginEndClassId = s_types[3].Id, // MyClass exposes the operation
BaseClassId = s_types[4].Id // AsyncResultNoResult is the base class
};
// Add the arguments to the begin/end method.
operation.Method = new BeginEndMethod();
foreach (Argument a in arguments)
{
operation.Method.Arguments.Add(a);
}
// Create the steps
AsynchronousStep step = null;
step = new AsynchronousStep
{
MethodName = "Connect",
InvokeString = "m_socket",
BeginException = ivoModel,
EndException = endMethodModel,
};
operation.Steps.Add(step);
step = new AsynchronousStep
{
MethodName = "Send",
InvokeString = "m_socket",
BeginException = ivoModel,
EndException = endMethodModel,
};
operation.Steps.Add(step);
step = new AsynchronousStep
{
MethodName = "Disconnect",
InvokeString = "m_socket",
EndException = endMethodModel
};
operation.Steps.Add(step);
return operation;
}
}
The code should not be too difficult to understand. First we need to have a list of the namespaces, types, and arguments important to the operation we are creating. Then the exception models for the Begin and End methods need to be created if they don't already exist. Building the model is then a matter of creating the AsynchronousOperation, adding the arguments to the public method information, and creating each of the asynchronous steps. The name of the asynchronous operation we are creating is "Send".
The code to generate the model above represents the customization we would have done in the code editor using our conceptual model. It is a bit more efficient than manual editing though, because if we forget to add an input argument to the operation, we only have to add it to the table rather than editing multiple places in the code where that parameter needs to be passed.
Summary
In this post, I gave an overview what a model for asynchronous programming might look like, and what the code would be to create an asynchronous operation using the model. Writing the code against the model could be easier than coding the asynchronous operation by hand. But don't forget that if we get too enthusiastic with writing code to create a model, it may become more work that it's worth as we found with the Code DOM. Having tools to enter the model into a database or a convenient Domain Specific Language (DSL) would likely be a better long term approach.
In the next post, I will take a look at more details of the model and show the advantages and results of code generation from the model.
Series