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.
The Async API
To use the Async API, you have to use the specific .NET 4.5+ build of the SD.LLBLGen.Pro.ORMSupportClasses.dll, which is located in the <LLBLGen Pro Installation folder>\Frameworks\RuntimeLibraries\CompiledRuntimeLibraries\NET4.5 folder. It's a drop-in replacement of the normal .NET 3.5 build and it contains all code of the normal build. This allows you to start with normal synchronous code and when appropriate reference the .NET 4.5 build to be able to use asynchronous code.
Make sure you reference the async SD.LLBLGen.Pro.ORMSupportClasses.dll in all projects which required you to reference the ORM Support Classes, including the generated code projects.
The easiest way to do this is to use nuget / Paket and add a reference to the ORM Support Classes package on nuget: it will then automatically select the .NET 4.5+ build.
How to determine you're using the right dll
To check which dll version you're using, right-click the dll in Windows explorer and select Properties. In the dialog that pops up, select the 'Details' tab. The description should say 'LLBLGen Pro ORM Support Classes Library with Async support'. If it says something else, you have the normal orm support classes dll.
In code you can also check which dll you're using, by reading RuntimeLibraryVersion.Build. If you see a number with the suffix "_WithAsync", like "5.1.0_WithAsync", you're using the async enabled version, otherwise you're using the normal ORM support classes dll shipped.
We kept the filename equal to the normal version because now it doesn't require specific DQE dll versions as well: the original ones work with the new Async enabled one. Be aware that this .NET 4.5 specific dll has been compiled against .NET 4.5. This means the VS.NET projects you're using the dll with have to be .NET 4.5 (or higher) projects as well.
General usage
After you've made sure your code references the Async version of the ORMSupportClasses dll, 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.
We added all asynchronous methods to 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.
Implementation related choices.
Below some of the implementation related choices are described as well as the affect they'll have on your code structure.
Security related attributes related to .NET 4 changes
The Async version is compiled against .NET 4.5, which means it has to
deal with the changed security settings of .NET 4. One of these changes
forces every ISerializable.GetObjectData
method to be marked with
[SecurityCritical]
. This is also required for all overrides of
GetObjectData in subclasses, including the generated code. This is
required also because our assembly is marked with
AllowPartialTrustedCallersAttribute, a requirement for partial trust.
While we can solve this in the runtime, it can't be done in the generated code without changing templates. This makes the ORMSupportClasses with async not a 'drop-in' replacement, so we opted for the specification of the following attribute:
[assembly: System.Security.SecurityRules(System.Security.SecurityRuleSet.Level1)]
This set the security levels on the ORMSupportClasses to the ones introduced in .NET 2, which is fine, as the normal one is using this setting as well, as it's a CLR 2 compiled assembly (.NET 3.5) anyway.
Performance related choices
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.
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:
- Change the method call used now into an asynchronous variant
- 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.