From 6578c248fa278fd7aad082936e280b01a30961bc Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Thu, 18 Jun 2026 09:41:37 +0200 Subject: [PATCH 1/4] update count result --- Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md | 6 ++++++ .../UnitTests.cs | 6 +++--- .../BulkInsert.cs | 20 +++++++++---------- .../Definitions/Options.cs | 11 +++++----- .../Definitions/Result.cs | 3 +-- .../Frends.MicrosoftSQL.BulkInsert.csproj | 2 +- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md b/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md index 2457cbd..a8dcf27 100644 --- a/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md +++ b/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [3.3.0] - 2026-06-18 + +### Changed + +- In successful execution, Result.Count will show number of all rows. + ## [3.2.0] - 2026-06-18 ### Changed diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.Tests/UnitTests.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.Tests/UnitTests.cs index 1f5de20..3cf3f45 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.Tests/UnitTests.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.Tests/UnitTests.cs @@ -332,7 +332,7 @@ public async Task TestBulkInsert_NotifyAfterZero() var result = await MicrosoftSQL.BulkInsert(_input, options, default); Assert.IsTrue(result.Success); - Assert.AreEqual(0, result.Count); + Assert.AreEqual(3, result.Count); Assert.AreEqual(3, GetRowCount()); await MicrosoftSQL.BulkInsert(_input, options, default); @@ -371,7 +371,7 @@ public async Task TestBulkInsert_NotifyAfterTooMuch() var result = await MicrosoftSQL.BulkInsert(_input, options, default); Assert.IsTrue(result.Success); - Assert.AreEqual(0, result.Count); + Assert.AreEqual(3, result.Count); Assert.AreEqual(3, GetRowCount()); await MicrosoftSQL.BulkInsert(_input, options, default); @@ -489,4 +489,4 @@ private static int GetRowCount() connection.Dispose(); return count; } -} \ No newline at end of file +} diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs index 5429bfa..8056f32 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs @@ -104,6 +104,7 @@ public static async Task BulkInsert([PropertyTab] Input input, [Property private static async Task ExecuteHandler(Options options, Input input, DataSet dataSet, SqlBulkCopy sqlBulkCopy, CancellationToken cancellationToken) { var rowsCopied = 0L; + var totalRows = dataSet.Tables[0].Rows.Count; // JsonPropertyOrder is handled implicitly (default behavior) by not adding any column mappings, // which means the columns will be mapped based on their order in the input JSON. @@ -126,16 +127,12 @@ private static async Task ExecuteHandler(Options options, Input input, Dat sqlBulkCopy.DestinationTableName = input.TableName; sqlBulkCopy.SqlRowsCopied += (s, e) => rowsCopied = e.RowsCopied; - if (options.NotifyAfter == 0) + sqlBulkCopy.NotifyAfter = options.NotifyAfter switch { - // Calculate the number of rows and set value for NotifyAfter - var rowCount = dataSet.Tables[0].Rows.Count; - sqlBulkCopy.NotifyAfter = rowCount > 0 ? Math.Max(1, rowCount / 10) : 1; - } - else if (options.NotifyAfter > 0) - sqlBulkCopy.NotifyAfter = options.NotifyAfter; - else - sqlBulkCopy.NotifyAfter = 0; + 0 => totalRows > 0 ? Math.Max(1, totalRows / 10) : 1, + > 0 => options.NotifyAfter, + _ => 0, + }; await sqlBulkCopy.WriteToServerAsync(dataSet.Tables[0], cancellationToken).ConfigureAwait(false); } @@ -146,7 +143,8 @@ private static async Task ExecuteHandler(Options options, Input input, Dat throw new Exception($"ExecuteHandler exception, processed row count between: {rowsCopied} and {notifyRange} (see NotifyAfter). {ex}"); } - return rowsCopied; + //If code goes up to here, it means all rows were inserted. + return totalRows; } private static void SetEmptyDataRowsToNull(DataSet dataSet) @@ -193,4 +191,4 @@ private static IsolationLevel GetIsolationLevel(Options options) _ => IsolationLevel.ReadCommitted, }; } -} \ No newline at end of file +} diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs index b120d23..fdd651b 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs @@ -15,11 +15,12 @@ public class Options public int CommandTimeoutSeconds { get; set; } /// - /// Defines the number of rows to be processed before generating a notification event. + /// Defines the number of rows to be processed before generating a notification event. /// The default value of 0 will set NotifyAfter dynamically to 10% of the total row count, with a minimum value of 1. - /// A value of -1 means there won't be any notifications until the task is completed, and Result.Count will be 0. - /// Setting a value greater than the total number of rows can cause Result.Count to be 0. + /// A value of -1 means there won't be any notifications until the task is completed. + /// Setting a value greater than the total number of rows can cause notification response to be 0. /// Notification events can be used for error handling to see approximately which row the error occurred at. + /// Notified value is useful for error handling and show approximately which row the error occured at /// /// 0 public int NotifyAfter { get; set; } @@ -46,7 +47,7 @@ public class Options public bool TableLock { get; set; } /// - /// Preserve null values in the destination table regardless of the settings for default values. + /// Preserve null values in the destination table regardless of the settings for default values. /// When not specified, null values are replaced by default values where applicable. /// /// false @@ -82,4 +83,4 @@ public class Options /// SqlTransactionIsolationLevel.ReadCommitted [DefaultValue(SqlTransactionIsolationLevel.ReadCommitted)] public SqlTransactionIsolationLevel SqlTransactionIsolationLevel { get; set; } -} \ No newline at end of file +} diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs index 04818be..190d3a7 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs @@ -12,8 +12,7 @@ public class Result public bool Success { get; private set; } /// - /// Number of processed rows reported by SqlBulkCopy notifications. - /// The value is approximate and can be rounded down to the nearest NotifyAfter interval (or 0 if no notification is raised). + /// Number of processed rows. /// /// 100 public long Count { get; private set; } diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.csproj b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.csproj index 09247b8..db33c1a 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.csproj +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert.csproj @@ -2,7 +2,7 @@ net6.0 - 3.2.0 + 3.3.0 Frends Frends Frends From dbba96143c1774e6eadc44ccd2dad9fc8c80f574 Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Thu, 18 Jun 2026 11:12:16 +0200 Subject: [PATCH 2/4] change logic of Count result --- Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md | 1 + .../BulkInsert.cs | 83 ++++++++++++++----- .../Definitions/Result.cs | 1 + 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md b/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md index a8dcf27..0d1fbab 100644 --- a/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md +++ b/Frends.MicrosoftSQL.BulkInsert/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - In successful execution, Result.Count will show number of all rows. +- In case of failure, Result.Count will show estimated number of rows copied before the failure. ## [3.2.0] - 2026-06-18 diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs index 8056f32..80c6157 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs @@ -24,17 +24,20 @@ public class MicrosoftSQL /// Optional parameters /// Token generated by Frends to stop this Task. /// Object { bool Success, long Count, string ErrorMessage } - public static async Task BulkInsert([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) + public static async Task BulkInsert([PropertyTab] Input input, [PropertyTab] Options options, + CancellationToken cancellationToken) { var inputJson = @"{""data"": {""Table"": " + input.InputData + @" } }"; + long rowsCopied; try { DataSet dataSet = JObject.Parse(inputJson)["data"].ToObject(); _ = dataSet.Tables["Table"]; + using var connection = new SqlConnection(input.ConnectionString); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -47,15 +50,23 @@ public static async Task BulkInsert([PropertyTab] Input input, [Property { try { - var result = await ExecuteHandler(options, input, dataSet, new SqlBulkCopy(connection, GetSqlBulkCopyOptions(options), null), cancellationToken).ConfigureAwait(false); + var result = await ExecuteHandler(options, input, dataSet, + new SqlBulkCopy(connection, GetSqlBulkCopyOptions(options), null), cancellationToken) + .ConfigureAwait(false); + return new Result(true, result, null); } catch (Exception ex) { if (options.ThrowErrorOnFailure) - throw new Exception("BulkInsert exception: 'Options.SqlTransactionIsolationLevel = None', so there was no transaction rollback.", ex); - else - return new Result(false, 0, $"ExecuteHandler exception: 'Options.SqlTransactionIsolationLevel = None', so there was no transaction rollback. {ex}"); + throw new Exception( + "BulkInsert exception: 'Options.SqlTransactionIsolationLevel = None', so there was no transaction rollback.", + ex); + + rowsCopied = ex.Data["RowsCopied"] != null ? (long)ex.Data["RowsCopied"] : 0; + + return new Result(false, rowsCopied, + $"ExecuteHandler exception: 'Options.SqlTransactionIsolationLevel = None', so there was no transaction rollback. {ex}"); } } @@ -64,8 +75,11 @@ public static async Task BulkInsert([PropertyTab] Input input, [Property try { - var result = await ExecuteHandler(options, input, dataSet, new SqlBulkCopy(connection, GetSqlBulkCopyOptions(options), transaction), cancellationToken).ConfigureAwait(false); + var result = await ExecuteHandler(options, input, dataSet, + new SqlBulkCopy(connection, GetSqlBulkCopyOptions(options), transaction), cancellationToken) + .ConfigureAwait(false); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + return new Result(true, result, null); } catch (Exception ex) @@ -77,15 +91,23 @@ public static async Task BulkInsert([PropertyTab] Input input, [Property catch (Exception rollbackEx) { if (options.ThrowErrorOnFailure) - throw new Exception("BulkInsert exception: An exception occurred on transaction rollback.", rollbackEx); - else - return new Result(false, 0, $"BulkInsert exception: An exception occurred on transaction rollback. Rollback exception: {rollbackEx}. || Exception leading to rollback: {ex}"); + throw new Exception("BulkInsert exception: An exception occurred on transaction rollback.", + rollbackEx); + + rowsCopied = ex.Data["RowsCopied"] != null ? (long)ex.Data["RowsCopied"] : 0; + + return new Result(false, rowsCopied, + $"BulkInsert exception: An exception occurred on transaction rollback. Rollback exception: {rollbackEx}. || Exception leading to rollback: {ex}"); } if (options.ThrowErrorOnFailure) - throw new Exception("BulkInsert exception: (If required) transaction rollback completed without exception.", ex); - else - return new Result(false, 0, $"BulkInsert exception: (If required) transaction rollback completed without exception. {ex}."); + throw new Exception( + "BulkInsert exception: (If required) transaction rollback completed without exception.", ex); + + rowsCopied = ex.Data["RowsCopied"] != null ? (long)ex.Data["RowsCopied"] : 0; + + return new Result(false, rowsCopied, + $"BulkInsert exception: (If required) transaction rollback completed without exception. {ex}."); } } catch (Exception e) @@ -93,7 +115,11 @@ public static async Task BulkInsert([PropertyTab] Input input, [Property if (options.ThrowErrorOnFailure) throw new Exception("BulkInsert exception: ", e); else - return new Result(false, 0, $"BulkInsert exception: {e}"); + { + rowsCopied = e.Data["RowsCopied"] != null ? (long)e.Data["RowsCopied"] : 0; + + return new Result(false, rowsCopied, $"BulkInsert exception: {e}"); + } } finally { @@ -101,7 +127,8 @@ public static async Task BulkInsert([PropertyTab] Input input, [Property } } - private static async Task ExecuteHandler(Options options, Input input, DataSet dataSet, SqlBulkCopy sqlBulkCopy, CancellationToken cancellationToken) + private static async Task ExecuteHandler(Options options, Input input, DataSet dataSet, + SqlBulkCopy sqlBulkCopy, CancellationToken cancellationToken) { var rowsCopied = 0L; var totalRows = dataSet.Tables[0].Rows.Count; @@ -139,8 +166,18 @@ private static async Task ExecuteHandler(Options options, Input input, Dat } catch (Exception ex) { - var notifyRange = rowsCopied + (sqlBulkCopy.NotifyAfter - 1); - throw new Exception($"ExecuteHandler exception, processed row count between: {rowsCopied} and {notifyRange} (see NotifyAfter). {ex}"); + var notifyAfter = sqlBulkCopy.NotifyAfter; + var notifyRange = notifyAfter > 0 ? rowsCopied + notifyAfter - 1 : rowsCopied; + + throw new Exception( + $"ExecuteHandler exception, processed row count between: {rowsCopied} and {notifyRange} (see NotifyAfter). {ex}", + ex) + { + Data = + { + ["RowsCopied"] = rowsCopied, + }, + }; } //If code goes up to here, it means all rows were inserted. @@ -150,13 +187,13 @@ private static async Task ExecuteHandler(Options options, Input input, Dat private static void SetEmptyDataRowsToNull(DataSet dataSet) { foreach (var table in dataSet.Tables.Cast()) - foreach (var row in table.Rows.Cast()) - foreach (var column in row.ItemArray) - if (column.ToString() == string.Empty) - { - var index = Array.IndexOf(row.ItemArray, column); - row[index] = null; - } + foreach (var row in table.Rows.Cast()) + foreach (var column in row.ItemArray) + if (column.ToString() == string.Empty) + { + var index = Array.IndexOf(row.ItemArray, column); + row[index] = null; + } } private static SqlBulkCopyOptions GetSqlBulkCopyOptions(Options options) diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs index 190d3a7..d9c500f 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Result.cs @@ -13,6 +13,7 @@ public class Result /// /// Number of processed rows. + /// In case of failure it shows notified number of processed rows. Approximation logic is defined by Options.NotifyAfter /// /// 100 public long Count { get; private set; } From fbd73bc56f82498e52473e7debcef89fcb45195e Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Thu, 18 Jun 2026 11:16:39 +0200 Subject: [PATCH 3/4] linter fix --- .../Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs index 80c6157..94d4e29 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs @@ -187,13 +187,17 @@ private static async Task ExecuteHandler(Options options, Input input, Dat private static void SetEmptyDataRowsToNull(DataSet dataSet) { foreach (var table in dataSet.Tables.Cast()) - foreach (var row in table.Rows.Cast()) - foreach (var column in row.ItemArray) - if (column.ToString() == string.Empty) + { + foreach (var row in table.Rows.Cast()) { - var index = Array.IndexOf(row.ItemArray, column); - row[index] = null; + foreach (var column in row.ItemArray) + if (column.ToString() == string.Empty) + { + var index = Array.IndexOf(row.ItemArray, column); + row[index] = null; + } } + } } private static SqlBulkCopyOptions GetSqlBulkCopyOptions(Options options) From 244b05ed6b9da50af55cbb8ac10fea6831d876dc Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Fri, 19 Jun 2026 15:28:09 +0200 Subject: [PATCH 4/4] cr fixes --- .../Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs | 11 +++++------ .../Definitions/Options.cs | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs index 94d4e29..c98c972 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/BulkInsert.cs @@ -190,12 +190,11 @@ private static void SetEmptyDataRowsToNull(DataSet dataSet) { foreach (var row in table.Rows.Cast()) { - foreach (var column in row.ItemArray) - if (column.ToString() == string.Empty) - { - var index = Array.IndexOf(row.ItemArray, column); - row[index] = null; - } + for (var i = 0; i < row.ItemArray.Length; i++) + { + if (row[i] is string value && value.Length == 0) + row[i] = DBNull.Value; + } } } } diff --git a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs index fdd651b..773a6ba 100644 --- a/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs +++ b/Frends.MicrosoftSQL.BulkInsert/Frends.MicrosoftSQL.BulkInsert/Definitions/Options.cs @@ -20,7 +20,6 @@ public class Options /// A value of -1 means there won't be any notifications until the task is completed. /// Setting a value greater than the total number of rows can cause notification response to be 0. /// Notification events can be used for error handling to see approximately which row the error occurred at. - /// Notified value is useful for error handling and show approximately which row the error occured at /// /// 0 public int NotifyAfter { get; set; }