Marklio.AsyncMaybe
1.1.2
Prefix Reserved
dotnet add package Marklio.AsyncMaybe --version 1.1.2
NuGet\Install-Package Marklio.AsyncMaybe -Version 1.1.2
<PackageReference Include="Marklio.AsyncMaybe" Version="1.1.2" />
<PackageVersion Include="Marklio.AsyncMaybe" Version="1.1.2" />
<PackageReference Include="Marklio.AsyncMaybe" />
paket add Marklio.AsyncMaybe --version 1.1.2
#r "nuget: Marklio.AsyncMaybe, 1.1.2"
#:package Marklio.AsyncMaybe@1.1.2
#addin nuget:?package=Marklio.AsyncMaybe&version=1.1.2
#tool nuget:?package=Marklio.AsyncMaybe&version=1.1.2
Marklio.AsyncMaybe
Marklio.AsyncMaybe is a pattern for writing code that allows a single implementation to serve both synchronous and asynchronous codepaths.
You follow a convention for method naming and parameters, and a source generator will automatically generate the public sync and async "APIs" that your library exposes.
All code is expressed as async code, but any given "await" statement can follow the synchronous or asynchronous codepath, eliminating the problems of sync-over-async or vice versa.
Synchronicity of synchronous codepaths is verified, so good test coverage can tell you if you have a bug.
TODO: examples
The AsyncMaybe pattern
AsyncMaybe is the idea that a single implementation expresses the logic, algorithms and general calls, but can run in synchronous mode or asynchronous mode. AsyncMaybe code has the following traits:
- Have "async" signatures
- Return Task/ValueTask
- Take CancellationTokens
- Are named *AsyncMaybe
- Take a bool parameter called "useAsync".
- For extension methods this is the second parameter (after the "this")
- For all other methods, this is the first parameter
- When useAsync is false, all "tasks" are expected to complete synchronously. That is, as soon as the "task" is returned, it is in the completed state and its value is available.
Typically, these async maybe implementations aren't part of the public API of a library. Instead, you want to expose fully async or fully sync APIs. AsyncMaybe can generate these for you. Just decorate an AsyncMaybe implementation with the AsyncMaybeWrappers attribute, and the source generator will produce both async and sync wrappers for you!
Enumerators/Iterators
One of the big wrinkles in this plan is the IEnumerable<T>/IAsyncEnumerable<T> split. AsyncMaybe provides a converged view called AsyncMaybeEnumerable that can wrap either of these and produce the necessary interface and ensure that the synchronous path remains truly synchronous.
So, any AsyncMaybe implementation should take AsyncMaybeEnumerable as both input parameters and return values.
AsyncMaybeWrappers options:
All of these options default to false unless otherwise stated
- IsPublic (default: true) - Indicates whether the produced wrappers should be public. If false, the wrappers will inherit the visibility of the implementation.
- UseCancellationTokenForSync - If true, the CancellationToken parameter will be exposed to the synchronous wrapper as well. Otherwise, the synchronous implementation will pass default/none.
- AllowNoCancellation - Normally, it is an error for an AsyncMaybe implementation to not take a CancellationToken as its final parameter. Setting this to true will suppress that error and create uncancellable wrappers.
- SyncOverrides - If true, it indicates that the synchronous wrapper should have the overrides modifier
- AsyncOverrides - If true, it indicates that the asynchronous wrapper should have the overrides modifier
- SyncVirtual - If true, it indicates that the synchronous wrapper should have the virtual modifier
- AsyncVirtual - If true, it indicates that the asynchronous wrapper should have the virtual modifier
- SyncAsProperty - If true (and the implementation contains no arguments other than useAsync and a cancellation token), this creates the synchronous wrapper as a Property
- SyncName - Allows the synchronous wrapper name to be overridden
- AsyncName - Allows the asynchronous wrapper name to be overridden
AsyncMaybeEnumerable
This type serves as the enumerable bridge between IEnumerable<T> and IAsyncEnumerable<T>. It should be used in the signature of AsyncMaybe implementations instead of either of those two interfaces.
When using an AsyncMaybeEnumerable in an implementation, call WrapAsAsync() to get an IAsyncEnumerable and use await foreach or other async interactions, even for synchronous paths. If everything is done correctly, everything will still be synchronous.
If you need to create an iterator, create an IAsyncEnumerable<T> iterator and call .AsMaybe() on it to return it.
Most of the time, the automatic wrapper generators will take care of converting to IEnumerable or IAsyncEnumerable, but if you need to do this conversion, you can call .WrapAsSync() or .WrapAsAsync(). When you .WrapAsSync(), the infrastructure will enforce that tasks have completed synchronously.
Sync-over-Async
AsyncMaybe is designed to provide "pure" execution. That is, everything is sync, or everything is async. This requires the leaf node calls to call into a synchronous or asynchronous method conditionally based on useAsync. However, .NET async evolved in several time periods where synchronous implementations were discouraged. This has resulted in libraries with only async surface area.
AsyncMaybe can help generate AsyncMaybe patterns over async calls that provide the best protection against deadlocks and produce exceptions in the expected way. This sync-over-async will not provide the best performance code, as it will use Task.Run to run async implementations on the thread pool (typically) and block the entering thread. In many cases that don't require high scalability, this is just fine.
If you want to create such an AsyncMaybe implementation, write an asynchronous implementation with the standard pattern, then decorate it with the AsyncMaybeOverAsync attribute. The source generator will create an AsyncMaybe implementation that can be called in an AsyncMaybe call chain and will be able to create sync codepaths.
TODO: more readme!
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Marklio.AsyncMaybe:
Package | Downloads |
---|---|
Marklio.Metadata
Allows low-level exploration of PE and metadata constructs. |
|
Marklio.IO
Useful IO patterns, particularly for reading binary file formats. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
1.1.2 | 255 | 4/25/2024 |
1.1.1 | 239 | 4/12/2024 |
1.1.0-preview1 | 221 | 4/6/2023 |
1.0.0 | 1,118 | 7/28/2022 |
1.0.0-preview13 | 357 | 7/5/2022 |
1.0.0-preview12 | 296 | 6/30/2022 |
1.0.0-preview11 | 222 | 6/29/2022 |
1.0.0-preview10 | 223 | 6/29/2022 |
1.0.0-preview09 | 265 | 6/29/2022 |
1.0.0-preview08 | 213 | 6/28/2022 |
1.0.0-preview07 | 215 | 6/23/2022 |
1.0.0-preview06 | 226 | 6/23/2022 |
1.0.0-preview05 | 227 | 6/23/2022 |
1.0.0-preview04 | 228 | 6/23/2022 |
1.0.0-preview03 | 235 | 6/23/2022 |
1.0.0-preview02 | 241 | 6/23/2022 |
1.0.0-preview01 | 281 | 6/23/2022 |