Everything Becomes Async!?

We are in the process of equipping NDO with asynchronous methods throughout, so that your applications can get by with fewer threads and therefore less overhead.

Experiences from the conversion of other projects suggest that a performance boost can be expected, especially for web projects.

The conversion is planned for NDO 4.5.

To put it briefly: The more traffic you have in your web applications, the more worthwhile it is to switch to consistent implementation with async/await constructs. At first glance, it all looks pretty simple. You query your objects with an asynchronous method:

var myObjects = await pm.Objects<Product>().Where(p=>p.Name==name).GetResultsAsync();

If you have changed any objects, you save it in an async transaction:

await pm.SaveAsync();

DataAdapter Replacement

Internally, we use the data structures of the System.Data namespace, as DataTable, DataRow, etc. The data structures have the advantage that the original status can be kept in a DataRow for each modified object. This can be used, for example, to generate audit records that allow changes to be checked.

The data structures were previously filled or transferred to the database by a DataAdapter. Unfortunately, Microsoft hasn't managed to switch the DataAdapter class to async/await to date. If you look at the code, you will quickly notice that the design of the DataAdapter classes (DbDataAdapter, SqlDataAdapter) has some flaws that make the transition difficult, while maintaining backwards compatibility to existing ADO.NET providers.

If you look at the discussion on Github (especially towards the end of the thread), you will see that there will probably never be a solution.

We have therefore replaced the DataAdapter's Fill and Update methods with custom code based on DataReader objects. These have asynchronous methods for reading. The DbCommand classes are sufficient for async writing. This means that DataAdapters will no longer be used in NDO.  By the way that transition gave us an opportunity to realize some optimizations.

ConfigureAwait

To enable compatibility with previous applications, the synchronous methods are retained. But they are just wrappers around the asynchronous methods and use

GetAwaiter().GetResult()

to make sure, the current thread will continue after the async work is done.

To ensure that this blocked thread does not create deadlocks, care was taken when using await to consistently create a new context with ConfigureAwait(false) so that the asynchronous processing is not continued in the original thread if the continuation does not happen in the wrapper method. This behavior  should be standard for any library that offers asynchronous methods.

Consequences for Programming with NDO

The conversion looks like a boring task until all methods have been converted to async/await. But it is not that easy. NDO is a transparent persistence solution. There are hollow objects and unloaded relations.

This means: If your code "touches" an object or a relation, the data is automatically reloaded (see description here). Now one might think that the code that reloads the data could simply be switched to async. But the code that takes care of this is generated by the NDOEnhancer in the persistent classes.

If the code suddenly became async, the entire method in which a field or relation is accessed would have to be switched from synchronous to asynchronous. But not only that: The entire call chain would have to be switched to async. In short: this option is out of question.

One could now say: We have an async and a sync version of the code that has to be called in order to load the objects. If the enclosing method is a synchronous method, the synchronous code is called, in an asynchronous environment, the asynchronous variant is used.

Anyone who has looked at the code generated by the compiler for asynchronous methods can see that the compiler completely rearranges the methods. An auxiliary class is created and a state machine is built in it. A state is created for each await call. The enhancer would now have to insert another state at the right place in the state machine in order to perform the asynchronous call necessary to load objects and relations.

We decided not to do this. The transparent reloading of objects and relations remains synchronous.

But you can always use the synchronous version manually. Consider the following code:

var emp = pm.Objects<Employee>().Where(p=>p.Name == "Matytschak").Single();
foreach(var travel in emp.Travels)
...

The Travel objects are reloaded as soon as the Travels property accesses the underlying field. However, you can change the code as follows:

var emp = pm.Objects<Employee>().Where(p=>p.Name == "Matytschak").Single();
await pm.LoadRelationAsync(emp, _=>emp.Travels); // Loads Travel objects async
foreach(var travel in emp.Travels)
...

In the synchronous call that tests whether the relation is already loaded, it is now determined that the objects already exist. Hence there is no need for a synchronous operation to load the objects. It's not pretty, but it's a trade: we trade efficiency for transparency.