├── 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