Roger Torres - March 5th, 2010
Comments
I just released version 1.5 to CodePlex. You can find the latest binaries, source code, and release notes there.
This version supports new data types, contains new optimized type materializer/mapper classes, and improved visual studio add-in.
Let’s say you need to start a new data centric application from scratch. For simplicity, we will create a new C# Console application and connect to AdventureWorks2008 (the one that’s compatible with Sql Server 2008, including the new data types.)
Here are the steps you need to follow:
- Make sure you have the CoNatural.Data.VisualStudio.AddIn.AddIn file under your {User Profile}\Documents\Visual Studio 2008\AddIns folder. This file is a pointer to the add-in library, here is what I have in my development system (note that yours must point to the location where you saved the binaries.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| <?xml version="1.0" encoding="UTF-16" standalone="no"?>
<Extensibility xmlns="http://schemas.microsoft.com/AutomationExtensibility">
<HostApplication>
<Name>Microsoft Visual Studio</Name>
<Version>9.0</Version>
</HostApplication>
<Addin>
<FriendlyName>CoNatural Data VisualStudio AddIn</FriendlyName>
<Description>Manage CoNatural data commands in Visual Studio 2008.</Description>
<Assembly>C:\Users\Roger\Documents\Visual Studio 2008\Projects\conatural\CoNatural Components\bin\CoNatural.Data.VisualStudio.AddIn.dll</Assembly>
<FullClassName>CoNatural.Data.VisualStudio.AddIn.Connect</FullClassName>
<LoadBehavior>1</LoadBehavior>
<CommandPreload>1</CommandPreload>
<CommandLineSafe>0</CommandLineSafe>
</Addin>
</Extensibility> |
- Open Visual Studio 2008 and create a new C# console application. Name it “AdventureWorks2008Console”.
- Create a new folder named “Commands”, and start the table import wizard (in this case we will import only “SelectAll” commands, but you can play with other options if you like).

- Point the wizard to your AdventureWorks database in step 1 of 5.
- Hit the Refresh button and select all the tables in step 2 of 5.

- In step 3 of 5, make sure “Include Schema” is checked, and “Generate .sql file” is unchecked. This is to follow the default naming conventions when generating the commands, and include the sql scripts inside the .cs class files.

- In step 4 of 5, make sure you are generating only “SelectAll” commands and the Model classes (under the /Model folder that will be created automatically).

- Complete the wizard and go to project/References to add references to CoNatural.Data (binaries folder) and Microsoft.SqlServer.Types (this should be in the gac if you have installed the sql server 2008 feature pack - with support to the new sql data types). If you don’t find the second assembly in your system, the project won’t compile, but you can always find the files with the missing references and delete them from the project.
- There is one final step. In the model you might find a couple of classes that won’t compile because a property and the type share the same name. You can rename the classes to anything you like. Here is a view of the project so far:

- Make sure the project compiles now. Then copy the following code to Program.cs. Note that you must change the connection string to fit your environment.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CoNatural.Data;
using CoNatural.Data.SqlClient;
namespace AdventureWorks2008Console {
class Program {
static void Main(string[] args) {
try {
IConnection c1 = new SqlClientConnection(@"Data Source=PHENOM-II\PHENOM_II;Initial Catalog=AdventureWorks2008;Integrated Security=True");
RunTest("Reading with default connection", c1);
IConnection c2 = new ConnectionBase(@"Data Source=PHENOM-II\PHENOM_II;Initial Catalog=AdventureWorks2008;Integrated Security=True",
new DataCommandFactoryBase(new SqlClientDbProvider()),
new OptimizedTypeMaterializerFactoryBase(),
new OptimizedTypeMapperFactoryBase());
RunTest("Reading with optimized connection", c2);
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
Console.WriteLine("DONE");
Console.ReadLine();
}
static void RunTest(string testName, IConnection c) {
DateTime startTime = DateTime.Now;
int iterations = 2;
while (iterations-- > 0) {
ReadAll<Commands.Sales.SalesOrderDetailSelectAll, Model.Sales.SalesOrderDetail>(c);
ReadAll<Commands.Production.TransactionHistorySelectAll, Model.Production.TransactionHistory>(c);
ReadAll<Commands.Production.TransactionHistoryArchiveSelectAll, Model.Production.TransactionHistoryArchive>(c);
ReadAll<Commands.Production.WorkOrderSelectAll, Model.Production.WorkOrder>(c);
ReadAll<Commands.Production.WorkOrderRoutingSelectAll, Model.Production.WorkOrderRouting>(c);
ReadAll<Commands.Sales.SalesOrderHeaderSelectAll, Model.Sales.SalesOrderHeader>(c);
ReadAll<Commands.Sales.SalesOrderHeaderSalesReasonSelectAll, Model.Sales.SalesOrderHeaderSalesReason>(c);
ReadAll<Commands.Person.BusinessEntitySelectAll, Model.Person.BusinessEntity>(c);
ReadAll<Commands.Person.PersonSelectAll, Model.Person.Person>(c);
ReadAll<Commands.Purchasing.PurchaseOrderDetailSelectAll, Model.Purchasing.PurchaseOrderDetail>(c);
}
Console.WriteLine("Test [{0}] completed in {1} seconds.", testName, DateTime.Now.Subtract(startTime).TotalSeconds);
}
static void ReadAll<TCommand, TModel>(IConnection connection) where TCommand : ICommand {
ICommand command = (ICommand)Activator.CreateInstance<TCommand>();
int rows = 0;
connection.ExecuteReader<TModel>(command).All(d => {
rows++;
return true;
});
Console.WriteLine("ReadAll returned {0} rows of {1}.", rows, typeof(TModel).Name);
}
}
} |
Here we selected 10 of the largest tables and decided to execute the “SelectAll” commands twice, using the standard type materializer and the new optimized type materializer. Here is the resulting console.

And that’s all. You have created a DAL to read from AdventureWorks to your own PONO model, it’s up to you now to play with this and find a more useful application.
I hope you enjoyed it!
Roger Torres - February 4th, 2010
Comments
This morning I released a new version of the CoNatural Components (version 1.4) to the CodePlex repository. There are some significant changes to CoNatural.Data, so existing code will require some modifications in order to compile with this version. The main theme is the re-factoring work started in v1.3 to follow interface oriented programming practices in order to make the framework more testable and IoC friendly. There are also some new features I will list below.
Release Notes
-
New interfaces
- IParameter: Data parameter to command property mapping interface. A concrete command factory instantiates commands with parameter mappings. A default implementation can be found in the /Base folder (ParameterBase).
- ITypeMaterializer<T>, ITypeMaterializerFactory: The components in charge of processing data reader results, instantiating types of T, and populating them with the results of each data record returned by the command reader. Default implementation using reflection with caching can be found in the /Base folder. Other implementations, using XML mappings for example, are possible.
- ICommandDeployer: The component in charge of deploying CoNatural data commands as sql stored procedures (creating or updating existing procedures). This functionality was moved from IConnection-IDbProvider to this new interface to decouple the basic framework from deployment scenarios. The default base class in /Base as usual, and a concrete SqlServer implementation can be found in the CoNatural.Data.SqlServer project (SqlServerCommandDeployer ).
- ITypeMapper<T>, ITypeMapperFactory: The components in charge of mapping commands bound to instances of type T. This basically replaces the ICommand<T> pattern in version 1.3, thus a code change is required to support the new pattern. Default TypeMapperBase, TypeMapperFactoryBase implementations can be found in the /Base folder, very similar to the default type materializer (using reflection and caching the mappings). IConnection now supports DefaultTypeMapperFactory and DefaultTypeMaterializerFactory properties (using /base by default) as a fallback mechanism to automatically perform the mapping of properties when no mapper is passed to the execution methods.
- ICommand: Added GetScript() method to ICommand interface. This allows users to embed SQL scripts inside command classes, making the extra .sql embedded resource file in projects an option (not a requirement). Modified basic command factory, script provider, and Visual Studio templates/Add-In to accommodate this change. When GetScript() returns null, the runtime calls the script provider associated to the connection. A CommandBase class was also added to the /Base folder with GetScript() returning null by default (useful to find/replace : ICommand to : CommandBase for a quick fix to existing commands).
- Project organization: As noted in the previous point, the project was reorganized to hold the interfaces in the main folder, and the default base class implementations in the /Base sub-folder (All base classes now with the XXXBase suffix). There are also folders for the attributes (/Attributes), and for each concrete database provider (/Odbc, /OleDb, /SqlClient). Renamed ICommandFactory to IDataCommandFactory, and moved default command factory to /Base folder.
- Helpers to simplify the API: New connection and dbprovider constructors added to all providers (SqlClient, Odbc, OleDb) using the default command and script factories.
- New Add-In features: A new wizard was implemented to generate table adapters (CRUD commands) from Sql Server tables. As mentioned above, there is a new option to generete .sql embedded resource files or inline SQL returned from GetScript() when importing stored procedures and tables.
Here are some AdventureWorks commands I just imported with the SQL script embedded in the .cs file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| //-------------------------------------------------------
//-- uspGetEmployeeManagers.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn
//-- Created on 1/31/2010 1:38:42 PM
//-------------------------------------------------------
using System;
using System.Data;
using CoNatural.Data;
namespace ConsoleApplication1.Commands.dbo {
public class uspGetEmployeeManagers : ICommand {
public uspGetEmployeeManagers() {}
public string GetScript() {
return
@"
BEGIN
SET NOCOUNT ON;
WITH [EMP_cte]([BusinessEntityID], [OrganizationNode], [FirstName], [LastName], [JobTitle], [RecursionLevel]) -- CTE name and columns
AS (
SELECT e.[BusinessEntityID], e.[OrganizationNode], p.[FirstName], p.[LastName], e.[JobTitle], 0 -- Get the initial Employee
FROM [HumanResources].[Employee] e
INNER JOIN [Person].[Person] as p
ON p.[BusinessEntityID] = e.[BusinessEntityID]
WHERE e.[BusinessEntityID] = @BusinessEntityID
UNION ALL
SELECT e.[BusinessEntityID], e.[OrganizationNode], p.[FirstName], p.[LastName], e.[JobTitle], [RecursionLevel] + 1 -- Join recursive member to anchor
FROM [HumanResources].[Employee] e
INNER JOIN [EMP_cte]
ON e.[OrganizationNode] = [EMP_cte].[OrganizationNode].GetAncestor(1)
INNER JOIN [Person].[Person] p
ON p.[BusinessEntityID] = e.[BusinessEntityID]
)
SELECT [EMP_cte].[RecursionLevel], [EMP_cte].[BusinessEntityID], [EMP_cte].[FirstName], [EMP_cte].[LastName],
[EMP_cte].[OrganizationNode].ToString() AS [OrganizationNode], p.[FirstName] AS 'ManagerFirstName', p.[LastName] AS 'ManagerLastName' -- Outer select from the CTE
FROM [EMP_cte]
INNER JOIN [HumanResources].[Employee] e
ON [EMP_cte].[OrganizationNode].GetAncestor(1) = e.[OrganizationNode]
INNER JOIN [Person].[Person] p
ON p.[BusinessEntityID] = e.[BusinessEntityID]
ORDER BY [RecursionLevel], [EMP_cte].[OrganizationNode].ToString()
OPTION (MAXRECURSION 25)
END;
";
}
public Int32? BusinessEntityID { get; set; }
}
} |
These are CRUD commands for the Department table:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
//-------------------------------------------------------
//-- DepartmentInsert.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn
//-- Created on 1/31/2010 12:39:08 PM
//-------------------------------------------------------
using System;
using System.Data;
using CoNatural.Data;
namespace ConsoleApplication1.Commands.HumanResources {
public class DepartmentInsert : ICommand {
public DepartmentInsert() {}
public string GetScript() {
return
@"
INSERT INTO [HumanResources].[Department] (
[Name],
[GroupName],
[ModifiedDate])
VALUES (
@Name,
@GroupName,
@ModifiedDate)
SET @DepartmentID = SCOPE_IDENTITY();
";
}
[Parameter(50)] public string Name { get; set; }
[Parameter(50)] public string GroupName { get; set; }
public DateTime? ModifiedDate { get; set; }
[Parameter(ParameterDirection.Output)] public Int16? DepartmentID { get; set; }
}
} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//-------------------------------------------------------
//-- DepartmentSelect.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn
//-- Created on 1/31/2010 12:39:08 PM
//-------------------------------------------------------
using System;
using System.Data;
using CoNatural.Data;
namespace ConsoleApplication1.Commands.HumanResources {
public class DepartmentSelect : ICommand {
public DepartmentSelect() {}
public string GetScript() {
return
@"
SELECT
@DepartmentID = [DepartmentID],
@Name = [Name],
@GroupName = [GroupName],
@ModifiedDate = [ModifiedDate]
FROM
[HumanResources].[Department]
WHERE
[DepartmentID] = @DepartmentID";
}
[Parameter(ParameterDirection.InputOutput)] public Int16? DepartmentID { get; set; }
[Parameter(ParameterDirection.Output, 50)] public string Name { get; set; }
[Parameter(ParameterDirection.Output, 50)] public string GroupName { get; set; }
[Parameter(ParameterDirection.Output)] public DateTime? ModifiedDate { get; set; }
}
} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
//-------------------------------------------------------
//-- DepartmentUpdate.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn
//-- Created on 1/31/2010 12:39:08 PM
//-------------------------------------------------------
using System;
using System.Data;
using CoNatural.Data;
namespace ConsoleApplication1.Commands.HumanResources {
public class DepartmentUpdate : ICommand {
public DepartmentUpdate() {}
public string GetScript() {
return
@"
UPDATE [HumanResources].[Department] SET
[Name] = @Name,
[GroupName] = @GroupName,
[ModifiedDate] = @ModifiedDate
WHERE
[DepartmentID] = @DepartmentID";
}
public Int16? DepartmentID { get; set; }
[Parameter(50)] public string Name { get; set; }
[Parameter(50)] public string GroupName { get; set; }
public DateTime? ModifiedDate { get; set; }
}
} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
//-------------------------------------------------------
//-- DepartmentDelete.cs
//--
//-- Created by Roger with CoNatural Visual Studio AddIn
//-- Created on 1/31/2010 12:39:08 PM
//-------------------------------------------------------
using System;
using System.Data;
using CoNatural.Data;
namespace ConsoleApplication1.Commands.HumanResources {
public class DepartmentDelete : ICommand {
public DepartmentDelete() {}
public string GetScript() {
return
@"
DELETE FROM [HumanResources].[Department] WHERE
[DepartmentID] = @DepartmentID";
}
public Int16? DepartmentID { get; set; }
}
} |
Let’s read all the managers for business entities 1 to 4, and print the results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CoNatural.Data;
using CoNatural.Data.SqlClient;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
try {
IConnection c = new SqlClientConnection(@"Data Source=PHENOM-II\PHENOM_II;Initial Catalog=AdventureWorks2008;Integrated Security=True");
Commands.dbo.uspGetEmployeeManagers cmd = new Commands.dbo.uspGetEmployeeManagers();
new int[] { 1, 2, 3, 4 }.All(i => {
Console.WriteLine("BusinessEntityId = " + i);
cmd.BusinessEntityID = i;
c.ExecuteReader<Model.Employee>(cmd).All(e => {
Console.WriteLine(e);
return true;
});
return true;
});
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
}
}
namespace ConsoleApplication1.Model {
public class Employee {
public string FirstName { get; set; }
public string LastName { get; set; }
public string ManagerFirstName { get; set; }
public string ManagerLastName { get; set; }
public override string ToString() {
return string.Format("{0} {1} - managed by {2} {3}", FirstName, LastName, ManagerFirstName, ManagerLastName);
}
}
} |
Can you see the default type materializer in action?
Conclusion
I believe this framework is now pretty stable, so I don’t anticipate any major design changes … but you never know what the next requirement is going to look like. I hope we will always find a good concrete implementation
Roger Torres - December 7th, 2009
Comments
This weekend I updated the CodePlex repository with the latest build (1.3) and source code. All source code files/links have been removed from this Blog — a lot of broken links ;-(, so there is only one valid repository (CodePlex). Note that the latest changes to the framework will break old code, and the examples in the original posts will require some modifications. I will try to post new examples based on the latest interfaces, but you can always read the posts explaining the reasoning behind these changes and make your modifications accordingly.
Roger Torres - September 11th, 2009
Comments
I just released a new version of the CoNatural.Data components (version 1.3). You can find the latest source code at http://conatural.codeplex.com.
This version required some code refactoring to support multiple CoNatural command factories in a single application. A command factory is just a concrete implementation of a mechanism to translate CoNatural commands to the internal representation used by the framework.
In the previous releases, there was only one command factory embedded inside the Command constructor, and the only mechanism supported was reading command assemblies via reflection and extracting parameters from properties and SQL scripts from the “.sql” file associated with the reflected types. In this new version, the same factory has been implemented in a new class “DefaultCommandFactory”, also optimized to process and cache command definitions by type.
A new ICommandFactory interface is also available for other implementations, making it more IoC (Dependency Injection) friendly, and also more testable. Now, user defined command types won’t need to inherit from CoNatural.Data.Command, but implement the empty ICommand interface. Note that this change is not backward compatible, but a simple project wide find/replace from “Command” to “ICommand” will take care of the problems.
If you have been using this framework, you might be wondering why the change now? Well, one of my clients needed to build CoNatural commands dynamically from metadata stored in a SQL Server database. After trying with static commands, modifying the scripts on the fly and applying IgnoreParameter attributes to the type properties, I decided that the best way to accommodate this requirement was by defining the command factory abstraction and move the default implementation to a new concrete class. This solution is cleaner, more testable, allows me to better mock some parts of the system and leaves my client with the responsibility to build its own command factory without further framework pollution. I hope we can describe the resulting work in future posts.
If you have any problems with the new changes or suggestions for new features, please let me know here, by email, or at the Codeplex site.
Thanks
Roger
Roger Torres - June 25th, 2009
1 Comment
This is just a quick note to let you know that today I uploaded the first functional version of the CoNatural Data Visual Studio AddIn to CodePlex (http://conatural.codeplex.com). The main feature is a wizard to import existing stored procedures as CoNatural Commands. Users will be able to select stored procedures from a data source, and apply naming conventions to generate commands in a folder structure.
I’m currently using this wizard in a project I’m migrating from regular ADO.NET with stored procedures to the CoNatural.Data framework. This client is very happy now that he doesn’t have to maintain stored procedures detached from the main source control system.
Note that this version only supports Visual Studio 2008 C# projects and the .NET 3.5 framework. The only data source supported is Sql Server 2005/2008.
Roger Torres - June 14th, 2009
1 Comment
I just published a CodePlex project to host the DAL and Asynchronous framework I have been presenting in this blog. This will allow me to track issues and manage releases the right way. Thanks to the readers who suggested this option.
You can find the project here: CoNatural Components
Please note that I made the following modifications to the original CoNatural.Data component:
- The IDbProvider interface was simplified.
- The implementation of the Sql Server provider was moved to a new assembly named CoNatural.Data.SqlServer
There are other minor (performance related) modifications, but this should be transparent to the users.
Roger Torres - May 3rd, 2009
4 Comments
In Part I of this article, I described how to write an asynchronous search component that spawns multiple parallel search operations, and notify the caller when completed or canceled. It’s now time to build a user interface to Start, Cancel, Delete, and Monitor our search operations asynchronously.
After playing with the ASP.NET MVC framework for a while, this technology is quickly becoming my favorite presentation platform, specially for small to medium size projects where I don’t need fancy controls… so I decided to develop this GUI with ASP.NET MVC. I will also write a small jQuery plugin to encapsulate the client side of the search, including a protocol to exchange AJAX/JSON messages with the controllers.
Preparing the Solution
Let’s start by creating a new ASP.NET MVC application. [Download ASP.NET MVC Here].

Create ASP.NET MVC Application
This step is very straightforward, just follow the default application template. Don’t forget to build and execute the solution to make sure all the pieces are in the right place. Note: I’m using Visual Studio 2008 SP1 and the .NET framework 3.5 SP1 which is a requirement for ASP.NET MVC.
Now you can add a reference to the AsyncOp library from Part I, and we are done configuring our solution.
Preparing the Site
Before we start writing code, let’s make sure our master page has a reference to the jQuery library under the /Scripts folder:
<script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
We are going to write a jQuery plugin, so let’s add that reference in advance:
<script src="../../Scripts/async.js" type="text/javascript"></script>
And finally, let’s define a block of CSS styles for the panel that will hold our search operations:
<style type="text/css">
.async ul {background-color:White;border:solid 1px black;padding:5px;list-style-type:none;}
.async li {border-bottom:dotted 1px silver; padding:5px;}
.async span {border:solid 1px silver;background-color:Yellow;padding:5px;font-size:80%;font-style:italic;margin:5px;}
.async input {border:solid 1px silver;padding:5px;margin:5px;}
.async input:hover {background-color:Gray;}
</style>
The master page should look like this:
Site.Master
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
<script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
<script src="../../Scripts/async.js" type="text/javascript"></script>
<style type="text/css">
.async ul {background-color:White;border:solid 1px black;padding:5px;list-style-type:none;}
.async li {border-bottom:dotted 1px silver; padding:5px;}
.async span {border:solid 1px silver;background-color:Yellow;padding:5px;font-size:80%;font-style:italic;margin:5px;}
.async input {border:solid 1px silver;padding:5px;margin:5px;}
.async input:hover {background-color:Gray;}
</style>
</head>
Managing Search Operations
Our goal is to provide a GUI to manage our asynchronous search operations, so we will need cover the following scenarios:
- List and monitor all running and completed operations.
- Start new search operations with different criteria.
- Cancel a running operation.
- Delete a completed operation (so it won’t be listed anymore).
- Present the results of a completed operation.
In order to simplify this example, the server side logic will be implemented inside the HomeController class, and I’m going to store the active search operations in the ASP.NET Session. In a production environment, I recommend to develop a more scalable “Pool” of operations where the administrator can control the maximum number of active searches and the memory used by the results.
We will start by writing a new Search action method to return all the active operations to the client. This method will only accept HTTP GET verbs, and will serialize the results back to the client using the new DataContractJsonSerializer implemented in .NET 3.5 SP1 under System.Runtime.Serialization.Json.
In Part I we annotated our AsyncOp class with DataContract and DataMember attributes to make sure only the elements defined in our contract are serialized to the client. The JsonResult class that ships with ASP.NET MVC Version 1.0 doesn’t use DataContractJsonSerializer, so the entire class (including the results) is serialized. I hope the MVC team is taking care of this little issue, but in the mean time, we can write our own JsonResult class as follows:
JsonResult.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Runtime.Serialization.Json;
namespace MvcAsyncSearch.MyTools {
public class JsonResult : ActionResult {
public Encoding ContentEncoding { get; set; }
public string ContentType { get; set; }
public object Data { get; set; }
public override void ExecuteResult(ControllerContext context) {
if (context == null)
throw new ArgumentNullException("context");
HttpResponseBase response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
response.ContentType = ContentType;
else
response.ContentType = "application/json";
if (ContentEncoding != null)
response.ContentEncoding = ContentEncoding;
if (Data != null) {
DataContractJsonSerializer serializer = new DataContractJsonSerializer(Data.GetType());
serializer.WriteObject(response.OutputStream, Data);
}
}
}
} |
Going back to our Action method, the following code takes care of presenting/monitoring all active searches. Note that I’m using a couple of helper methods to find the operations in the ASP.NET Session, and refresh the Status property of the AsyncOp instances before the results are serialized to the client.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| [AcceptVerbs(HttpVerbs.Get)]
public ActionResult Search() {
Dictionary<string, AsyncOp.AsyncOp> operations = GetAsyncOps(HttpContext.Session);
SetStatus(operations);
MyTools.JsonResult result = new MvcAsyncSearch.MyTools.JsonResult();
result.Data = operations.Values.ToArray();
return result;
}
private Dictionary<string, AsyncOp.AsyncOp> GetAsyncOps(HttpSessionStateBase session) {
object ops = session["async"];
if (ops == null) {
ops = new Dictionary<string, AsyncOp.AsyncOp>();
session["async"] = ops;
}
return (Dictionary<string, AsyncOp.AsyncOp>)ops;
}
private void SetStatus(Dictionary<string, AsyncOp.AsyncOp> operations) {
foreach(AsyncOp.AsyncOp op in operations.Values) {
if (op.Error != null)
op.Status = "Error: " + op.Error.Message;
else {
if (op.Completed || op.Cancelled) {
if (op.Completed)
op.Status = "DONE.";
else
op.Status = "Cancelled.";
if (op.Results.Count > 0)
op.Status += " <a href=\"" + Url.Action("Results",
new { asyncId = op.AsyncId }) + "\">" + op.Results.Count + " results found.</a>";
else
op.Status += " No results found.";
}
else
op.Status = op.Results.Count + " results found ...";
}
}
} |
Initially, we won’t have any active searches to display… so, we need another Search action method to Start, Cancel, and Remove operations from the pool. In this case, only HTTP POST verbs will be allowed, and the client side will be responsible for configuring the request with “Form” parameters as follows:
- To cancel an operation, include parameter “cancel={AsyncId}”
- To delete an operation, include parameter “dismiss={AsyncId}”
- To start an operation, include parameter “criteria={Search Criteria}”
This Action will also return all the active operations to the client (same JSON results), effectively updating the user interface without having to wait for the next refresh request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| [AcceptVerbs(HttpVerbs.Post)]
public ActionResult Search(FormCollection collection) {
Dictionary<string, AsyncOp.AsyncOp> operations = GetAsyncOps(HttpContext.Session);
AsyncOp.AsyncOp op;
if (collection.AllKeys.Contains("cancel")) {
string asyncId = collection["cancel"];
if (operations.TryGetValue(asyncId, out op)) {
op.Cancel();
op.Status = "Cancelling...";
}
}
else if (collection.AllKeys.Contains("dismiss")) {
string asyncId = collection["dismiss"];
operations.Remove(asyncId);
}
else {
string criteria = collection["criteria"];
op = new AsyncOp.AsyncOp(criteria.Trim().ToUpper());
operations.Add(op.AsyncId.ToString(), op);
op.Start();
}
SetStatus(operations);
MyTools.JsonResult result = new MvcAsyncSearch.MyTools.JsonResult();
result.Data = operations.Values.ToArray();
return result;
} |
Here is how the final HomeController class should look like:
HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
| using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using AsyncOp;
namespace MvcAsyncSearch.Controllers {
[HandleError]
public class HomeController : Controller {
public ActionResult Index() {
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult About() {
return View();
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Search() {
Dictionary<string, AsyncOp.AsyncOp> operations = GetAsyncOps(HttpContext.Session);
SetStatus(operations);
MyTools.JsonResult result = new MvcAsyncSearch.MyTools.JsonResult();
result.Data = operations.Values.ToArray();
return result;
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Search(FormCollection collection) {
Dictionary<string, AsyncOp.AsyncOp> operations = GetAsyncOps(HttpContext.Session);
AsyncOp.AsyncOp op;
if (collection.AllKeys.Contains("cancel")) {
string asyncId = collection["cancel"];
if (operations.TryGetValue(asyncId, out op)) {
op.Cancel();
op.Status = "Cancelling...";
}
}
else if (collection.AllKeys.Contains("dismiss")) {
string asyncId = collection["dismiss"];
operations.Remove(asyncId);
}
else {
string criteria = collection["criteria"];
op = new AsyncOp.AsyncOp(criteria.Trim().ToUpper());
operations.Add(op.AsyncId.ToString(), op);
op.Start();
}
SetStatus(operations);
MyTools.JsonResult result = new MvcAsyncSearch.MyTools.JsonResult();
result.Data = operations.Values.ToArray();
return result;
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Results(string asyncId) {
Dictionary<string, AsyncOp.AsyncOp> operations = GetAsyncOps(HttpContext.Session);
AsyncOp.AsyncOp op;
if (operations.TryGetValue(asyncId, out op))
return View(op.Results);
else
return View(new List<AsyncOp.AsyncOp.AsyncOpResult>());
}
private Dictionary<string, AsyncOp.AsyncOp> GetAsyncOps(HttpSessionStateBase session) {
object ops = session["async"];
if (ops == null) {
ops = new Dictionary<string, AsyncOp.AsyncOp>();
session["async"] = ops;
}
return (Dictionary<string, AsyncOp.AsyncOp>)ops;
}
private void SetStatus(Dictionary<string, AsyncOp.AsyncOp> operations) {
foreach(AsyncOp.AsyncOp op in operations.Values) {
if (op.Error != null)
op.Status = "Error: " + op.Error.Message;
else {
if (op.Completed || op.Cancelled) {
if (op.Completed)
op.Status = "DONE.";
else
op.Status = "Cancelled.";
if (op.Results.Count > 0)
op.Status += " <a href=\"" + Url.Action("Results",
new { asyncId = op.AsyncId }) + "\">" + op.Results.Count + " results found.</a>";
else
op.Status += " No results found.";
}
else
op.Status = op.Results.Count + " results found ...";
}
}
}
}
} |
Implementing the client side with jQuery.
I really started loving client side JavaScript after I discovered jQuery. I don’t want to get into the details here (tales of my previous suffering with JS), but I will tell you this: If you haven’t tried jQuery yet, please do so and you won’t regret it.
One of the nicest things about jQuery is that you can extend the API very naturally. In this case, I’m writing a plugin to encapsulate the client side of my async search module, including presentation of active searches, polling the server for updates via AJAX requests with JSON serialization, and handling client events to start, cancel, and delete operations.
The first thing we should do when writing a jQuery plugin (or any other JavaScript library) is define a “namespace” where we can implement our own private and public variables and interfaces. I usually start with a JavaScript closure, passing the jQuery instance (to be extended internally). Next, declare the private namespace to define my private variables and functions (visible only inside the closure), and extend jQuery with a public namespace to expose my public interface.
Let’s begin our plugin from the following template. In this case we will be extending jQuery with two public methods ($.async.get and $.async.post):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // create closure
(function($) {
// "async" namespace for private variables and functions
var async = {
var1: {},
var2: [],
f1 : function() {...},
f2 : function() {...}
};
// extend jQuery with "async" namespace
$.async = {
defaults: {
//TODO good practice to define default values
},
// GET ajax request to update the status of running async operations
get: function(url, options) {
// TODO ajax get (url) to the Search action defined previously in HomeController
// TODO options to override the default parameters
},
// POST ajax request to start,cancel,delete async operations
post: function(url, data, options) {
// TODO ajax post (url) to the Search action defined previously in HomeController
// TODO data with form parameters (criteria={criteria} | cancel={asyncId} | dismiss={asyncId})
// TODO options to override the default parameters
}
};
})(jQuery); |
Our plugin is initialized the first time a “Get” or “Post” operation is invoked from a Page, and by default it will use the “body” element as the container for the panels showing the async search operations. The plugin will also monitor the operations by polling the server every 5 seconds (by default) until all the searches are completed. Of course, I will override these defaults later to display the progress of my searches inside a smaller panel (styled with CSS). Let’s take a look at the public interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // extend jQuery with "async" namespace
$.async = {
defaults: {
container: "body",
pollingInterval: 5000
},
// GET ajax request to update the status of running async operations
get: function(url, options) {
async.options = $.extend({ url: url }, $.async.defaults, options, async.options);
async.poll();
},
// posts request to start a new async operation
post: function(url, data, options) {
async.options = $.extend({ url: url }, $.async.defaults, options, async.options);
$.post(url, data, async.callback, "json");
}
}; |
There is nothing special here other than storing the options inside a private variable and invoking the server actions. The core of the plugin is encapsulated inside the private namespace, where a group of callback functions are responsible for adding and removing search operation panels, polling the server for updates, and handling click events (to start-cancel-remove operations). Here I keep a list of panels synchronized with the server, so the plugin will know when to display a new panel. The “cancel” and “dismiss” handlers should be straightforward (unless you are new to jQuery).
The main engine is implemented inside the “callback” function, which receives a list of JSON-formatted active operations from AJAX requests to the server, and depending on the status of each operation, it creates a new panel, updates the status labels and buttons, and restarts the timer to keep polling the server for updates:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| // "async" namespace private variables and functions
var async = {
// panels showing async operations
panels: [],
// cancel handler
cancel: function() {
var asyncId = $(this).attr("asyncId");
var panel = async.panels[asyncId];
if (panel) {
$("input", panel).hide();
$.post(async.options.url, { cancel: asyncId }, async.callback, "json");
}
},
// dismiss handler
dismiss: function() {
var asyncId = $(this).attr("asyncId");
var panel = async.panels[asyncId];
if (panel) {
$.post(async.options.url, { dismiss: asyncId }, function() {
$("#" + asyncId, async.panelcontainer).remove();
async.panels[asyncId] = null;
}, "json");
}
},
// ajax callback to handle async operations
callback: function(result) {
var pending = 0;
for (var i in result) {
var asyncOp = result[i];
var panel = async.panels[asyncOp.AsyncId];
// handle new operations
if (!panel) {
// create panel container on demand
if (!async.panelcontainer) {
async.panelcontainer = $("<ul id='async'></ul>");
$(async.options.container).prepend(async.panelcontainer);
}
// create panel for this operation
panel = async.panels[asyncOp.AsyncId] = $("<li id='" + asyncOp.AsyncId + "'></li>");
async.panelcontainer.append(panel);
// add status and cancel button to panel
panel.append("<b>Searching [" + asyncOp.Criteria + "]: </b>");
panel.append("<span>Starting...</span>");
var cancelBtn = $("<input type='button' value='Cancel' asyncId='" + asyncOp.AsyncId + "' />").click(async.cancel);
panel.append(cancelBtn);
}
// check op status
if (asyncOp.Completed) {
$("input", panel).val("Dismiss").unbind("click", async.cancel).click(async.dismiss).show();
}
else if (asyncOp.Cancelled) {
$("input", panel).val("Dismiss").unbind("click", async.cancel).click(async.dismiss).show();
}
else {
// report progress
pending++;
}
$("span", panel).html(asyncOp.Status);
}
// polling
if (async.timeoutId) {
clearTimeout(async.timeoutId);
async.timeoutId = null;
}
if (pending > 0)
async.timeoutId = setTimeout(async.poll, async.options.pollingInterval);
},
// polling timeout callback
poll: function() {
$.get(async.options.url, {}, async.callback, "json");
}
}; |
The final async.js JavaScript file should be added to the /Scripts folder and will look like this:
async.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
| // create closure
(function($) {
// "async" namespace private variables and functions
var async = {
// panels showing async operations
panels: [],
// cancel handler
cancel: function() {
var asyncId = $(this).attr("asyncId");
var panel = async.panels[asyncId];
if (panel) {
$("input", panel).hide();
$.post(async.options.url, { cancel: asyncId }, async.callback, "json");
}
},
// dismiss handler
dismiss: function() {
var asyncId = $(this).attr("asyncId");
var panel = async.panels[asyncId];
if (panel) {
$.post(async.options.url, { dismiss: asyncId }, function() {
$("#" + asyncId, async.panelcontainer).remove();
async.panels[asyncId] = null;
}, "json");
}
},
// ajax callback to handle async operations
callback: function(result) {
var pending = 0;
for (var i in result) {
var asyncOp = result[i];
var panel = async.panels[asyncOp.AsyncId];
// handle new operations
if (!panel) {
// create panel container on demand
if (!async.panelcontainer) {
async.panelcontainer = $("<ul id='async'></ul>");
$(async.options.container).prepend(async.panelcontainer);
}
// create panel for this operation
panel = async.panels[asyncOp.AsyncId] = $("<li id='" + asyncOp.AsyncId + "'></li>");
async.panelcontainer.append(panel);
// add status and cancel button to panel
panel.append("<b>Searching [" + asyncOp.Criteria + "]: </b>");
panel.append("<span>Starting...</span>");
var cancelBtn = $("<input type='button' value='Cancel' asyncId='" + asyncOp.AsyncId + "' />").click(async.cancel);
panel.append(cancelBtn);
}
// check op status
if (asyncOp.Completed) {
$("input", panel).val("Dismiss").unbind("click", async.cancel).click(async.dismiss).show();
}
else if (asyncOp.Cancelled) {
$("input", panel).val("Dismiss").unbind("click", async.cancel).click(async.dismiss).show();
}
else {
// report progress
pending++;
}
$("span", panel).html(asyncOp.Status);
}
// polling
if (async.timeoutId) {
clearTimeout(async.timeoutId);
async.timeoutId = null;
}
if (pending > 0)
async.timeoutId = setTimeout(async.poll, async.options.pollingInterval);
},
// polling timeout callback
poll: function() {
$.get(async.options.url, {}, async.callback, "json");
}
};
// extend jQuery with "async" namespace
$.async = {
defaults: {
container: "body",
pollingInterval: 5000
},
// GET ajax request to update the status of running async operations
get: function(url, options) {
async.options = $.extend({ url: url }, $.async.defaults, options, async.options);
async.poll();
},
// posts request to start a new async operation
post: function(url, data, options) {
async.options = $.extend({ url: url }, $.async.defaults, options, async.options);
$.post(url, data, async.callback, "json");
}
};
})(jQuery); |
Almost there
OK, we have the server and client sides ready to go. It’s now time to decide which pages will display or initiate our async search operations. We are going to use the main page in this case.
Let’s insert the following HTML block to /Views/Home/Index.aspx:
<div id="container" class="async">
</div>
<h2>Search</h2>
<input id="searchCriteria" type="text" maxlength="20" />
<br />
<input id="startSearch" type="button" value="Start Search" />
<br />
<script type="text/javascript">
$(function() {
$("#startSearch").click(function() {
$.async.post("/Home/Search", { criteria: $("#searchCriteria").val() });
});
// refresh
$.async.get("/Home/Search", { container: $("#container") });
});
</script>
Here we are defining a container to hold our search panels (styled with CSS class “async”), a couple of input elements to initiate new search operations, and a block of JavaScript code that gets called when the page is ready. We want to display all active searches every time we refresh this page, that’s why we need to invoke $.async.get(GetUrl, options) to synchronize with the server. The click handler to initiate new operations is also trivial, invoking $.async.post(PostUrl, data, options) and passing the “criteria” argument from the input element #searchCriteria. The application is almost ready!, we just need to take care of one final detail…
Creating the View to display Results.
Let’s review the HomeController for a second. You might have noticed the “Results” action, and how the operation status is updated with a link to this action when the search completed with some results.
HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using AsyncOp;
namespace MvcAsyncSearch.Controllers {
[HandleError]
public class HomeController : Controller {
public ActionResult Index() {... }
public ActionResult About() {... }
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Search() {...}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Search(FormCollection collection) {...}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Results(string asyncId) {
Dictionary<string, AsyncOp.AsyncOp> operations = GetAsyncOps(HttpContext.Session);
AsyncOp.AsyncOp op;
if (operations.TryGetValue(asyncId, out op))
return View(op.Results);
else
return View(new List<AsyncOp.AsyncOp.AsyncOpResult>());
}
private Dictionary<string, AsyncOp.AsyncOp> GetAsyncOps(HttpSessionStateBase session) {...}
private void SetStatus(Dictionary<string, AsyncOp.AsyncOp> operations) {
foreach(AsyncOp.AsyncOp op in operations.Values) {
if (op.Error != null)
op.Status = "Error: " + op.Error.Message;
else {
if (op.Completed || op.Cancelled) {
if (op.Completed)
op.Status = "DONE.";
else
op.Status = "Cancelled.";
if (op.Results.Count > 0)
op.Status += " <a href=\"" + Url.Action("Results",
new { asyncId = op.AsyncId }) + "\">" + op.Results.Count + " results found.</a>";
else
op.Status += " No results found.";
}
else
op.Status = op.Results.Count + " results found ...";
}
}
}
}
} |
This is the last scenario we needed to cover, and with MVC we just create a new View under /Views/Home/Result.aspx (the view name matching the action name is a nice MVC shortcut) as follows:
Results.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<AsyncOp.AsyncOp.AsyncOpResult>>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Results
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Results</h2>
<table>
<thead>
<tr>
<th>Type</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<% foreach (AsyncOp.AsyncOp.AsyncOpResult r in ViewData.Model) { %>
<tr>
<td><%= r.Type %></td>
<td><%= r.Name %></td>
</tr>
<% } %>
</tbody>
</table>
</asp:Content>
So here is how it goes. The plugin eventually receives a status with a link to the “Results”/{asyncId} action for each completed operation with results… and when clicked… the HomeController finds the operation in the Session cache and redirects to the Results.aspx view, passing the operations results as arguments.
Conclusion
In this article I wanted to show how to solve a common programming problem with a combination of modern technologies and some of the components I use in my projects. But I’m only scratching the surface… so, if you really want to learn how to exploit these technologies, I recommend some reading first [ASP.NET MVC, jQuery], and many butt hours with your debuggers on
Roger Torres - April 25th, 2009
1 Comment
After a few extremely busy weeks (and weekends) dedicated to my clients, I finally found some time to polish a little bit my components, including support for asynchronous data operations. This is something that I needed for a small project I’m refactoring, but wasn’t at the top of my priorities. Anyway, now that it’s done, I will use the opportunity to please some of the readers who asked me to include more examples in this Blog, … so I’m going to try with another series of posts focusing on practical ways in which we can use these technologies.
I prepared a simple real world scenario where asynchronous data operations are usually desired. The goal is to start a search against the AdventureWorks database and continue working normally until the operation notifies the initiator with complete or partial results (in case it’s canceled or errors are found). To make things more interesting, one operation will spawn multiple individual searches (as CoNatural Commands) in parallel, and the application will be implemented using the newly released ASP.NET MVC framework.
I don’t want to miss the opportunity to congratulate the ASP.NET MVC team for this great piece of work. I truly believe that this architecture is not only superior to ASP.NET, but will eventually become the standard .NET web platform. I also want to commend the managers who decided to involve the community from the beginning, accepting many of their recommendations, and for not reinventing the wheel when they adopted jQuery.
The Commands
As usual, we will need a module to encapsulate our “search” data commands and launch the asynchronous operations. So, let’s create the AsyncOp class library and add the following CoNatural commands to the “Commands” folder:
SearchCustomers Command
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| -------------------------------------------------------
-- SearchCustomers
--
-- Created by Roger
-- Created on 4/16/2009 7:18:35 PM
-------------------------------------------------------
WAITFOR DELAY @Delay
SELECT
I.CustomerID,
'Customer',
P.FirstName + ' ' + P.LastName AS Name
FROM
Sales.Individual I
JOIN Person.Contact P ON I.ContactID = P.ContactID
WHERE
UPPER(P.FirstName) LIKE @Criteria + '%'
OR UPPER(P.LastName) LIKE @Criteria + '%' |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //-------------------------------------------------------
//-- SearchCustomers
//--
//-- Created by Roger
//-- Created on 4/16/2009 7:18:35 PM
//-------------------------------------------------------
using System;
namespace AsyncOp.Commands {
public class SearchCustomers : CoNatural.Data.Command {
[CoNatural.Data.Parameter(30)]
public string Delay { get; private set; }
[CoNatural.Data.Parameter(30)]
public string Criteria { get; private set; }
public SearchCustomers(string delay, string criteria) {
Delay = delay;
Criteria = criteria;
}
}
} |
SearchEmployees Command
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| -------------------------------------------------------
-- SearchEmployees
--
-- Created by Roger
-- Created on 4/16/2009 7:18:35 PM
-------------------------------------------------------
WAITFOR DELAY @Delay
SELECT
P.ContactID,
'Employee',
P.FirstName + ' ' + P.LastName AS Name
FROM
HumanResources.Employee E
JOIN Person.Contact P ON E.ContactID = P.ContactID
WHERE
UPPER(P.FirstName) LIKE @Criteria + '%'
OR UPPER(P.LastName) LIKE @Criteria + '%' |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //-------------------------------------------------------
//-- SearchEmployees
//--
//-- Created by Roger
//-- Created on 4/16/2009 7:18:35 PM
//-------------------------------------------------------
using System;
namespace AsyncOp.Commands {
public class SearchEmployees : CoNatural.Data.Command {
[CoNatural.Data.Parameter(30)]
public string Delay { get; private set; }
[CoNatural.Data.Parameter(30)]
public string Criteria { get; private set; }
public SearchEmployees(string delay, string criteria) {
Delay = delay;
Criteria = criteria;
}
}
} |
SearchVendors Command
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| -------------------------------------------------------
-- SearchVendors
--
-- Created by Roger
-- Created on 4/16/2009 7:18:35 PM
-------------------------------------------------------
WAITFOR DELAY @Delay
SELECT
V.VendorID,
'Vendor',
V.Name
FROM
Purchasing.Vendor V
WHERE
UPPER(V.Name) LIKE @Criteria + '%' |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //-------------------------------------------------------
//-- SearchVendors
//--
//-- Created by Roger
//-- Created on 4/16/2009 7:18:35 PM
//-------------------------------------------------------
using System;
namespace AsyncOp.Commands {
public class SearchVendors : CoNatural.Data.Command {
[CoNatural.Data.Parameter(30)]
public string Delay { get; private set; }
[CoNatural.Data.Parameter(30)]
public string Criteria { get; private set; }
public SearchVendors(string delay, string criteria) {
Delay = delay;
Criteria = criteria;
}
}
} |
Note that I’m introducing an artificial delay to the SQL statements in order to simulate long running search operations.
The Parallel Async Search
We just need to add one more class to take care of our async search operation. In case you are new to this Blog, here you can find a detailed explanation of CoNatural Commands and CoNatural Async Operations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
| using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Runtime.Serialization;
using CoNatural.Threading.Async;
using CoNatural.Data;
using CoNatural.Data.SqlClient;
using CoNatural.Extensions.Threading.Async.Data;
namespace AsyncOp {
[DataContract]
public class AsyncOp {
public class AsyncOpResult {
public int Id { get; private set; }
public string Type { get; private set; }
public string Name { get; private set; }
internal AsyncOpResult(int id, string type, string name) {
Id = id;
Type = type;
Name = name;
}
}
[DataMember]
public string Status { get; set; }
[DataMember]
public Guid AsyncId { get; private set; }
[DataMember]
public bool Cancelled { get; private set; }
[DataMember]
public bool Completed { get; private set; }
[DataMember]
public DateTime StartTime { get; private set; }
[DataMember]
public string Criteria { get; private set; }
public List<AsyncOpResult> Results { get; private set; }
public Exception Error { get; private set; }
private AsyncContext ctx;
public AsyncOp(string criteria) {
AsyncId = Guid.NewGuid();
Cancelled = false;
Completed = false;
Status = "Started";
StartTime = DateTime.Now;
Criteria = criteria;
Results = new List<AsyncOpResult>();
Error = null;
}
public void Start() {
ctx = new AsyncContext(false);
ctx.AsyncActionCompleted += new EventHandler<AsyncActionCompletedEventArgs>(ctx_AsyncActionCompleted);
ctx.Completed += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(ctx_Completed);
// our operation will search three tables in parallel
string connectionString = @"data source=INSPIRON\SQLEXPRESS;Initial Catalog=AdventureWorks;Integrated Security=True;async=true;";
AsyncConnection conn = new AsyncConnection(connectionString, new SqlDbProvider());
Commands.SearchEmployees se = new Commands.SearchEmployees("000:00:10", Criteria);
Commands.SearchCustomers sc = new Commands.SearchCustomers("000:00:20", Criteria);
Commands.SearchVendors sv = new Commands.SearchVendors("000:00:30", Criteria);
AsyncAction<IEnumerable<AsyncOpResult>> s1 = conn.ExecuteReaderAsync<AsyncOpResult>(se, ReadResult);
AsyncAction<IEnumerable<AsyncOpResult>> s2 = conn.ExecuteReaderAsync<AsyncOpResult>(sc, ReadResult);
AsyncAction<IEnumerable<AsyncOpResult>> s3 = conn.ExecuteReaderAsync<AsyncOpResult>(sv, ReadResult);
Parallel p = new Parallel("main", true, s1, s2, s3);
p.Start(ctx);
}
public void Cancel() {
if (ctx != null) {
ctx.Cancel();
// cancel commands
Parallel root = (Parallel)ctx.RootAction;
if (root != null) {
foreach (AsyncAction<IEnumerable<AsyncOpResult>> action in root.Branches)
((CoNatural.Data.SqlClient.AsyncConnection.StateObject)action.AsyncState).DbCommand.Cancel();
}
}
}
void ctx_Completed(object sender, System.ComponentModel.AsyncCompletedEventArgs e) {
if (e.Cancelled)
Cancelled = true;
else {
Completed = true;
Error = e.Error;
}
}
void ctx_AsyncActionCompleted(object sender, AsyncActionCompletedEventArgs e) {
if (e.Action.Name != "main") {
lock (this) {
AsyncAction<IEnumerable<AsyncOpResult>> action = (AsyncAction<IEnumerable<AsyncOpResult>>)e.Action;
if(action.Result != null)
Results.AddRange(action.Result);
}
}
}
AsyncOpResult ReadResult(IDataRecord record, int resultIndex) {
int id = record.GetInt32(0);
string type = record.GetString(1);
string name = record.GetString(2);
return new AsyncOpResult(id, type, name);
}
}
} |
There are a couple of subtleties in this code:
- The class is decorated with the DataContract attribute and some of the properties with the DataMember attribute. I will explain why this is necessary later when presenting all the active searches in ASP.NET MVC views, passing information via JSON action results.
- The connection string must contain the async=true attribute to support asynchronous commands.
- In order to cancel the search, we must loop through all the actions (branches) in the main parallel operation, and cast the AsyncState property to a StateObject holding the SqlCommand.
And that’s all we need. The CoNatural components will take care of the rest!
I’m attaching the new source code with the modifications to support asynchronous data commands, and the compiled CoNatural assemblies for your convenience. I encourage you to try building this project and play a little with it while I prepare the next post.
In the second part I will show how to present the operations in an ASP.NET MVC application, using a combination of jQuery, AJAX, and JSON serialization to efficiently start, cancel, and remove multiple searches.
Attachments
[Download not found]
CoNatural.Extensions Source Code (188)
[Download not found]
[Download not found]
[Download not found]
[Download not found]
Roger Torres - February 21st, 2009
Comments
This is the last part of my series about .NET asynchronous operations. Here you can download:
Since we didn’t quite finish our example in part II, in this post I’m going to show you a simple Windows Forms application that will:
- Provide a GUI to our last example.
- Allow the user to cancel the asynchronous operation.
- Show the managed thread id where internal actions execute, so you can understand how ThreadPool threads are created.
- Show the code that handles operation events and errors.
The Form
Let’s start by creating a new Windows Application Project named WinFormDemo. The default Form1 will do, we just need to add a toolbar with two buttons (to start and cancel the asynchronous operation), and a list view to show the execution log - see the screenshots below.
Now we can associate the following code-behind to our form:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
| using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using CoNatural.Threading.Async;
using CoNatural.Extensions.Threading.Async.IO;
using CoNatural.Extensions.Threading.Async.Net;
namespace WinFormDemo {
public partial class Form1 : Form {
AsyncContext context;
public Form1() {
InitializeComponent();
// create context
context = new AsyncContext();
context.Completed += new EventHandler<AsyncCompletedEventArgs>(Completed);
context.AsyncActionCompleted += new EventHandler<AsyncActionCompletedEventArgs>(ActionCompleted);
context.AsyncActionProgressChanged += new EventHandler<AsyncActionProgressChangedEventArgs>(ReportProgress);
context.AsyncActionTraced += new EventHandler<AsyncActionTracedEventArgs>(TraceAction);
}
private void WriteLine(string message, params object[] args) {
logListView.Items.Add(string.Format(message, args));
}
private IEnumerable<AsyncAction> DownloadAsync(string url) {
WebRequest req = HttpWebRequest.Create(url);
// get contents of site
AsyncAction<WebResponse> response = req.GetResponseAsync();
yield return response;
// copy results to memory stream
Stream responseStream = response.Result.GetResponseStream();
MemoryStream copyTo = new MemoryStream();
yield return new Serial(url, true, responseStream.CopyAsync(copyTo));
// return results
copyTo.Seek(0, SeekOrigin.Begin);
string html = new StreamReader(copyTo).ReadToEnd();
yield return new Return<string>(html);
}
void DownloadAll() {
Serial<string> microsoft = new Serial<string>("microsoft", true, DownloadAsync("http://www.microsoft.com"));
Serial<string> conatural = new Serial<string>("conatural", true, DownloadAsync("http://www.conatural.com"));
Serial<string> google = new Serial<string>("google", true, DownloadAsync("http://www.google.com"));
Serial<string> yahoo = new Serial<string>("yahoo", true, DownloadAsync("http://www.yahoo.com"));
Parallel favorite = new Parallel("Favorite Sites", true, google, yahoo);
Parallel all = new Parallel("All Sites", true, microsoft, conatural, favorite);
all.Start(context);
}
private void Completed(object sender, AsyncCompletedEventArgs args) {
if (args.Cancelled)
WriteLine("CONTEXT WAS CANCELLED.");
if (args.Error != null)
foreach (Exception ex in ((AsyncContextException)args.Error).Errors)
DisplayError(ex);
WriteLine("CONTEXT COMPLETED.");
}
private void ActionCompleted(object sender, AsyncActionCompletedEventArgs args) {
if (args.Error != null)
DisplayError(args.Error);
if (args.Cancelled)
WriteLine(args.Action.Name + " Cancelled");
if (args.Error == null && !args.Cancelled) {
if (args.Action is Serial<string>) {
// Form2 displays the contents of a web site in a Browser control
//Form2 f2 = new Form2();
//f2.SetContent(((Serial<string>)args.Action).Result);
//f2.Text = args.Action.Name;
//f2.Show();
}
else {
WriteLine("[{0}][{1}] completed", args.ManagedThreadId, args.Action.Name);
}
}
}
private void ReportProgress(object sender, AsyncActionProgressChangedEventArgs args) {
WriteLine("[{0}][{1}] {2}", args.ManagedThreadId, args.Action.Name, args.ProgressPercentage);
}
private void TraceAction(object sender, AsyncActionTracedEventArgs args) {
WriteLine("[{0}][{1}] {2}", args.ManagedThreadId, args.Action.Name, args.Message);
}
private void DisplayError(Exception ex) {
if (ex != null) {
if (ex is AsyncActionException)
WriteLine("{0} -> {1}", ((AsyncActionException)ex).Action.Name, ex.Message);
else
WriteLine(ex.Message);
DisplayError(ex.InnerException);
}
}
private void startToolStripButton_Click(object sender, EventArgs e) {
try {
DownloadAll();
logListView.Items.Clear();
WriteLine("[{0}] GUI thread is alive...", System.Threading.Thread.CurrentThread.ManagedThreadId);
}
catch (Exception ex) {
WriteLine(ex.Message);
}
}
private void cancelToolStripButton_Click(object sender, EventArgs e) {
context.Cancel();
}
}
} |
We are using exactly the same code from our example in part II, but we are now handling operation events. Note that we don’t need to check if the event handling methods are executing in the GUI thread since the AsyncContext class is already taking care of the details by posting the events to the current synchronization context.
The first of the following two screenshots depicts the application after successfully downloading the contents of several web sites. The log is created by handling Trace events that return the managed thread id where the action is executing, the action’s name, and total bytes downloaded so far.
The second screenshot shows what happens when the Start button is hit twice after the operation has been started - An exception is raised by a “busy” context.
Also note that when the Cancel button is hit, the entire operation stops (the completion event is raised with the Cancelled flag set to true).

Async Test Results
The best way to understand what’s going on here is by playing with this code, so I encourage you to build your own prototypes… and let me know if you find any issues please
What’s next?
Now that I have a system to invoke asynchronous operations in any predetermined sequence, I think it would be a good idea to go back to the drawing board and add an asynchronous component to the CoNatural.Data DAL. The ADO.NET SQL Server provider (under System.Data.SqlClient) already supports asynchronous execution, so with a couple of extension methods I think it will be very easy to invoke asynchronous data commands that will:
- Execute without blocking the calling thread, making our GUI more responsive.
- Allow the user to cancel commands.
- Invoke multiple commands in an ordered sequence or in parallel to maximize performance.
- The commands can be executed on one or multiple databases, and under the same transaction scope.
Stay tuned.
Roger Torres - February 15th, 2009
1 Comment
In Part I of this series I questioned the use of traditional .NET asynchronous patterns when executing a large number of asynchronous operations in a predetermined sequence, showing how the advantages in performance are overshadowed by the complexity in the implementation.
Thanks to the functional language capabilities of C#, we can easily streamline our code by following the guidelines I’m going to discuss here today. The whole idea is based on coroutines, where unlike regular methods (subroutines), we have multiple entry points to resume execution. C# doesn’t support coroutines natively (I hope this is something we will have in future versions), but we can simulate some of that behavior using generators (best known as iterators in C# terms).
The key is to return an enumeration of asynchronous actions that must be executed in sequence by yielding each individual action to the iterator… and as usual, there are many ways to accomplish this goal. I’m going to present a solution that I found well balanced in terms of complexity and functionality.
The Solution
The following class diagram depicts the components of my solution:

Note that all actions have a common base abstract class AsyncAction, so we can nest actions to create compositions of parallel and serial sequences as long as we want. Today we are going to work with an enhanced version of the example discussed in part I, this time downloading the contents of several web pages as shown in the following diagram:

We will see how this mechanism uses ThreadPool threads very efficiently, only starting new threads when necessary.
AsyncAction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| using System;
using System.Threading;
namespace CoNatural.Threading.Async {
public abstract class AsyncAction {
public string Name { get; private set; }
public bool RaiseEvents { get; private set; }
protected AsyncAction(string name, bool raiseEvents) {
Name = name;
RaiseEvents = raiseEvents;
}
internal void Complete(bool cancelled, AsyncActionException error) {
Context.OnAsyncActionCompleted(this, cancelled, error);
}
internal void ChangeProgress(int progressPercentage) {
Context.OnAsyncActionProgressChanged(this, progressPercentage);
}
internal void Trace(string message) {
Context.OnAsyncActionTraced(this, message);
}
internal AsyncContext Context { get; set; }
public virtual void Start(AsyncContext context) {
Context = context.OnStart();
ThreadPool.QueueUserWorkItem((state) => { BeginInvoke(this, () => context.OnCompleted()); });
}
internal abstract void BeginInvoke(AsyncAction source, Action nextAction);
}
} |
This is the base (abstract) class representing any type of asynchronous action returned by the generator. All our actions are instantiated with a name (to facilitate tracing and debugging), and a flag used by the operation context to raise events generated by the action (Completed, ProgressChanged, Traced). Raising events is something that must be reserved only for actions that you want to debug or display their progress report and termination feedback to the GUI.
We can define an asynchronous operation as the execution of one or many nested asynchronous actions. To start an operation, the user invokes the Start method of the root action, passing an execution context that is reserved by the engine until the operation is completed. Note that the root’s BeginInvoke method is queued in the ThreadPool, so the main calling thread is not blocked by the operation.
BeginInvoke is the method that binds our nested actions together. Each concrete action implements BeginInvoke according to their specific compositional properties. When the asynchronous operation is started, the engine binds each executing action to the execution context (AsyncContext) and starts calling each action’s BeginInvoke in order until the operation is completed. We will see below how the AsyncContext takes care of posting individual AsyncAction events (and its own Completed event) to the current synchronization context.
AsyncAction<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| using System;
using System.Threading;
namespace CoNatural.Threading.Async {
public class AsyncAction<T> : AsyncAction {
private Func<AsyncCallback, object, IAsyncResult> _beginXxx;
private Func<IAsyncResult, T> _endXxx;
public T Result { get; private set; }
public AsyncAction(string name, Func<AsyncCallback, object, IAsyncResult> beginXxx, Func<IAsyncResult, T> endXxx)
: base(name, false) {
_beginXxx = beginXxx;
_endXxx = endXxx;
}
internal override void BeginInvoke(AsyncAction source, Action nextAction) {
Context = source.Context;
try {
_beginXxx(
delegate(IAsyncResult asyncResult) {
try {
Result = _endXxx(asyncResult);
Complete(false, null);
}
catch (Exception ex) {
Complete(false, new AsyncActionException(this, string.Empty, ex));
}
nextAction();
},
null
);
}
catch (Exception ex) {
Complete(false, new AsyncActionException(this, string.Empty, ex));
nextAction();
}
}
}
} |
This is our first and most important concrete AsyncAction. Its main function is to wrap all .NET APIs using the BeginXxx-EndXxx asynchronous pattern, returning a result of type T (in most cases). The constructor accepts the parameters of the base class plus delegates to the BeginXxx-EndXxx methods.
Note how BeginInvoke wraps the .NET asynchronous pattern, effectively blocking the sequence until the action is completed by EndXxx. In case of success, the result of the action is saved and the Completed event is raised. In case of errors, the Completed event is also raised, but this time with the exception returned in the Error property of the event’s arguments. Finally, the sequence is resumed by invoking the delegate of the next action.
We are achieving our first goal of executing multiple asynchronous actions in a predetermined serial sequence, but we need a way to encapsulate the specific details of every .NET API in a nicer form. Now is when the C# 3.0 extension methods come to the rescue. The code below shows one way to extend System.Net.WebRequest and System.IO.Stream by wrapping their BeginXxx-EndXxx .NET asynchronous operations inside AsyncAction<T> actions.
1
2
3
4
5
6
7
8
9
10
11
| using System;
using System.Net;
namespace CoNatural.Threading.Async.Extensions {
public static class WebRequestExtensions {
public static AsyncAction<WebResponse> GetResponseAsync(this WebRequest request) {
return new AsyncAction<WebResponse>(request.RequestUri.AbsoluteUri,
request.BeginGetResponse, request.EndGetResponse);
}
}
} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| using System;
using System.Collections.Generic;
using System.IO;
namespace CoNatural.Threading.Async.Extensions {
public static class StreamExtensions {
public static AsyncAction<int> ReadAsync(this Stream stream, byte[] buffer, int offset, int count) {
return new AsyncAction<int>("ReadAsync",
(callback, state) =>
stream.BeginRead(buffer, offset, count, callback, state), stream.EndRead);
}
public static AsyncAction<int> WriteAsync(this Stream stream, byte[] buffer, int offset, int count) {
return new AsyncAction<int>("WriteAsync",
(callback, state) =>
stream.BeginWrite(buffer, offset, count, callback, state),
(asyncResult) => { stream.EndWrite(asyncResult); return 0; });
}
public static IEnumerable<AsyncAction> CopyAsync(this Stream stream, Stream copyTo) {
long total = stream.CanSeek ? stream.Length : -1;
int read = -1;
byte[] buffer = new byte[1024];
while (read != 0) {
AsyncAction<int> count = stream.ReadAsync(buffer, 0, 1024);
yield return count;
yield return copyTo.WriteAsync(buffer, 0, count.Result);
if (total > 0)
yield return new Progress((int)(((double)count.Result / (double)total) * 100.0));
else
yield return new Trace(count.Result.ToString());
read = count.Result;
}
}
}
} |
We will see later how we can use these extension methods to implement our asynchronous operations.
Serial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| using System;
using System.Collections.Generic;
namespace CoNatural.Threading.Async {
public class Serial : AsyncAction {
protected IEnumerable<AsyncAction> _actions;
public Serial(string name, bool raiseEvents, IEnumerable<AsyncAction> actions)
: base(name, raiseEvents) {
_actions = actions;
}
internal override void BeginInvoke(AsyncAction source, Action nextAction) {
Context = source.Context;
MoveNext(_actions.GetEnumerator(), nextAction);
}
private void MoveNext(IEnumerator<AsyncAction> actionEnumerator, Action nextAction) {
try {
// stop when cancellation requested in context
if (Context.CancelRequested) {
Complete(true, null);
nextAction();
}
// advance iteration
else if (actionEnumerator.MoveNext()) {
actionEnumerator.Current.BeginInvoke(this, () => MoveNext(actionEnumerator, nextAction));
}
// we are done
else {
Complete(false, null);
nextAction();
}
}
catch (AsyncActionException aex) {
Complete(false, aex);
nextAction();
}
catch (Exception ex) {
Complete(false, new AsyncActionException(this, string.Empty, ex));
nextAction();
}
}
}
} |
Going back to our first goal, we needed serial actions to synchronize our sequences. Our Serial class takes an IEnumerable list of AsyncAction actions that must be invoked in order.
BeginInvoke is where the fun begins, in this case we save the execution context as usual and start the enumeration of actions by calling MoveNext. Here we check if the context has received a cancellation request to stop the execution, otherwise we just need to advance the iteration and call BeginInvoke again, passing a delegate to MoveNext (this is the recursive part) to guarantee the order of the sequence. Once we reach the end of the enumeration, the serial operation is completed and we can stop the recursion. Of course, in case of exceptions, we also stop and report the error in the completion event.
Serial<T>
1
2
3
4
5
6
7
8
9
10
11
| using System;
using System.Collections.Generic;
namespace CoNatural.Threading.Async {
public class Serial<T> : Serial {
public T Result { get; internal set; }
public Serial(string name, bool raiseEvents, IEnumerable<AsyncAction> actions)
: base(name, raiseEvents, actions) { }
}
} |
In some cases our sequence will return a result of type T, so we need a special Serial<T> class with a property to store the result.
Parallel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| using System;
using System.Collections.Generic;
using System.Threading;
namespace CoNatural.Threading.Async {
/// <summary>
/// Executes multiple asynchronous actions in parallel.
/// </summary>
public class Parallel : AsyncAction {
public AsyncAction[] Branches { get; private set; }
public Parallel(string name, bool raiseEvents, params AsyncAction[] branches)
: base(name, raiseEvents) {
Branches = branches;
}
public override void Start(AsyncContext context) {
Context = context.OnStart();
BeginInvoke(this, () => context.OnCompleted());
}
internal override void BeginInvoke(AsyncAction source, Action nextAction) {
Context = source.Context;
// parallel actions start in their own managed threads because they must wait
// for all branches to complete before resuming
ThreadPool.QueueUserWorkItem(
(state) => {
long activeBranches = Branches.Length;
ManualResetEvent yield = new ManualResetEvent(false);
foreach (AsyncAction branch in Branches)
branch.BeginInvoke(
this,
() => {
// this branch is done, decrement counter
long pending = Interlocked.Decrement(ref activeBranches);
// release parallel action when all branches are completed
if (pending == 0)
yield.Set();
}
);
// wait for all branches to complete
yield.WaitOne();
// we are done
Complete(false, null);
nextAction();
}
);
}
}
} |
Parallel actions complete our compositional requirements. Here we take a list of actions that must be executed concurrently (called branches), and schedule a thread to invoke each one of them individually. After all branches are invoked, the thread controlling the parallel operation is blocked, waiting for all branches to complete. Note the lambda expression that’s assigned as the “next action” to complete a branch: The counter of pending branches is decremented, and the ManuelResetEvent is signaled to release the parallel thread when the counter = 0.
Return<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| using System;
namespace CoNatural.Threading.Async {
/// <summary>
/// Returns Serial of T action results.
/// </summary>
/// <typeparam name="T">Result type.</typeparam>
public class Return<T> : AsyncAction {
public T Value { get; private set; }
public Return(T value)
: base(string.Empty, false) {
Value = value;
}
internal override void BeginInvoke(AsyncAction source, Action nextAction) {
if (source is Serial<T>) {
((Serial<T>)source).Result = Value;
nextAction();
}
else
throw new InvalidAsyncActionException(source, "Source of Return<T> must be Serial<T>.");
}
}
} |
Since we are emulating coroutines with C# iterators, we must “yield” all results to the engine as AsyncAction actions, including action results, progress and trace events. The Return<T> action is a helper class to assign the result of a Serial<T> action. Progress and Trace actions are helpers to report progress and events to the execution context. We will see all these classes “in action” later when we implement our example.
Progress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System;
namespace CoNatural.Threading.Async {
public class Progress : AsyncAction {
public int ProgressPercentage { get; private set; }
public Progress(int progressPercentage)
: base(string.Empty, false) {
ProgressPercentage = progressPercentage;
}
internal override void BeginInvoke(AsyncAction source, Action nextAction) {
source.ChangeProgress(ProgressPercentage);
nextAction();
}
}
} |
Trace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System;
namespace CoNatural.Threading.Async {
public class Trace : AsyncAction {
public string Message { get; private set; }
public Trace(string message)
: base(string.Empty, false) {
Message = message;
}
internal override void BeginInvoke(AsyncAction source, Action nextAction) {
source.Trace(Message);
nextAction();
}
}
} |
AsyncContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
| using System;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;
namespace CoNatural.Threading.Async {
public class AsyncContext {
public event EventHandler<AsyncCompletedEventArgs> Completed;
public event EventHandler<AsyncActionCompletedEventArgs> AsyncActionCompleted;
public event EventHandler<AsyncActionProgressChangedEventArgs> AsyncActionProgressChanged;
public event EventHandler<AsyncActionTracedEventArgs> AsyncActionTraced;
private SynchronizationContext _syncContext;
public AsyncContext() {
_syncContext = SynchronizationContext.Current ?? new SynchronizationContext();
}
private volatile bool _busy;
public bool Busy { get { return _busy; } }
private volatile bool _cancelRequested;
private volatile bool _cancelled;
internal bool CancelRequested { get { return _cancelRequested; } }
public void Cancel() {
if (_busy)
_cancelRequested = true;
}
private List<AsyncActionException> _errors = new List<AsyncActionException>();
internal AsyncContext OnStart() {
lock (this) {
if (_busy)
throw new Exception("Context is busy running other actions.");
_busy = true;
_cancelRequested = false;
_cancelled = false;
_errors.Clear();
return this;
}
}
internal void OnCompleted() {
_busy = false;
if (Completed != null) {
AsyncContextException error = (_errors.Count > 0 ? new AsyncContextException(_errors.ToArray()) : null);
_syncContext.Post((state) =>
Completed(this, new AsyncCompletedEventArgs(error, _cancelled, state)), null);
}
}
internal void OnAsyncActionCompleted(AsyncAction action, bool cancelled, AsyncActionException error) {
lock (this) {
if (error != null) {
_errors.Add(error);
// request cancellation after errors found
_cancelRequested = true;
}
// at least one action was cancelled
if (cancelled) {
_cancelRequested = true;
_cancelled = true;
}
}
if (action.RaiseEvents && AsyncActionCompleted != null)
_syncContext.Post((state) =>
AsyncActionCompleted(action,
new AsyncActionCompletedEventArgs(action, Thread.CurrentThread.ManagedThreadId, cancelled, error)), null);
}
internal void OnAsyncActionProgressChanged(AsyncAction action, int progressPercentage) {
if (action.RaiseEvents && AsyncActionProgressChanged != null) {
progressPercentage = Math.Max(0, progressPercentage);
progressPercentage = Math.Max(100, progressPercentage);
_syncContext.Post((state) =>
AsyncActionProgressChanged(action,
new AsyncActionProgressChangedEventArgs(action, Thread.CurrentThread.ManagedThreadId, progressPercentage)), null);
}
}
internal void OnAsyncActionTraced(AsyncAction action, string message) {
if (action.RaiseEvents && AsyncActionTraced != null)
_syncContext.Post((state) =>
AsyncActionTraced(action,
new AsyncActionTracedEventArgs(action, Thread.CurrentThread.ManagedThreadId, message)), null);
}
}
} |
The execution context is controlled by one AsyncContext instance. We can subscribe for the entire operation’s completion event, or for individual action events (only the actions that are flagged to raise events). The context can receive cancellation requests that will stop all the executing actions. Note that in case of exceptions, the cancellation flag is also set to stop the operation, but I’ll probably reconsider this point later to allow other independent actions to complete.
Putting it all together
Let’s now review our initial example. We need to implement a method to download the contents of a few web sites as follows:
1
2
3
4
5
6
7
8
9
10
11
| void DownloadAll() {
Serial<string> microsoft = new Serial<string>("microsoft", true, DownloadAsync("http://www.microsoft.com"));
Serial<string> conatural = new Serial<string>("conatural", true, DownloadAsync("http://www.conatural.com"));
Serial<string> google = new Serial<string>("google", true, DownloadAsync("http://www.google.com"));
Serial<string> yahoo = new Serial<string>("yahoo", true, DownloadAsync("http://www.yahoo.com"));
Parallel favorite = new Parallel("Favorite Sites", true, google, yahoo);
Parallel all = new Parallel("All Sites", true, microsoft, conatural, favorite);
all.Start(context);
} |
While the structure of our operation is controlled by the method above, the details are implemented here:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private IEnumerable<AsyncAction> DownloadAsync(string url) {
WebRequest req = HttpWebRequest.Create(url);
// get contents of site
AsyncAction<WebResponse> response = req.GetResponseAsync();
yield return response;
// copy results to memory stream
Stream responseStream = response.Result.GetResponseStream();
MemoryStream copyTo = new MemoryStream();
yield return new Serial(url, true, responseStream.CopyAsync(copyTo));
// return results
copyTo.Seek(0, SeekOrigin.Begin);
string html = new StreamReader(copyTo).ReadToEnd();
yield return new Return<string>(html);
} |
Note how we are using the new extension methods to simplify our code.
In the last part of this series I’ll show how to implement asynchronous operations in a Windows Forms application that will:
- Allow to cancel the operation.
- Report the progress of internal actions in the GUI.
- Show how the ThreadPool threads are using efficiently.
- Show how exceptions are handled by the engine.
Stay tuned.