├── SqlBulkHelpers.Tests ├── Usings.cs ├── appsettings.tests.json ├── BaseTest.cs ├── GlobalTestInitialization.cs ├── TestHelpers │ ├── TestConfiguration.cs │ ├── SqlConnectionHelper.cs │ └── RetryWithExponentialBackoffAsync.cs ├── SqlBulkHelpers.Tests.csproj ├── SqlScripts │ └── InitTestDBTable.sql └── IntegrationTests │ ├── SchemaLoadingTests │ └── ProcessingDefinitionTests.cs │ ├── SqlBulkLoadingTests │ └── ConnectionAndConstructorTests.cs │ └── MaterializeDataTests │ ├── CopyTableDataTests.cs │ └── TableIdentityColumnApiTests.cs ├── DotNetFramework.SqlBulkHelpers ├── packages.config ├── SqlBulkHelpers │ ├── Interfaces │ │ ├── ISqlBulkHelpersDBSchemaLoader.cs │ │ ├── ISqlBulkHelpersConnectionProvider.cs │ │ └── ISqlBulkHelper.cs │ ├── SqlBulkHelpersConstants.cs │ ├── QueryProcessing │ │ ├── SqlBulkHelpersMergeAction.cs │ │ ├── SqlBulkHelpersObjectReflectionFactory.cs │ │ ├── SqlBulkHelpersObjectMapper.cs │ │ └── SqlBulkHelpersMergeQueryBuilder.cs │ ├── CustomExtensions │ │ ├── SqlBulkHelpersCustomExtensions.cs │ │ └── SystemDataSqlClientCustomExtensions.cs │ ├── Database │ │ ├── SqlBulkHelpersConnectionProvider.cs │ │ ├── SqlBulkHelpersDBSchemaModels.cs │ │ └── SqlBulkHelpersDBSchemaLoader.cs │ ├── SqlBulkCopyFactory.cs │ ├── SqlBulkNaturalKeyHelper.cs │ └── BaseSqlBulkHelper.cs ├── Properties │ └── AssemblyInfo.cs └── DotNetFramework.SqlBulkHelpers-Deprecated.csproj ├── NetStandard.SqlBulkHelpers ├── MaterializedData │ ├── Interfaces │ │ └── ISqlScriptBuilder.cs │ ├── IMaterializeDataContextCompletionSource.cs │ ├── MaterializationTableInfo.cs │ ├── IMaterializeDataContext.cs │ └── CloneTableInfo.cs ├── Database │ ├── TableSchemaDetailLevelEnum.cs │ ├── SqlQueries │ │ ├── Check for Non-Trusted Constraints.sql │ │ └── QueryDBTableSchemaBasicDetailsJson.sql │ ├── SqlBulkHelpersConnectionProxyExistingProvider.cs │ ├── SqlBulkHelpersSchemaLoaderCache.cs │ ├── TableNameTerm.cs │ ├── SqlBulkHelpersConnectionProvider.cs │ └── SqlBulkHelpersDBSchemaLoader.Sync.cs ├── SqlBulkHelpersConstants.cs ├── Attributes │ ├── SqlBulkMatchQualifierAttribute.cs │ ├── SqlBulkColumnAttribute.cs │ └── SqlBulkTableAttribute.cs ├── SqlBulkHelper │ ├── Interfaces │ │ ├── ISqlBulkHelpersHasTransaction.cs │ │ ├── ISqlBulkHelperIdentitySetter.cs │ │ ├── ISqlBulkHelpersConnectionProvider.cs │ │ └── ISqlBulkHelpersDBSchemaLoader.cs │ ├── QueryProcessing │ │ ├── SqlBulkHelpersMergeAction.cs │ │ ├── SqlBulkHelpersMergeMatchQualifiers.cs │ │ └── SqlBulkHelpersDataReader.cs │ ├── SqlBulkCopyFactory.cs │ ├── SqlBulkHelper.Invocation.DeprecatedV1.cs │ └── SqlBulkHelper.Invocation.cs ├── CustomExtensions │ ├── SystemIOCustomExtensions.cs │ ├── ReflectionExtensions.cs │ ├── SystemCollectionCustomExtensions.cs │ ├── SqlClientCustomExtensions.cs │ ├── StringExtensions.cs │ ├── EmbeddedResourceExtensions.cs │ └── SqlBulkHelpersCustomExtensions.cs ├── Utilities │ └── IdGenerator.cs └── BaseHelper.cs ├── SqlBulkHelpers.SampleApp.Common ├── SqlBulkHelpers.SampleApp.Common.csproj └── TestHelpers.cs ├── SqlBulkHelpers.SampleApp.NetFramework ├── SampleApp │ ├── SqlBulkHelpersSampleAppConstants.cs │ ├── SqlBulkHelpersSample.cs │ ├── SqlBulkHelpersSampleSynchronous.cs │ └── SqlBulkHelpersSampleAsync.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── App.config └── SqlBulkHelpers.SampleApp.NetFramework.csproj ├── SqlBulkHelpers.SampleApp.Net6 ├── SampleApp │ ├── SqlBulkHelpersSampleAppConstants.cs │ ├── SqlBulkHelpersSample.cs │ └── SqlBulkHelpersSampleAsync.cs ├── SqlBulkHelpers.SampleApp.Net6.csproj └── Program.cs ├── LICENSE ├── SqlBulkHelpers.sln.DotSettings ├── .github └── workflows │ └── main.yml ├── SqlBulkHelpers.sln └── .gitignore /SqlBulkHelpers.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/appsettings.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "SqlConnectionString": "{{SQL_CONNECTION_STRING_GOES_HERE}}" 3 | } 4 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | 4 | namespace SqlBulkHelpers.Tests 5 | { 6 | [TestClass] 7 | public class BaseTest 8 | { 9 | public TestContext TestContext { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/MaterializedData/Interfaces/ISqlScriptBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers.MaterializedData.Interfaces 4 | { 5 | public interface ISqlScriptBuilder 6 | { 7 | string BuildSqlScript(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/Interfaces/ISqlBulkHelpersDBSchemaLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers 4 | { 5 | public interface ISqlBulkHelpersDBSchemaLoader 6 | { 7 | SqlBulkHelpersTableDefinition GetTableSchemaDefinition(String tableName); 8 | } 9 | } -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/TableSchemaDetailLevelEnum.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | public enum TableSchemaDetailLevel 8 | { 9 | BasicDetails, 10 | ExtendedDetails 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelpersConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers 4 | { 5 | public static class SqlBulkHelpersConstants 6 | { 7 | public const int DefaultBulkOperationPerBatchTimeoutSeconds = 60; 8 | 9 | public const string ROWNUMBER_COLUMN_NAME = "SQLBULKHELPERS_ROWNUMBER"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/SqlBulkHelpersConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers 4 | { 5 | public static class SqlBulkHelpersConstants 6 | { 7 | public const String DEFAULT_IDENTITY_COLUMN_NAME = "Id"; 8 | public const String ROWNUMBER_COLUMN_NAME = "SQLBULKHELPERS_ROWNUMBER"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Attributes/SqlBulkMatchQualifierAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers 4 | { 5 | [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 6 | public class SqlBulkMatchQualifierAttribute : Attribute 7 | { 8 | public SqlBulkMatchQualifierAttribute() 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersHasTransaction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace SqlBulkHelpers.SqlBulkHelpers.Interfaces 7 | { 8 | public interface ISqlBulkHelpersHasTransaction 9 | { 10 | SqlTransaction GetTransaction(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/MaterializedData/IMaterializeDataContextCompletionSource.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Data.SqlClient; 3 | 4 | namespace SqlBulkHelpers.MaterializedData 5 | { 6 | public interface IMaterializeDataContextCompletionSource : IMaterializeDataContext 7 | { 8 | Task FinishMaterializeDataProcessAsync(SqlTransaction sqlTransaction); 9 | } 10 | } -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelperIdentitySetter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers.Interfaces 4 | { 5 | public interface ISqlBulkHelperIdentitySetter 6 | { 7 | void SetIdentityId(int id); 8 | } 9 | 10 | public interface ISqlBulkHelperBigIntIdentitySetter 11 | { 12 | void SetIdentityId(long id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Common/SqlBulkHelpers.SampleApp.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/SampleApp/SqlBulkHelpersSampleAppConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SqlBulkHelpersSample.ConsoleApp 8 | { 9 | public static class SqlBulkHelpersSampleApp 10 | { 11 | public const string TestTableName = "[dbo].[SqlBulkHelpersTestElements]"; 12 | public const int SqlTimeoutSeconds = 120; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/Interfaces/ISqlBulkHelpersConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Threading.Tasks; 3 | 4 | namespace SqlBulkHelpers 5 | { 6 | /// 7 | /// BBernard 8 | /// Interface to support custom implementations for initializing Sql Database connections as needed. 9 | /// 10 | public interface ISqlBulkHelpersConnectionProvider 11 | { 12 | SqlConnection NewConnection(); 13 | Task NewConnectionAsync(); 14 | } 15 | } -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Attributes/SqlBulkColumnAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 8 | public class SqlBulkColumnAttribute : Attribute 9 | { 10 | public string Name { get; set; } 11 | public SqlBulkColumnAttribute(string mappedDbColumnName) 12 | { 13 | this.Name = mappedDbColumnName; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Net6/SampleApp/SqlBulkHelpersSampleAppConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SqlBulkHelpersSample.ConsoleApp 8 | { 9 | public static class SqlBulkHelpersSampleApp 10 | { 11 | public const string TestTableName = "[dbo].[SqlBulkHelpersTestElements]"; 12 | public const string TestChildTableName = "[dbo].[SqlBulkHelpersTestElements_Child_NoIdentity]"; 13 | public const int SqlTimeoutSeconds = 120; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Net6/SampleApp/SqlBulkHelpersSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using SqlBulkHelpers.Tests; 4 | 5 | namespace SqlBulkHelpersSample.ConsoleApp 6 | { 7 | public static class SqlBulkHelpersSample 8 | { 9 | public static List CreateTestData(int dataSize) 10 | => TestHelpers.CreateTestData(dataSize, prefix: "TEST_CSHARP_DotNet6"); 11 | public static List CreateChildTestData(List testData) 12 | => TestHelpers.CreateChildTestData(testData); 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/SampleApp/SqlBulkHelpersSample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using SqlBulkHelpers.Tests; 4 | 5 | namespace SqlBulkHelpersSample.ConsoleApp 6 | { 7 | public static class SqlBulkHelpersSample 8 | { 9 | public static List CreateTestData(int dataSize) 10 | => TestHelpers.CreateTestData(dataSize, "TEST_CSHARP_NetFramework"); 11 | 12 | public static List CreateChildTestData(List testData) 13 | => TestHelpers.CreateChildTestData(testData); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/GlobalTestInitialization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using RepoDb; 7 | using RepoDb.Options; 8 | 9 | namespace SqlBulkHelpers.Tests 10 | { 11 | [TestClass] 12 | internal class GlobalTestInitialization 13 | { 14 | [AssemblyInitialize] 15 | public static void Initialize(TestContext testContext) 16 | { 17 | GlobalConfiguration.Setup().UseSqlServer(); 18 | } 19 | 20 | [AssemblyCleanup] 21 | public static void TearDown() 22 | { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Net6/SqlBulkHelpers.SampleApp.Net6.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/TestHelpers/TestConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace SqlBulkHelpers.Tests 5 | { 6 | internal static class TestConfiguration 7 | { 8 | private static readonly IConfiguration _config; 9 | 10 | static TestConfiguration() 11 | { 12 | _config = new ConfigurationBuilder() 13 | .AddJsonFile("appsettings.tests.json") 14 | .AddEnvironmentVariables() 15 | .Build(); 16 | } 17 | 18 | public static string? SqlConnectionString => _config[SqlBulkHelpersConnectionProvider.SqlConnectionStringConfigKey]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/QueryProcessing/SqlBulkHelpersMergeAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | [Flags] 10 | public enum SqlBulkHelpersMergeAction 11 | { 12 | Insert = 1, 13 | Update = 2, 14 | Delete = 4, 15 | InsertOrUpdate = Insert | Update 16 | } 17 | 18 | public class SqlBulkHelpersMerge 19 | { 20 | public static SqlBulkHelpersMergeAction ParseMergeActionString(String actionString) 21 | { 22 | SqlBulkHelpersMergeAction mergeAction; 23 | Enum.TryParse(actionString, true, out mergeAction); 24 | return mergeAction; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/CustomExtensions/SqlBulkHelpersCustomExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SqlBulkHelpers 5 | { 6 | public static class SqlBulkHelpersCustomExtensions 7 | { 8 | public static T AssertArgumentNotNull(this T arg, string argName) 9 | { 10 | if (arg == null) throw new ArgumentNullException(argName); 11 | return arg; 12 | } 13 | 14 | public static TValue GetValueOrDefault(this IDictionary dictionary, TKey key) 15 | { 16 | TValue value; 17 | return dictionary.TryGetValue(key, out value) ? value : default(TValue); 18 | } 19 | 20 | public static String ToCSV(this IEnumerable enumerableList) 21 | { 22 | return String.Join(", ", enumerableList); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/QueryProcessing/SqlBulkHelpersMergeAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers 4 | { 5 | [Flags] 6 | public enum SqlBulkHelpersMergeAction 7 | { 8 | Insert = 1, 9 | Update = 2, 10 | //Delete = 4, 11 | InsertOrUpdate = Insert | Update 12 | } 13 | 14 | public class SqlBulkHelpersMerge 15 | { 16 | public static SqlBulkHelpersMergeAction ParseMergeActionString(string actionString) 17 | { 18 | switch (actionString.ToLowerInvariant()) 19 | { 20 | case "insertorupdate": return SqlBulkHelpersMergeAction.InsertOrUpdate; 21 | case "insert": return SqlBulkHelpersMergeAction.Insert; 22 | case "update": return SqlBulkHelpersMergeAction.Update; 23 | //case "delete": return SqlBulkHelpersMergeAction.Delete; 24 | //Attempt an InsertOrUpdate by Default 25 | default: return SqlBulkHelpersMergeAction.InsertOrUpdate; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Net6/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using SqlBulkHelpersSample.ConsoleApp; 4 | 5 | namespace SqlBulkHelpers.SampleApp.NetCore 6 | { 7 | class Program 8 | { 9 | static async Task Main(string[] args) 10 | { 11 | try 12 | { 13 | Console.WriteLine("Starting Sample .NetCore Console App process..."); 14 | 15 | var sqlConnectionString = Environment.GetEnvironmentVariable(SqlBulkHelpersConnectionProvider.SqlConnectionStringConfigKey); 16 | 17 | await SqlBulkHelpersSampleAsync.RunSampleAsync(sqlConnectionString).ConfigureAwait(false); 18 | 19 | Console.WriteLine("Process Finished Successfully (e.g. without Error)!"); 20 | Console.ReadKey(); 21 | } 22 | catch (Exception exc) 23 | { 24 | Console.WriteLine(exc.Message); 25 | Console.WriteLine(exc.StackTrace); 26 | Console.ReadKey(); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/TestHelpers/SqlConnectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using Microsoft.Data.SqlClient; 4 | 5 | namespace SqlBulkHelpers.Tests 6 | { 7 | public static class SqlConnectionHelper 8 | { 9 | public static string GetSqlConnectionString() 10 | { 11 | var sqlConnectionString = TestConfiguration.SqlConnectionString; 12 | return sqlConnectionString; 13 | } 14 | 15 | public static ISqlBulkHelpersConnectionProvider GetConnectionProvider() 16 | { 17 | return new SqlBulkHelpersConnectionProvider(GetSqlConnectionString()); 18 | } 19 | 20 | public static SqlConnection NewConnection() 21 | { 22 | var sqlConn = new SqlConnection(GetSqlConnectionString()); 23 | sqlConn.Open(); 24 | return sqlConn; 25 | } 26 | 27 | public static Task NewConnectionAsync() 28 | { 29 | var sqlConnectionProvider = GetConnectionProvider(); 30 | return sqlConnectionProvider.NewConnectionAsync(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 - Brandon Bernard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/MaterializedData/MaterializationTableInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlBulkHelpers.CustomExtensions; 3 | 4 | namespace SqlBulkHelpers.MaterializedData 5 | { 6 | public class MaterializationTableInfo 7 | { 8 | internal string OriginalTableName { get; } 9 | 10 | public TableNameTerm LiveTable { get; } 11 | 12 | public SqlBulkHelpersTableDefinition LiveTableDefinition { get; } 13 | 14 | public TableNameTerm LoadingTable { get; } 15 | 16 | public TableNameTerm DiscardingTable { get; } 17 | 18 | public MaterializationTableInfo(string originalTableName, SqlBulkHelpersTableDefinition originalTableDef, TableNameTerm loadingTableTerm, TableNameTerm discardingTableNameTerm) 19 | { 20 | OriginalTableName = originalTableName; 21 | LiveTableDefinition = originalTableDef; 22 | LiveTable = originalTableDef.TableNameTerm; 23 | LoadingTable = loadingTableTerm.AssertArgumentIsNotNull(nameof(loadingTableTerm)); 24 | DiscardingTable = discardingTableNameTerm.AssertArgumentIsNotNull(nameof(discardingTableNameTerm)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/Interfaces/ISqlBulkHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Threading.Tasks; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | public interface ISqlBulkHelper where T: class 9 | { 10 | #region Dependency / Helper Methods 11 | SqlBulkHelpersTableDefinition GetTableSchemaDefinition(String tableName); 12 | #endregion 13 | 14 | #region Async Operation Methods 15 | Task> BulkInsertAsync(IEnumerable entityList, String tableName, SqlTransaction transaction); 16 | Task> BulkUpdateAsync(IEnumerable entityList, String tableName, SqlTransaction transaction); 17 | Task> BulkInsertOrUpdateAsync(IEnumerable entityList, String tableName, SqlTransaction transaction); 18 | #endregion 19 | 20 | #region Synchronous Operation Methods 21 | IEnumerable BulkInsert(IEnumerable entityList, String tableName, SqlTransaction transaction); 22 | IEnumerable BulkUpdate(IEnumerable entityList, String tableName, SqlTransaction transaction); 23 | IEnumerable BulkInsertOrUpdate(IEnumerable entityList, String tableName, SqlTransaction transaction); 24 | #endregion 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/SqlQueries/Check for Non-Trusted Constraints.sql: -------------------------------------------------------------------------------- 1 | WITH FKeyCte AS ( 2 | SELECT 3 | FKeyConstraintName = QUOTENAME(fk.name), 4 | ObjectId = fk.[object_id], 5 | SourceTable = CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(fk.parent_object_id)), '.', QUOTENAME(OBJECT_NAME(fk.parent_object_id))), 6 | --'[' + object_schema_name(fk.parent_object_id) + '].[' + object_name(fk.parent_object_id) + ']', 7 | ReferenceTable = CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(fk.referenced_object_id)), '.', QUOTENAME(OBJECT_NAME(fk.referenced_object_id))), 8 | IsDisabled = is_disabled, 9 | IsNotTrusted = is_not_trusted 10 | FROM sys.foreign_keys fk 11 | ) 12 | SELECT 13 | fk.FKeyConstraintName, 14 | fk.SourceTable, 15 | fk.ReferenceTable, 16 | ReferenceColumns = STRING_AGG(c.Name, ', '), 17 | fk.IsDisabled, 18 | fk.IsNotTrusted, 19 | DisableScript = CONCAT('ALTER TABLE ', fk.SourceTable, ' NOCHECK CONSTRAINT ', fk.FKeyConstraintName, ';'), 20 | EnableScript = CONCAT('ALTER TABLE ', fk.SourceTable, ' WITH CHECK CHECK CONSTRAINT ', fk.FKeyConstraintName, ';') 21 | FROM FKeyCte fk 22 | JOIN sys.foreign_key_columns fkc ON (fkc.constraint_object_id = fk.ObjectId) 23 | JOIN sys.columns c ON (c.[object_id] = fkc.parent_object_id AND c.column_id = fkc.parent_column_id) 24 | GROUP BY fk.FKeyConstraintName, fk.SourceTable, fk.ReferenceTable, fk.IsDisabled, fk.IsNotTrusted 25 | -------------------------------------------------------------------------------- /SqlBulkHelpers.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSV 3 | DB 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Attributes/SqlBulkTableAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlBulkHelpers.CustomExtensions; 3 | 4 | namespace SqlBulkHelpers 5 | { 6 | [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] 7 | public class SqlBulkTableAttribute : Attribute 8 | { 9 | public string SchemaName { get; protected set; } 10 | public string TableName { get; protected set; } 11 | public string FullyQualifiedTableName { get; protected set; } 12 | public bool UniqueMatchMergeValidationEnabled { get; protected set; } 13 | 14 | public SqlBulkTableAttribute(string schemaName, string tableName, bool uniqueMatchMergeValidationEnabled = true) 15 | : this($"[{schemaName.TrimTableNameTerm()}].[{tableName.TrimTableNameTerm()}]") 16 | { 17 | this.UniqueMatchMergeValidationEnabled = uniqueMatchMergeValidationEnabled; 18 | } 19 | 20 | public SqlBulkTableAttribute(string tableName, bool uniqueMatchMergeValidationEnabled = true) 21 | { 22 | var tableNameTerm = tableName.ParseAsTableNameTerm(); 23 | this.SchemaName = tableNameTerm.SchemaName; 24 | this.TableName = tableNameTerm.TableName; 25 | this.FullyQualifiedTableName = tableNameTerm.FullyQualifiedTableName; 26 | this.UniqueMatchMergeValidationEnabled = uniqueMatchMergeValidationEnabled; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System.Threading.Tasks; 3 | 4 | namespace SqlBulkHelpers 5 | { 6 | /// 7 | /// BBernard 8 | /// Interface to support custom implementations for initializing Sql Database connections as needed. 9 | /// 10 | public interface ISqlBulkHelpersConnectionProvider 11 | { 12 | /// 13 | /// Create a brand new open database connection to use for retrieving Schema details 14 | /// NOTE: It is expected that this connection is NOT already enrolled in a Transaction! 15 | /// NOTE: If a connection must be reused with a transaction then the SqlBulkHelpersConnectionProxyExistingProvider should be used. 16 | /// 17 | /// 18 | SqlConnection NewConnection(); 19 | 20 | /// 21 | /// Create a brand new open database connection to use for retrieving Schema details 22 | /// NOTE: It is expected that this connection is NOT already enrolled in a Transaction! 23 | /// NOTE: If a connection must be reused with a transaction then the SqlBulkHelpersConnectionProxyExistingProvider should be used. 24 | /// 25 | /// 26 | Task NewConnectionAsync(); 27 | 28 | string GetDbConnectionUniqueIdentifier(); 29 | } 30 | } -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Threading.Tasks; 4 | using SqlBulkHelpers; 5 | 6 | namespace SqlBulkHelpersSample.ConsoleApp 7 | { 8 | class Program 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | try 13 | { 14 | Console.WriteLine("Starting Sample .Net Console App process..."); 15 | 16 | var sqlConnectionString = ConfigurationManager.AppSettings[SqlBulkHelpersConnectionProvider.SqlConnectionStringConfigKey]; 17 | 18 | await SqlBulkHelpersSampleAsync.RunSampleAsync(sqlConnectionString).ConfigureAwait(false); 19 | 20 | Console.WriteLine("Process Finished Successfully (e.g. without Error)!"); 21 | Console.ReadKey(); 22 | } 23 | catch (Exception exc) 24 | { 25 | Console.WriteLine(exc.Message); 26 | Console.WriteLine(exc.StackTrace); 27 | Console.ReadKey(); 28 | } 29 | } 30 | 31 | //static void Main(string[] args) 32 | //{ 33 | // try 34 | // { 35 | // SqlBulkHelpersSampleSynchronous.Run(); 36 | // } 37 | // catch (Exception exc) 38 | // { 39 | // Console.WriteLine(exc.Message); 40 | // Console.WriteLine(exc.StackTrace); 41 | // Console.ReadKey(); 42 | // } 43 | //} 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersDBSchemaLoader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | public interface ISqlBulkHelpersDBSchemaLoader 8 | { 9 | Task GetTableSchemaDefinitionAsync( 10 | string tableName, 11 | TableSchemaDetailLevel detailLevel, 12 | SqlConnection sqlConnection, 13 | SqlTransaction sqlTransaction = null, 14 | bool forceCacheReload = false 15 | ); 16 | 17 | Task GetTableSchemaDefinitionAsync( 18 | string tableName, 19 | TableSchemaDetailLevel detailLevel, 20 | Func> sqlConnectionAsyncFactory, 21 | bool forceCacheReload = false 22 | ); 23 | 24 | SqlBulkHelpersTableDefinition GetTableSchemaDefinition( 25 | string tableName, 26 | TableSchemaDetailLevel detailLevel, 27 | SqlConnection sqlConnection, 28 | SqlTransaction sqlTransaction = null, 29 | bool forceCacheReload = false 30 | ); 31 | 32 | SqlBulkHelpersTableDefinition GetTableSchemaDefinition( 33 | string tableName, 34 | TableSchemaDetailLevel detailLevel, 35 | Func sqlConnectionFactory, 36 | bool forceCacheReload = false 37 | ); 38 | 39 | ValueTask ClearCacheAsync(); 40 | 41 | void ClearCache(); 42 | } 43 | } -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Debug.ConsoleApp")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Debug.ConsoleApp")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1317662e-bdb5-4e83-be8f-56dcb955141a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SqlBulkHelpers.ClassLibrary")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SqlBulkHelpers.ClassLibrary")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("3abf8f4c-6371-419e-b104-2db62b2f643d")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Nuget Publish for Main Branch 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | 26 | # Runs a single command using the runners shell 27 | - name: Run a one-line script 28 | run: echo Executing Main Branch commit Workflow! 29 | 30 | # Runs a set of commands using the runners shell 31 | #- name: Run a multi-line script 32 | # run: | 33 | # echo Add other actions to build, 34 | # echo test, and deploy your project. 35 | - name: "Publish NuGet: SqlBulkHelpers" 36 | uses: alirezanet/publish-nuget@v3.0.4 37 | with: 38 | # Filepath of the project to be packaged, relative to root of repository 39 | PROJECT_FILE_PATH: NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj 40 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 41 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/SqlBulkHelpers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | enable 5 | enable 6 | false 7 | {D5CFE2BA-4FB1-4ACA-BD14-FD6FB75EFCC8} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | {0cabb737-46c2-460f-98fe-55d4871ed841} 27 | NetStandard.SqlBulkHelpers 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/SqlScripts/InitTestDBTable.sql: -------------------------------------------------------------------------------- 1 | --DROP TABLE [dbo].[SqlBulkHelpersTestElements]; 2 | CREATE TABLE [dbo].[SqlBulkHelpersTestElements]( 3 | [Id] [int] IDENTITY(1,1) NOT NULL, 4 | [Key] [nvarchar](max) NULL, 5 | [Value] [nvarchar](max) NULL, 6 | CONSTRAINT [PK_SqlBulkHelpersTestElements] PRIMARY KEY CLUSTERED ([Id] ASC) 7 | WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] 8 | ) 9 | ON [PRIMARY] 10 | TEXTIMAGE_ON [PRIMARY]; 11 | GO 12 | 13 | --DROP TABLE [dbo].[SqlBulkHelpersTestElements_Child_NoIdentity]; 14 | CREATE TABLE [dbo].[SqlBulkHelpersTestElements_Child_NoIdentity]( 15 | [ChildKey] [nvarchar](250) NOT NULL, 16 | [ParentId] [int] NOT NULL, 17 | [ChildValue] [nvarchar](max) NULL, 18 | CONSTRAINT [PK_SqlBulkHelpersTestElements_Child] PRIMARY KEY CLUSTERED ([ChildKey] ASC, [ParentId] ASC) 19 | WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] 20 | ) 21 | ON [PRIMARY] 22 | TEXTIMAGE_ON [PRIMARY]; 23 | 24 | ALTER TABLE [dbo].[SqlBulkHelpersTestElements_Child_NoIdentity] ADD FOREIGN KEY (ParentId) REFERENCES [dbo].[SqlBulkHelpersTestElements](Id); 25 | GO 26 | 27 | --DROP TABLE [dbo].[SqlBulkHelpersTestElements_WithFullTextIndex]; 28 | CREATE TABLE [dbo].[SqlBulkHelpersTestElements_WithFullTextIndex]( 29 | [Id] [int] IDENTITY(1,1) NOT NULL, 30 | [Key] [nvarchar](max) NULL, 31 | [Value] [nvarchar](max) NULL, 32 | CONSTRAINT [PK_SqlBulkHelpersTestElements_WithFullTextIndex] PRIMARY KEY CLUSTERED ([Id] ASC) 33 | WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] 34 | ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 35 | GO 36 | 37 | CREATE FULLTEXT CATALOG SearchCatalog AS DEFAULT; 38 | CREATE FULLTEXT INDEX ON [dbo].[SqlBulkHelpersTestElements_WithFullTextIndex]([Key],[Value]) 39 | KEY INDEX [PK_SqlBulkHelpersTestElements_WithFullTextIndex] ON [SearchCatalog] 40 | WITH STOPLIST = SYSTEM; 41 | GO 42 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/MaterializedData/IMaterializeDataContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers.MaterializedData 4 | { 5 | public interface IMaterializeDataContext 6 | { 7 | MaterializationTableInfo[] Tables { get; } 8 | 9 | /// 10 | /// Allows disabling of data validation during materialization, but may put data integrity at risk. 11 | /// This will improve performance for large data loads, but if disabled then the implementor is responsible 12 | /// for ensuring all data integrity of the data populated into the tables! 13 | /// NOTE: The downside of this is that larger tables will take longer to Switch over but Data Integrity is maintained therefore this 14 | /// is the default and normal behavior that should be used. 15 | /// NOTE: In addition, Disabling this poses other implications in SQL Server as the Constraints then become Untrusted which affects 16 | /// the Query Optimizer and may may adversely impact Query performance. 17 | /// 18 | bool EnableDataConstraintChecksOnCompletion { get; set; } 19 | 20 | MaterializationTableInfo this[int index] { get; } 21 | MaterializationTableInfo this[string tableName] { get; } 22 | MaterializationTableInfo this[Type modelType] { get; } 23 | MaterializationTableInfo FindMaterializationTableInfoCaseInsensitive(string tableName); 24 | MaterializationTableInfo FindMaterializationTableInfoCaseInsensitive(); 25 | MaterializationTableInfo FindMaterializationTableInfoCaseInsensitive(Type modelType); 26 | TableNameTerm GetLoadingTableName(string tableName); 27 | TableNameTerm GetLoadingTableName(); 28 | TableNameTerm GetLoadingTableName(Type modelType); 29 | bool IsCancelled { get; } 30 | IMaterializeDataContext CancelMaterializationProcess(); 31 | bool IsMaterializedLoadingTableCleanupEnabled { get; } 32 | IMaterializeDataContext DisableMaterializedLoadingTableCleanup(); 33 | } 34 | } -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/SystemIOCustomExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace SqlBulkHelpers.CustomExtensions 6 | { 7 | internal static class SystemIOCustomExtensions 8 | { 9 | public static byte[] ToByteArray(this Stream stream) 10 | { 11 | byte[] bytes = null; 12 | if (stream is MemoryStream existingMemoryStream) 13 | { 14 | //Memory stream is easy to work with and natively supports converting to ByteArray. 15 | bytes = existingMemoryStream.ToArray(); 16 | } 17 | else 18 | { 19 | //For all other stream types we need to validate that we can Read Them! 20 | if (!stream.CanRead) throw new ArgumentException("Stream specified does not support Read operations."); 21 | 22 | using (var memoryStream = new MemoryStream()) 23 | { 24 | stream.CopyTo(memoryStream); 25 | bytes = memoryStream.ToArray(); 26 | } 27 | } 28 | 29 | return bytes; 30 | } 31 | 32 | public static async Task ToByteArrayAsync(this Stream stream) 33 | { 34 | byte[] bytes; 35 | if (stream is MemoryStream existingMemoryStream) 36 | { 37 | //Memory stream is easy to work with and natively supports converting to ByteArray. 38 | bytes = existingMemoryStream.ToArray(); 39 | } 40 | else 41 | { 42 | //For all other stream types we need to validate that we can Read Them! 43 | if (!stream.CanRead) throw new ArgumentException("Stream specified does not support Read operations."); 44 | 45 | using (var memoryStream = new MemoryStream()) 46 | { 47 | await stream.CopyToAsync(memoryStream); 48 | bytes = memoryStream.ToArray(); 49 | } 50 | } 51 | 52 | return bytes; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/Database/SqlBulkHelpersConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Data.SqlClient; 4 | using System.Threading.Tasks; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | /// 9 | /// BBernard 10 | /// Connection string provider class to keep the responsibility for Loading the connection string in on only one class; 11 | /// but also supports custom implementations that would initialize the Connection string in any other custom way. 12 | /// 13 | /// The Default implementation will load the Connection String from teh AppSettings key "SqlConnectionString" 14 | /// 15 | public class SqlBulkHelpersConnectionProvider : ISqlBulkHelpersConnectionProvider 16 | { 17 | /// 18 | /// Provides a Default instance of the Sql Bulk Helpers Connection Provider. 19 | /// 20 | public static ISqlBulkHelpersConnectionProvider Default = new SqlBulkHelpersConnectionProvider(); 21 | 22 | //For performance we load this via a Lazy to ensure we only ever access AppSettings one time. 23 | private static readonly Lazy _connectionStringLoaderLazy = new Lazy( 24 | () => ConfigurationManager.AppSettings["SqlConnectionString"] 25 | ); 26 | 27 | protected virtual String GetConnectionString() 28 | { 29 | //LOAD from internal Constant, other configuration, etc. 30 | return _connectionStringLoaderLazy.Value; 31 | } 32 | 33 | public virtual SqlConnection NewConnection() 34 | { 35 | var connectionString = GetConnectionString(); 36 | var sqlConn = new SqlConnection(connectionString); 37 | sqlConn.Open(); 38 | 39 | return sqlConn; 40 | } 41 | 42 | public virtual async Task NewConnectionAsync() 43 | { 44 | var connectionString = GetConnectionString(); 45 | var sqlConn = new SqlConnection(connectionString); 46 | await sqlConn.OpenAsync(); 47 | return sqlConn; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using SqlBulkHelpers.CustomExtensions; 6 | 7 | namespace SqlBulkHelpers.SqlBulkHelpers.CustomExtensions 8 | { 9 | internal static class ReflectionExtensions 10 | { 11 | public static IEnumerable FindAttributes(this Type type, params string[] attributeNames) 12 | { 13 | if (type == null) 14 | return null; 15 | 16 | var attributes = type.GetCustomAttributes(true).OfType(); 17 | return FindAttributes(attributes, attributeNames); 18 | } 19 | 20 | public static IEnumerable FindAttributes(this PropertyInfo propInfo, params string[] attributeNames) 21 | { 22 | if (propInfo == null) 23 | return null; 24 | 25 | var attributes = propInfo.GetCustomAttributes(true).OfType(); 26 | return FindAttributes(attributes, attributeNames); 27 | } 28 | 29 | public static IEnumerable FindAttributes(this IEnumerable attributes, params string[] attributeNamesToFind) 30 | { 31 | 32 | if (attributeNamesToFind.IsNullOrEmpty()) 33 | throw new ArgumentNullException(nameof(attributeNamesToFind)); 34 | 35 | var results = new List(); 36 | var attributesArray = attributes as Attribute[] ?? attributes.AsArray(); 37 | 38 | foreach (var findName in attributeNamesToFind) 39 | { 40 | var findAttrName = findName.EndsWith(nameof(Attribute), StringComparison.OrdinalIgnoreCase) 41 | ? findName 42 | : string.Concat(findName, nameof(Attribute)); 43 | 44 | var foundAttr = attributesArray.FirstOrDefault(attr => attr.GetType().Name.Equals(findAttrName, StringComparison.OrdinalIgnoreCase)); 45 | if(foundAttr != null) 46 | results.Add(foundAttr); 47 | } 48 | 49 | return results; 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersConnectionProxyExistingProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Data.SqlClient; 3 | using System.Threading.Tasks; 4 | using SqlBulkHelpers.CustomExtensions; 5 | using SqlBulkHelpers.SqlBulkHelpers.Interfaces; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | /// 10 | /// BBernard 11 | /// Connection string provider class to allow re-use of existing connection that may already be initialized to be used 12 | /// for implementations that may already create connections and just need us to proxy them for use within SqlBulkHelpers. 13 | /// NOTE: This is INTERNAL for special use so we minimize impacts to our Interfaces, it is not recommended for any external 14 | /// consumers to use this. 15 | /// 16 | internal class SqlBulkHelpersConnectionProxyExistingProvider : ISqlBulkHelpersConnectionProvider, ISqlBulkHelpersHasTransaction 17 | { 18 | private readonly SqlConnection _sqlConnection; 19 | private readonly SqlTransaction _sqlTransaction; 20 | 21 | public SqlBulkHelpersConnectionProxyExistingProvider(SqlConnection sqlConnection, SqlTransaction sqlTransaction = null) 22 | { 23 | _sqlConnection = sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); 24 | _sqlTransaction = sqlTransaction; 25 | } 26 | 27 | /// 28 | /// Provide Internal access to the Connection String to help uniquely identify the Connections from this Provider. 29 | /// 30 | /// Unique string representing connections provided by this provider 31 | public virtual string GetDbConnectionUniqueIdentifier() => _sqlConnection.ConnectionString; 32 | 33 | public virtual SqlConnection NewConnection() => _sqlConnection; 34 | 35 | public virtual Task NewConnectionAsync() => Task.FromResult(_sqlConnection); 36 | 37 | /// 38 | /// Method that provides direct access to the Sql Transaction when this is used to proxy an Existing Connection 39 | /// that also has an associated open transaction. 40 | /// 41 | /// 42 | public virtual SqlTransaction GetTransaction() => _sqlTransaction; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Utilities/IdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | /// 8 | /// BBernard / CajunCoding 9 | /// A simple, relatively efficient, class for generating very unique Ids of arbitrary length. 10 | /// They are not guaranteed to universally unique but the risk of collisions is of no practical implication for many uses (e.g. temporary names, copied names, etc.). 11 | /// With the ability to control the length (longer lengths will be more unique) it becomes suitable for many use cases where a full GUID is simply too long. 12 | /// NOTE: Inspired by the Stack Overflow Answer here: https://stackoverflow.com/a/44960751/7293142 13 | /// The Original author claims 0.001% duplicates in 100 million. 14 | /// 15 | public static class IdGenerator 16 | { 17 | private static readonly string[] allCharsArray = Enumerable 18 | //Start with the Full Uppercase Alphabet (26 chars) starting at 'A' 19 | .Range(65, 26).Select(e => ((char)e).ToString()) 20 | //Append Full Lowercase Alphabet (26 chars) starting at 'a' 21 | .Concat(Enumerable.Range(97, 26).Select(e => ((char)e).ToString())) 22 | //Append Integer values 0-9 23 | .Concat(Enumerable.Range(0, 10).Select(e => e.ToString())) 24 | .ToArray(); 25 | 26 | 27 | /// 28 | /// Generates a unique short ID that is much smaller than a GUID while being very unique -- 29 | /// but still not 100% unique because neither is a GUID. 30 | /// 31 | /// 32 | public static string NewId(int length = 10) 33 | { 34 | var stringBuilder = new StringBuilder(); 35 | allCharsArray 36 | //Randomize by Sorting on Completely Unique GUID values (that change with every request); 37 | // effectively deriving the uniqueness from GUIDs! 38 | .OrderBy(e => Guid.NewGuid()) 39 | .Take(length) 40 | .ToList() 41 | .ForEach(e => stringBuilder.Append(e)); 42 | 43 | var linqId = stringBuilder.ToString(); 44 | return linqId; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/QueryProcessing/SqlBulkHelpersObjectReflectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Concurrent; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | public class SqlBulkHelpersObjectReflectionFactory 10 | { 11 | private static ConcurrentDictionary>> _propInfoLazyCache = new ConcurrentDictionary>>(); 12 | 13 | public static List GetPropertyDefinitions(SqlBulkHelpersColumnDefinition identityColumnDefinition = null) 14 | { 15 | var type = typeof(T); 16 | var propInfoLazy = _propInfoLazyCache.GetOrAdd( 17 | $"[Type={type.Name}][Identity={identityColumnDefinition?.ColumnName ?? "N/A"}]", //Cache Key 18 | new Lazy>(() => //Lazy (Thread Safe Lazy Loader) 19 | { 20 | var propertyInfos = type.GetProperties().Select((pi) => new PropInfoDefinition(pi, identityColumnDefinition)).ToList(); 21 | return propertyInfos; 22 | }) 23 | ); 24 | 25 | return propInfoLazy.Value; 26 | } 27 | } 28 | 29 | public class PropInfoDefinition 30 | { 31 | public PropInfoDefinition(PropertyInfo propInfo, SqlBulkHelpersColumnDefinition identityColumnDef = null) 32 | { 33 | this.PropInfo = propInfo; 34 | this.Name = propInfo.Name; 35 | this.PropertyType = propInfo.PropertyType; 36 | //Early determination if a Property is an Identity Property for Fast processing later. 37 | this.IsIdentityProperty = identityColumnDef?.ColumnName?.Equals(propInfo.Name, StringComparison.OrdinalIgnoreCase) ?? false; 38 | } 39 | 40 | public String Name { get; private set; } 41 | public bool IsIdentityProperty { get; private set; } 42 | public PropertyInfo PropInfo { get; private set; } 43 | public Type PropertyType { get; private set; } 44 | 45 | public override string ToString() 46 | { 47 | return $"{this.Name} [{this.PropertyType.Name}]"; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/SqlBulkCopyFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Data.SqlClient; 3 | using System.Linq; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | public class SqlBulkCopyFactory 8 | { 9 | public virtual int BulkCopyBatchSize { get; set; } = 2000; //General guidance is that 2000-5000 is efficient enough. 10 | public virtual int BulkCopyTimeoutSeconds { get; set; } = 60; //Default is only 30 seconds, but we can wait a bit longer if needed. 11 | 12 | public virtual SqlBulkCopyOptions BulkCopyOptions { get; set; } = SqlBulkCopyOptions.Default; 13 | 14 | public virtual SqlBulkCopy CreateSqlBulkCopy(DataTable dataTable, SqlBulkHelpersTableDefinition tableDefinition, SqlTransaction transaction) 15 | { 16 | var sqlBulk = new SqlBulkCopy(transaction.Connection, this.BulkCopyOptions, transaction); 17 | //Always initialize a Batch size & Timeout 18 | sqlBulk.BatchSize = this.BulkCopyBatchSize; 19 | sqlBulk.BulkCopyTimeout = this.BulkCopyTimeoutSeconds; 20 | 21 | //First initialize the Column Mappings for the SqlBulkCopy 22 | //NOTE: BBernard - We only map valid columns that exist in both the Model & the Table Schema! 23 | //NOTE: BBernard - We Map All valid columns (including Identity Key column) to support Insert or Updates! 24 | var dataTableColumnNames = dataTable.Columns.Cast().Select(c => c.ColumnName); 25 | foreach (var dataTableColumnName in dataTableColumnNames) 26 | { 27 | var dbColumnDef = tableDefinition.FindColumnCaseInsensitive(dataTableColumnName); 28 | if (dbColumnDef != null) 29 | { 30 | sqlBulk.ColumnMappings.Add(dataTableColumnName, dbColumnDef.ColumnName); 31 | } 32 | } 33 | 34 | //BBernard 35 | //Now that we konw we have only valid columns from the Model/DataTable, we must manually add a mapping 36 | // for the Row Number Column for Bulk Loading . . . but Only if the data table has a RowNumber column defined. 37 | if (dataTable.Columns.Contains(SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME)) 38 | { 39 | sqlBulk.ColumnMappings.Add(SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME, SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME); 40 | } 41 | 42 | return sqlBulk; 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/QueryProcessing/SqlBulkHelpersMergeMatchQualifiers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SqlBulkHelpers.CustomExtensions; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | public class SqlMatchQualifierField 9 | { 10 | public SqlMatchQualifierField(string fieldName) 11 | { 12 | this.Name = fieldName; 13 | this.SanitizedName = fieldName.TrimTableNameTerm(); 14 | } 15 | 16 | public string Name { get; } 17 | public string SanitizedName { get; } 18 | 19 | public override string ToString() 20 | { 21 | return this.Name.QualifySqlTerm(); 22 | } 23 | } 24 | 25 | public class SqlMergeMatchQualifierExpression 26 | { 27 | public SqlMergeMatchQualifierExpression() 28 | { 29 | } 30 | 31 | public SqlMergeMatchQualifierExpression(params string[] fieldNames) 32 | : this(fieldNames.ToList()) 33 | { 34 | } 35 | 36 | public SqlMergeMatchQualifierExpression(params SqlMatchQualifierField[] matchQualifierFields) 37 | : this(matchQualifierFields.ToList()) 38 | { 39 | } 40 | 41 | public SqlMergeMatchQualifierExpression(IEnumerable fieldNames) 42 | : this(fieldNames?.Select(n => new SqlMatchQualifierField(n)).ToList()) 43 | { 44 | } 45 | 46 | public SqlMergeMatchQualifierExpression(IEnumerable fields) 47 | { 48 | var fieldsList = fields?.ToList(); 49 | if (fieldsList.IsNullOrEmpty()) 50 | throw new ArgumentException(nameof(fieldsList)); 51 | 52 | MatchQualifierFields = fieldsList; 53 | } 54 | 55 | public List MatchQualifierFields { get; protected set; } 56 | 57 | /// 58 | /// BBernard - 12/08/2020 59 | /// When non-identity field match qualifiers are specified it's possible that multiple 60 | /// records may match if the fields are non-unique. This will result in potentially erroneous 61 | /// postprocessing for results, therefore we will throw an Exception when this is detected by Default! 62 | /// 63 | public bool ThrowExceptionIfNonUniqueMatchesOccur { get; set; } = true; 64 | 65 | //public QualifierLogicalOperator LogicalOperator { get; } = QualifierLogicalOperator.And; 66 | 67 | public override string ToString() 68 | { 69 | return MatchQualifierFields.Select(f => f.ToString()).ToCsv(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersSchemaLoaderCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LazyCacheHelpers; 3 | using SqlBulkHelpers.CustomExtensions; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | /// 8 | /// Convenience Class to simplify internal caching of SqlBulkHelpersDbSchemaLoader instances. 9 | /// 10 | public static class SqlBulkHelpersSchemaLoaderCache 11 | { 12 | private static readonly LazyStaticInMemoryCache SchemaLoaderLazyCache = 13 | new LazyStaticInMemoryCache(); 14 | 15 | /// 16 | /// Load the DB Schema Provider based on the unique caching identifier specified (e.g. usually a unique string for each Datasource/Database). 17 | /// 18 | /// 19 | /// 20 | public static ISqlBulkHelpersDBSchemaLoader GetSchemaLoader(string uniqueDbCachingIdentifier) 21 | { 22 | //Init cached version if it exists; which may already be initialized! 23 | var resultLoader = SchemaLoaderLazyCache.GetOrAdd( 24 | key: uniqueDbCachingIdentifier, 25 | cacheValueFactory: (uniqueId) => new SqlBulkHelpersDBSchemaLoader() 26 | ); 27 | 28 | return resultLoader; 29 | } 30 | 31 | /// 32 | /// This is the preferred way to initialize the Schema Loader this will Load the DB Schema Provider based unique connection identifier provided 33 | /// by the ISqlBulkHelpersConnectionProvider. 34 | /// 35 | /// 36 | /// 37 | public static ISqlBulkHelpersDBSchemaLoader GetSchemaLoader(ISqlBulkHelpersConnectionProvider sqlConnectionProvider) 38 | { 39 | //Validate arg is a Static Schema Loader... 40 | sqlConnectionProvider.AssertArgumentIsNotNull(nameof(sqlConnectionProvider)); 41 | return GetSchemaLoader(sqlConnectionProvider.GetDbConnectionUniqueIdentifier()); 42 | } 43 | 44 | /// 45 | /// Clear the DB Schema Loader cache and enable lazy re-initialization on-demand at next request for a given DB Schema Loader. 46 | /// 47 | public static void ClearCache() 48 | => SchemaLoaderLazyCache.ClearCache(); 49 | 50 | /// 51 | /// Gets the Count of SchemaLoaders in the Cache. 52 | /// 53 | public static int Count => SchemaLoaderLazyCache.GetCacheCount(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/CustomExtensions/SystemDataSqlClientCustomExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.SqlClient; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SqlBulkHelpers 9 | { 10 | public static class SystemDataSqlClientCustomExtensions 11 | { 12 | public static IEnumerable Enumerate(this T reader) where T : IDataReader 13 | { 14 | while (reader.Read()) yield return reader; 15 | } 16 | 17 | public static T ExecuteForJson(this SqlCommand sqlCmd) where T : class 18 | { 19 | //Quickly Read the FIRST record fully from Sql Server Reader response. 20 | using (var sqlReader = sqlCmd.ExecuteReader()) 21 | { 22 | //Short circuit if no data is returned. 23 | if (sqlReader.HasRows) 24 | { 25 | var jsonStringBuilder = new StringBuilder(); 26 | while (sqlReader.Read()) 27 | { 28 | jsonStringBuilder.Append(sqlReader.GetString(0)); 29 | } 30 | 31 | var json = jsonStringBuilder.ToString(); 32 | var result = JsonConvert.DeserializeObject(json); 33 | return result; 34 | } 35 | } 36 | 37 | return null; 38 | } 39 | 40 | 41 | public static async Task ExecuteForJsonAsync(this SqlCommand sqlCmd) where T : class 42 | { 43 | //Quickly Read the FIRST record fully from Sql Server Reader response. 44 | using (var sqlReader = await sqlCmd.ExecuteReaderAsync()) 45 | { 46 | //Short circuit if no data is returned. 47 | if (sqlReader.HasRows) 48 | { 49 | var jsonStringBuilder = new StringBuilder(); 50 | while (await sqlReader.ReadAsync()) 51 | { 52 | //So far all calls to SqlDataReader have been asynchronous, but since the data reader is in 53 | //non -sequential mode and ReadAsync was used, the column data should be read synchronously. 54 | jsonStringBuilder.Append(sqlReader.GetString(0)); 55 | } 56 | 57 | var json = jsonStringBuilder.ToString(); 58 | var result = JsonConvert.DeserializeObject(json); 59 | return result; 60 | } 61 | } 62 | 63 | return null; 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/SqlBulkCopyFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Data.SqlClient; 4 | using SqlBulkHelpers.CustomExtensions; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | public class SqlBulkCopyFactory 9 | { 10 | protected ISqlBulkHelpersConfig SqlBulkConfig { get; set; } 11 | 12 | public SqlBulkCopyFactory(ISqlBulkHelpersConfig bulkHelpersConfig = null) 13 | { 14 | SqlBulkConfig = bulkHelpersConfig ?? SqlBulkHelpersConfig.DefaultConfig; 15 | } 16 | 17 | public virtual SqlBulkCopy CreateSqlBulkCopy( 18 | List entities, 19 | SqlBulkHelpersProcessingDefinition processingDefinition, 20 | SqlBulkHelpersTableDefinition tableDefinition, 21 | SqlTransaction transaction 22 | ) 23 | { 24 | entities.AssertArgumentIsNotNull(nameof(entities)); 25 | processingDefinition.AssertArgumentIsNotNull(nameof(processingDefinition)); 26 | tableDefinition.AssertArgumentIsNotNull(nameof(tableDefinition)); 27 | 28 | var sqlBulk = new SqlBulkCopy(transaction.Connection, SqlBulkConfig.SqlBulkCopyOptions, transaction) 29 | { 30 | //Always initialize a Batch size & Timeout 31 | BatchSize = SqlBulkConfig.SqlBulkBatchSize, 32 | BulkCopyTimeout = SqlBulkConfig.SqlBulkPerBatchTimeoutSeconds, 33 | }; 34 | 35 | //First initialize the Column Mappings for the SqlBulkCopy 36 | //NOTE: BBernard - We only map valid columns that exist in both the Model & the Table Schema! 37 | //NOTE: BBernard - We Map All valid columns (including Identity Key column) to support Insert or Updates! 38 | foreach (var fieldDefinition in processingDefinition.PropertyDefinitions) 39 | { 40 | var dbColumnDef = tableDefinition.FindColumnCaseInsensitive(fieldDefinition.MappedDbColumnName); 41 | if (dbColumnDef != null) 42 | sqlBulk.ColumnMappings.Add(fieldDefinition.MappedDbColumnName, dbColumnDef.ColumnName); 43 | } 44 | 45 | //BBernard 46 | //Now that we know we have only valid columns from the ProcessingDefinition, we must manually add a mapping 47 | // for the Row Number Column for Bulk Loading, but Only if appropriate... 48 | if (processingDefinition.IsRowNumberColumnNameEnabled) 49 | { 50 | sqlBulk.ColumnMappings.Add(SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME, SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME); 51 | } 52 | 53 | return sqlBulk; 54 | } 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/SystemCollectionCustomExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SqlBulkHelpers.CustomExtensions 9 | { 10 | internal static class SystemCollectionCustomExtensions 11 | { 12 | /// 13 | /// ForEach processing loop that supports full asynchronous processing of all values via the supplied async function! 14 | /// This will control the async threshold according the maximum number of concurrent tasks. 15 | /// Based on the original post by Jason Toub:https://devblogs.microsoft.com/pfxteam/implementing-a-simple-foreachasync-part-2/ 16 | /// More info also here: https://stackoverflow.com/questions/11564506/nesting-await-in-parallel-foreach 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static async Task ForEachAsync(this IEnumerable source, int maxDegreesOfConcurrency, Func asyncFunc) 24 | { 25 | if (source == null) throw new ArgumentNullException(nameof(source)); 26 | if (asyncFunc == null) throw new ArgumentNullException(nameof(asyncFunc)); 27 | if (maxDegreesOfConcurrency <= 0) throw new ArgumentException($"{nameof(maxDegreesOfConcurrency)} must be an integer value greater than zero."); 28 | 29 | //Short Circuit if there is nothing to process... 30 | if (!source.Any()) return; 31 | 32 | #if NET6_0 33 | 34 | //BBernard - Implemented optimization now in .NET6 using the new OOTB support now with Parallel.ForEachAsync()! 35 | var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = maxDegreesOfConcurrency }; 36 | await Parallel.ForEachAsync(source, parallelOptions, async (item, token) => 37 | { 38 | await asyncFunc(item).ConfigureAwait(false); 39 | }).ConfigureAwait(false); 40 | 41 | #else //NetStandard2.1 will use the following custom implementation! 42 | 43 | var partitions = Partitioner.Create(source).GetPartitions(maxDegreesOfConcurrency); 44 | var asyncTasksEnumerable = partitions.Select(async partition => 45 | { 46 | using (partition) 47 | { 48 | while (partition.MoveNext()) await asyncFunc(partition.Current).ConfigureAwait(false); 49 | } 50 | }); 51 | await Task.WhenAll(asyncTasksEnumerable).ConfigureAwait(false); 52 | 53 | #endif 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/SqlBulkNaturalKeyHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Threading.Tasks; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | //TODO: BBernard - IF Needed for flexibility with tables that use Natural Keys (vs Surrogate Key via Identity Id) 9 | // WE COULD IMPLEMENT this as a SqlBulkHelper that has Support for PrimaryKey lookups vs Identity PKeys! 10 | // But this will take a bit more work to determine the PKey fields from teh DB Schema, and dynamically process them 11 | // in the MERGE query instead of using the Surrogate key via Identity Id (which does simplify this process!). 12 | public class SqlBulkNaturalKeyHelper : BaseSqlBulkHelper, ISqlBulkHelper where T: class 13 | { 14 | public SqlBulkNaturalKeyHelper() 15 | { 16 | throw new NotImplementedException("Potential future enhancement may be added to support Natural Keys within the existing framework, " + 17 | "however for now it's easier to manually implement the SqlBulkCopy directly for collections that " + 18 | "do not use Identity columns that need to be returned."); 19 | } 20 | 21 | public virtual Task> BulkInsertAsync(IEnumerable entityList, string tableName, SqlTransaction transaction) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | 26 | public virtual IEnumerable BulkInsert(IEnumerable entityList, string tableName, SqlTransaction transaction) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | 31 | public virtual IEnumerable BulkInsertOrUpdate(IEnumerable entityList, string tableName, SqlTransaction transaction) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public virtual Task> BulkInsertOrUpdateAsync(IEnumerable entityList, string tableName, SqlTransaction transaction) 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | 41 | public virtual IEnumerable BulkUpdate(IEnumerable entityList, string tableName, SqlTransaction transaction) 42 | { 43 | throw new NotImplementedException(); 44 | } 45 | 46 | public virtual Task> BulkUpdateAsync(IEnumerable entityList, string tableName, SqlTransaction transaction) 47 | { 48 | throw new NotImplementedException(); 49 | } 50 | 51 | public virtual IEnumerable BulkInsertOrUpdate(IEnumerable entityList, String tableName, SqlBulkHelpersMergeAction mergeAction, SqlTransaction transaction) 52 | { 53 | throw new NotImplementedException(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/TableNameTerm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlBulkHelpers.CustomExtensions; 3 | 4 | namespace SqlBulkHelpers 5 | { 6 | public readonly struct TableNameTerm 7 | { 8 | public const string DefaultSchemaName = "dbo"; 9 | public const char TermSeparator = '.'; 10 | 11 | public TableNameTerm(string schemaName, string tableName) 12 | { 13 | SchemaName = schemaName.AssertArgumentIsNotNullOrWhiteSpace(nameof(schemaName)).TrimTableNameTerm(); 14 | TableName = tableName.AssertArgumentIsNotNullOrWhiteSpace(nameof(tableName)).TrimTableNameTerm(); 15 | TableNameVariable = TableName.Replace(" ", string.Empty); 16 | //NOTE: We don't use QualifySqlTerm() here to prevent unnecessary additional trimming (that is done above). 17 | FullyQualifiedTableName = $"[{SchemaName}].[{TableName}]"; 18 | } 19 | 20 | public string SchemaName { get; } 21 | public string TableName { get; } 22 | public string TableNameVariable { get; } 23 | public string FullyQualifiedTableName { get; } 24 | public bool IsTempTableName => TableName.StartsWith("#"); 25 | 26 | public override string ToString() => FullyQualifiedTableName; 27 | 28 | public bool Equals(TableNameTerm other) 29 | => FullyQualifiedTableName.Equals(other.FullyQualifiedTableName); 30 | 31 | public bool EqualsIgnoreCase(TableNameTerm other) 32 | => FullyQualifiedTableName.Equals(other.FullyQualifiedTableName, StringComparison.OrdinalIgnoreCase); 33 | 34 | public TableNameTerm SwitchSchema(string newSchema) 35 | => new TableNameTerm(newSchema, TableName); 36 | 37 | public TableNameTerm ApplyNamePrefixOrSuffix(string prefix = null, string suffix = null) 38 | => new TableNameTerm(SchemaName, string.Concat(prefix?.Trim() ?? string.Empty, TableName, suffix?.Trim() ?? string.Empty)); 39 | 40 | //Handle Automatic String conversions for simplified APIs... 41 | public static implicit operator string(TableNameTerm t) => t.ToString(); 42 | 43 | public static TableNameTerm From(string schemaName, string tableName) 44 | => new TableNameTerm(schemaName, tableName); 45 | 46 | public static TableNameTerm From(string tableName) 47 | => From(tableName); 48 | 49 | public static TableNameTerm From(string tableNameOverride = null) 50 | { 51 | TableNameTerm tableNameTerm; 52 | if (tableNameOverride != null) 53 | { 54 | tableNameTerm = tableNameOverride.ParseAsTableNameTerm(); 55 | } 56 | else 57 | { 58 | var processingDef = SqlBulkHelpersProcessingDefinition.GetProcessingDefinition(); 59 | tableNameTerm = processingDef.MappedDbTableName.ParseAsTableNameTerm(); 60 | } 61 | return tableNameTerm; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Net6/SampleApp/SqlBulkHelpersSampleAsync.cs: -------------------------------------------------------------------------------- 1 | using SqlBulkHelpers; 2 | using System; 3 | using System.Collections.Generic; 4 | using Microsoft.Data.SqlClient; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using SqlBulkHelpers.SqlBulkHelpers; 9 | 10 | namespace SqlBulkHelpersSample.ConsoleApp 11 | { 12 | public class SqlBulkHelpersSampleAsync 13 | { 14 | public static async Task RunSampleAsync(string sqlConnectionString) 15 | { 16 | //Initialize Sql Bulk Helpers Configuration Defaults... 17 | SqlBulkHelpersConfig.ConfigureDefaults(config => 18 | { 19 | config.SqlBulkPerBatchTimeoutSeconds = SqlBulkHelpersSampleApp.SqlTimeoutSeconds; 20 | }); 21 | 22 | //Initialize with a Connection String (using our Config Key or your own, or any other initialization 23 | // of the Connection String (e.g. perfect for DI initialization, etc.): 24 | //NOTE: The ISqlBulkHelpersConnectionProvider interface provides a great abstraction that most projects don't 25 | // take the time to do, so it is provided here for convenience (e.g. extremely helpful with DI). 26 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 27 | 28 | //Initialize large list of Data to Insert or Update in a Table 29 | var testData = SqlBulkHelpersSample.CreateTestData(1000); 30 | var timer = Stopwatch.StartNew(); 31 | 32 | //Bulk Inserting is now as easy as: 33 | // 1) Initialize the DB Connection & Transaction (IDisposable) 34 | // 2) Execute the insert/update (e.g. new Extension Method API greatly simplifies this and allows InsertOrUpdate in one execution!) 35 | // 3) Map the results to Child Data and then repeat to create related Child data! 36 | await using SqlConnection sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false); 37 | await using SqlTransaction sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false); 38 | 39 | //Test using manual Table name provided... 40 | var results = (await sqlTrans.BulkInsertOrUpdateAsync(testData, tableName: SqlBulkHelpersSampleApp.TestTableName).ConfigureAwait(false)).ToList(); 41 | 42 | //Test using Table Name derived from Model Annotation [SqlBulkTable(...)] 43 | var childTestData = SqlBulkHelpersSample.CreateChildTestData(results); 44 | var childResults = await sqlTrans.BulkInsertOrUpdateAsync(childTestData).ConfigureAwait(false); 45 | 46 | await sqlTrans.CommitAsync(); 47 | 48 | timer.Stop(); 49 | Console.WriteLine($"Successfully Inserted or Updated [{testData.Count}] items and [{childTestData.Count}] related child items in [{timer.ElapsedMilliseconds}] millis!"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/SampleApp/SqlBulkHelpersSampleSynchronous.cs: -------------------------------------------------------------------------------- 1 | using SqlBulkHelpers; 2 | using System; 3 | using System.Collections.Generic; 4 | using Microsoft.Data.SqlClient; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using SqlBulkHelpers.SqlBulkHelpers; 8 | using SqlBulkHelpers.Tests; 9 | 10 | namespace SqlBulkHelpersSample.ConsoleApp 11 | { 12 | public class SqlBulkHelpersSampleSynchronous 13 | { 14 | public static void RunSample(string sqlConnectionString) 15 | { 16 | //Initialize Sql Bulk Helpers Configuration Defaults... 17 | SqlBulkHelpersConfig.ConfigureDefaults(config => 18 | { 19 | config.SqlBulkPerBatchTimeoutSeconds = SqlBulkHelpersSampleApp.SqlTimeoutSeconds; 20 | }); 21 | 22 | //Initialize with a Connection String (using our Config Key or your own, or any other initialization 23 | // of the Connection String (e.g. perfect for DI initialization, etc.): 24 | //NOTE: The ISqlBulkHelpersConnectionProvider interface provides a great abstraction that most projects don't 25 | // take the time to do, so it is provided here for convenience (e.g. extremely helpful with DI). 26 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 27 | 28 | //Initialize large list of Data to Insert or Update in a Table 29 | var testData = SqlBulkHelpersSample.CreateTestData(1000); 30 | var timer = Stopwatch.StartNew(); 31 | 32 | //Bulk Inserting is now as easy as: 33 | // 1) Initialize the DB Connection & Transaction (IDisposable) 34 | // 2) Execute the insert/update (e.g. new Extension Method API greatly simplifies this and allows InsertOrUpdate in one execution!) 35 | // 3) Map the results to Child Data and then repeat to create related Child data! 36 | using (var sqlConn = sqlConnectionProvider.NewConnection()) 37 | using (var sqlTrans = sqlConn.BeginTransaction())//NET Framework did not fully support Async Transaction handling... 38 | { 39 | //Test using manual Table name provided... 40 | var results = sqlTrans.BulkInsertOrUpdate(testData, SqlBulkHelpersSampleApp.TestTableName).ToList(); 41 | 42 | //Test using Table Name derived from Model Annotation [SqlBulkTable(...)] 43 | var childTestData = SqlBulkHelpersSample.CreateChildTestData(results); 44 | var childResults = sqlTrans.BulkInsertOrUpdate(childTestData); 45 | 46 | sqlTrans.Commit(); //NET Framework did not fully support Async Transaction handling... 47 | 48 | timer.Stop(); 49 | Console.WriteLine($"Successfully Inserted or Updated [{testData.Count}] items and [{childTestData.Count}] related child items in [{timer.ElapsedMilliseconds}] millis!"); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/SampleApp/SqlBulkHelpersSampleAsync.cs: -------------------------------------------------------------------------------- 1 | using SqlBulkHelpers; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using SqlBulkHelpers.SqlBulkHelpers; 7 | 8 | namespace SqlBulkHelpersSample.ConsoleApp 9 | { 10 | public class SqlBulkHelpersSampleAsync 11 | { 12 | public static async Task RunSampleAsync(string sqlConnectionString) 13 | { 14 | //Initialize Sql Bulk Helpers Configuration Defaults... 15 | SqlBulkHelpersConfig.ConfigureDefaults(config => 16 | { 17 | config.SqlBulkPerBatchTimeoutSeconds = SqlBulkHelpersSampleApp.SqlTimeoutSeconds; 18 | }); 19 | 20 | //Initialize with a Connection String (using our Config Key or your own, or any other initialization 21 | // of the Connection String (e.g. perfect for DI initialization, etc.): 22 | //NOTE: The ISqlBulkHelpersConnectionProvider interface provides a great abstraction that most projects don't 23 | // take the time to do, so it is provided here for convenience (e.g. extremely helpful with DI). 24 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 25 | 26 | //Initialize large list of Data to Insert or Update in a Table 27 | var testData = SqlBulkHelpersSample.CreateTestData(1000); 28 | var timer = Stopwatch.StartNew(); 29 | 30 | //Bulk Inserting is now as easy as: 31 | // 1) Initialize the DB Connection & Transaction (IDisposable) 32 | // 2) Execute the insert/update (e.g. new Extension Method API greatly simplifies this and allows InsertOrUpdate in one execution!) 33 | // 3) Map the results to Child Data and then repeat to create related Child data! 34 | using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 35 | using (var sqlTrans = sqlConn.BeginTransaction())//NET Framework did not fully support Async Transaction handling... 36 | { 37 | //Test using manual Table name provided... 38 | var results = (await sqlTrans.BulkInsertOrUpdateAsync(testData, SqlBulkHelpersSampleApp.TestTableName).ConfigureAwait(false)).ToList(); 39 | 40 | //Test using Table Name derived from Model Annotation [SqlBulkTable(...)] 41 | var childTestData = SqlBulkHelpersSample.CreateChildTestData(results); 42 | var childResults = await sqlTrans.BulkInsertOrUpdateAsync(childTestData).ConfigureAwait(false); 43 | 44 | sqlTrans.Commit(); //NET Framework did not fully support Async Transaction handling... 45 | 46 | timer.Stop(); 47 | Console.WriteLine($"Successfully Inserted or Updated [{testData.Count}] items and [{childTestData.Count}] related child items in [{timer.ElapsedMilliseconds}] millis!"); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/IntegrationTests/SchemaLoadingTests/ProcessingDefinitionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlBulkHelpers.Tests.IntegrationTests 4 | { 5 | [TestClass] 6 | public class ProcessingDefinitionTests : BaseTest 7 | { 8 | [TestMethod] 9 | public void TestProcessDefinitionLoading() 10 | { 11 | //Test retrieving for a specific class type... 12 | var processingDef = SqlBulkHelpersProcessingDefinition.GetProcessingDefinition(); 13 | 14 | Assert.IsNotNull(processingDef); 15 | Assert.AreEqual(TableNameTerm.From(TestHelpers.TestTableName).FullyQualifiedTableName, processingDef.MappedDbTableName); 16 | Assert.IsFalse(processingDef.UniqueMatchMergeValidationEnabled); 17 | Assert.IsTrue(processingDef.IsRowNumberColumnNameEnabled); 18 | Assert.IsTrue(processingDef.IsMappingLookupEnabled); 19 | 20 | const string unmappedPropertyName = nameof(TestElementWithMappedNames.UnMappedProperty); 21 | 22 | foreach (var propDef in processingDef.PropertyDefinitions) 23 | { 24 | var expectedMappedName = propDef.PropertyName switch 25 | { 26 | nameof(TestElementWithMappedNames.MyId) => "Id", 27 | nameof(TestElementWithMappedNames.MyKey) => "Key", 28 | nameof(TestElementWithMappedNames.MyValue) => "Value", 29 | //For Test case clarity we explicitly Test Linq2Db Column Attr. with no Name specified: https://github.com/cajuncoding/SqlBulkHelpers/issues/20 30 | nameof(TestElementWithMappedNames.MyColWithNullName) => nameof(TestElementWithMappedNames.MyColWithNullName), 31 | unmappedPropertyName => unmappedPropertyName, 32 | _ => null 33 | }; 34 | 35 | Assert.AreEqual(expectedMappedName, propDef.MappedDbColumnName); 36 | //NONE of these should be an Identity Property since no Identity Column Table Definition was provided when Initializing! 37 | Assert.IsFalse(propDef.IsIdentityProperty); 38 | } 39 | 40 | var matchQualifierExpression = processingDef.MergeMatchQualifierExpressionFromEntityModel; 41 | Assert.IsNotNull(matchQualifierExpression); 42 | //We intentionally set this to False to validate/test the setting (though Default is True) 43 | Assert.IsFalse(matchQualifierExpression.ThrowExceptionIfNonUniqueMatchesOccur); 44 | 45 | Assert.IsNotNull(matchQualifierExpression.MatchQualifierFields); 46 | Assert.AreEqual(2, matchQualifierExpression.MatchQualifierFields.Count); 47 | //Match Qualifiers should always use their Mapped DB Name! 48 | Assert.AreEqual("Id", matchQualifierExpression.MatchQualifierFields[0].SanitizedName); 49 | Assert.AreEqual("Key", matchQualifierExpression.MatchQualifierFields[1].SanitizedName); 50 | } 51 | 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/SqlQueries/QueryDBTableSchemaBasicDetailsJson.sql: -------------------------------------------------------------------------------- 1 | --NOTE: For Temp Table support all references to INFORMATION_SCHEMA must be replaced with tempdb.INFORMATION_SCHEMA 2 | -- and DB_NAME() must be changed to 'tempdb', otherwise we dynamically resolve the true Temp Table Name in the Cte... 3 | WITH TablesCte AS ( 4 | SELECT TOP (1) 5 | TableSchema = t.[TABLE_SCHEMA], 6 | TableName = t.[TABLE_NAME], 7 | TableCatalog = t.[TABLE_CATALOG], 8 | ObjectId = OBJECT_ID(CONCAT('[', t.TABLE_CATALOG, '].[', t.TABLE_SCHEMA, '].[', t.TABLE_NAME, ']')) 9 | FROM INFORMATION_SCHEMA.TABLES t 10 | WHERE 11 | t.TABLE_SCHEMA = @TableSchema 12 | AND t.TABLE_CATALOG = DB_NAME() 13 | AND t.TABLE_NAME = CASE 14 | WHEN @IsTempTable = 0 THEN @TableName 15 | ELSE (SELECT TOP (1) t.[name] FROM tempdb.sys.objects t WHERE t.[object_id] = OBJECT_ID(CONCAT(N'tempdb.[', @TableSchema, '].[', @TableName, ']'))) COLLATE DATABASE_DEFAULT 16 | END 17 | ) 18 | SELECT 19 | t.TableSchema, 20 | t.TableName, 21 | [SchemaDetailLevel] = 'BasicDetails', 22 | [TableColumns] = ( 23 | SELECT 24 | SourceTableSchema = t.TableSchema, 25 | SourceTableName = t.TableName, 26 | OrdinalPosition = ORDINAL_POSITION, 27 | ColumnName = COLUMN_NAME, 28 | DataType = DATA_TYPE, 29 | IsIdentityColumn = CAST(COLUMNPROPERTY(t.ObjectId, COLUMN_NAME, 'IsIdentity') AS bit), 30 | CharacterMaxLength = CHARACTER_MAXIMUM_LENGTH, 31 | NumericPrecision = NUMERIC_PRECISION, 32 | NumericPrecisionRadix = NUMERIC_PRECISION_RADIX, 33 | NumericScale = NUMERIC_SCALE, 34 | DateTimePrecision = DATETIME_PRECISION 35 | FROM INFORMATION_SCHEMA.COLUMNS c 36 | WHERE 37 | c.TABLE_CATALOG = t.TableCatalog 38 | AND c.TABLE_SCHEMA = t.TableSchema 39 | AND c.TABLE_NAME = t.TableName 40 | ORDER BY c.ORDINAL_POSITION 41 | FOR JSON PATH 42 | ), 43 | [PrimaryKeyConstraint] = JSON_QUERY(( 44 | SELECT TOP (1) 45 | SourceTableSchema = t.TableSchema, 46 | SourceTableName = t.TableName, 47 | ConstraintName = c.CONSTRAINT_NAME, 48 | ConstraintType = 'PrimaryKey', 49 | [KeyColumns] = ( 50 | SELECT 51 | OrdinalPosition = col.ORDINAL_POSITION, 52 | ColumnName = col.COLUMN_NAME 53 | FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE col 54 | WHERE 55 | col.TABLE_SCHEMA = c.TABLE_SCHEMA 56 | AND col.TABLE_NAME = c.TABLE_NAME 57 | AND col.CONSTRAINT_SCHEMA = c.CONSTRAINT_SCHEMA 58 | AND col.CONSTRAINT_NAME = c.CONSTRAINT_NAME 59 | ORDER BY col.ORDINAL_POSITION 60 | FOR JSON PATH 61 | ) 62 | FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c 63 | WHERE 64 | c.TABLE_CATALOG = t.TableCatalog 65 | AND c.TABLE_SCHEMA = t.TableSchema 66 | AND c.TABLE_NAME = t.TableName 67 | AND c.CONSTRAINT_TYPE = 'PRIMARY KEY' 68 | FOR JSON PATH, WITHOUT_ARRAY_WRAPPER 69 | )) 70 | FROM TablesCte t 71 | ORDER BY t.TableName 72 | FOR JSON PATH, WITHOUT_ARRAY_WRAPPER 73 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/IntegrationTests/SqlBulkLoadingTests/ConnectionAndConstructorTests.cs: -------------------------------------------------------------------------------- 1 | using SqlBulkHelpers.Tests; 2 | using Microsoft.Data.SqlClient; 3 | 4 | namespace SqlBulkHelpers.IntegrationTests 5 | { 6 | [TestClass] 7 | public class SqlBulkHelpersConnectionAndConstructorTests : BaseTest 8 | { 9 | [TestMethod] 10 | public async Task TestBulkInsertConstructorWithExistingConnectionAndTransactionAsync() 11 | { 12 | var sqlConnectionProvider = SqlConnectionHelper.GetConnectionProvider(); 13 | 14 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 15 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 16 | { 17 | await DoInsertOrUpdateTestAsync(sqlTrans).ConfigureAwait(false); 18 | } 19 | } 20 | 21 | protected async Task DoInsertOrUpdateTestAsync(SqlTransaction sqlTrans) 22 | { 23 | List testData = TestHelpers.CreateTestData(10); 24 | 25 | ////Must clear all Data and Related Data to maintain Data Integrity... 26 | ////NOTE: If we don't clear the related table then the FKey Constraint Check on the Related data (Child table) will FAIL! 27 | //await sqlTrans.ClearTablesAsync(new[] 28 | //{ 29 | // TestHelpers.TestChildTableNameFullyQualified, 30 | // TestHelpers.TestTableNameFullyQualified 31 | //}, forceOverrideOfConstraints: true).ConfigureAwait(false); 32 | 33 | var results = (await sqlTrans.BulkInsertOrUpdateAsync( 34 | testData, 35 | TestHelpers.TestTableName 36 | ).ConfigureAwait(false)).ToList(); 37 | 38 | //Test Inserting of Child Data with Table Name derived from Model Annotation, and FKey constraints to the Parents... 39 | var childTestData = TestHelpers.CreateChildTestData(results); 40 | var childResults = await sqlTrans.BulkInsertOrUpdateAsync(childTestData).ConfigureAwait(false); 41 | 42 | await sqlTrans.CommitAsync().ConfigureAwait(false); 43 | 44 | //ASSERT Results are Valid... 45 | Assert.IsNotNull(results); 46 | 47 | //We Sort the Results by Identity Id to ensure that the inserts occurred in the correct 48 | // order with incrementing ID values as specified in the original Data! 49 | //This validates that data is inserted as expected for Identity columns so that it can 50 | // be correctly sorted by Incrementing Identity value when Queried (e.g. ORDER BY Id) 51 | var resultsSorted = results.OrderBy(r => r.Id).ToList(); 52 | Assert.AreEqual(resultsSorted.Count(), testData.Count); 53 | 54 | var i = 0; 55 | foreach (var result in resultsSorted) 56 | { 57 | Assert.IsNotNull(result); 58 | Assert.IsTrue(result.Id > 0); 59 | Assert.AreEqual(result.Key, testData[i].Key); 60 | Assert.AreEqual(result.Value, testData[i].Value); 61 | i++; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | public class SqlBulkHelpersTableDefinition 10 | { 11 | private readonly ILookup _caseInsensitiveColumnLookup; 12 | 13 | public SqlBulkHelpersTableDefinition(String tableSchema, String tableName, List columns) 14 | { 15 | this.TableSchema = tableSchema; 16 | this.TableName = tableName; 17 | //Ensure that the Columns Collection is always NullSafe and is Immutable/ReadOnly! 18 | this.Columns = (columns ?? new List()).AsReadOnly(); 19 | 20 | //Initialize Helper Elements for Fast Processing (Cached Immutable data references) 21 | this.IdentityColumn = this.Columns.FirstOrDefault(c => c.IsIdentityColumn); 22 | this._caseInsensitiveColumnLookup = this.Columns.ToLookup(c => c.ColumnName.ToLower()); 23 | } 24 | public String TableSchema { get; private set; } 25 | public String TableName { get; private set; } 26 | public String TableFullyQualifiedName => $"[{TableSchema}].[{TableName}]"; 27 | public IList Columns { get; private set; } 28 | public SqlBulkHelpersColumnDefinition IdentityColumn { get; private set; } 29 | 30 | public IList GetColumnNames(bool includeIdentityColumn = true) 31 | { 32 | IEnumerable results = includeIdentityColumn 33 | ? this.Columns.Select(c => c.ColumnName) 34 | : this.Columns.Where(c => !c.IsIdentityColumn).Select(c => c.ColumnName); 35 | 36 | //Ensure that our List is Immutable/ReadOnly! 37 | return results.ToList().AsReadOnly(); 38 | } 39 | 40 | public SqlBulkHelpersColumnDefinition FindColumnCaseInsensitive(String columnName) 41 | { 42 | var lookup = _caseInsensitiveColumnLookup; 43 | return lookup[columnName.ToLower()]?.FirstOrDefault(); 44 | } 45 | 46 | public override string ToString() 47 | { 48 | return this.TableFullyQualifiedName; 49 | } 50 | } 51 | 52 | public class SqlBulkHelpersColumnDefinition 53 | { 54 | public SqlBulkHelpersColumnDefinition(String columnName, int ordinalPosition, String dataType, bool isIdentityColumn) 55 | { 56 | this.ColumnName = columnName; 57 | this.OrdinalPosition = ordinalPosition; 58 | this.DataType = dataType; 59 | this.IsIdentityColumn = isIdentityColumn; 60 | } 61 | 62 | public String ColumnName { get; private set; } 63 | public int OrdinalPosition { get; private set; } 64 | public String DataType { get; private set; } 65 | public bool IsIdentityColumn { get; private set; } 66 | 67 | public override string ToString() 68 | { 69 | return $"{this.ColumnName} [{this.DataType}]"; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/SqlClientCustomExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Microsoft.Data.SqlClient; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | internal static class SqlClientCustomExtensions 9 | { 10 | public static async Task ExecuteForJsonAsync(this SqlCommand sqlCmd) where T : class 11 | { 12 | //Quickly Read the FIRST record fully from Sql Server Reader response. 13 | using (var sqlReader = await sqlCmd.ExecuteReaderAsync().ConfigureAwait(false)) 14 | { 15 | //Short circuit if no data is returned. 16 | if (sqlReader.HasRows) 17 | { 18 | var jsonStringBuilder = new StringBuilder(); 19 | while (await sqlReader.ReadAsync().ConfigureAwait(false)) 20 | { 21 | jsonStringBuilder.Append(sqlReader.GetString(0)); 22 | } 23 | 24 | var json = jsonStringBuilder.ToString(); 25 | var result = JsonConvert.DeserializeObject(json); 26 | return result; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | 33 | public static T ExecuteForJson(this SqlCommand sqlCmd) where T : class 34 | { 35 | //Quickly Read the FIRST record fully from Sql Server Reader response. 36 | using (var sqlReader = sqlCmd.ExecuteReader()) 37 | { 38 | //Short circuit if no data is returned. 39 | if (sqlReader.HasRows) 40 | { 41 | var jsonStringBuilder = new StringBuilder(); 42 | while (sqlReader.Read()) 43 | { 44 | jsonStringBuilder.Append(sqlReader.GetString(0)); 45 | } 46 | 47 | var json = jsonStringBuilder.ToString(); 48 | var result = JsonConvert.DeserializeObject(json); 49 | return result; 50 | } 51 | } 52 | 53 | return null; 54 | } 55 | 56 | //public static async Task ExecuteForJsonAsync(this SqlCommand sqlCmd) where T : class 57 | //{ 58 | // //Quickly Read the FIRST record fully from Sql Server Reader response. 59 | // using (var sqlReader = await sqlCmd.ExecuteReaderAsync()) 60 | // { 61 | // //Short circuit if no data is returned. 62 | // if (sqlReader.HasRows) 63 | // { 64 | // var jsonStringBuilder = new StringBuilder(); 65 | // while (await sqlReader.ReadAsync()) 66 | // { 67 | // //So far all calls to SqlDataReader have been asynchronous, but since the data reader is in 68 | // //non -sequential mode and ReadAsync was used, the column data should be read synchronously. 69 | // jsonStringBuilder.Append(sqlReader.GetString(0)); 70 | // } 71 | 72 | // var json = jsonStringBuilder.ToString(); 73 | // var result = JsonConvert.DeserializeObject(json); 74 | // return result; 75 | // } 76 | // } 77 | 78 | // return null; 79 | //} 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/IntegrationTests/MaterializeDataTests/CopyTableDataTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlBulkHelpers.Tests; 3 | using SqlBulkHelpers.MaterializedData; 4 | using Microsoft.Data.SqlClient; 5 | using RepoDb; 6 | using SqlBulkHelpers.CustomExtensions; 7 | using SqlBulkHelpers.SqlBulkHelpers; 8 | 9 | namespace SqlBulkHelpers.IntegrationTests 10 | { 11 | [TestClass] 12 | public class MaterializeDataCopyTableDataTests : BaseTest 13 | { 14 | 15 | [TestMethod] 16 | public async Task TestBasicCopyTableDataAsync() 17 | { 18 | var sqlConnectionProvider = SqlConnectionHelper.GetConnectionProvider(); 19 | 20 | //We can construct this multiple ways, so here we test the Parse and Switch Schema methods... 21 | var targetTableNameTerm = TestHelpers.TestTableNameFullyQualified 22 | .ParseAsTableNameTerm() 23 | .ApplyNamePrefixOrSuffix(suffix: "_CopyTableDataTest"); 24 | 25 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 26 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 27 | { 28 | //FIRST Copy the Table to ensure we have a Target Table... 29 | var cloneInfo = await sqlTrans.CloneTableAsync( 30 | sourceTableName: TestHelpers.TestTableNameFullyQualified, 31 | targetTableName: targetTableNameTerm, 32 | recreateIfExists: true, 33 | copyDataFromSource: false 34 | ).ConfigureAwait(false); 35 | 36 | //Second Copy the Data using the New explicit API.. 37 | var resultCopyInfo = await sqlTrans.CopyTableDataAsync(TestHelpers.TestTableNameFullyQualified, targetTableNameTerm).ConfigureAwait(false); 38 | 39 | await sqlTrans.CommitAsync().ConfigureAwait(false); 40 | //await sqlTrans.RollbackAsync().ConfigureAwait(false); 41 | 42 | //Validate that the new table has No Data! 43 | Assert.IsNotNull(cloneInfo); 44 | Assert.IsNotNull(resultCopyInfo); 45 | Assert.AreEqual(resultCopyInfo.SourceTable.FullyQualifiedTableName, TestHelpers.TestTableNameFullyQualified); 46 | Assert.AreEqual(resultCopyInfo.TargetTable.FullyQualifiedTableName, targetTableNameTerm); 47 | var sourceTableCount = await sqlConn.CountAllAsync(tableName: cloneInfo.SourceTable).ConfigureAwait(false); 48 | var targetTableCount = await sqlConn.CountAllAsync(tableName: cloneInfo.TargetTable).ConfigureAwait(false); 49 | 50 | //Ensure both Source & Target contain the same number of records! 51 | Assert.AreEqual(sourceTableCount, targetTableCount); 52 | 53 | ////CLEANUP The Cloned Table so that other Tests Work as expected (e.g. Some tests validate Referencing FKeys, etc. 54 | //// that are now increased with the table clone). 55 | //await using (var sqlTransForCleanup = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 56 | //{ 57 | // await sqlTransForCleanup.DropTableAsync(cloneInfo.TargetTable).ConfigureAwait(false); 58 | // await sqlTransForCleanup.CommitAsync().ConfigureAwait(false); 59 | //} 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/TestHelpers/RetryWithExponentialBackoffAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace SqlBulkHelpers.Utilities 6 | { 7 | /// 8 | /// Simple but effective Retry mechanism for C# with Exponential backoff and support for validating each result to determine if it should continue trying or accept the result. 9 | /// https://en.wikipedia.org/wiki/Exponential_backoff 10 | /// 11 | /// 12 | /// The Max number of attempts that will be made. 13 | /// The Func<T> process/action that will be attempted and will return generic type <T> when successful. 14 | /// A dynamic validation rule that can determine if a given result of generic type <T> is acceptable or if the action should be re-attempted. 15 | /// The initial delay time if the action fails; after which it will be exponentially expanded for longer delays with each iteration. 16 | /// Generic type <T> result from the Func<T> action specified. 17 | /// Any and all exceptions that occur from all attempts made before the max number of retries was encountered. 18 | public class Retry 19 | { 20 | public static async Task WithExponentialBackoffAsync( 21 | int maxRetries, 22 | Func> action, 23 | Func validationAction = null, 24 | int initialRetryWaitTimeMillis = 1000 25 | ) 26 | { 27 | var exceptions = new List(); 28 | var maxRetryValidatedCount = Math.Max(maxRetries, 1); 29 | 30 | //NOTE: We always make an initial attempt (index = 0, with NO delay) + the max number of retries attempts with 31 | // exponential back-off delays; so for example with a maxRetries specified of 3 + 1 for the initial 32 | // we will make a total of 4 attempts! 33 | for (var failCount = 0; failCount <= maxRetryValidatedCount; failCount++) 34 | { 35 | try 36 | { 37 | //If we are retrying then we wait using an exponential back-off delay... 38 | if (failCount > 0) 39 | { 40 | var powerFactor = Math.Pow(failCount, 2); //This is our Exponential Factor 41 | var waitTimeSpan = TimeSpan.FromMilliseconds(powerFactor * initialRetryWaitTimeMillis); //Total Wait Time 42 | await Task.Delay(waitTimeSpan).ConfigureAwait(false); 43 | } 44 | 45 | //Attempt the Action... 46 | var result = await action().ConfigureAwait(false); 47 | 48 | //If successful and specified then validate the result; if invalid then continue retrying... 49 | var isValid = validationAction?.Invoke(result) ?? true; 50 | if (isValid) 51 | return result; 52 | } 53 | catch (Exception exc) 54 | { 55 | exceptions.Add(exc); 56 | } 57 | } 58 | 59 | //If we have Exceptions that were handled then we attempt to re-throw them so calling code can handle... 60 | if (exceptions.Count > 0) 61 | throw new AggregateException(exceptions); 62 | 63 | //Finally if no exceptions were handled (e.g. all failures were due to validateResult Func failing them) then we return the default (e.g. null)... 64 | return default; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/QueryProcessing/SqlBulkHelpersObjectMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Data; 5 | using System.Collections.Concurrent; 6 | using System.Reflection; 7 | 8 | namespace SqlBulkHelpers 9 | { 10 | public class SqlBulkHelpersObjectMapper 11 | { 12 | 13 | //TODO: BBernard - If beneficial (Need to Add Timers) we can improve the Reflection Performance here with Caching of PropertyInfo results, 14 | // use of Delegates for FASTer access to model members, etc. (IF practical and/or needed). 15 | public DataTable ConvertEntitiesToDataTable(IEnumerable entityList, SqlBulkHelpersColumnDefinition identityColumnDefinition) 16 | { 17 | //Get the name of hte Identity Column 18 | //NOTE: BBernard - We take in the strongly typed class (immutable) to ensure that we have validated Parameter vs raw string! 19 | var identityColumnName = identityColumnDefinition.ColumnName; 20 | 21 | //TODO: BBERNARD - Optimilze this with internal Type level caching and possbily mapping these to Delegates for faster execution! 22 | //NOTE: We Map all Properties to an anonymous type with Index, Name, and base PropInfo here for easier logic below, 23 | // and we ALWAYS convert to a List<> so that we always preserve the critical order of the PropertyInfo items! 24 | // to simplify all following code. 25 | var propertyDefs = SqlBulkHelpersObjectReflectionFactory.GetPropertyDefinitions(identityColumnDefinition); 26 | 27 | DataTable dataTable = new DataTable(); 28 | dataTable.Columns.AddRange(propertyDefs.Select(pi => new DataColumn 29 | { 30 | ColumnName = pi.Name, 31 | DataType = Nullable.GetUnderlyingType(pi.PropInfo.PropertyType) ?? pi.PropInfo.PropertyType, 32 | //We Always allow Null to make below logic easier, and it's the Responsibility of the Model to ensure values are Not Null vs Nullable. 33 | AllowDBNull = true //Nullable.GetUnderlyingType(pi.PropertyType) == null ? false : true 34 | }).ToArray()); 35 | 36 | //BBernaard - We ALWAYS Add the internal RowNumber reference so that we can exactly correllate Identity values 37 | // from the Server back to original records that we passed! 38 | dataTable.Columns.Add(new DataColumn() 39 | { 40 | ColumnName = SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME, 41 | DataType = typeof(int), 42 | AllowDBNull = true 43 | }); 44 | 45 | int rowCounter = 1; 46 | int identityIdFakeCounter = -1; 47 | foreach (T entity in entityList) 48 | { 49 | var rowValues = propertyDefs.Select(p => { 50 | var value = p.PropInfo.GetValue(entity); 51 | 52 | //Handle special cases to ensure that Identity values are mapped to unique invalid values. 53 | if (p.IsIdentityProperty && (int)value <= 0) 54 | { 55 | //Create a Unique but Invalid Fake Identity Id (e.g. negative number)! 56 | value = identityIdFakeCounter--; 57 | } 58 | 59 | return value; 60 | 61 | }).ToArray(); 62 | 63 | //Add the Values (must be critically in the same order as the PropertyInfos List) as a new Row! 64 | var newRow = dataTable.Rows.Add(rowValues); 65 | 66 | //Alwasy set the unique Row Number identifier 67 | newRow[SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME] = rowCounter++; 68 | } 69 | 70 | return dataTable; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SqlBulkHelpers.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.32804.182 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlBulkHelpers.SampleApp.NetFramework", "SqlBulkHelpers.SampleApp.NetFramework\SqlBulkHelpers.SampleApp.NetFramework.csproj", "{1317662E-BDB5-4E83-BE8F-56DCB955141A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetFramework.SqlBulkHelpers-Deprecated", "DotNetFramework.SqlBulkHelpers\DotNetFramework.SqlBulkHelpers-Deprecated.csproj", "{3ABF8F4C-6371-419E-B104-2DB62B2F643D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetStandard.SqlBulkHelpers", "NetStandard.SqlBulkHelpers\NetStandard.SqlBulkHelpers.csproj", "{0CABB737-46C2-460F-98FE-55D4871ED841}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlBulkHelpers.SampleApp.Net6", "SqlBulkHelpers.SampleApp.Net6\SqlBulkHelpers.SampleApp.Net6.csproj", "{15F32C79-E50E-448B-A797-C759F73033EE}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlBulkHelpers.Tests", "SqlBulkHelpers.Tests\SqlBulkHelpers.Tests.csproj", "{D5CFE2BA-4FB1-4ACA-BD14-FD6FB75EFCC8}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlBulkHelpers.SampleApp.Common", "SqlBulkHelpers.SampleApp.Common\SqlBulkHelpers.SampleApp.Common.csproj", "{2427ABF5-EFE4-47F9-9EEE-954A0ED466BD}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {1317662E-BDB5-4E83-BE8F-56DCB955141A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {1317662E-BDB5-4E83-BE8F-56DCB955141A}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {1317662E-BDB5-4E83-BE8F-56DCB955141A}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {1317662E-BDB5-4E83-BE8F-56DCB955141A}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {3ABF8F4C-6371-419E-B104-2DB62B2F643D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {3ABF8F4C-6371-419E-B104-2DB62B2F643D}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {3ABF8F4C-6371-419E-B104-2DB62B2F643D}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {3ABF8F4C-6371-419E-B104-2DB62B2F643D}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {0CABB737-46C2-460F-98FE-55D4871ED841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {0CABB737-46C2-460F-98FE-55D4871ED841}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {0CABB737-46C2-460F-98FE-55D4871ED841}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {0CABB737-46C2-460F-98FE-55D4871ED841}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {15F32C79-E50E-448B-A797-C759F73033EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {15F32C79-E50E-448B-A797-C759F73033EE}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {15F32C79-E50E-448B-A797-C759F73033EE}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {15F32C79-E50E-448B-A797-C759F73033EE}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {D5CFE2BA-4FB1-4ACA-BD14-FD6FB75EFCC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {D5CFE2BA-4FB1-4ACA-BD14-FD6FB75EFCC8}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {D5CFE2BA-4FB1-4ACA-BD14-FD6FB75EFCC8}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {D5CFE2BA-4FB1-4ACA-BD14-FD6FB75EFCC8}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {2427ABF5-EFE4-47F9-9EEE-954A0ED466BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {2427ABF5-EFE4-47F9-9EEE-954A0ED466BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {2427ABF5-EFE4-47F9-9EEE-954A0ED466BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {2427ABF5-EFE4-47F9-9EEE-954A0ED466BD}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {AD27AC63-C249-4FBA-AE9E-3F8BBC7741B7} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/DotNetFramework.SqlBulkHelpers-Deprecated.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3ABF8F4C-6371-419E-B104-2DB62B2F643D} 8 | Library 9 | Properties 10 | SqlBulkHelpers 11 | SqlBulkHelpers 12 | v4.8 13 | 512 14 | true 15 | 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersConnectionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Microsoft.Data.SqlClient; 4 | using System.Threading.Tasks; 5 | using SqlBulkHelpers.CustomExtensions; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | /// 10 | /// BBernard 11 | /// Connection string provider class to keep the responsibility for Loading the connection string in on only one class; 12 | /// but also supports custom implementations that would initialize the Connection string in any other custom way. 13 | /// 14 | /// Supports initializing SqlConnection directly from the Connection string, or from an SqlConnection factory Func provided. 15 | /// 16 | public class SqlBulkHelpersConnectionProvider : ISqlBulkHelpersConnectionProvider 17 | { 18 | public const string SqlConnectionStringConfigKey = "SqlConnectionString"; 19 | 20 | protected string SqlDbConnectionUniqueIdentifier { get; set; } 21 | 22 | protected Func NewSqlConnectionFactory { get; set; } 23 | 24 | public SqlBulkHelpersConnectionProvider(string sqlConnectionString) 25 | : this(sqlConnectionString, () => new SqlConnection(sqlConnectionString)) 26 | { 27 | } 28 | 29 | /// 30 | /// Uses the specified unique identifier parameter for caching (e.g. DB Schema caching) elements unique to this DB Connection. 31 | /// Initializes connections using hte provided SqlConnection Factory specified. 32 | /// 33 | /// Most likely the Connection String! 34 | /// 35 | /// 36 | public SqlBulkHelpersConnectionProvider(string sqlDbConnectionUniqueIdentifier, Func sqlConnectionFactory) 37 | { 38 | if (string.IsNullOrWhiteSpace(sqlDbConnectionUniqueIdentifier)) 39 | throw new ArgumentException($"The Unique DB Connection Identifier specified is null/empty; a valid identifier must be specified."); 40 | 41 | SqlDbConnectionUniqueIdentifier = sqlDbConnectionUniqueIdentifier; 42 | NewSqlConnectionFactory = sqlConnectionFactory.AssertArgumentIsNotNull(nameof(sqlConnectionFactory)); 43 | } 44 | 45 | /// 46 | /// Uses the specified unique identifier parameter for caching (e.g. DB Schema caching) elements unique to this DB Connection. 47 | /// Initializes connections using hte provided SqlConnection Factory specified. 48 | /// 49 | /// 50 | /// 51 | public SqlBulkHelpersConnectionProvider(Func sqlConnectionFactory) 52 | { 53 | NewSqlConnectionFactory = sqlConnectionFactory.AssertArgumentIsNotNull(nameof(sqlConnectionFactory)); 54 | using (var sqlTempConnection = sqlConnectionFactory.Invoke()) 55 | { 56 | SqlDbConnectionUniqueIdentifier = sqlTempConnection.ConnectionString; 57 | } 58 | } 59 | 60 | /// 61 | /// Provide Internal access to the Connection String to help uniquely identify the Connections from this Provider. 62 | /// 63 | /// Unique string representing connections provided by this provider 64 | public virtual string GetDbConnectionUniqueIdentifier() 65 | { 66 | return SqlDbConnectionUniqueIdentifier; 67 | } 68 | 69 | public virtual SqlConnection NewConnection() 70 | { 71 | var sqlConn = NewSqlConnectionFactory.Invoke(); 72 | if (sqlConn.State != ConnectionState.Open) 73 | sqlConn.Open(); 74 | 75 | return sqlConn; 76 | } 77 | 78 | public virtual async Task NewConnectionAsync() 79 | { 80 | var sqlConn = await NewSqlConnectionFactory 81 | .Invoke() 82 | .EnsureSqlConnectionIsOpenAsync() 83 | .ConfigureAwait(false); 84 | 85 | return sqlConn; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/MaterializedData/CloneTableInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlBulkHelpers.CustomExtensions; 3 | 4 | namespace SqlBulkHelpers.MaterializedData 5 | { 6 | public readonly struct CloneTableInfo 7 | { 8 | public TableNameTerm SourceTable { get; } 9 | public TableNameTerm TargetTable { get; } 10 | public bool CopyDataFromSource { get; } 11 | 12 | public CloneTableInfo(TableNameTerm sourceTable, TableNameTerm? targetTable = null, bool copyDataFromSource = false) 13 | { 14 | sourceTable.AssertArgumentIsNotNull(nameof(sourceTable)); 15 | 16 | //If both Source & Target are the same (e.g. Target was not explicitly specified) then we adjust 17 | // the Target to ensure we create a copy and append a unique Copy Id... 18 | var validTargetTable = targetTable == null || targetTable.Value.EqualsIgnoreCase(sourceTable) 19 | ? MakeTableNameUniqueInternal(sourceTable) 20 | : targetTable.Value; 21 | 22 | SourceTable = sourceTable; 23 | TargetTable = validTargetTable; 24 | CopyDataFromSource = copyDataFromSource; 25 | } 26 | 27 | /// 28 | /// Ensures that the Target Table Name is truly unique (and not simply scoped to a different Schema. 29 | /// 30 | /// 31 | public CloneTableInfo MakeTargetTableNameUnique() 32 | => new CloneTableInfo(SourceTable, MakeTableNameUniqueInternal(TargetTable)); 33 | 34 | private static TableNameTerm MakeTableNameUniqueInternal(TableNameTerm tableNameTerm) 35 | => TableNameTerm.From(tableNameTerm.SchemaName, string.Concat(tableNameTerm.TableName, "_", IdGenerator.NewId(10))); 36 | 37 | public static CloneTableInfo From(string sourceTableName = null, string targetTableName = null, string targetPrefix = null, string targetSuffix = null, bool copyDataFromSource = false) 38 | { 39 | //If the generic type is ISkipMappingLookup then we must have a valid sourceTableName specified as a param... 40 | if (SqlBulkHelpersProcessingDefinition.SkipMappingLookupType.IsAssignableFrom(typeof(TSource))) 41 | sourceTableName.AssertArgumentIsNotNullOrWhiteSpace(nameof(sourceTableName)); 42 | 43 | var sourceTable = TableNameTerm.From(sourceTableName); 44 | 45 | //For Target Table name we support falling back to original Source Table Name to automatically create a clone 46 | // with unique 'Copy' name... 47 | var validTargetTableName = string.IsNullOrWhiteSpace(targetTableName) 48 | ? sourceTableName 49 | : targetTableName; 50 | 51 | //If the generic type is ISkipMappingLookup then we must have a valid validTargetTableName specified as a param... 52 | if (SqlBulkHelpersProcessingDefinition.SkipMappingLookupType.IsAssignableFrom(typeof(TTarget))) 53 | { 54 | //We validate the valid target table name but if it's still blank then we throw an Argument 55 | // exception because no 'targetTableName' could be resolved... 56 | validTargetTableName.AssertArgumentIsNotNullOrWhiteSpace(nameof(targetTableName)); 57 | } 58 | 59 | var targetTable = TableNameTerm.From(targetTableName ?? sourceTableName).ApplyNamePrefixOrSuffix(targetPrefix, targetSuffix); 60 | return new CloneTableInfo(sourceTable, targetTable, copyDataFromSource); 61 | } 62 | 63 | public static CloneTableInfo From(string sourceTableName, string targetTableName = null, string targetPrefix = null, string targetSuffix = null, bool copyDataFromSource = false) 64 | => From(sourceTableName, targetTableName, targetPrefix, targetSuffix, copyDataFromSource); 65 | 66 | public static CloneTableInfo ForNewSchema(TableNameTerm sourceTable, string targetSchemaName, string targetTablePrefix = null, string targetTableSuffix = null, bool copyDataFromSource = false) 67 | => new CloneTableInfo(sourceTable, sourceTable.SwitchSchema(targetSchemaName).ApplyNamePrefixOrSuffix(targetTablePrefix, targetTableSuffix), copyDataFromSource); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.NetFramework/SqlBulkHelpers.SampleApp.NetFramework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | true 8 | Debug 9 | AnyCPU 10 | {1317662E-BDB5-4E83-BE8F-56DCB955141A} 11 | Exe 12 | SqlBulkHelpers.ConsoleApp 13 | SqlBulkHelpers.ConsoleApp 14 | v4.8 15 | 512 16 | true 17 | 18 | 19 | 20 | 21 | 22 | AnyCPU 23 | true 24 | full 25 | false 26 | bin\Debug\ 27 | DEBUG;TRACE 28 | prompt 29 | 4 30 | 7.1 31 | 32 | 33 | AnyCPU 34 | pdbonly 35 | true 36 | bin\Release\ 37 | TRACE 38 | prompt 39 | 4 40 | latest 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {0cabb737-46c2-460f-98fe-55d4871ed841} 77 | NetStandard.SqlBulkHelpers 78 | 79 | 80 | {2427ABF5-EFE4-47F9-9EEE-954A0ED466BD} 81 | SqlBulkHelpers.SampleApp.Common 82 | 83 | 84 | 85 | 86 | 5.1.3 87 | 88 | 89 | 13.0.4 90 | 91 | 92 | 4.3.1 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Text; 4 | 5 | namespace SqlBulkHelpers.CustomExtensions 6 | { 7 | internal static class StringExtensions 8 | { 9 | public static string TruncateToLength(this string str, int length) 10 | => str.Length > length ? str.Substring(0, length) : str; 11 | 12 | public static string ReplaceCaseInsensitive(this string str, string oldValue, string @newValue) 13 | => Replace(str, oldValue, @newValue, StringComparison.OrdinalIgnoreCase); 14 | 15 | /// 16 | /// Returns a new string in which all occurrences of a specified string in the current instance are replaced with another 17 | /// specified string according the type of search to use for the specified string. 18 | /// Inspired/Adapted from original solution at: https://stackoverflow.com/a/45756981/7293142 19 | /// 20 | /// The string performing the replace method. 21 | /// The string to be replaced. 22 | /// The string replace all occurrences of . 23 | /// If value is equal to null, than all occurrences of will be removed from the . 24 | /// One of the enumeration values that specifies the rules for the search. 25 | /// A string that is equivalent to the current string except that all instances of are replaced with . 26 | /// If is not found in the current instance, the method returns the current instance unchanged. 27 | public static string Replace(this string str, string oldValue, string @newValue, StringComparison comparisonType) 28 | { 29 | // Check inputs... 30 | if (str == null) 31 | // Same as original .NET C# string.Replace behavior. 32 | throw new ArgumentNullException(nameof(str)); 33 | 34 | if (str.Length == 0) 35 | // Same as original .NET C# string.Replace behavior. 36 | return str; 37 | 38 | if (oldValue == null) 39 | // Same as original .NET C# string.Replace behavior. 40 | throw new ArgumentNullException(nameof(oldValue)); 41 | 42 | if (oldValue.Length == 0) 43 | // Same as original .NET C# string.Replace behavior. 44 | throw new ArgumentException("String cannot be of zero length."); 45 | 46 | StringBuilder resultStringBuilder = new StringBuilder(str.Length); 47 | bool isReplacementNullOrEmpty = string.IsNullOrEmpty(@newValue); 48 | 49 | const int NOT_FOUND = -1; 50 | int foundAt, startSearchFromIndex = 0; 51 | while ((foundAt = str.IndexOf(oldValue, startSearchFromIndex, comparisonType)) != NOT_FOUND) 52 | { 53 | // Append all characters until the found replacement. 54 | int @charsUntilReplacment = foundAt - startSearchFromIndex; 55 | bool isNothingToAppend = @charsUntilReplacment == 0; 56 | if (!isNothingToAppend) 57 | resultStringBuilder.Append(str, startSearchFromIndex, @charsUntilReplacment); 58 | 59 | // Process the replacement. 60 | if (!isReplacementNullOrEmpty) 61 | resultStringBuilder.Append(@newValue); 62 | 63 | // Prepare start index for the next search; 64 | // This needed to prevent infinite loop, otherwise method always start search 65 | // from the start of the string. For example: if an oldValue == "EXAMPLE", newValue == "example" 66 | // and comparisonType == "any ignore case" will conquer to replacing: 67 | // "EXAMPLE" to "example" to "example" to "example" … infinite loop. 68 | startSearchFromIndex = foundAt + oldValue.Length; 69 | if (startSearchFromIndex == str.Length) 70 | { 71 | // It is end of the input string: no more space for the next search. 72 | // The input string ends with a value that has already been replaced. 73 | // Therefore, the string builder with the result is complete and no further action is required. 74 | return resultStringBuilder.ToString(); 75 | } 76 | } 77 | 78 | // Append the last part to the result. 79 | int @charsUntilStringEnd = str.Length - startSearchFromIndex; 80 | resultStringBuilder.Append(str, startSearchFromIndex, @charsUntilStringEnd); 81 | 82 | return resultStringBuilder.ToString(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/SqlBulkHelper.Invocation.DeprecatedV1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Data.SqlClient; 4 | using System.Threading.Tasks; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | internal partial class SqlBulkHelper : BaseSqlBulkHelper where T : class 9 | { 10 | #region Deprecated V1 API Overloads 11 | 12 | [Obsolete("This method is from v1 API and is deprecated, it will be removed eventually as it is replaced by the overload with optional parameters.")] 13 | public virtual async Task> BulkInsertAsync( 14 | IEnumerable entityList, 15 | string tableName, 16 | SqlTransaction transaction, 17 | SqlMergeMatchQualifierExpression matchQualifierExpression = null 18 | ) 19 | { 20 | return await BulkInsertOrUpdateInternalAsync( 21 | entityList, 22 | SqlBulkHelpersMergeAction.Insert, 23 | transaction, 24 | tableNameParam: tableName, 25 | matchQualifierExpressionParam: matchQualifierExpression 26 | ).ConfigureAwait(false); 27 | } 28 | 29 | [Obsolete("This method is from v1 API and is deprecated, it will be removed eventually as it is replaced by the overload with optional parameters.")] 30 | public virtual async Task> BulkUpdateAsync( 31 | IEnumerable entityList, 32 | string tableName, 33 | SqlTransaction transaction, 34 | SqlMergeMatchQualifierExpression matchQualifierExpression = null 35 | ) 36 | { 37 | return await BulkInsertOrUpdateInternalAsync( 38 | entityList, 39 | SqlBulkHelpersMergeAction.Update, 40 | transaction, 41 | tableNameParam: tableName, 42 | matchQualifierExpressionParam: matchQualifierExpression 43 | ).ConfigureAwait(false); 44 | } 45 | 46 | [Obsolete("This method is from v1 API and is deprecated, it will be removed eventually as it is replaced by the overload with optional parameters.")] 47 | public virtual async Task> BulkInsertOrUpdateAsync( 48 | IEnumerable entityList, 49 | string tableName, 50 | SqlTransaction transaction, 51 | SqlMergeMatchQualifierExpression matchQualifierExpression = null 52 | ) 53 | { 54 | return await BulkInsertOrUpdateInternalAsync( 55 | entityList, 56 | SqlBulkHelpersMergeAction.InsertOrUpdate, 57 | transaction, 58 | tableNameParam: tableName, 59 | matchQualifierExpressionParam: matchQualifierExpression 60 | ).ConfigureAwait(false); 61 | } 62 | 63 | [Obsolete("This method is from v1 API and is deprecated, it will be removed eventually as it is replaced by the overload with optional parameters.")] 64 | public virtual IEnumerable BulkInsert( 65 | IEnumerable entityList, 66 | string tableName, 67 | SqlTransaction transaction, 68 | SqlMergeMatchQualifierExpression matchQualifierExpression = null 69 | ) 70 | { 71 | return BulkInsertOrUpdateInternal( 72 | entityList, 73 | SqlBulkHelpersMergeAction.Insert, 74 | transaction, 75 | tableNameParam: tableName, 76 | matchQualifierExpressionParam: matchQualifierExpression 77 | ); 78 | } 79 | 80 | [Obsolete("This method is from v1 API and is deprecated, it will be removed eventually as it is replaced by the overload with optional parameters.")] 81 | public virtual IEnumerable BulkUpdate( 82 | IEnumerable entityList, 83 | string tableName, 84 | SqlTransaction transaction, 85 | SqlMergeMatchQualifierExpression matchQualifierExpression = null 86 | ) 87 | { 88 | return BulkInsertOrUpdateInternal( 89 | entityList, 90 | SqlBulkHelpersMergeAction.Update, 91 | transaction, 92 | tableNameParam: tableName, 93 | matchQualifierExpressionParam: matchQualifierExpression 94 | ); 95 | } 96 | 97 | [Obsolete("This method is from v1 API and is deprecated, it will be removed eventually as it is replaced by the overload with optional parameters.")] 98 | public virtual IEnumerable BulkInsertOrUpdate( 99 | IEnumerable entityList, 100 | string tableName, 101 | SqlTransaction transaction, 102 | SqlMergeMatchQualifierExpression matchQualifierExpression = null 103 | ) 104 | { 105 | return BulkInsertOrUpdateInternal( 106 | entityList, 107 | SqlBulkHelpersMergeAction.InsertOrUpdate, 108 | transaction, 109 | tableNameParam: tableName, 110 | matchQualifierExpressionParam: matchQualifierExpression 111 | ); 112 | } 113 | 114 | #endregion 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/BaseSqlBulkHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.SqlClient; 5 | using System.Linq; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | //BBernard - Base Class for future flexibility... 10 | public abstract class BaseSqlBulkHelper where T: class 11 | { 12 | public virtual ISqlBulkHelpersDBSchemaLoader SqlDbSchemaLoader { get; protected set; } 13 | 14 | /// 15 | /// Constructor that support passing in a customized Sql DB Schema Loader implementation. 16 | /// 17 | /// 18 | protected BaseSqlBulkHelper(ISqlBulkHelpersDBSchemaLoader sqlDbSchemaLoader) 19 | { 20 | this.SqlDbSchemaLoader = sqlDbSchemaLoader.AssertArgumentNotNull(nameof(sqlDbSchemaLoader)); 21 | } 22 | 23 | /// 24 | /// Default constructor that will implement the default implementation for Sql Server DB Schema loading that supports static caching/lazy loading for performance. 25 | /// 26 | protected BaseSqlBulkHelper() 27 | { 28 | //Initialize the default Sql DB Schema Loader (which is dependent on the Sql Connection Provider); 29 | this.SqlDbSchemaLoader = SqlBulkHelpersDBSchemaStaticLoader.Default; 30 | } 31 | 32 | public virtual SqlBulkHelpersTableDefinition GetTableSchemaDefinition(String tableName) 33 | { 34 | //BBernard 35 | //NOTE: Prevent SqlInjection - by validating that the TableName must be a valid value (as retrieved from the DB Schema) 36 | // we eliminate risk of Sql Injection. 37 | var tableDefinition = this.SqlDbSchemaLoader.GetTableSchemaDefinition(tableName); 38 | if (tableDefinition == null) throw new ArgumentOutOfRangeException(nameof(tableName), $"The specified argument [{tableName}] is invalid."); 39 | return tableDefinition; 40 | } 41 | 42 | protected virtual DataTable ConvertEntitiesToDataTableHelper(IEnumerable entityList, SqlBulkHelpersColumnDefinition identityColumnDefinition = null) 43 | { 44 | SqlBulkHelpersObjectMapper _sqlBulkHelperModelMapper = new SqlBulkHelpersObjectMapper(); 45 | DataTable dataTable = _sqlBulkHelperModelMapper.ConvertEntitiesToDataTable(entityList, identityColumnDefinition); 46 | return dataTable; 47 | } 48 | 49 | protected virtual SqlBulkCopy CreateSqlBulkCopyHelper(DataTable dataTable, SqlBulkHelpersTableDefinition tableDefinition, SqlTransaction transaction) 50 | { 51 | var factory = new SqlBulkCopyFactory(); //Load with all Defaults from our Factory. 52 | var sqlBulkCopy = factory.CreateSqlBulkCopy(dataTable, tableDefinition, transaction); 53 | return sqlBulkCopy; 54 | } 55 | 56 | //TODO: BBernard - If beneficial, we can Add Caching here at this point to cache the fully formed Merge Queries! 57 | protected virtual SqlMergeScriptResults BuildSqlMergeScriptsHelper(SqlBulkHelpersTableDefinition tableDefinition, SqlBulkHelpersMergeAction mergeAction) 58 | { 59 | var mergeScriptBuilder = new SqlBulkHelpersMergeScriptBuilder(); 60 | var sqlScripts = mergeScriptBuilder.BuildSqlMergeScripts(tableDefinition, mergeAction); 61 | return sqlScripts; 62 | } 63 | 64 | //NOTE: This is Protected Class because it is ONLY needed by the SqlBulkHelper implementations with Merge Operations 65 | // for organized code when post-processing results. 66 | protected class MergeResult 67 | { 68 | public int RowNumber { get; set; } 69 | public int IdentityId { get; set; } 70 | public SqlBulkHelpersMergeAction MergeAction { get; set; } 71 | } 72 | 73 | protected virtual List PostProcessEntitiesWithMergeResults(List entityList, List mergeResultsList, SqlBulkHelpersColumnDefinition identityColumnDefinition) 74 | { 75 | var propDefs = SqlBulkHelpersObjectReflectionFactory.GetPropertyDefinitions(identityColumnDefinition); 76 | var identityPropDef = propDefs.FirstOrDefault(pi => pi.IsIdentityProperty); 77 | var identityPropInfo = identityPropDef.PropInfo; 78 | 79 | foreach (var mergeResult in mergeResultsList.Where(r => r.MergeAction.HasFlag(SqlBulkHelpersMergeAction.Insert))) 80 | { 81 | //NOTE: List is 0 (zero) based, but our RowNumber is 1 (one) based. 82 | var entity = entityList[mergeResult.RowNumber - 1]; 83 | 84 | //BBernard 85 | //GENERICALLY Set the Identity Value to the Int value returned, this eliminates any dependency on a Base Class! 86 | //TODO: If needed we can optimize this with a Delegate for faster property access (vs pure Reflection). 87 | //(entity as Debug.ConsoleApp.TestElement).Id = mergeResult.IdentityId; 88 | identityPropInfo.SetValue(entity, mergeResult.IdentityId); 89 | } 90 | 91 | //Return the Updated Entities List (for chainability) and easier to read code 92 | //NOTE: even though we have actually mutated the original list by reference this helps with code readability. 93 | return entityList; 94 | } 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/BaseHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System; 3 | using System.Threading.Tasks; 4 | using SqlBulkHelpers.CustomExtensions; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | internal abstract class BaseHelper where T : class 9 | { 10 | protected ISqlBulkHelpersDBSchemaLoader SqlDbSchemaLoader { get; set; } 11 | 12 | public ISqlBulkHelpersConfig BulkHelpersConfig { get; protected set; } 13 | 14 | protected SqlBulkHelpersProcessingDefinition BulkHelpersProcessingDefinition { get; set; } 15 | 16 | protected static readonly Type GenericType = typeof(T); 17 | 18 | #region Constructors 19 | 20 | /// 21 | /// Constructor that should be used for most use cases; Sql DB Schemas will be automatically resolved internally 22 | /// with caching support via SqlBulkHelpersSchemaLoaderCache. 23 | /// 24 | protected BaseHelper(ISqlBulkHelpersConfig bulkHelpersConfig = null) 25 | { 26 | this.BulkHelpersConfig = bulkHelpersConfig ?? SqlBulkHelpersConfig.DefaultConfig; 27 | this.BulkHelpersProcessingDefinition = SqlBulkHelpersProcessingDefinition.GetProcessingDefinition(); 28 | } 29 | 30 | #endregion 31 | 32 | protected virtual TableNameTerm GetMappedTableNameTerm(string tableNameOverride = null) 33 | => GenericType.GetSqlBulkHelpersMappedTableNameTerm(tableNameOverride); 34 | 35 | //BBernard 36 | //NOTE: MOST APIs will use a Transaction to get the DB Schema loader so this is the recommended method to use for all cases except for edge cases 37 | // that cannot run within a Transaction (e.g. FullTableIndex APIs). 38 | //NOTE: Prevent SqlInjection - by validating that the TableName must be a valid value (as retrieved from the DB Schema) 39 | // we eliminate risk of Sql Injection. 40 | //NOTE: All other parameters are Strongly typed (vs raw Strings) thus eliminating risk of Sql Injection 41 | protected virtual SqlBulkHelpersTableDefinition GetTableSchemaDefinitionInternal( 42 | TableSchemaDetailLevel detailLevel, 43 | SqlConnection sqlConnection, 44 | SqlTransaction sqlTransaction = null, 45 | string tableNameOverride = null, 46 | bool forceCacheReload = false 47 | ) 48 | { 49 | //Initialize the DB Schema loader (if specified, or from our Cache)... 50 | var dbSchemaLoader = SqlBulkHelpersSchemaLoaderCache.GetSchemaLoader(sqlConnection.ConnectionString); 51 | 52 | //BBernard 53 | //Load the Table Schema Definitions from the table name term provided or fall-back to use mapped data 54 | //NOTE: Prevent SqlInjection - by validating that the TableName must be a valid value (as retrieved from the DB Schema) 55 | // we eliminate risk of Sql Injection. 56 | var tableNameTerm = GetMappedTableNameTerm(tableNameOverride); 57 | var tableDefinition = dbSchemaLoader.GetTableSchemaDefinition(tableNameTerm, detailLevel, sqlConnection, sqlTransaction, forceCacheReload); 58 | AssertTableDefinitionIsValid(tableNameTerm, tableDefinition); 59 | 60 | return tableDefinition; 61 | } 62 | 63 | //BBernard 64 | //NOTE: MOST APIs will use a Transaction to get the DB Schema loader so this is the recommended method to use for all cases except for edge cases 65 | // that cannot run within a Transaction (e.g. FullTableIndex APIs). 66 | //NOTE: Prevent SqlInjection - by validating that the TableName must be a valid value (as retrieved from the DB Schema) 67 | // we eliminate risk of Sql Injection. 68 | //NOTE: All other parameters are Strongly typed (vs raw Strings) thus eliminating risk of Sql Injection 69 | protected virtual async Task GetTableSchemaDefinitionInternalAsync( 70 | TableSchemaDetailLevel detailLevel, 71 | SqlConnection sqlConnection, 72 | SqlTransaction sqlTransaction = null, 73 | string tableNameOverride = null, 74 | bool forceCacheReload = false 75 | ) 76 | { 77 | //Initialize the DB Schema loader (if specified, or from our Cache)... 78 | var dbSchemaLoader = SqlBulkHelpersSchemaLoaderCache.GetSchemaLoader(sqlConnection.ConnectionString); 79 | 80 | //BBernard 81 | //Load the Table Schema Definitions from the table name term provided or fall-back to use mapped data 82 | //NOTE: Prevent SqlInjection - by validating that the TableName must be a valid value (as retrieved from the DB Schema) 83 | // we eliminate risk of Sql Injection. 84 | var tableNameTerm = GetMappedTableNameTerm(tableNameOverride); 85 | var tableDefinition = await dbSchemaLoader.GetTableSchemaDefinitionAsync(tableNameTerm, detailLevel, sqlConnection, sqlTransaction, forceCacheReload).ConfigureAwait(false); 86 | AssertTableDefinitionIsValid(tableNameTerm, tableDefinition); 87 | 88 | return tableDefinition; 89 | } 90 | 91 | protected void AssertTableDefinitionIsValid(TableNameTerm tableNameTerm, SqlBulkHelpersTableDefinition tableDefinition) 92 | { 93 | if (tableDefinition == null) 94 | throw new ArgumentOutOfRangeException(nameof(tableNameTerm), $"The specified {nameof(tableNameTerm)} argument value of [{tableNameTerm}] is invalid; no table definition could be resolved."); 95 | } 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Sync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Data.SqlClient; 3 | using SqlBulkHelpers.CustomExtensions; 4 | 5 | namespace SqlBulkHelpers 6 | { 7 | /// 8 | /// BBernard 9 | /// DB Schema Loader class to keep the responsibility for Loading the Schema Definitions of Sql Server tables in on only one class; 10 | /// but also supports custom implementations that would initialize the DB Schema Definitions in any other custom way. 11 | /// 12 | /// The Default implementation will load the Database schema with Lazy/Deferred loading for performance, but it will use the Sql Connection Provider 13 | /// specified in the first instance that this class is initialized from because the Schema Definitions will be statically cached across 14 | /// all instances for high performance! 15 | /// 16 | /// NOTE: The static caching of the DB Schema is great for performance, and this default implementation will work well for most users (e.g. single database use), 17 | /// however more advanced usage may require the consumer/author to implement & manage their own ISqlBulkHelpersDBSchemaLoader. 18 | /// 19 | public partial class SqlBulkHelpersDBSchemaLoader : ISqlBulkHelpersDBSchemaLoader 20 | { 21 | public SqlBulkHelpersTableDefinition GetTableSchemaDefinition( 22 | string tableName, 23 | TableSchemaDetailLevel detailLevel, 24 | SqlConnection sqlConnection, 25 | SqlTransaction sqlTransaction = null, 26 | bool forceCacheReload = false 27 | ) 28 | { 29 | sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); 30 | 31 | var tableDefinition = GetTableSchemaDefinitionInternal( 32 | tableName, 33 | detailLevel, 34 | () => (sqlConnection, sqlTransaction), 35 | disposeOfConnection: false, //DO Not dispose of Existing Connection/Transaction... 36 | forceCacheReload 37 | ); 38 | 39 | return tableDefinition; 40 | } 41 | 42 | public SqlBulkHelpersTableDefinition GetTableSchemaDefinition( 43 | string tableName, 44 | TableSchemaDetailLevel detailLevel, 45 | Func sqlConnectionFactory, 46 | bool forceCacheReload = false 47 | ) 48 | { 49 | sqlConnectionFactory.AssertArgumentIsNotNull(nameof(sqlConnectionFactory)); 50 | 51 | var tableDefinition = GetTableSchemaDefinitionInternal( 52 | tableName, 53 | detailLevel, 54 | () => (sqlConnectionFactory(), (SqlTransaction)null), 55 | disposeOfConnection: true, //Always DISPOSE of New Connections created by the Factory... 56 | forceCacheReload 57 | ); 58 | 59 | return tableDefinition; 60 | } 61 | 62 | protected SqlBulkHelpersTableDefinition GetTableSchemaDefinitionInternal( 63 | string tableName, 64 | TableSchemaDetailLevel detailLevel, 65 | Func<(SqlConnection, SqlTransaction)> sqlConnectionAndTransactionFactory, 66 | bool disposeOfConnection, 67 | bool forceCacheReload = false 68 | ) 69 | { 70 | sqlConnectionAndTransactionFactory.AssertArgumentIsNotNull(nameof(sqlConnectionAndTransactionFactory)); 71 | 72 | if (string.IsNullOrWhiteSpace(tableName)) 73 | return null; 74 | 75 | var tableNameTerm = tableName.ParseAsTableNameTerm(); 76 | var cacheKey = CreateCacheKeyInternal(tableNameTerm, detailLevel); 77 | 78 | //NOTE: We also prevent caching of Temp Table schemas that are by definition Transient! 79 | if (forceCacheReload || tableNameTerm.IsTempTableName) 80 | TableDefinitionsCaseInsensitiveLazyCache.TryRemove(cacheKey); 81 | 82 | var tableDefinitionResult = TableDefinitionsCaseInsensitiveLazyCache.GetOrAdd( 83 | key: cacheKey, 84 | cacheValueFactory: key => 85 | { 86 | var (sqlConnection, sqlTransaction) = sqlConnectionAndTransactionFactory(); 87 | try 88 | { 89 | //If we don't have a Transaction then offer lazy opening of the Connection, 90 | // but if we do have a Transaction we assume the Connection is open & valid for the Transaction... 91 | if (sqlTransaction == null) 92 | sqlConnection.EnsureSqlConnectionIsOpen(); 93 | 94 | using (var sqlCmd = CreateSchemaQuerySqlCommand(tableNameTerm, detailLevel, sqlConnection, sqlTransaction)) 95 | { 96 | //Execute and load results from the Json... 97 | var tableDef = sqlCmd.ExecuteForJson(); 98 | return tableDef; 99 | } 100 | } 101 | finally 102 | { 103 | if(disposeOfConnection) 104 | sqlConnection.Dispose(); 105 | } 106 | }); 107 | 108 | return tableDefinitionResult; 109 | } 110 | 111 | public void ClearCache() 112 | { 113 | TableDefinitionsCaseInsensitiveLazyCache.ClearCache(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/EmbeddedResourceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Collections.Generic; 4 | using System.Text.RegularExpressions; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Diagnostics; 8 | 9 | namespace SqlBulkHelpers.CustomExtensions 10 | { 11 | //BBernard: 12 | //Shared from my common library via MIT License! 13 | internal static class EmbeddedResourcesCustomExtensions 14 | { 15 | public static byte[] LoadEmbeddedResourceDataAsBytes(this Assembly assembly, String resourceNameLiteral, bool enableLoosePathSeparatorMatching = true) 16 | { 17 | //BBernard 18 | //NOTE: For literal values we assume they are the File Name that would be at the END of a RegEx Resource pattern match 19 | //so we dynamically construct the proper regex to match the literal at the End of a match. 20 | var regexNamePatternText = GetRegexPatternFromLiteral(resourceNameLiteral, enableLoosePathSeparatorMatching); 21 | var bytes = assembly?.LoadEmbeddedResourceData(regexNamePatternText); 22 | return bytes; 23 | } 24 | 25 | public static String LoadEmbeddedResourceDataAsString(this Assembly assembly, String resourceNameLiteral, bool enableLoosePathSeparatorMatching = true) 26 | { 27 | //BBernard 28 | //NOTE: For literal values we assume they are the File Name that would be at the END of a RegEx Resource pattern match 29 | //so we dynamically construct the proper regex to match the literal at the End of a match. 30 | var regexNamePatternText = GetRegexPatternFromLiteral(resourceNameLiteral, enableLoosePathSeparatorMatching); 31 | return assembly?.LoadEmbeddedResourceDataFromRegexAsString(regexNamePatternText); 32 | } 33 | 34 | private static readonly Regex _loosePathSeparatorRegex = new Regex(@"[\.\\/]", RegexOptions.Compiled); 35 | 36 | private static string GetRegexPatternFromLiteral(string resourceNameLiteral, bool enableLoosePathSeparatorMatching = true) 37 | { 38 | var resourceRegex = enableLoosePathSeparatorMatching 39 | ? string.Join(@"\.", _loosePathSeparatorRegex.Split(resourceNameLiteral).Select(Regex.Escape)) 40 | : Regex.Escape(resourceNameLiteral); 41 | return $".*{resourceRegex}$"; 42 | } 43 | 44 | public static String LoadEmbeddedResourceDataFromRegexAsString(this Assembly assembly, String resourceNameRegexPattern) 45 | { 46 | var bytes = assembly?.LoadEmbeddedResourceData(resourceNameRegexPattern); 47 | if (bytes == null) 48 | return null; 49 | 50 | using (var memoryStream = new MemoryStream(bytes)) 51 | using (var streamReader = new StreamReader(memoryStream)) 52 | { 53 | //BBernard 54 | //NOTE: UTF8 Encoding does NOT handle byte-order-mark correctly (e.g. JObject.Parse() may fail due to unexpected BOM), 55 | // therefore we need to use StreamReader for more robust handling of text content; 56 | // Since it is initially loaded as a Stream we need to Parse it as a Stream also! 57 | // More Info here: https://stackoverflow.com/a/11701560/7293142 58 | //var textContent = Encoding.UTF8.GetString(bytes); 59 | var textContent = streamReader.ReadToEnd(); 60 | return textContent; 61 | } 62 | } 63 | 64 | public static byte[] LoadEmbeddedResourceData(this Assembly assembly, String resourceNameRegexPattern) 65 | { 66 | var enumerableResults = assembly?.GetManifestResourceBytesRegex(resourceNameRegexPattern); 67 | return enumerableResults?.FirstOrDefault(); 68 | } 69 | 70 | public static IEnumerable GetManifestResourceBytesRegex(this Assembly assembly, String resourceNameRegexPattern) 71 | { 72 | var resourceBytes = assembly 73 | ?.GetManifestResourceNamesByRegex(resourceNameRegexPattern) 74 | ?.Select(assembly.GetManifestResourceBytes); 75 | 76 | return resourceBytes; 77 | } 78 | 79 | public static List GetManifestResourceNamesByRegex(this Assembly assembly, String resourceNameRegexPattern) 80 | { 81 | Regex rx = new Regex(resourceNameRegexPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); 82 | return assembly?.GetManifestResourceNamesByRegex(rx); 83 | } 84 | 85 | public static List GetManifestResourceNamesByRegex(this Assembly assembly, Regex rxResourceNamePatter) 86 | { 87 | var resourceNames = assembly 88 | ?.GetManifestResourceNames() 89 | .Where(n => rxResourceNamePatter.IsMatch(n)) 90 | .ToList(); 91 | 92 | #if DEBUG 93 | Debug.WriteLine("Matched Resource Names:"); 94 | if (resourceNames.HasAny()) 95 | { 96 | foreach (var name in resourceNames) 97 | { 98 | Debug.WriteLine($" - {name}"); 99 | } 100 | } 101 | #endif 102 | 103 | return resourceNames; 104 | } 105 | 106 | public static byte[] GetManifestResourceBytes(this Assembly assembly, string fullyQualifiedName) 107 | { 108 | using (var stream = assembly?.GetManifestResourceStream(fullyQualifiedName)) 109 | { 110 | return stream?.ToByteArray(); 111 | } 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /SqlBulkHelpers.SampleApp.Common/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using RepoDb.Attributes; 6 | using SqlBulkHelpers.Interfaces; 7 | 8 | namespace SqlBulkHelpers.Tests 9 | { 10 | public static class TestHelpers 11 | { 12 | public const string TestTableName = "SqlBulkHelpersTestElements"; 13 | public const string TestTableNameFullyQualified = "[dbo].[SqlBulkHelpersTestElements]"; 14 | public const string TestChildTableNameFullyQualified = "[dbo].[SqlBulkHelpersTestElements_Child_NoIdentity]"; 15 | public const string TestTableWithFullTextIndexFullyQualified = "[dbo].[SqlBulkHelpersTestElements_WithFullTextIndex]"; 16 | 17 | public const int SqlTimeoutSeconds = 150; 18 | 19 | public static SqlBulkHelpersConfig BulkHelpersConfig { get; } = SqlBulkHelpersConfig.Create(config => 20 | { 21 | config.SqlBulkPerBatchTimeoutSeconds = SqlTimeoutSeconds; 22 | }); 23 | 24 | public static List CreateTestData(int dataSize, string prefix = "TEST_CSHARP_DotNet6") 25 | { 26 | var list = new List(); 27 | var childList = new List(); 28 | for (var x = 1; x <= dataSize; x++) 29 | { 30 | var testElement = new TestElement() 31 | { 32 | Id = default, 33 | Key = $"{prefix}[{x:0000}]_GUID[{Guid.NewGuid().ToString().ToUpper()}]", 34 | Value = $"VALUE_{x:0000}" 35 | }; 36 | 37 | list.Add(testElement); 38 | 39 | for (var c = 1; c <= 3; c++) 40 | { 41 | childList.Add(new ChildTestElement() 42 | { 43 | ParentId = testElement.Id, 44 | ChildKey = $"CHILD #{c} Of: {testElement.Key}", 45 | ChildValue = testElement.Value 46 | }); 47 | } 48 | } 49 | 50 | return list; 51 | } 52 | 53 | public static List CreateChildTestData(List testData) 54 | { 55 | var childList = new List(); 56 | foreach (var testElement in testData) 57 | { 58 | for (var c = 1; c <= 3; c++) 59 | { 60 | childList.Add(new ChildTestElement() 61 | { 62 | ParentId = testElement.Id, 63 | ChildKey = $"CHILD #{c:0000} Of: {testElement.Key}", 64 | ChildValue = testElement.Value 65 | }); 66 | } 67 | } 68 | 69 | return childList; 70 | } 71 | 72 | public static List CreateTestDataWithIdentitySetter(int dataSize) 73 | { 74 | var testData = CreateTestData(dataSize); 75 | var list = testData.Select(t => new TestElementWithIdentitySetter() 76 | { 77 | Id = t.Id, 78 | Key = t.Key, 79 | Value = t.Value 80 | }).ToList(); 81 | 82 | return list; 83 | } 84 | } 85 | 86 | public class TestElement 87 | { 88 | public int Id { get; set; } 89 | public string Key { get; set; } 90 | public string Value { get; set; } 91 | public override string ToString() => $"Id=[{Id}], Key=[{Key}]"; 92 | } 93 | 94 | [SqlBulkTable(TestHelpers.TestChildTableNameFullyQualified)] 95 | public class ChildTestElement 96 | { 97 | public string ChildKey { get; set; } 98 | public int ParentId { get; set; } 99 | public string ChildValue { get; set; } 100 | public override string ToString() => $"ParentId=[{ParentId}], ChildKey=[{ChildKey}]"; 101 | } 102 | 103 | [SqlBulkTable(TestHelpers.TestTableName, uniqueMatchMergeValidationEnabled: false)] 104 | public class TestElementWithMappedNames 105 | { 106 | public TestElementWithMappedNames() 107 | { 108 | } 109 | 110 | public TestElementWithMappedNames(TestElement testElement) 111 | { 112 | MyId = testElement.Id; 113 | MyKey = testElement.Key; 114 | MyValue = testElement.Value; 115 | UnMappedProperty = -1; 116 | } 117 | 118 | [SqlBulkMatchQualifier] 119 | [Map("Id")] 120 | public int MyId { get; set; } 121 | 122 | [Column("Key")] 123 | [SqlBulkMatchQualifier] 124 | public string MyKey { get; set; } 125 | 126 | //TEST case where Name is not Specified in the Linq2Db Column Attribute 127 | // since it is actually Optional: https://github.com/cajuncoding/SqlBulkHelpers/issues/20 128 | [Column()] 129 | public string MyColWithNullName { get; set; } 130 | 131 | //Regardless of attribute order the SqlBulkColumn should take precedent! 132 | [Column("INCORRECT_NAME_SHOULD_NOT_RESOLVE")] 133 | [SqlBulkColumn("Value")] 134 | [Map("INCORRECT_NAME_SHOULD_NOT_RESOLVE")] 135 | public string MyValue { get; set; } 136 | 137 | public int UnMappedProperty { get; set; } 138 | 139 | public override string ToString() 140 | { 141 | return $"Id=[{MyId}], Key=[{MyKey}]"; 142 | } 143 | } 144 | 145 | 146 | public class TestElementWithIdentitySetter : TestElement, ISqlBulkHelperIdentitySetter 147 | { 148 | public void SetIdentityId(int id) 149 | { 150 | this.Id = id; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/QueryProcessing/SqlBulkHelpersMergeQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace SqlBulkHelpers 5 | { 6 | public class SqlBulkHelpersMergeScriptBuilder 7 | { 8 | public SqlMergeScriptResults BuildSqlMergeScripts(SqlBulkHelpersTableDefinition tableDefinition, SqlBulkHelpersMergeAction mergeAction) 9 | { 10 | //NOTE: BBernard - This temp table name MUST begin with 1 (and only 1) hash "#" to ensure it is a Transaction Scoped table! 11 | var tempStagingTableName = $"#SqlBulkHelpers_STAGING_TABLE_{Guid.NewGuid()}"; 12 | var tempOutputIdentityTableName = $"#SqlBulkHelpers_OUTPUT_IDENTITY_TABLE_{Guid.NewGuid()}"; 13 | var identityColumnName = tableDefinition.IdentityColumn?.ColumnName ?? String.Empty; 14 | 15 | var columnNamesListWithoutIdentity = tableDefinition.GetColumnNames(false); 16 | var columnNamesWithoutIdentityCSV = columnNamesListWithoutIdentity.Select(c => $"[{c}]").ToCSV(); 17 | 18 | //Initialize/Create the Staging Table! 19 | String sqlScriptToInitializeTempTables = $@" 20 | SELECT TOP(0) 21 | -1 as [{identityColumnName}], 22 | {columnNamesWithoutIdentityCSV}, 23 | -1 as [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] 24 | INTO [{tempStagingTableName}] 25 | FROM [{tableDefinition.TableFullyQualifiedName}]; 26 | 27 | ALTER TABLE [{tempStagingTableName}] ADD PRIMARY KEY ([{identityColumnName}]); 28 | 29 | SELECT TOP(0) 30 | CAST('' AS nvarchar(10)) as [MERGE_ACTION], 31 | CAST(-1 AS int) as [IDENTITY_ID], 32 | CAST(-1 AS int) [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] 33 | INTO [{tempOutputIdentityTableName}]; 34 | "; 35 | 36 | 37 | //NOTE: This is ALL now completed very efficiently on the Sql Server Database side with 38 | // NO unnecessary round trips to the Database! 39 | var mergeInsertSql = String.Empty; 40 | if (mergeAction.HasFlag(SqlBulkHelpersMergeAction.Insert)) 41 | { 42 | mergeInsertSql = $@" 43 | WHEN NOT MATCHED BY TARGET THEN 44 | INSERT ({columnNamesWithoutIdentityCSV}) 45 | VALUES ({columnNamesListWithoutIdentity.Select(c => $"source.[{c}]").ToCSV()}) 46 | "; 47 | } 48 | 49 | var mergeUpdateSql = String.Empty; 50 | if (mergeAction.HasFlag(SqlBulkHelpersMergeAction.Update)) 51 | { 52 | mergeUpdateSql = $@" 53 | WHEN MATCHED THEN 54 | UPDATE SET {columnNamesListWithoutIdentity.Select(c => $"target.[{c}] = source.[{c}]").ToCSV()} 55 | "; 56 | } 57 | 58 | //Build the FULL Dynamic Merge Script here... 59 | //BBernard - 2019-08-07 60 | //NOTE: We now sort on the RowNumber column that we define; this FIXES issue with SqlBulkCopy.WriteToServer() 61 | // where the order of data being written is NOT guaranteed, and there is still no support for the ORDER() hint. 62 | // In general it results in inverting the order of data being sent in Bulk which then resulted in Identity 63 | // values being incorrect based on the order of data specified. 64 | String sqlScriptToExecuteMergeProcess = $@" 65 | MERGE [{tableDefinition.TableFullyQualifiedName}] as target 66 | USING ( 67 | SELECT TOP 100 PERCENT * 68 | FROM [{tempStagingTableName}] 69 | ORDER BY [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] ASC 70 | ) as source 71 | ON target.[{identityColumnName}] = source.[{identityColumnName}] 72 | {mergeUpdateSql} 73 | {mergeInsertSql} 74 | OUTPUT $action, INSERTED.[{identityColumnName}], source.[{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] 75 | INTO [{tempOutputIdentityTableName}] ([MERGE_ACTION], [IDENTITY_ID], [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}]); 76 | 77 | SELECT 78 | [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}], 79 | [IDENTITY_ID], 80 | [MERGE_ACTION] 81 | FROM [{tempOutputIdentityTableName}] 82 | ORDER BY [{SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME}] ASC; 83 | 84 | DROP TABLE [{tempStagingTableName}]; 85 | DROP TABLE [{tempOutputIdentityTableName}]; 86 | "; 87 | 88 | return new SqlMergeScriptResults( 89 | tempStagingTableName, 90 | tempOutputIdentityTableName, 91 | sqlScriptToInitializeTempTables, 92 | sqlScriptToExecuteMergeProcess 93 | ); 94 | } 95 | } 96 | 97 | public class SqlMergeScriptResults 98 | { 99 | public SqlMergeScriptResults(String tempStagingTableName, String tempOutputTableName, String tempTableScript, String mergeProcessScript) 100 | { 101 | this.SqlScriptToInitializeTempTables = tempTableScript; 102 | this.SqlScriptToExecuteMergeProcess = mergeProcessScript; 103 | this.TempStagingTableName = tempStagingTableName; 104 | this.TempOutputTableName = tempOutputTableName; 105 | } 106 | 107 | public String TempOutputTableName { get; private set; } 108 | public String TempStagingTableName { get; private set; } 109 | public String SqlScriptToInitializeTempTables { get; private set; } 110 | public String SqlScriptToExecuteMergeProcess { get; private set; } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /DotNetFramework.SqlBulkHelpers/SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.Linq; 5 | 6 | namespace SqlBulkHelpers 7 | { 8 | /// 9 | /// BBernard 10 | /// DB Schema Loader class to keep the responsibility for Loading the Schema Definitions of Sql Server tables in on only one class; 11 | /// but also supports custom implementations that would initialize the DB Schema Definitions in any other custom way. 12 | /// 13 | /// The Default implementation will load the Database schema with Lazy/Deferred loading for performance, but it will use the Sql Connection Provider 14 | /// specified in the first instance that this class is initialized from because the Schema Definitions will be statically cached across 15 | /// all instances for high performance! 16 | /// 17 | /// NOTE: The static caching of the DB Schema is great for performance, and this default implementation will work well for most users (e.g. single database use), 18 | /// however more advanced usage may require the consumer/author to implement & manage their own ISqlBulkHelpersDBSchemaLoader. 19 | /// 20 | public class SqlBulkHelpersDBSchemaStaticLoader : ISqlBulkHelpersDBSchemaLoader 21 | { 22 | private static Lazy> _tableDefinitionsLookupLazy; 23 | private static readonly object _padlock = new object(); 24 | 25 | /// 26 | /// Provides a Default instance of the Sql Bulk Helpers DB Schema Loader that uses Static/Lazy loading for high performance. 27 | /// NOTE: This will use the Default instance of the SqlBulkHelpersConnectionProvider as it's dependency. 28 | /// 29 | public static ISqlBulkHelpersDBSchemaLoader Default = new SqlBulkHelpersDBSchemaStaticLoader(SqlBulkHelpersConnectionProvider.Default); 30 | 31 | public SqlBulkHelpersDBSchemaStaticLoader(ISqlBulkHelpersConnectionProvider sqlConnectionProvider) 32 | { 33 | sqlConnectionProvider.AssertArgumentNotNull(nameof(sqlConnectionProvider)); 34 | 35 | //Lock the padlock to safely initialize the Lazy<> loader for Table Definition Schemas, but only if it hasn't yet been initialized! 36 | //NOTE: We use a Lazy<> here so that our manual locking does as little work as possible and simply initializes the Lazy<> reference, 37 | // leaving the optimized locking for execution of the long-running logic to the underlying Lazy<> object to manage with 38 | // maximum efficiency 39 | //NOTE: Once initialized we will only have a null check before the lock can be released making this completely safe but still very lightweight. 40 | lock (_padlock) 41 | { 42 | if (_tableDefinitionsLookupLazy != null) 43 | { 44 | _tableDefinitionsLookupLazy = new Lazy>(() => 45 | { 46 | //Get a local reference so that it's scoping will be preserved... 47 | var localScopeSqlConnectionProviderRef = sqlConnectionProvider; 48 | var dbSchemaResults = LoadSqlBulkHelpersDBSchemaHelper(localScopeSqlConnectionProviderRef); 49 | return dbSchemaResults; 50 | }); 51 | } 52 | } 53 | } 54 | 55 | /// 56 | /// BBernard 57 | /// Add all table and their columns from the database into the dictionary in a fully Thread Safe manner using 58 | /// the Static Constructor! 59 | /// 60 | private ILookup LoadSqlBulkHelpersDBSchemaHelper(ISqlBulkHelpersConnectionProvider sqlConnectionProvider) 61 | { 62 | var tableSchemaSql = @" 63 | SELECT 64 | [TABLE_SCHEMA] as TableSchema, 65 | [TABLE_NAME] as TableName, 66 | [Columns] = ( 67 | SELECT 68 | COLUMN_NAME as ColumnName, 69 | ORDINAL_POSITION as OrdinalPosition, 70 | DATA_TYPE as DataType, 71 | COLUMNPROPERTY(OBJECT_ID(table_schema+'.'+table_name), COLUMN_NAME, 'IsIdentity') as IsIdentityColumn 72 | FROM INFORMATION_SCHEMA.COLUMNS c 73 | WHERE 74 | c.TABLE_NAME = t.TABLE_NAME 75 | and c.TABLE_SCHEMA = t.TABLE_SCHEMA 76 | and c.TABLE_CATALOG = t.TABLE_CATALOG 77 | ORDER BY c.ORDINAL_POSITION 78 | FOR JSON PATH 79 | ) 80 | FROM INFORMATION_SCHEMA.TABLES t 81 | ORDER BY t.TABLE_NAME 82 | FOR JSON PATH 83 | "; 84 | 85 | using (SqlConnection sqlConn = sqlConnectionProvider.NewConnection()) 86 | using (SqlCommand sqlCmd = new SqlCommand(tableSchemaSql, sqlConn)) 87 | { 88 | var tableDefinitionsList = sqlCmd.ExecuteForJson>(); 89 | 90 | //Dynamically convert to a Lookup for immutable cache of data. 91 | //NOTE: Lookup is immutable (vs Dictionary which is not) and performance for lookups is just as fast. 92 | var tableDefinitionsLookup = tableDefinitionsList.Where(t => t != null).ToLookup(t => t.TableFullyQualifiedName.ToLowerInvariant()); 93 | return tableDefinitionsLookup; 94 | } 95 | } 96 | 97 | public virtual SqlBulkHelpersTableDefinition GetTableSchemaDefinition(String tableName) 98 | { 99 | if (String.IsNullOrEmpty(tableName)) return null; 100 | 101 | var schemaLookup = _tableDefinitionsLookupLazy.Value; 102 | var tableDefinition = schemaLookup[tableName.ToLowerInvariant()]?.FirstOrDefault(); 103 | return tableDefinition; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/CustomExtensions/SqlBulkHelpersCustomExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.Data.SqlClient; 7 | 8 | namespace SqlBulkHelpers.CustomExtensions 9 | { 10 | public static class SqlBulkHelpersPublicCustomExtensions 11 | { 12 | private const int MaxTableNameLength = 116; 13 | 14 | public static T AssertArgumentIsNotNull(this T arg, string argName) 15 | { 16 | if (arg == null) throw new ArgumentNullException(argName); 17 | return arg; 18 | } 19 | 20 | public static string AssertArgumentIsNotNullOrWhiteSpace(this string arg, string argName) 21 | { 22 | if (string.IsNullOrWhiteSpace(arg)) throw new ArgumentNullException(argName); 23 | return arg; 24 | } 25 | 26 | public static async Task EnsureSqlConnectionIsOpenAsync(this SqlConnection sqlConnection) 27 | { 28 | sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); 29 | if (sqlConnection.State != ConnectionState.Open) 30 | await sqlConnection.OpenAsync().ConfigureAwait(false); 31 | 32 | return sqlConnection; 33 | } 34 | 35 | public static SqlConnection EnsureSqlConnectionIsOpen(this SqlConnection sqlConnection) 36 | { 37 | sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); 38 | if (sqlConnection.State != ConnectionState.Open) 39 | sqlConnection.Open(); 40 | 41 | return sqlConnection; 42 | } 43 | 44 | public static TableNameTerm GetSqlBulkHelpersMappedTableNameTerm(this Type type, string tableNameOverride = null) 45 | { 46 | string tableName = tableNameOverride; 47 | if (string.IsNullOrWhiteSpace(tableName)) 48 | { 49 | var processingDef = SqlBulkHelpersProcessingDefinition.GetProcessingDefinition(type); 50 | if (processingDef.IsMappingLookupEnabled) 51 | tableName = processingDef.MappedDbTableName; 52 | } 53 | 54 | return tableName.ParseAsTableNameTerm(); 55 | } 56 | 57 | public static TableNameTerm ParseAsTableNameTerm(this string tableName) 58 | { 59 | string parsedSchemaName, parsedTableName; 60 | 61 | //Second Try Parsing the Table & Schema name a Direct Lookup and return if found... 62 | var terms = tableName.Split(TableNameTerm.TermSeparator); 63 | switch (terms.Length) 64 | { 65 | //Split will always return an array with at least 1 element 66 | case 1: 67 | parsedSchemaName = TableNameTerm.DefaultSchemaName; 68 | parsedTableName = terms[0].TrimTableNameTerm(); 69 | break; 70 | default: 71 | var schemaTerm = terms[0].TrimTableNameTerm(); 72 | parsedSchemaName = schemaTerm ?? TableNameTerm.DefaultSchemaName; 73 | parsedTableName = terms[1].TrimTableNameTerm(); 74 | break; 75 | } 76 | 77 | if(parsedTableName == null) 78 | throw new ArgumentException("The Table Name specified could not be parsed; parsing resulted in null/empty value."); 79 | 80 | return new TableNameTerm(parsedSchemaName, parsedTableName); 81 | } 82 | 83 | public static string TrimTableNameTerm(this string term) 84 | { 85 | if (string.IsNullOrWhiteSpace(term)) 86 | return null; 87 | 88 | var trimmedTerm = term.Trim('[', ']', ' '); 89 | return trimmedTerm; 90 | } 91 | 92 | public static string EnforceUnderscoreTableNameTerm(this string term) 93 | => term?.Replace(" ", "_"); 94 | 95 | public static string QualifySqlTerm(this string term) 96 | { 97 | return string.IsNullOrWhiteSpace(term) 98 | ? null 99 | : $"[{term.TrimTableNameTerm()}]"; 100 | } 101 | 102 | public static IEnumerable QualifySqlTerms(this IEnumerable terms) 103 | => terms.Select(t => t.QualifySqlTerm()); 104 | 105 | public static string MakeTableNameUnique(this string tableNameToMakeUnique, int uniqueTokenLength = 10) 106 | { 107 | if (string.IsNullOrWhiteSpace(tableNameToMakeUnique)) 108 | throw new ArgumentNullException(nameof(tableNameToMakeUnique)); 109 | 110 | var uniqueTokenSuffix = string.Concat("_", IdGenerator.NewId(uniqueTokenLength)); 111 | var uniqueName = string.Concat(tableNameToMakeUnique, uniqueTokenSuffix); 112 | 113 | if (uniqueName.Length > MaxTableNameLength) 114 | uniqueName = string.Concat(tableNameToMakeUnique.Substring(0, MaxTableNameLength - uniqueTokenSuffix.Length), uniqueTokenSuffix); 115 | 116 | return uniqueName; 117 | } 118 | 119 | public static string ToCsv(this IEnumerable enumerableList) 120 | => string.Join(", ", enumerableList); 121 | 122 | public static bool HasAny(this IEnumerable items) 123 | => items != null && items.Any(); 124 | 125 | public static bool IsNullOrEmpty(this IEnumerable items) 126 | => !items.HasAny(); 127 | 128 | public static T[] AsArray(this IEnumerable items) 129 | => items is T[] itemArray ? itemArray : items.ToArray(); 130 | 131 | public static bool ContainsIgnoreCase(this IEnumerable items, string valueToFind) 132 | => items != null && items.Any(i => i.Equals(valueToFind, StringComparison.OrdinalIgnoreCase)); 133 | 134 | public static bool TryAdd(this Dictionary dictionary, TKey key, TValue value) 135 | { 136 | if (dictionary.ContainsKey(key)) return false; 137 | dictionary.Add(key, value); 138 | return true; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/QueryProcessing/SqlBulkHelpersDataReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using FastMember; 6 | using SqlBulkHelpers.CustomExtensions; 7 | 8 | namespace SqlBulkHelpers 9 | { 10 | internal sealed class SqlBulkHelpersDataReader : IDataReader, IDisposable 11 | { 12 | private readonly PropInfoDefinition[] _processingFields; 13 | private readonly TypeAccessor _fastTypeAccessor = TypeAccessor.Create(typeof(T)); 14 | private readonly int _rowNumberPseudoColumnOrdinal; 15 | 16 | private IEnumerator _dataEnumerator; 17 | private Dictionary _processingDefinitionOrdinalDictionary; 18 | private int _entityCounter = 0; 19 | 20 | public SqlBulkHelpersDataReader(IEnumerable entityData, SqlBulkHelpersProcessingDefinition processingDefinition, SqlBulkHelpersTableDefinition tableDefinition) 21 | { 22 | // ReSharper disable once PossibleMultipleEnumeration 23 | _dataEnumerator = entityData.AssertArgumentIsNotNull(nameof(entityData)).GetEnumerator(); 24 | 25 | processingDefinition.AssertArgumentIsNotNull(nameof(processingDefinition)); 26 | tableDefinition.AssertArgumentIsNotNull(nameof(tableDefinition)); 27 | 28 | _processingFields = processingDefinition.PropertyDefinitions.Where( 29 | p => tableDefinition.FindColumnCaseInsensitive(p.MappedDbColumnName) != null 30 | ).AsArray(); 31 | 32 | _rowNumberPseudoColumnOrdinal = _processingFields.Length; 33 | 34 | //Must ensure we include all Entity data fields as well as the Row Number pseudo-Column... 35 | this.FieldCount = _processingFields.Length + 1; 36 | } 37 | 38 | #region IDataReader Members (Minimal methods to be Implemented as required by SqlBulkCopy) 39 | 40 | public int Depth => 1; 41 | public bool IsClosed => _dataEnumerator == null; 42 | 43 | public bool Read() 44 | { 45 | if (IsClosed) 46 | throw new ObjectDisposedException(GetType().Name); 47 | 48 | _entityCounter++; 49 | return _dataEnumerator.MoveNext(); 50 | } 51 | 52 | public int GetOrdinal(string dbColumnName) 53 | { 54 | //Lazy Load the Ordinal reverse lookup dictionary (ONLY if needed) 55 | if (_processingDefinitionOrdinalDictionary == null) 56 | { 57 | //Populate our Property Ordinal reverse lookup dictionary... 58 | int i = 0; 59 | _processingDefinitionOrdinalDictionary = new Dictionary(); 60 | foreach (var propDef in _processingFields) 61 | _processingDefinitionOrdinalDictionary[propDef.MappedDbColumnName] = i++; 62 | } 63 | 64 | if (SqlBulkHelpersConstants.ROWNUMBER_COLUMN_NAME.Equals(dbColumnName)) 65 | return _rowNumberPseudoColumnOrdinal; 66 | else if (_processingDefinitionOrdinalDictionary.TryGetValue(dbColumnName, out var ordinalIndex)) 67 | return ordinalIndex; 68 | 69 | throw new ArgumentOutOfRangeException($"Property name [{dbColumnName}] could not be found."); 70 | } 71 | 72 | public object GetValue(int i) 73 | { 74 | if (_dataEnumerator == null) 75 | throw new ObjectDisposedException(GetType().Name); 76 | 77 | //Handle RowNumber Pseudo Column... 78 | if (i == _rowNumberPseudoColumnOrdinal) 79 | return _entityCounter; 80 | 81 | //Otherwise retrieve from our Entity Data model... 82 | var fieldDefinition = _processingFields[i]; 83 | var fieldValue = _fastTypeAccessor[_dataEnumerator.Current, fieldDefinition.PropertyName]; 84 | 85 | return fieldValue; 86 | } 87 | 88 | //Must ensure we include all Entity data fields as well as the Row Number pseudo-Column... 89 | public int FieldCount { get; } 90 | 91 | public void Close() => Dispose(); 92 | public bool NextResult() => false; 93 | 94 | #endregion 95 | 96 | #region IDisposable Members 97 | 98 | public void Dispose() 99 | { 100 | Dispose(true); 101 | GC.SuppressFinalize(this); 102 | } 103 | 104 | private void Dispose(bool disposing) 105 | { 106 | if (!disposing) return; 107 | _dataEnumerator?.Dispose(); 108 | _dataEnumerator = null; 109 | } 110 | 111 | #endregion 112 | 113 | #region Not Implemented Members 114 | 115 | public int RecordsAffected => -1; 116 | 117 | private static TValue ThrowNotImplementedException() => throw new NotImplementedException(); 118 | 119 | public DataTable GetSchemaTable() => ThrowNotImplementedException(); 120 | 121 | public bool GetBoolean(int i) => ThrowNotImplementedException(); 122 | public byte GetByte(int i) => ThrowNotImplementedException(); 123 | public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) => ThrowNotImplementedException(); 124 | public char GetChar(int i) => ThrowNotImplementedException(); 125 | public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) => ThrowNotImplementedException(); 126 | public IDataReader GetData(int i) => ThrowNotImplementedException(); 127 | public string GetDataTypeName(int i) => ThrowNotImplementedException(); 128 | public DateTime GetDateTime(int i) => ThrowNotImplementedException(); 129 | public decimal GetDecimal(int i) => ThrowNotImplementedException(); 130 | public double GetDouble(int i) => ThrowNotImplementedException(); 131 | public Type GetFieldType(int i) => ThrowNotImplementedException(); 132 | public float GetFloat(int i) => ThrowNotImplementedException(); 133 | public Guid GetGuid(int i) => ThrowNotImplementedException(); 134 | public short GetInt16(int i) => ThrowNotImplementedException(); 135 | public int GetInt32(int i) => ThrowNotImplementedException(); 136 | public long GetInt64(int i) => ThrowNotImplementedException(); 137 | public string GetName(int i) => ThrowNotImplementedException(); 138 | public string GetString(int i) => ThrowNotImplementedException(); 139 | public int GetValues(object[] values) => ThrowNotImplementedException(); 140 | public bool IsDBNull(int i) => ThrowNotImplementedException(); 141 | public object this[string name] => ThrowNotImplementedException(); 142 | public object this[int i] => ThrowNotImplementedException(); 143 | 144 | #endregion 145 | }} 146 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /SqlBulkHelpers.Tests/IntegrationTests/MaterializeDataTests/TableIdentityColumnApiTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SqlBulkHelpers.Tests; 3 | using Microsoft.Data.SqlClient; 4 | using SqlBulkHelpers.MaterializedData; 5 | 6 | namespace SqlBulkHelpers.Tests.IntegrationTests.MaterializeDataTests 7 | { 8 | [TestClass] 9 | public class BulkHelpersMetadataMethodTests : BaseTest 10 | { 11 | [TestMethod] 12 | public async Task TestGetTableCurrentIdentityValueFromSqlTransactionSyncAndAsync() 13 | { 14 | var sqlConnectionString = SqlConnectionHelper.GetSqlConnectionString(); 15 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 16 | 17 | long asyncCurrentIdentityValue = -1; 18 | long syncCurrentIdentityValue = -1; 19 | 20 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 21 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 22 | { 23 | asyncCurrentIdentityValue = await sqlTrans.GetTableCurrentIdentityValueAsync(TestHelpers.TestTableName).ConfigureAwait(false); 24 | } 25 | 26 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 27 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 28 | { 29 | // ReSharper disable once MethodHasAsyncOverload 30 | syncCurrentIdentityValue = sqlTrans.GetTableCurrentIdentityValue(TestHelpers.TestTableName); 31 | } 32 | 33 | Assert.IsTrue(asyncCurrentIdentityValue > 0); 34 | Assert.IsTrue(syncCurrentIdentityValue > 0); 35 | Assert.AreEqual(asyncCurrentIdentityValue, syncCurrentIdentityValue); 36 | } 37 | 38 | [TestMethod] 39 | public async Task TestGetTableCurrentIdentityValueFromSqlConnectionSyncAndAsync() 40 | { 41 | var sqlConnectionString = SqlConnectionHelper.GetSqlConnectionString(); 42 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 43 | 44 | long asyncCurrentIdentityValueFromTrans = -1; 45 | long syncCurrentIdentityValueFromConn = -1; 46 | 47 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 48 | { 49 | asyncCurrentIdentityValueFromTrans = await sqlConn.GetTableCurrentIdentityValueAsync(TestHelpers.TestTableName).ConfigureAwait(false); 50 | } 51 | 52 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 53 | { 54 | // ReSharper disable once MethodHasAsyncOverload 55 | syncCurrentIdentityValueFromConn = sqlConn.GetTableCurrentIdentityValue(TestHelpers.TestTableName); 56 | } 57 | 58 | Assert.IsTrue(asyncCurrentIdentityValueFromTrans > 0); 59 | Assert.IsTrue(syncCurrentIdentityValueFromConn > 0); 60 | Assert.AreEqual(asyncCurrentIdentityValueFromTrans, syncCurrentIdentityValueFromConn); 61 | } 62 | 63 | [TestMethod] 64 | public async Task TestReSeedTableIdentityValueFromSqlTransactionSyncAndAsync() 65 | { 66 | var sqlConnectionString = SqlConnectionHelper.GetSqlConnectionString(); 67 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 68 | 69 | long initialIdentitySeedValue = 0; 70 | var firstNewIdentitySeedValue = 777888; 71 | var secondNewIdentitySeedValue = 888999; 72 | 73 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 74 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 75 | { 76 | initialIdentitySeedValue = await sqlTrans.GetTableCurrentIdentityValueAsync(TestHelpers.TestTableName).ConfigureAwait(false); 77 | Assert.IsTrue(initialIdentitySeedValue > 0); 78 | Assert.AreNotEqual(initialIdentitySeedValue, firstNewIdentitySeedValue); 79 | 80 | await sqlTrans.ReSeedTableIdentityValueAsync(TestHelpers.TestTableName, firstNewIdentitySeedValue).ConfigureAwait(false); 81 | var asyncUpdatedIdentityValue = await sqlTrans.GetTableCurrentIdentityValueAsync(TestHelpers.TestTableName).ConfigureAwait(false); 82 | Assert.AreEqual(asyncUpdatedIdentityValue, firstNewIdentitySeedValue); 83 | Assert.AreNotEqual(initialIdentitySeedValue, asyncUpdatedIdentityValue); 84 | 85 | await sqlTrans.CommitAsync().ConfigureAwait(false); 86 | } 87 | 88 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 89 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 90 | { 91 | // ReSharper disable once MethodHasAsyncOverload 92 | var syncCurrentIdentityValue = sqlTrans.GetTableCurrentIdentityValue(TestHelpers.TestTableName); 93 | Assert.AreEqual(firstNewIdentitySeedValue, syncCurrentIdentityValue); 94 | 95 | await sqlTrans.ReSeedTableIdentityValueAsync(TestHelpers.TestTableName, secondNewIdentitySeedValue).ConfigureAwait(false); 96 | // ReSharper disable once MethodHasAsyncOverload 97 | var syncUpdatedIdentityValue = sqlTrans.GetTableCurrentIdentityValue(TestHelpers.TestTableName); 98 | Assert.AreEqual(syncUpdatedIdentityValue, secondNewIdentitySeedValue); 99 | Assert.AreNotEqual(syncUpdatedIdentityValue, initialIdentitySeedValue); 100 | 101 | await sqlTrans.ReSeedTableIdentityValueAsync(TestHelpers.TestTableName, initialIdentitySeedValue).ConfigureAwait(false); 102 | sqlTrans.Commit(); 103 | } 104 | } 105 | 106 | [TestMethod] 107 | public async Task TestReSeedTableIdentityValueWithMaxIdFromSqlTransactionSyncAndAsync() 108 | { 109 | var sqlConnectionString = SqlConnectionHelper.GetSqlConnectionString(); 110 | ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); 111 | 112 | var firstNewIdentitySeedValue = 555888; 113 | var secondNewIdentitySeedValue = 888444; 114 | 115 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 116 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 117 | { 118 | await sqlTrans.ReSeedTableIdentityValueAsync(TestHelpers.TestTableName, firstNewIdentitySeedValue).ConfigureAwait(false); 119 | var asyncInitialIdentityValue = await sqlTrans.GetTableCurrentIdentityValueAsync(TestHelpers.TestTableName).ConfigureAwait(false); 120 | Assert.AreEqual(firstNewIdentitySeedValue, asyncInitialIdentityValue); 121 | 122 | var maxId = await sqlTrans.ReSeedTableIdentityValueWithMaxIdAsync(TestHelpers.TestTableName); 123 | Assert.IsTrue(maxId > 0); 124 | 125 | var asyncMaxIdIdentityValue = await sqlTrans.GetTableCurrentIdentityValueAsync(TestHelpers.TestTableName).ConfigureAwait(false); 126 | Assert.AreEqual(maxId, asyncMaxIdIdentityValue); 127 | Assert.AreNotEqual(asyncInitialIdentityValue, asyncMaxIdIdentityValue); 128 | 129 | await sqlTrans.CommitAsync().ConfigureAwait(false); 130 | } 131 | 132 | await using (var sqlConn = await sqlConnectionProvider.NewConnectionAsync().ConfigureAwait(false)) 133 | await using (var sqlTrans = (SqlTransaction)await sqlConn.BeginTransactionAsync().ConfigureAwait(false)) 134 | { 135 | // ReSharper disable once MethodHasAsyncOverload 136 | sqlTrans.ReSeedTableIdentityValue(TestHelpers.TestTableName, secondNewIdentitySeedValue); 137 | // ReSharper disable once MethodHasAsyncOverload 138 | var syncInitialIdentityValue = sqlTrans.GetTableCurrentIdentityValue(TestHelpers.TestTableName); 139 | Assert.AreEqual(secondNewIdentitySeedValue, syncInitialIdentityValue); 140 | 141 | // ReSharper disable once MethodHasAsyncOverload 142 | var maxId = sqlTrans.ReSeedTableIdentityValueWithMaxId(TestHelpers.TestTableName); 143 | Assert.IsTrue(maxId > 0); 144 | 145 | // ReSharper disable once MethodHasAsyncOverload 146 | var syncMaxIdIdentityValue = sqlTrans.GetTableCurrentIdentityValue(TestHelpers.TestTableName); 147 | Assert.AreEqual(maxId, syncMaxIdIdentityValue); 148 | Assert.AreNotEqual(syncInitialIdentityValue, syncMaxIdIdentityValue); 149 | 150 | sqlTrans.Commit(); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /NetStandard.SqlBulkHelpers/SqlBulkHelper/SqlBulkHelper.Invocation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using Microsoft.Data.SqlClient; 5 | using System.Threading.Tasks; 6 | 7 | namespace SqlBulkHelpers 8 | { 9 | internal partial class SqlBulkHelper : BaseSqlBulkHelper where T : class 10 | { 11 | #region Constructors 12 | 13 | /// 14 | public SqlBulkHelper(ISqlBulkHelpersConfig bulkHelpersConfig = null) 15 | : base(bulkHelpersConfig) 16 | { 17 | } 18 | 19 | #endregion 20 | 21 | #region Async ISqlBulkHelper implementations 22 | 23 | public virtual async Task> BulkInsertAsync( 24 | IEnumerable entityList, 25 | SqlTransaction sqlTransaction, 26 | string tableNameParam = null, 27 | SqlMergeMatchQualifierExpression matchQualifierExpression = null, 28 | bool enableIdentityValueInsert = false 29 | ) 30 | { 31 | return await BulkInsertOrUpdateInternalAsync( 32 | entityList, 33 | SqlBulkHelpersMergeAction.Insert, 34 | sqlTransaction, 35 | tableNameParam: tableNameParam, 36 | matchQualifierExpressionParam: matchQualifierExpression, 37 | enableIdentityInsert: enableIdentityValueInsert 38 | ).ConfigureAwait(false); 39 | } 40 | 41 | public virtual async Task> BulkUpdateAsync( 42 | IEnumerable entityList, 43 | SqlTransaction sqlTransaction, 44 | string tableNameParam = null, 45 | SqlMergeMatchQualifierExpression matchQualifierExpression = null, 46 | bool enableIdentityValueInsert = false 47 | ) 48 | { 49 | return await BulkInsertOrUpdateInternalAsync( 50 | entityList, 51 | SqlBulkHelpersMergeAction.Update, 52 | sqlTransaction, 53 | tableNameParam: tableNameParam, 54 | matchQualifierExpressionParam: matchQualifierExpression, 55 | enableIdentityInsert: enableIdentityValueInsert 56 | ).ConfigureAwait(false); 57 | } 58 | 59 | public virtual async Task> BulkInsertOrUpdateAsync( 60 | IEnumerable entityList, 61 | SqlTransaction sqlTransaction, 62 | string tableNameParam = null, 63 | SqlMergeMatchQualifierExpression matchQualifierExpression = null, 64 | bool enableIdentityValueInsert = false 65 | ) 66 | { 67 | return await BulkInsertOrUpdateInternalAsync( 68 | entityList, 69 | SqlBulkHelpersMergeAction.InsertOrUpdate, 70 | sqlTransaction, 71 | tableNameParam: tableNameParam, 72 | matchQualifierExpressionParam: matchQualifierExpression, 73 | enableIdentityInsert: enableIdentityValueInsert 74 | ).ConfigureAwait(false); 75 | } 76 | 77 | /// 78 | /// Retrieve the Schema Definition for the specified Table using either an SqlConnection or with an existing SqlTransaction. 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 84 | /// 85 | /// 86 | public async Task GetTableSchemaDefinitionAsync( 87 | SqlConnection sqlConnection, 88 | SqlTransaction sqlTransaction = null, 89 | string tableNameOverride = null, 90 | TableSchemaDetailLevel detailLevel = TableSchemaDetailLevel.ExtendedDetails, 91 | bool forceCacheReload = false 92 | ) 93 | { 94 | var tableDefinition = await this.GetTableSchemaDefinitionInternalAsync( 95 | detailLevel, 96 | sqlConnection, 97 | sqlTransaction, 98 | tableNameOverride, 99 | forceCacheReload 100 | ).ConfigureAwait(false); 101 | 102 | return tableDefinition; 103 | } 104 | 105 | 106 | /// 107 | /// Retrieve the Current Identity Value for the specified Table or Table mapped by Model (annotation). 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | public async Task GetTableCurrentIdentityValueAsync( 114 | SqlConnection sqlConnection, 115 | SqlTransaction sqlTransaction = null, 116 | string tableNameOverride = null 117 | ) 118 | { 119 | using (var sqlCmd = new SqlCommand("SELECT CURRENT_IDENTITY_VALUE = IDENT_CURRENT(@TableName)", sqlConnection, sqlTransaction)) 120 | { 121 | var fullyQualifiedTableName = GetMappedTableNameTerm(tableNameOverride).FullyQualifiedTableName; 122 | sqlCmd.Parameters.Add("@TableName", SqlDbType.NVarChar).Value = fullyQualifiedTableName; 123 | var currentIdentityValue = Convert.ToInt64(await sqlCmd.ExecuteScalarAsync().ConfigureAwait(false)); 124 | return currentIdentityValue; 125 | } 126 | } 127 | 128 | #endregion 129 | 130 | #region Sync ISqlBulkHelper implementations 131 | 132 | public virtual IEnumerable BulkInsert( 133 | IEnumerable entityList, 134 | SqlTransaction sqlTransaction, 135 | string tableNameParam = null, 136 | SqlMergeMatchQualifierExpression matchQualifierExpression = null, 137 | bool enableIdentityValueInsert = false 138 | ) 139 | { 140 | return BulkInsertOrUpdateInternal( 141 | entityList, 142 | SqlBulkHelpersMergeAction.Insert, 143 | sqlTransaction, 144 | tableNameParam: tableNameParam, 145 | matchQualifierExpressionParam: matchQualifierExpression, 146 | enableIdentityInsert: enableIdentityValueInsert 147 | ); 148 | } 149 | 150 | public virtual IEnumerable BulkUpdate( 151 | IEnumerable entityList, 152 | SqlTransaction sqlTransaction, 153 | string tableNameParam = null, 154 | SqlMergeMatchQualifierExpression matchQualifierExpression = null, 155 | bool enableIdentityValueInsert = false 156 | ) 157 | { 158 | return BulkInsertOrUpdateInternal( 159 | entityList, 160 | SqlBulkHelpersMergeAction.Update, 161 | sqlTransaction, 162 | tableNameParam: tableNameParam, 163 | matchQualifierExpressionParam: matchQualifierExpression, 164 | enableIdentityInsert: enableIdentityValueInsert 165 | ); 166 | } 167 | 168 | public virtual IEnumerable BulkInsertOrUpdate( 169 | IEnumerable entityList, 170 | SqlTransaction sqlTransaction, 171 | string tableNameParam = null, 172 | SqlMergeMatchQualifierExpression matchQualifierExpression = null, 173 | bool enableIdentityValueInsert = false 174 | ) 175 | { 176 | return BulkInsertOrUpdateInternal( 177 | entityList, 178 | SqlBulkHelpersMergeAction.InsertOrUpdate, 179 | sqlTransaction, 180 | tableNameParam: tableNameParam, 181 | matchQualifierExpressionParam: matchQualifierExpression, 182 | enableIdentityInsert: enableIdentityValueInsert 183 | ); 184 | } 185 | 186 | /// 187 | /// Retrieve the Schema Definition for the specified Table using either an SqlConnection or with an existing SqlTransaction. 188 | /// 189 | /// 190 | /// 191 | /// 192 | /// 193 | /// 194 | /// 195 | public SqlBulkHelpersTableDefinition GetTableSchemaDefinition( 196 | SqlConnection sqlConnection, 197 | SqlTransaction sqlTransaction = null, 198 | string tableNameOverride = null, 199 | TableSchemaDetailLevel detailLevel = TableSchemaDetailLevel.ExtendedDetails, 200 | bool forceCacheReload = false 201 | ) 202 | { 203 | var tableDefinition = this.GetTableSchemaDefinitionInternal( 204 | detailLevel, 205 | sqlConnection, 206 | sqlTransaction, 207 | tableNameOverride, 208 | forceCacheReload 209 | ); 210 | return tableDefinition; 211 | } 212 | 213 | #endregion 214 | } 215 | } 216 | --------------------------------------------------------------------------------