Getting started

LLBLGen Pro Runtime Framework supports a full async API, which is usable with the async/await keywords of C# and VB.NET to have full asynchronous data persistence in your application. This section will get you started with asynchronous programming and the LLBLGen Pro Runtime Framework. This documentation assumes you're familiar with the .NET async/await system introduced in .NET 4.5.

General usage

After referencing the ORMSupportClasses dll in your code, the async methods are available in your code like the normal synchronous equivalents. This means that if you e.g. use QuerySpec to query the database, you can write your queries normally and use the async variant of the fetch method you'd want to use instead of the synchronous variant you're used to use.

All asynchronous methods are available through the same namespaces as their synchronous equivalents are located and use the same method signatures. Additionally we added some extra methods, especially for Linq, which are described more in detail in the API docs per query API.

Documentation

Additionally to this documentation, it's recommended to consult the LLBLGen Pro Runtime Library reference manual (additional download from the LLBLGen Pro website) for details regarding the async methods and their overloads.

The Async API works in combination of the async/await keywords and the Task Processing Library (TPL) of .NET 4/4.5 or higher. It's recommended you familiar yourself with how these work to be as efficient as possible with the Async API and asynchronous programming.

Below some of the implementation related choices are described as well as the affect they'll have on your code structure.

The following choices were made to get the best performance and avoid excessive overhead.

  • As our code is a library, all Tasks which are awaited have their ConfigureAwait(false) method called first, false is passed as parameter. This will cause the task to run on the default context, which is the one of the default threadpool.
  • DbDataReader.Read is used instead of ReadAsync, as it's not worth the overhead in almost all cases, as ReadAsync will run synchronously anyway because most datareaders read the first rows after execution. The ReadAsync methods are often implemented as synchronous methods (like SqlDataReader.ReadAsync) as in: they call into the synchronous method called by Read(), except they're wrapped in an async wrapper. As Read is called in a tight loop, it will cause a lot of overhead.
  • Local variables are avoided as much as possible in async methods to avoid performance problems with a task being yielded through the await keyword.

Canceling an async operation.

All async methods have been implemented with an optional overload which accepts a CancellationToken. This token is passed along to the actual async methods of ADO.NET, which perform the async operation and which allows you to cancel async operations.

Opening a connection asynchronously

When an asynchronous method needs to open a connection, it will always do that asynchronously. It's also possible to open a connection asynchronously manually (Adapter), by calling OpenConnectionAsync on the IDataAccessAdapter implementation. It's OK if the connection is opened synchronously (e.g. by calling IDataAccessAdapter.OpenConnection) and after that an asynchronous method is called.

For SelfServicing, a connection owned by a Transaction instance is always opened synchronously.

Re-entrance protection

Re-entrancy can occur if the developer calls an Async method on the adapter and doesn't await the method directly:

// wrong, causes re-entrance
IEntityCollection2 toFetch = null;
using(var adapter = new DataAccessAdapter())
{
    var q = //... create queryspec query
    var t = adapter.FetchQueryAsync(q, CancellationToken.None); // task will continue
    var e = adapter.FetchNewEntity<EmployeeEntity>(   // X : will go wrong, 't' is still busy
                new RelationPredicateBucket(EmployeeFields.EmployeeId==2));
    await t;
    toFetch = t.Result;
}

The line marked with 'X: will go wrong' causes a re-entrance problem: the Task 't' hasn't been completed yet, but due to the async nature of the FetchQueryAsync, the code returns immediately. This means the line 'X' will be executed right after the async method returns, however the work the async method is supposed to do hasn't been completed yet.

To avoid this, the task t has to be awaited before the next call to the adapter (and before the end of the using statement!) is executed:

// correct, won't cause re-entrance
IEntityCollection2 toFetch = null;
using(var adapter = new DataAccessAdapter())
{
    var q = //... create queryspec query
    toFetch = await adapter.FetchQueryAsync(q, CancellationToken.None);
    var e = adapter.FetchNewEntity<EmployeeEntity>(   
                new RelationPredicateBucket(EmployeeFields.EmployeeId==2));
}

The code above will make the compiler wrap all code after the await into a continuation of the task being awaited and will execute that after that task is finished, which makes the code below the await avoid re-entering the adapter during the async task.

This is equal to re-using an adapter instance in a multi-threaded environment: it's not thread safe and calling an async method doesn't free you from taking care of this: calling an async method could create multi-threading related issues with an adapter instance if you're not careful.

This also means that if you share an IDataAccessAdapter across methods on a form for example, you can have re-entrancy problems if you have async event handlers. In this case, create a new DataAccessAdapter instance every time you need one (e.g. one per method). Creating a new DataAccessAdapter is very fast (The constructor is almost completely empty) so it doesn't make your code slower.

Important!

Always await a Task returned by an async method before doing anything else on the adapter.

Query definitions

Nothing changes in the way how queries are defined, only in how queries are executed. This means that all code which has to be made asynchronous only has to change at the point where a query is actually executed. For QuerySpec and UnitOfWork/Persistence actions this is simple:

  1. Change the method call used now into an asynchronous variant
  2. await the result using the await keyword.

For Linq this is a bit different as enumerating the result of a linq query is always synchronous. More on that in the Async API Linq section.

DbConnection.Open

When a query is executed asynchronously and the framework has to open a connection, the connection is opened asynchronously as well. In the case of SelfServicing, if a Transaction is active, the connection of that Transaction is used, which is opened synchronously (as it's opened in the constructor, which can't be awaited).

DbDataReader.Read

Reading data from a datareader is doable using asynchronous methods (ReadAsync instead of Read), however we opted to keep this synchronous. The main reason is that it's highly unlikely the Read call will make a call to the server (as the DbDataReader reads data in batches from the server) with every Read call, so an async call will only create overhead as it will return immediately in most situations, so in effect behave like a synchronous method. If there's a use case for going full ReadAsync, we'll address this in the future. For now, the datareader loops use DbDataReader.Read, not DbDataReader.ReadAsync.

Nested queries and prefetch paths

When a query is executed asynchronously, any nested queries and prefetch paths are executed asynchronously automatically. This means that they're executed asynchronously from the main query and awaited inside the query execution engine.

There's no parallellism implemented: nodes/nested queries at the same level aren't executed in parallel by default. They're executed asynchronously, but the results are awaited before a next node is executed to avoid re-entrancy.