diff --git a/NetStandard.SqlBulkHelpers/CustomExtensions/SqlBulkHelpersCustomExtensions.cs b/NetStandard.SqlBulkHelpers/CustomExtensions/SqlBulkHelpersCustomExtensions.cs index d930b24..ff3be68 100644 --- a/NetStandard.SqlBulkHelpers/CustomExtensions/SqlBulkHelpersCustomExtensions.cs +++ b/NetStandard.SqlBulkHelpers/CustomExtensions/SqlBulkHelpersCustomExtensions.cs @@ -32,6 +32,15 @@ public static async Task EnsureSqlConnectionIsOpenAsync(this SqlC return sqlConnection; } + public static SqlConnection EnsureSqlConnectionIsOpen(this SqlConnection sqlConnection) + { + sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); + if (sqlConnection.State != ConnectionState.Open) + sqlConnection.Open(); + + return sqlConnection; + } + public static TableNameTerm GetSqlBulkHelpersMappedTableNameTerm(this Type type, string tableNameOverride = null) { string tableName = tableNameOverride; diff --git a/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Async.cs b/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Async.cs index e4ad45a..7caa77a 100644 --- a/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Async.cs +++ b/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Async.cs @@ -80,6 +80,51 @@ public async Task GetTableSchemaDefinitionAsync( { sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); + var tableDefinition = await GetTableSchemaDefinitionInternalAsync( + tableName, + detailLevel, + () => Task.FromResult((sqlConnection, sqlTransaction)), + disposeOfConnection: false, //DO Not dispose of Existing Connection/Transaction... + forceCacheReload + ).ConfigureAwait(false); + + return tableDefinition; + } + + public async Task GetTableSchemaDefinitionAsync( + string tableName, + TableSchemaDetailLevel detailLevel, + Func> sqlConnectionAsyncFactory, + bool forceCacheReload = false + ) + { + sqlConnectionAsyncFactory.AssertArgumentIsNotNull(nameof(sqlConnectionAsyncFactory)); + + var tableDefinition = await GetTableSchemaDefinitionInternalAsync( + tableName, + detailLevel, + async () => + { + var sqlConnection = await sqlConnectionAsyncFactory().ConfigureAwait(false); + return (sqlConnection, (SqlTransaction)null); + }, + disposeOfConnection: true, //Always DISPOSE of New Connections created by the Factory... + forceCacheReload + ).ConfigureAwait(false); + + return tableDefinition; + } + + protected async Task GetTableSchemaDefinitionInternalAsync( + string tableName, + TableSchemaDetailLevel detailLevel, + Func> sqlConnectionAndTransactionAsyncFactory, + bool disposeOfConnection, + bool forceCacheReload = false + ) + { + sqlConnectionAndTransactionAsyncFactory.AssertArgumentIsNotNull(nameof(sqlConnectionAndTransactionAsyncFactory)); + if (string.IsNullOrWhiteSpace(tableName)) return null; @@ -93,16 +138,37 @@ public async Task GetTableSchemaDefinitionAsync( key: cacheKey, cacheValueFactoryAsync: async key => { - using (var sqlCmd = CreateSchemaQuerySqlCommand(tableNameTerm, detailLevel, sqlConnection, sqlTransaction)) + var (sqlConnection, sqlTransaction) = await sqlConnectionAndTransactionAsyncFactory().ConfigureAwait(false); + try { - //Execute and load results from the Json... - var tableDef = await sqlCmd.ExecuteForJsonAsync().ConfigureAwait(false); - return tableDef; + //If we don't have a Transaction then offer lazy opening of the Connection, + // but if we do have a Transaction we assume the Connection is open & valid for the Transaction... + if (sqlTransaction == null) + await sqlConnection.EnsureSqlConnectionIsOpenAsync().ConfigureAwait(false); + + using (var sqlCmd = CreateSchemaQuerySqlCommand(tableNameTerm, detailLevel, sqlConnection, sqlTransaction)) + { + //Execute and load results from the Json... + var tableDef = await sqlCmd.ExecuteForJsonAsync().ConfigureAwait(false); + return tableDef; + } + + } + finally + { + #if NETSTANDARD2_1 + if(disposeOfConnection) + await sqlConnection.DisposeAsync().ConfigureAwait(false); + #else + if(disposeOfConnection) + sqlConnection.Dispose(); + #endif } } ).ConfigureAwait(false); return tableDefinition; } + } } diff --git a/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Sync.cs b/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Sync.cs index 79a014b..ff1257d 100644 --- a/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Sync.cs +++ b/NetStandard.SqlBulkHelpers/Database/SqlBulkHelpersDBSchemaLoader.Sync.cs @@ -25,7 +25,48 @@ public SqlBulkHelpersTableDefinition GetTableSchemaDefinition( bool forceCacheReload = false ) { - sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); + sqlConnection.AssertArgumentIsNotNull(nameof(sqlConnection)); + + var tableDefinition = GetTableSchemaDefinitionInternal( + tableName, + detailLevel, + () => (sqlConnection, sqlTransaction), + disposeOfConnection: false, //DO Not dispose of Existing Connection/Transaction... + forceCacheReload + ); + + return tableDefinition; + } + + public SqlBulkHelpersTableDefinition GetTableSchemaDefinition( + string tableName, + TableSchemaDetailLevel detailLevel, + Func sqlConnectionFactory, + bool forceCacheReload = false + ) + { + sqlConnectionFactory.AssertArgumentIsNotNull(nameof(sqlConnectionFactory)); + + var tableDefinition = GetTableSchemaDefinitionInternal( + tableName, + detailLevel, + () => (sqlConnectionFactory(), (SqlTransaction)null), + disposeOfConnection: true, //Always DISPOSE of New Connections created by the Factory... + forceCacheReload + ); + + return tableDefinition; + } + + protected SqlBulkHelpersTableDefinition GetTableSchemaDefinitionInternal( + string tableName, + TableSchemaDetailLevel detailLevel, + Func<(SqlConnection, SqlTransaction)> sqlConnectionAndTransactionFactory, + bool disposeOfConnection, + bool forceCacheReload = false + ) + { + sqlConnectionAndTransactionFactory.AssertArgumentIsNotNull(nameof(sqlConnectionAndTransactionFactory)); if (string.IsNullOrWhiteSpace(tableName)) return null; @@ -40,11 +81,25 @@ public SqlBulkHelpersTableDefinition GetTableSchemaDefinition( key: cacheKey, cacheValueFactory: key => { - using (var sqlCmd = CreateSchemaQuerySqlCommand(tableNameTerm, detailLevel, sqlConnection, sqlTransaction)) + var (sqlConnection, sqlTransaction) = sqlConnectionAndTransactionFactory(); + try + { + //If we don't have a Transaction then offer lazy opening of the Connection, + // but if we do have a Transaction we assume the Connection is open & valid for the Transaction... + if (sqlTransaction == null) + sqlConnection.EnsureSqlConnectionIsOpen(); + + using (var sqlCmd = CreateSchemaQuerySqlCommand(tableNameTerm, detailLevel, sqlConnection, sqlTransaction)) + { + //Execute and load results from the Json... + var tableDef = sqlCmd.ExecuteForJson(); + return tableDef; + } + } + finally { - //Execute and load results from the Json... - var tableDef = sqlCmd.ExecuteForJson(); - return tableDef; + if(disposeOfConnection) + sqlConnection.Dispose(); } }); diff --git a/NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj b/NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj index e6bfd12..83dcaf3 100644 --- a/NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj +++ b/NetStandard.SqlBulkHelpers/NetStandard.SqlBulkHelpers.csproj @@ -13,6 +13,7 @@ A library for easy, efficient and high performance bulk insert and update of data, into a Sql Database, from .Net applications. By leveraging the power of the SqlBulkCopy classes with added support for Identity primary key table columns this library provides a greatly simplified interface to process Identity based Entities with Bulk Performance with the wide compatibility of .NetStandard 2.0. sql server database table bulk insert update identity column sqlbulkcopy orm dapper linq2sql materialization materialized data view materialized-data materialized-view sync replication replica readonly + - Restored support for SqlConnection Factory (simplified now as a Func<SqlConnection> when manually using the SqlDbSchemaLoader to dynamically retrieve Table Schema definitions for performance. - Added support for other Identity column data types including (INT, BIGINT, SMALLINT, & TINYINT); per feature request (https://github.com/cajuncoding/SqlBulkHelpers/issues/10). - Added support to explicitly set Identity Values (aka SET IDENTITY_INSERT ON) via new `enableIdentityInsert` api parameter. - Added support to retreive and re-seed (aka set) the current Identity Value on a given table via new apis in the MaterializedData helpers. @@ -47,7 +48,7 @@ - Added more Integration Tests for Constructors and Connections, as well as the new DB Schema Loader caching implementation. - Fixed bug in dynamic initialization of SqlBulkHelpersConnectionProvider and SqlBulkHelpersDBSchemaLoader when not using the Default instances that automtically load the connection string from the application configuration setting. - 2.2 + 2.2.1 diff --git a/NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersDBSchemaLoader.cs b/NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersDBSchemaLoader.cs index 0daa14f..f58e5a5 100644 --- a/NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersDBSchemaLoader.cs +++ b/NetStandard.SqlBulkHelpers/SqlBulkHelper/Interfaces/ISqlBulkHelpersDBSchemaLoader.cs @@ -14,16 +14,30 @@ Task GetTableSchemaDefinitionAsync( bool forceCacheReload = false ); - ValueTask ClearCacheAsync(); + Task GetTableSchemaDefinitionAsync( + string tableName, + TableSchemaDetailLevel detailLevel, + Func> sqlConnectionAsyncFactory, + bool forceCacheReload = false + ); SqlBulkHelpersTableDefinition GetTableSchemaDefinition( - string tableName, + string tableName, TableSchemaDetailLevel detailLevel, SqlConnection sqlConnection, SqlTransaction sqlTransaction = null, bool forceCacheReload = false ); + SqlBulkHelpersTableDefinition GetTableSchemaDefinition( + string tableName, + TableSchemaDetailLevel detailLevel, + Func sqlConnectionFactory, + bool forceCacheReload = false + ); + + ValueTask ClearCacheAsync(); + void ClearCache(); } } \ No newline at end of file diff --git a/README.md b/README.md index bc67d58..820ba89 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,9 @@ public class TestDataService ## Nuget Package To use in your project, add the [SqlBulkHelpers NuGet package](https://www.nuget.org/packages/SqlBulkHelpers/) to your project. +## v2.2.1 Release Notes: +- Restored support for SqlConnection Factory (simplified now as a Func<SqlConnection> when manually using the SqlDbSchemaLoader to dynamically retrieve Table Schema definitions for performance. + ## v2.2 Release Notes: - Added support for other Identity column data types including (INT, BIGINT, SMALLINT, & TINYINT); per [feature request here](https://github.com/cajuncoding/SqlBulkHelpers/issues/10). - Added support to explicitly set Identity Values (aka SET IDENTITY_INSERT ON) via new `enableIdentityInsert` api parameter. @@ -334,26 +337,29 @@ It offers ability to retrieve basic or extended details; both of which are inter *NOTE: The internal schema caching can be invalidated using the `forceCacheReload` method parameter.* +NOTE: You man use an existing SqlConnection and/or SqlTransaction with this api, however for maximum performance it's recommended to +use a SqlConnection Factory Func so connections are not created at all if the results are already cached... ```csharp + //Normally would be provided by Dependency Injection... + //This is a DI friendly connection factory/provider pattern that can be used... + private readonly ISqlBulkHelpersConnectionProvider _sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); + public async Task GetSanitizedTableName(string tableNameToValidate) { - //Normally would be provided by Dependency Injection... - //This is a DI friendly connection factory/provider pattern that can be used... - ISqlBulkHelpersConnectionProvider sqlConnectionProvider = new SqlBulkHelpersConnectionProvider(sqlConnectionString); - - using (SqlConnection sqlConnection = await sqlConnectionProvider.NewConnectionAsync()) - { - //We can get the basic or extended (slower query) schema details for the table (both types are cached)... - //NOTE: Basic details includes table name, columns, data types, etc. while Extended details includes FKey constraintes, - // Indexes, relationship details, etc. - //NOTE: This is cached, so no DB call is made if it's already been loaded and the forceCacheReload flag is not set to true. - var tableDefinition = await sqlConnection.GetTableSchemaDefinitionAsync(tablNameToValidate, TableSchemaDetailLevel.BasicDetails) + //We can get the basic or extended (slower query) schema details for the table (both types are cached)... + //NOTE: Basic details includes table name, columns, data types, etc. while Extended details includes FKey constraintes, + // Indexes, relationship details, etc. + //NOTE: This is cached, so no DB call is made if it's already been loaded and the forceCacheReload flag is not set to true. + var tableDefinition = await sqlConnection.GetTableSchemaDefinitionAsync( + tablNameToValidate, + TableSchemaDetailLevel.BasicDetails + async () => await _sqlConnectionProvider.NewConnectionAsync() + ); - if (tableDefinition == null) - throw new NullReferenceException($"The Table Definition is null and could not be found for the table name specified [{tableNameToValidate}]."); + if (tableDefinition == null) + throw new NullReferenceException($"The Table Definition is null and could not be found for the table name specified [{tableNameToValidate}]."); - return tableDefinition.TableFullyQualifiedName; - } + return tableDefinition.TableFullyQualifiedName; } ``` diff --git a/SqlBulkHelpers.Tests/IntegrationTests/SchemaLoadingTests/SchemaLoaderCacheTests.cs b/SqlBulkHelpers.Tests/IntegrationTests/SchemaLoadingTests/SchemaLoaderCacheTests.cs index cdb2899..f720f7d 100644 --- a/SqlBulkHelpers.Tests/IntegrationTests/SchemaLoadingTests/SchemaLoaderCacheTests.cs +++ b/SqlBulkHelpers.Tests/IntegrationTests/SchemaLoadingTests/SchemaLoaderCacheTests.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Data.SqlClient; using SqlBulkHelpers.Tests; namespace SqlBulkHelpers.IntegrationTests @@ -72,7 +73,7 @@ public async Task TestSchemaLoaderCacheWithExistingConnectionAsync() } [TestMethod] - public async Task TestSchemaLoaderCacheWithBadConnectionDueToPendingTransactionFor_v1_2() + public async Task TestGetTableSchemaFromLoaderCacheWithBadConnectionDueToPendingTransactionFor_v1_2() { //**************************************************************************************************** // Check that Invalid Connection Fails as expected and Lazy continues to re-throw the Exception!!! @@ -130,5 +131,98 @@ public async Task TestSchemaLoaderCacheWithBadConnectionDueToPendingTransactionF //Initial Call should result in SQL Exception due to Pending Transaction... Assert.IsNotNull(validTableDefinition); } + + [TestMethod] + public async Task TestGetTableSchemaFromLoaderWithConnectionFactoryAsync() + { + var dbSchemaLoader = SqlBulkHelpersSchemaLoaderCache.GetSchemaLoader($"SQL_CONNECTION_CACHE_KEY::{Guid.NewGuid()}"); + + Assert.IsNotNull(dbSchemaLoader); + + //Initial Call should result in SQL Exception due to Pending Transaction... + var tableDefinition = await dbSchemaLoader.GetTableSchemaDefinitionAsync( + TestHelpers.TestTableNameFullyQualified, + TableSchemaDetailLevel.BasicDetails, + sqlConnectionAsyncFactory: async () => await SqlConnectionHelper.NewConnectionAsync().ConfigureAwait(false), + forceCacheReload: true + ); + + Assert.IsNotNull(tableDefinition); + Assert.AreEqual(TestHelpers.TestTableNameFullyQualified, tableDefinition.TableFullyQualifiedName); + Assert.AreEqual(TableSchemaDetailLevel.BasicDetails, tableDefinition.SchemaDetailLevel); + Assert.AreEqual(3, tableDefinition.TableColumns.Count); + } + + [TestMethod] + public async Task TestGetTableSchemaFromLoaderWithConnectionFactory() + { + var dbSchemaLoader = SqlBulkHelpersSchemaLoaderCache.GetSchemaLoader($"SQL_CONNECTION_CACHE_KEY::{Guid.NewGuid()}"); + + Assert.IsNotNull(dbSchemaLoader); + + //Initial Call should result in SQL Exception due to Pending Transaction... + var tableDefinition = dbSchemaLoader.GetTableSchemaDefinition( + TestHelpers.TestTableNameFullyQualified, + TableSchemaDetailLevel.BasicDetails, + sqlConnectionFactory: SqlConnectionHelper.NewConnection + ); + + Assert.IsNotNull(tableDefinition); + Assert.AreEqual(TestHelpers.TestTableNameFullyQualified, tableDefinition.TableFullyQualifiedName); + Assert.AreEqual(TableSchemaDetailLevel.BasicDetails, tableDefinition.SchemaDetailLevel); + Assert.AreEqual(3, tableDefinition.TableColumns.Count); + } + + [TestMethod] + public async Task TestGetTableSchemaFromLoaderWithExistingConnectionAndTransactionAsync() + { + var dbSchemaLoader = SqlBulkHelpersSchemaLoaderCache.GetSchemaLoader($"SQL_CONNECTION_CACHE_KEY::{Guid.NewGuid()}"); + + Assert.IsNotNull(dbSchemaLoader); + + using(var sqlConnection = await SqlConnectionHelper.NewConnectionAsync().ConfigureAwait(false)) + using (var sqlTransaction = (SqlTransaction)await sqlConnection.BeginTransactionAsync().ConfigureAwait(false)) + { + //Initial Call should result in SQL Exception due to Pending Transaction... + var tableDefinition = await dbSchemaLoader.GetTableSchemaDefinitionAsync( + TestHelpers.TestTableNameFullyQualified, + TableSchemaDetailLevel.BasicDetails, + sqlConnection, + sqlTransaction, + forceCacheReload: true + ); + + Assert.IsNotNull(tableDefinition); + Assert.AreEqual(TestHelpers.TestTableNameFullyQualified, tableDefinition.TableFullyQualifiedName); + Assert.AreEqual(TableSchemaDetailLevel.BasicDetails, tableDefinition.SchemaDetailLevel); + Assert.AreEqual(3, tableDefinition.TableColumns.Count); + } + } + + [TestMethod] + public async Task TestGetTableSchemaFromLoaderWithExistingConnectionAndTransaction() + { + var dbSchemaLoader = SqlBulkHelpersSchemaLoaderCache.GetSchemaLoader($"SQL_CONNECTION_CACHE_KEY::{Guid.NewGuid()}"); + + Assert.IsNotNull(dbSchemaLoader); + + using (var sqlConnection = SqlConnectionHelper.NewConnection()) + using (var sqlTransaction = (SqlTransaction)sqlConnection.BeginTransaction()) + { + //Initial Call should result in SQL Exception due to Pending Transaction... + var tableDefinition = await dbSchemaLoader.GetTableSchemaDefinitionAsync( + TestHelpers.TestTableNameFullyQualified, + TableSchemaDetailLevel.BasicDetails, + sqlConnection, + sqlTransaction, + forceCacheReload: true + ); + + Assert.IsNotNull(tableDefinition); + Assert.AreEqual(TestHelpers.TestTableNameFullyQualified, tableDefinition.TableFullyQualifiedName); + Assert.AreEqual(TableSchemaDetailLevel.BasicDetails, tableDefinition.SchemaDetailLevel); + Assert.AreEqual(3, tableDefinition.TableColumns.Count); + } + } } }