diff --git a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/IPostgreSQLCopyHelper.cs b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/IPostgreSQLCopyHelper.cs index 99264f3..8b90770 100644 --- a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/IPostgreSQLCopyHelper.cs +++ b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/IPostgreSQLCopyHelper.cs @@ -1,6 +1,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Npgsql; @@ -9,6 +10,6 @@ namespace PostgreSQLCopyHelper public interface IPostgreSQLCopyHelper { ulong SaveAll(NpgsqlConnection connection, IEnumerable entities); - Task SaveAllAsync(NpgsqlConnection connection, IEnumerable entities); + ValueTask SaveAllAsync(NpgsqlConnection connection, IEnumerable entities, CancellationToken cancellationToken = default); } } diff --git a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Model/ColumnDefinition.cs b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Model/ColumnDefinition.cs index 7e0f45f..94bc9e6 100644 --- a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Model/ColumnDefinition.cs +++ b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Model/ColumnDefinition.cs @@ -1,6 +1,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Threading.Tasks; using Npgsql; namespace PostgreSQLCopyHelper.Model @@ -9,7 +10,7 @@ internal class ColumnDefinition { public string ColumnName { get; set; } - public Action Write { get; set; } + public Func Write { get; set; } public override string ToString() { diff --git a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.cs b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.cs index b2e5949..3b13976 100644 --- a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.cs +++ b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Npgsql; using NpgsqlTypes; @@ -38,15 +39,28 @@ public PostgreSQLCopyHelper(string schemaName, string tableName) } public ulong SaveAll(NpgsqlConnection connection, IEnumerable entities) => - SaveAllAsync(connection, entities).GetAwaiter().GetResult(); + DoSaveAllAsync(connection, entities).GetAwaiter().GetResult(); - public async Task SaveAllAsync(NpgsqlConnection connection, IEnumerable entities) + public ValueTask SaveAllAsync(NpgsqlConnection connection, IEnumerable entities, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + using (NoSynchronizationContextScope.Enter()) + { + return DoSaveAllAsync(connection, entities); + } + } + + private async ValueTask DoSaveAllAsync(NpgsqlConnection connection, IEnumerable entities) { using (var binaryCopyWriter = connection.BeginBinaryImport(GetCopyCommand())) { await WriteToStream(binaryCopyWriter, entities); - return await binaryCopyWriter.Complete(async: true); + return await binaryCopyWriter.CompleteAsync(); } } @@ -59,23 +73,23 @@ public PostgreSQLCopyHelper UsePostgresQuoting(bool enabled = true) public PostgreSQLCopyHelper Map(string columnName, Func propertyGetter, NpgsqlDbType type) { - return AddColumn(columnName, (writer, entity) => writer.Write(propertyGetter(entity), type)); + return AddColumn(columnName, (writer, entity) => writer.WriteAsync(propertyGetter(entity), type)); } public PostgreSQLCopyHelper MapNullable(string columnName, Func propertyGetter, NpgsqlDbType type) where TProperty : struct { - return AddColumn(columnName, (writer, entity) => + return AddColumn(columnName, async (writer, entity) => { var val = propertyGetter(entity); if (!val.HasValue) { - writer.WriteNull(); + await writer.WriteNullAsync(); } else { - writer.Write(val.Value, type); + await writer.WriteAsync(val.Value, type); } }); } @@ -84,16 +98,16 @@ private async Task WriteToStream(NpgsqlBinaryImporter writer, IEnumerable AddColumn(string columnName, Action action) + private PostgreSQLCopyHelper AddColumn(string columnName, Func action) { _columns.Add(new ColumnDefinition { diff --git a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.csproj b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.csproj index a6a21c9..20212d7 100644 --- a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.csproj +++ b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper.csproj @@ -21,11 +21,11 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive + \ No newline at end of file diff --git a/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Utils/NoSynchronizationContextScope.cs b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Utils/NoSynchronizationContextScope.cs new file mode 100644 index 0000000..3ccc433 --- /dev/null +++ b/PostgreSQLCopyHelper/PostgreSQLCopyHelper/PostgreSQLCopyHelper/Utils/NoSynchronizationContextScope.cs @@ -0,0 +1,40 @@ +// Code taken from https://github.com/npgsql/npgsql/blob/dev/src/Npgsql/NoSynchronizationContextScope.cs + +using System; +using System.Threading; + +namespace PostgreSQLCopyHelper.Utils +{ + /// + /// This mechanism is used to temporarily set the current synchronization context to null while + /// executing Npgsql code, making all await continuations execute on the thread pool. This replaces + /// the need to place ConfigureAwait(false) everywhere, and should be used in all surface async methods, + /// without exception. + /// + /// Warning: do not use this directly in async methods, use it in sync wrappers of async methods + /// (see https://github.com/npgsql/npgsql/issues/1593) + /// + /// + /// http://stackoverflow.com/a/28307965/640325 + /// + internal static class NoSynchronizationContextScope + { + internal static Disposable Enter() + { + var sc = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + return new Disposable(sc); + } + + internal struct Disposable : IDisposable + { + private readonly SynchronizationContext _synchronizationContext; + + internal Disposable(SynchronizationContext synchronizationContext) + => _synchronizationContext = synchronizationContext; + + public void Dispose() + => SynchronizationContext.SetSynchronizationContext(_synchronizationContext); + } + } +}