This .NET source generator produces a sync method from an async one.
⭐ Star this project if you hate code duplication or calling async methods from sync.
- A library which exposes both sync and async version of a method
- An application has to process two kinds of data in the same way:
- Large data from I/O which cannot be stored in memory before processing: Original async method
- Small sample of data in memory, usually a sample of the larger data: Generated sync method
- FluentValidation - validation library
- MiniExcel - spreadsheet reader and writer
- SharpCompress - compression library
SharpCompress's migration notes are worth reading before adopting this in a codebase which already has both halves written by hand. They cover how to prove that a generated method is the one it replaces, and when attributing a method would add a member rather than remove a duplicate - a generated Read(Span<byte>) displaces the shim Stream provides, which is a change in behaviour rather than a deduplication.
Decorate your async method with CreateSyncVersionAttribute in your partial class, struct, record, or interface:
[Zomp.SyncMethodGenerator.CreateSyncVersion]staticasyncTaskWriteAsync(ReadOnlyMemory<byte>buffer,Streamstream,CancellationTokenct)=>awaitstream.WriteAsync(buffer,ct).ConfigureAwait(true);and it will generate a sync version of the method:
staticvoidWrite(ReadOnlySpan<byte>buffer,Streamstream)=>stream.Write(buffer);A list of changes applied to the new synchronized method:
Remove
asyncmodifierRemove
awaitfrom methods as well asforeachstatementChange types
* ValueTasks are handled exactly like Tasks
**
MemoryandReadOnlyMemoryis preserved in sync methods if it is a type argument of a collection. This is due to a compiler limitation which states that aref structcan't be the element type of an array.Remove parameters
- CancellationToken, unless the
PreserveCancellationTokenproperty is set totrue. - IProgress<T>, unless the
PreserveProgressproperty is set totrue.
- CancellationToken, unless the
Invocation changes
- Remove
ConfigureAwaitfrom Tasks and Asynchronous Enumerations - Remove standalone
ConfigureAwaitstatements - Remove
WaitAsyncfrom Tasks - Remove WithCancellation
- Rewrite asynchronous invocations with
Asyncsuffix to call synchronous version (e.g. MoveNextAsync() becomes MoveNext()) - Remove asynchronous invocations without the
Asyncsuffix - Remove CancellationToken parameter
- Remove IProgress<T>.Report(T) call
- Remove Memory<T>.Span property
- Change
awaitTask<TResult>.FromResult(value) tovalue - Change
awaitTask.Delay(value) to Thread.Sleep(value) - Change any invocation returning ConfiguredCancelableAsyncEnumerable<T> to IEnumerable.GetEnumerator()
- Remove
Remove
CreateSyncVersionAttributeUpdate XML documentation
This source generator detects language version during the compilation. By default it will generate #nullable enable directive if and only if the language version is 8 or above. Since it is impossible to reliably determine whether nullable context is turned on or not, OmitNullableDirective property is available to omit that directive from generating.
[Zomp.SyncMethodGenerator.CreateSyncVersion(OmitNullableDirective=true)]publicasyncTaskMethodAsync(){stringf=null;}By default, this source generator removes IProgress<T> parameters from async methods. To preserve them, use the PreserveProgress option.
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress=true)]publicasyncTaskMethodAsync(IProgress<double>progress){progress.Report(0.0);}By default, this source generator removes CancellationToken parameters from async methods. To preserve them, use the PreserveCancellationToken option.
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveCancellationToken=true)]publicasyncTaskMethodAsync(CancellationTokencancellationToken=default){cancellationToken.ThrowIfCancellationRequested();}You can also decorate your type (class, struct, record, or interface) to generate a sync version for every asynchronous method.
[Zomp.SyncMethodGenerator.CreateSyncVersion]partialclassMyClass{asyncTaskMethod1Async(...){ ...}asyncIAsyncEnumerable<...>Method2Async(...){ ...}[Zomp.SyncMethodGenerator.SkipSyncVersion]asyncTaskWillNotGenerateAsync(...){ ...}}This will generate their sync counterparts:
[Zomp.SyncMethodGenerator.CreateSyncVersion]partialclassMyClass{voidMethod1(...){ ...}}and
[Zomp.SyncMethodGenerator.CreateSyncVersion]partialclassMyClass{IEnumerable<...>Method2(...){ ...}}To exclude a method from generating a sync version use SkipSyncVersionAttribute on a method. See WillNotGenerateAsync method in the example above.
In case there is logic which should only be executed in the synchronized version of the method, wrap it in SYNC_ONLY #if directive.
SYNC_ONLY must not be defined anywhere. The source generator will scan #if directives for this symbol.
Code inside SYNC_ONLY block will be copied as is. Unless global namespaces are used in the project, this code should contain fully qualified namespaces.
The following syntax:
[Zomp.SyncMethodGenerator.CreateSyncVersion]publicasyncTaskWithSyncOnlyDirectiveAsync(CancellationTokenct){
#if SYNC_ONLYSystem.Console.Write("Sync");
#endif
awaitTask.CompletedTask;}will output:
publicvoidWithSyncOnlyDirective(){System.Console.Write("Sync");}If you only want to execute in the original async version, flip the flag like this: #if !SYNC_ONLY.
Note: SYNC_ONLY cannot be mixed with other symbols in a conditional expression and cannot have #elif directive.
Warning
SYNC_ONLY flag currently works in parameter lists, argument lists and statements.
Please always double check your code when using this flag.
If your use case is not supported, please log an issue.
To add the library use:
dotnet add package Zomp.SyncMethodGenerator- SyncToAsyncExtension - Allows switching between sync and async versions of a method. Very useful in development of this library.
This project is fully compatible with act.
Other than required packages to run act itself, GitHub Actions script installs anything else that might be missing, such as node, pnpm and dotnet. On Windows platform, software installation is performed on the host itself due to lack of container support.
To build the project using act follow these instructions:
Install chocolatey if missing.
Install the following packages if missing:
choco install git -y
choco install act-cli -y
refreshenvIn the project directory run:
act -P windows-latest=-self-hosted --artifact-server-path c:/tmp/artifactsInstall act by following these instructions.
In the project directory run:
act --artifact-server-path /tmp/artifacts