diff --git a/src/Dapper.Bulk/Dapper.Bulk.csproj b/src/Dapper.Bulk/Dapper.Bulk.csproj index 3cca1b8..a2fab4a 100644 --- a/src/Dapper.Bulk/Dapper.Bulk.csproj +++ b/src/Dapper.Bulk/Dapper.Bulk.csproj @@ -36,6 +36,14 @@ bin\Release\net6.0\Dapper.Bulk.xml + + + + + + + + @@ -53,7 +61,7 @@ True - + diff --git a/src/Dapper.Bulk/DapperBulk.cs b/src/Dapper.Bulk/DapperBulk.cs index 956060c..dc6ba1f 100644 --- a/src/Dapper.Bulk/DapperBulk.cs +++ b/src/Dapper.Bulk/DapperBulk.cs @@ -9,339 +9,296 @@ [assembly: InternalsVisibleTo("Dapper.Bulk.Tests")] -namespace Dapper.Bulk; - -/// -/// Bulk inserts for Dapper -/// -public static class DapperBulk +namespace Dapper.Bulk { /// - /// Inserts entities into table s (by default). + /// Bulk inserts for Dapper /// - /// The type being inserted. - /// Open SqlConnection - /// Entities to insert - /// The transaction to run under, null (the default) if none - /// Number of bulk items inserted together, 0 (the default) if all - /// Number of seconds before bulk command execution timeout, 30 (the default) - /// Usage of db generated ids. By default DB generated IDs are used (identityInsert=false) - public static void BulkInsert(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) + public static class DapperBulk { - var type = typeof(T); - BulkInsert(connection,type,data.Cast(),transaction,batchSize,bulkCopyTimeout,identityInsert); - } - - /// - /// Inserts entities into table. - /// by default, the table is named after the data type specified. - /// - /// Open SqlConnection - /// The type being inserted. - /// Entities to insert - /// The transaction to run under, null (the default) if none - /// Number of bulk items inserted together, 0 (the default) if all - /// Number of seconds before bulk command execution timeout, 30 (the default) - /// Usage of db generated ids. By default DB generated IDs are used (identityInsert=false) - public static void BulkInsert(this SqlConnection connection, Type type, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) - { - var tableName = TableMapper.GetTableName(type); - var allProperties = PropertiesCache.TypePropertiesCache(type); - var keyProperties = PropertiesCache.KeyPropertiesCache(type); - var computedProperties = PropertiesCache.ComputedPropertiesCache(type); - var columns = PropertiesCache.GetColumnNamesCache(type); - - var insertProperties = allProperties.Except(computedProperties).ToList(); - - if (!identityInsert) - insertProperties = insertProperties.Except(keyProperties).ToList(); - - var (identityInsertOn, identityInsertOff, sqlBulkCopyOptions) = GetIdentityInsertOptions(identityInsert, tableName); - - var insertPropertiesString = GetColumnsStringSqlServer(insertProperties, columns); - var tempToBeInserted = $"#TempInsert_{tableName}".Replace(".", string.Empty); - - connection.Execute($@"SELECT TOP 0 {insertPropertiesString} INTO {tempToBeInserted} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); - - using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) + private class PropertiesContainer { - bulkCopy.BulkCopyTimeout = bulkCopyTimeout; - bulkCopy.BatchSize = batchSize; - bulkCopy.DestinationTableName = tempToBeInserted; - bulkCopy.WriteToServer(ToDataTable(data, insertProperties).CreateDataReader()); + public List AllProperties; + public List KeyProperties; + public List ComputedProperties; + public IReadOnlyDictionary ColumnNameMap; } - connection.Execute($@" - {identityInsertOn} - INSERT INTO {FormatTableName(tableName)}({insertPropertiesString}) - SELECT {insertPropertiesString} FROM {tempToBeInserted} - {identityInsertOff} - DROP TABLE {tempToBeInserted};", null, transaction); - } + /// + /// Inserts entities into table s (by default). + /// + public static void BulkInsert(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) + { + BulkInsert(connection, typeof(T), data.Cast(), transaction, batchSize, bulkCopyTimeout, identityInsert); + } - /// - /// Inserts entities into table s (by default) returns inserted entities. - /// - /// The element type of the array - /// Open SqlConnection - /// Entities to insert - /// The transaction to run under, null (the default) if none - /// Number of bulk items inserted together, 0 (the default) if all - /// Number of seconds before bulk command execution timeout, 30 (the default) - /// Usage of db generated ids. By default DB generated IDs are used (identityInsert=false) - /// Inserted entities - public static IEnumerable BulkInsertAndSelect(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) - { - var type = typeof(T); - var tableName = TableMapper.GetTableName(type); - var allProperties = PropertiesCache.TypePropertiesCache(type); - var keyProperties = PropertiesCache.KeyPropertiesCache(type); - var computedProperties = PropertiesCache.ComputedPropertiesCache(type); - var columns = PropertiesCache.GetColumnNamesCache(type); - - if (keyProperties.Count == 0) + /// + /// Inserts entities into table. + /// + public static void BulkInsert(this SqlConnection connection, Type type, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) { - var dataList = data.ToList(); - connection.BulkInsert(dataList, transaction, batchSize, bulkCopyTimeout); - return dataList; + var properties = GetProperties(type); + var (tableName, tempTableName, identityInsertOnStr, identityInsertOffStr, insertPropertiesStr, insertProperties, sqlBulkCopyOptions) = PrepareInfo(type, identityInsert, properties); + + connection.Execute($@"SELECT TOP 0 {insertPropertiesStr} INTO {tempTableName} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); + + using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) + { + bulkCopy.BulkCopyTimeout = bulkCopyTimeout; + bulkCopy.BatchSize = batchSize; + bulkCopy.DestinationTableName = tempTableName; + bulkCopy.WriteToServer(ToDataTable(data, insertProperties).CreateDataReader()); + } + + var sqlString = GetInsertSql(tableName, tempTableName, identityInsertOnStr, identityInsertOffStr, insertPropertiesStr); + connection.Execute(sqlString, null, transaction); } - var insertProperties = allProperties.Except(computedProperties).ToList(); - if (!identityInsert) - insertProperties = insertProperties.Except(keyProperties).ToList(); + /// + /// Inserts entities into table s (by default) returns inserted entities. + /// + public static IEnumerable BulkInsertAndSelect(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) + { + var type = typeof(T); - var (identityInsertOn, identityInsertOff, sqlBulkCopyOptions) = GetIdentityInsertOptions(identityInsert, tableName); - - var keyPropertiesString = GetColumnsStringSqlServer(keyProperties,columns); - var keyPropertiesInsertedString = GetColumnsStringSqlServer(keyProperties, columns, "inserted."); - var insertPropertiesString = GetColumnsStringSqlServer(insertProperties, columns); - var allPropertiesString = GetColumnsStringSqlServer(allProperties, columns, "target."); + var properties = GetProperties(type); + var (tableName, tempTableName, identityInsertOnStr, identityInsertOffStr, insertPropertiesStr, insertProperties, sqlBulkCopyOptions) = PrepareInfo(type, identityInsert, properties); - var tempToBeInserted = $"#TempInsert_{tableName}".Replace(".", string.Empty); - var tempInsertedWithIdentity = $"@TempInserted_{tableName}".Replace(".", string.Empty); + if (!properties.KeyProperties.Any()) + { + var dataList = data.ToList(); + connection.BulkInsert(dataList, transaction, batchSize, bulkCopyTimeout); + return dataList; + } - connection.Execute($"SELECT TOP 0 {insertPropertiesString} INTO {tempToBeInserted} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); + var keyPropertiesStr = GetColumnsStringSqlServer(properties.KeyProperties, properties.ColumnNameMap); + var keyPropertiesInsertedStr = GetColumnsStringSqlServer(properties.KeyProperties, properties.ColumnNameMap, "inserted."); + var allPropertiesStr = GetColumnsStringSqlServer(PropertiesCache.TypePropertiesCache(type), properties.ColumnNameMap, "target."); - using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) - { - bulkCopy.BulkCopyTimeout = bulkCopyTimeout; - bulkCopy.BatchSize = batchSize; - bulkCopy.DestinationTableName = tempToBeInserted; - bulkCopy.WriteToServer(ToDataTable(data, insertProperties).CreateDataReader()); - } + var tempInsertedWithIdentity = $"@TempInserted_{tableName}".Replace(".", string.Empty); - var table = string.Join(", ", keyProperties.Select(k => $"[{(columns.ContainsKey(k.Name) ? columns[k.Name] : k.Name)}] bigint")); - var joinOn = string.Join(" AND ", keyProperties.Select(k => $"target.[{(columns.ContainsKey(k.Name) ? columns[k.Name] : k.Name)}] = ins.[{(columns.ContainsKey(k.Name) ? columns[k.Name] : k.Name)}]")); - - return connection.Query($@" - {identityInsertOn} - DECLARE {tempInsertedWithIdentity} TABLE ({table}) - INSERT INTO {FormatTableName(tableName)}({insertPropertiesString}) - OUTPUT {keyPropertiesInsertedString} INTO {tempInsertedWithIdentity} ({keyPropertiesString}) - SELECT {insertPropertiesString} FROM {tempToBeInserted} - {identityInsertOff} - - SELECT {allPropertiesString} - FROM {FormatTableName(tableName)} target INNER JOIN {tempInsertedWithIdentity} ins ON {joinOn} - - DROP TABLE {tempToBeInserted};", null, transaction); - } + connection.Execute($@"SELECT TOP 0 {insertPropertiesStr} INTO {tempTableName} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); - /// - /// Inserts entities into table s (by default) asynchronously. - /// - /// The type being inserted. - /// Open SqlConnection - /// Entities to insert - /// The transaction to run under, null (the default) if none - /// Number of bulk items inserted together, 0 (the default) if all - /// Number of seconds before bulk command execution timeout, 30 (the default) - /// Usage of db generated ids. By default DB generated IDs are used (identityInsert=false) - public static async Task BulkInsertAsync(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) - { - var type = typeof(T); - var tableName = TableMapper.GetTableName(type); - var allProperties = PropertiesCache.TypePropertiesCache(type); - var keyProperties = PropertiesCache.KeyPropertiesCache(type); - var computedProperties = PropertiesCache.ComputedPropertiesCache(type); - var columns = PropertiesCache.GetColumnNamesCache(type); - - var insertProperties = allProperties.Except(computedProperties).ToList(); - - if (!identityInsert) - insertProperties = insertProperties.Except(keyProperties).ToList(); + using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) + { + bulkCopy.BulkCopyTimeout = bulkCopyTimeout; + bulkCopy.BatchSize = batchSize; + bulkCopy.DestinationTableName = tempTableName; + bulkCopy.WriteToServer(ToDataTable(data, insertProperties).CreateDataReader()); + } - var (identityInsertOn, identityInsertOff, sqlBulkCopyOptions) = GetIdentityInsertOptions(identityInsert, tableName); - - var insertPropertiesString = GetColumnsStringSqlServer(insertProperties,columns); - var tempToBeInserted = $"#TempInsert_{tableName}".Replace(".", string.Empty); + var tableStr = GetKeysString(properties.KeyProperties, properties.ColumnNameMap); + var joinOnStr = GetJoinString(properties); - await connection.ExecuteAsync($@"SELECT TOP 0 {insertPropertiesString} INTO {tempToBeInserted} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); + var sqlStr = GetInsertAndSelectSql(tableName, identityInsertOnStr, identityInsertOffStr, tempTableName, insertPropertiesStr, keyPropertiesStr, keyPropertiesInsertedStr, allPropertiesStr, tempInsertedWithIdentity, tableStr, joinOnStr); + return connection.Query(sqlStr, null, transaction); + } - using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) + /// + /// Inserts entities into table s (by default) asynchronously. + /// + public static async Task BulkInsertAsync(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) { - bulkCopy.BulkCopyTimeout = bulkCopyTimeout; - bulkCopy.BatchSize = batchSize; - bulkCopy.DestinationTableName = tempToBeInserted; - await bulkCopy.WriteToServerAsync(ToDataTable(data, insertProperties).CreateDataReader()); - } + var type = typeof(T); + var properties = GetProperties(type); + var (tableName, tempTableName, identityInsertOnStr, identityInsertOffStr, insertPropertiesStr, insertProperties, sqlBulkCopyOptions) = PrepareInfo(type, identityInsert, properties); - await connection.ExecuteAsync($@" - {identityInsertOn} - INSERT INTO {FormatTableName(tableName)}({insertPropertiesString}) - SELECT {insertPropertiesString} FROM {tempToBeInserted} - {identityInsertOff} + await connection.ExecuteAsync($@"SELECT TOP 0 {insertPropertiesStr} INTO {tempTableName} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); - DROP TABLE {tempToBeInserted};", null, transaction); - } + using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) + { + bulkCopy.BulkCopyTimeout = bulkCopyTimeout; + bulkCopy.BatchSize = batchSize; + bulkCopy.DestinationTableName = tempTableName; + await bulkCopy.WriteToServerAsync(ToDataTable(data, insertProperties).CreateDataReader()); + } - /// - /// Inserts entities into table s (by default) asynchronously and returns inserted entities. - /// - /// The type being inserted. - /// Open SqlConnection - /// Entities to insert - /// The transaction to run under, null (the default) if none - /// Number of bulk items inserted together, 0 (the default) if all - /// Number of seconds before bulk command execution timeout, 30 (the default) - /// Usage of db generated ids. By default DB generated IDs are used (identityInsert=false) - /// Inserted entities - public static async Task> BulkInsertAndSelectAsync(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) - { - var type = typeof(T); - var tableName = TableMapper.GetTableName(type); - var allProperties = PropertiesCache.TypePropertiesCache(type); - var keyProperties = PropertiesCache.KeyPropertiesCache(type); - var computedProperties = PropertiesCache.ComputedPropertiesCache(type); - var columns = PropertiesCache.GetColumnNamesCache(type); - - if (keyProperties.Count == 0) + var sqlString = GetInsertSql(tableName, tempTableName, identityInsertOnStr, identityInsertOffStr, insertPropertiesStr); + await connection.ExecuteAsync(sqlString, null, transaction); + } + + /// + /// Inserts entities into table s (by default) asynchronously and returns inserted entities. + /// + public static async Task> BulkInsertAndSelectAsync(this SqlConnection connection, IEnumerable data, SqlTransaction transaction = null, int batchSize = 0, int bulkCopyTimeout = 30, bool identityInsert = false) { - var dataList = data.ToList(); - await connection.BulkInsertAsync(dataList, transaction, batchSize, bulkCopyTimeout); - return dataList; + var type = typeof(T); + var properties = GetProperties(type); + var (tableName, tempTableName, identityInsertOnStr, identityInsertOffStr, insertPropertiesStr, insertProperties, sqlBulkCopyOptions) = PrepareInfo(type, identityInsert, properties); + + if (!properties.KeyProperties.Any()) + { + var dataList = data.ToList(); + await connection.BulkInsertAsync(dataList, transaction, batchSize, bulkCopyTimeout); + return dataList; + } + + var keyPropertiesStr = GetColumnsStringSqlServer(properties.KeyProperties, properties.ColumnNameMap); + var keyPropertiesInsertedStr = GetColumnsStringSqlServer(properties.KeyProperties, properties.ColumnNameMap, "inserted."); + var allPropertiesStr = GetColumnsStringSqlServer(PropertiesCache.TypePropertiesCache(type), properties.ColumnNameMap, "target."); + + var tempInsertedWithIdentity = $"@TempInserted_{tableName}".Replace(".", string.Empty); + + await connection.ExecuteAsync($@"SELECT TOP 0 {insertPropertiesStr} INTO {tempTableName} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); + + using (var bulkCopy = new SqlBulkCopy(connection, sqlBulkCopyOptions, transaction)) + { + bulkCopy.BulkCopyTimeout = bulkCopyTimeout; + bulkCopy.BatchSize = batchSize; + bulkCopy.DestinationTableName = tempTableName; + await bulkCopy.WriteToServerAsync(ToDataTable(data, insertProperties).CreateDataReader()); + } + + var tableStr = GetKeysString(properties.KeyProperties, properties.ColumnNameMap); + string joinOnStr = GetJoinString(properties); + + var sqlStr = GetInsertAndSelectSql(tableName, identityInsertOnStr, identityInsertOffStr, tempTableName, insertPropertiesStr, keyPropertiesStr, keyPropertiesInsertedStr, allPropertiesStr, tempInsertedWithIdentity, tableStr, joinOnStr); + return await connection.QueryAsync(sqlStr, null, transaction); } - var insertProperties = allProperties.Except(computedProperties).ToList(); + private static string GetJoinString(PropertiesContainer properties) + { + var str = properties.KeyProperties.Select(k => + $"target.[{(properties.ColumnNameMap.ContainsKey(k.Name) ? properties.ColumnNameMap[k.Name] : k.Name)}] = ins.[{(properties.ColumnNameMap.ContainsKey(k.Name) ? properties.ColumnNameMap[k.Name] : k.Name)}]"); + return string.Join(" AND ", str); + } - if (!identityInsert) - insertProperties = insertProperties.Except(keyProperties).ToList(); + private static PropertiesContainer GetProperties(Type type) + { + var properties = new PropertiesContainer(); + properties.AllProperties = PropertiesCache.TypePropertiesCache(type); + properties.KeyProperties = PropertiesCache.KeyPropertiesCache(type); + properties.ComputedProperties = PropertiesCache.ComputedPropertiesCache(type); + properties.ColumnNameMap = PropertiesCache.GetColumnNamesCache(type); - var (identityInsertOn, identityInsertOff, sqlBulkCopyOptions) = GetIdentityInsertOptions(identityInsert, tableName); - - var keyPropertiesString = GetColumnsStringSqlServer(keyProperties,columns); - var keyPropertiesInsertedString = GetColumnsStringSqlServer(keyProperties,columns, "inserted."); - var insertPropertiesString = GetColumnsStringSqlServer(insertProperties,columns); - var allPropertiesString = GetColumnsStringSqlServer(allProperties, columns, "target."); + return properties; + } - var tempToBeInserted = $"#TempInsert_{tableName}".Replace(".", string.Empty); - var tempInsertedWithIdentity = $"@TempInserted_{tableName}".Replace(".", string.Empty); + private static (string tableName, string tempTableName, string identityInsertOn, string identityInsertOff, string insertPropertiesStr, List insertProperties, SqlBulkCopyOptions sqlBulkCopyOptions) PrepareInfo(Type type, bool identityInsert, PropertiesContainer properties) + { + var insertProperties = properties.AllProperties.Except(properties.ComputedProperties).ToList(); + if (!identityInsert) + insertProperties = insertProperties.Except(properties.KeyProperties).ToList(); + var insertPropertiesStr = GetColumnsStringSqlServer(insertProperties, properties.ColumnNameMap); - await connection.ExecuteAsync($@"SELECT TOP 0 {insertPropertiesString} INTO {tempToBeInserted} FROM {FormatTableName(tableName)} target WITH(NOLOCK);", null, transaction); + var tableName = TableMapper.GetTableName(type); + var tempTableName = GetTempTableName(tableName); - using (var bulkCopy = new SqlBulkCopy(connection,sqlBulkCopyOptions, transaction)) - { - bulkCopy.BulkCopyTimeout = bulkCopyTimeout; - bulkCopy.BatchSize = batchSize; - bulkCopy.DestinationTableName = tempToBeInserted; - await bulkCopy.WriteToServerAsync(ToDataTable(data, insertProperties).CreateDataReader()); + var keyIsGuid = AnyKeyIsGuid(properties.KeyProperties); + var (identityInsertOn, identityInsertOff, sqlBulkCopyOptions) = GetIdentityInsertOptions(identityInsert, keyIsGuid, tableName); + + return (tableName, tempTableName, identityInsertOn, identityInsertOff, insertPropertiesStr, insertProperties, sqlBulkCopyOptions); } - var table = string.Join(", ", keyProperties.Select(k => $"[{(columns.ContainsKey(k.Name) ? columns[k.Name] : k.Name)}] bigint")); - var joinOn = string.Join(" AND ", keyProperties.Select(k => $"target.[{(columns.ContainsKey(k.Name) ? columns[k.Name] : k.Name)}] = ins.[{(columns.ContainsKey(k.Name) ? columns[k.Name] : k.Name)}]")); - return await connection.QueryAsync($@" - {identityInsertOn} - DECLARE {tempInsertedWithIdentity} TABLE ({table}) - INSERT INTO {FormatTableName(tableName)}({insertPropertiesString}) - OUTPUT {keyPropertiesInsertedString} INTO {tempInsertedWithIdentity} ({keyPropertiesString}) - SELECT {insertPropertiesString} FROM {tempToBeInserted} - {identityInsertOff} - SELECT {allPropertiesString} - FROM {FormatTableName(tableName)} target INNER JOIN {tempInsertedWithIdentity} ins ON {joinOn} - - DROP TABLE {tempToBeInserted};", null, transaction); - } + private static string GetTempTableName(string tableName) => $"#TempInsert_{tableName}".Replace(".", string.Empty); - private static string GetColumnsStringSqlServer(IEnumerable properties, IReadOnlyDictionary columnNames, string tablePrefix = null) - { - if (tablePrefix == "target.") + /// + /// this provides support for the feature allowing object property names to be different than table columns names. + /// + /// + /// + /// + /// + private static string GetColumnsStringSqlServer(IEnumerable properties, IReadOnlyDictionary columnNames, string tablePrefix = null) { - return string.Join(", ", properties.Select(property => $"{tablePrefix}[{columnNames[property.Name]}] as [{property.Name}] ")); + if (tablePrefix == "target.") + { + return string.Join(", ", properties.Select(property => $"{tablePrefix}[{columnNames[property.Name]}] as [{property.Name}] ")); + } + + return string.Join(", ", properties.Select(property => $"{tablePrefix}[{columnNames[property.Name]}] ")); } - return string.Join(", ", properties.Select(property => $"{tablePrefix}[{columnNames[property.Name]}] ")); - } - - private static DataTable ToDataTable(IEnumerable data, IList properties) - { - var typeCasts = new Type[properties.Count]; - for (var i = 0; i < properties.Count; i++) + private static DataTable ToDataTable(IEnumerable data, IList properties) { - if (properties[i].PropertyType.IsEnum) + var dataTable = new DataTable(); + var typeCasts = properties.Select(p => p.PropertyType.IsEnum ? Enum.GetUnderlyingType(p.PropertyType) : null).ToArray(); + + foreach (var property in properties) { - typeCasts[i] = Enum.GetUnderlyingType(properties[i].PropertyType); + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + dataTable.Columns.Add(property.Name, typeCasts[properties.IndexOf(property)] ?? propertyType); } - else + + foreach (var item in data) { - typeCasts[i] = null; + var values = properties.Select((p, i) => typeCasts[i] == null ? p.GetValue(item) : Convert.ChangeType(p.GetValue(item), typeCasts[i])).ToArray(); + dataTable.Rows.Add(values); } + + return dataTable; } - var dataTable = new DataTable(); - for (var i = 0; i < properties.Count; i++) + internal static string FormatTableName(string table) { - // Nullable types are not supported. - var propertyNonNullType = Nullable.GetUnderlyingType(properties[i].PropertyType) ?? properties[i].PropertyType; - dataTable.Columns.Add(properties[i].Name, typeCasts[i] ?? propertyNonNullType); + if (string.IsNullOrEmpty(table)) return table; + return string.Join(".", table.Split('.').Select(part => $"[{part}]")); } - foreach (var item in data) + private static (string identityInsertOn, string identityInsertOff, SqlBulkCopyOptions bulkCopyOptions) GetIdentityInsertOptions(bool identityInsert, bool keyIsGuid, string tableName) { - var values = new object[properties.Count]; - for (var i = 0; i < properties.Count; i++) + var identityInsertOn = string.Empty; + var identityInsertOff = string.Empty; + var bulkCopyOptions = identityInsert ? SqlBulkCopyOptions.KeepIdentity : SqlBulkCopyOptions.Default; + + if (identityInsert && !keyIsGuid) { - var value = properties[i].GetValue(item, null); - values[i] = typeCasts[i] == null ? value : Convert.ChangeType(value, typeCasts[i]); + identityInsertOn = $"SET IDENTITY_INSERT {FormatTableName(tableName)} ON"; + identityInsertOff = $"SET IDENTITY_INSERT {FormatTableName(tableName)} OFF"; } - dataTable.Rows.Add(values); + return (identityInsertOn, identityInsertOff, bulkCopyOptions); } - return dataTable; - } + private static bool AnyKeyIsGuid(IEnumerable propertyInfos) => propertyInfos.Any(p => p.PropertyType == typeof(Guid)); - internal static string FormatTableName(string table) - { - if (string.IsNullOrEmpty(table)) + private static string GetKeysString(IEnumerable keyProperties, IReadOnlyDictionary columns) { - return table; + var keys = keyProperties.Select(k => + { + if (columns.ContainsKey(k.Name)) + { + var typeString = k.PropertyType.Name switch + { + "Guid" => "uniqueidentifier", + "Int32" or "UInt32" => "int", + "Int64" or "UInt64" => "bigint", + _ => throw new ArgumentException($"Invalid data type used in primary key. Type='{k.PropertyType.Name}'.") + }; + return $"[{columns[k.Name]}] {typeString}"; + } + else + { + return $"[{k.Name}]" + " bigint"; + } + }); + + return String.Join(",", keys); } - var parts = table.Split('.'); - - if (parts.Length == 1) + private static string GetInsertSql(string tableName, string tempTableNameStr, string identityInsertOnStr, string identityInsertOffStr, string insertPropertiesStr) { - return $"[{table}]"; + return + $@"{identityInsertOnStr} + INSERT INTO {FormatTableName(tableName)} ({insertPropertiesStr}) + SELECT {insertPropertiesStr} FROM {tempTableNameStr} + {identityInsertOffStr} + DROP TABLE {tempTableNameStr};"; } - var tableName = ""; - for (int i = 0; i < parts.Length; i++) + private static string GetInsertAndSelectSql(string tableName, string identityInsertOnStr, string identityInsertOffStr, string tempTableName, string insertPropertiesStr, string keyPropertiesStr, string keyPropertiesInsertedStr, string allPropertiesStr, string tempInsertedWithIdentity, string tableStr, string joinOnStr) { - tableName += $"[{parts[i]}]"; - if (i + 1 < parts.Length) - { - tableName += "."; - } + return + $@"{identityInsertOnStr} + DECLARE {tempInsertedWithIdentity} TABLE ({tableStr}) + INSERT INTO {FormatTableName(tableName)} ({insertPropertiesStr}) + OUTPUT {keyPropertiesInsertedStr} INTO {tempInsertedWithIdentity} ({keyPropertiesStr}) + SELECT {insertPropertiesStr} FROM {tempTableName} + {identityInsertOffStr} + SELECT {allPropertiesStr} + FROM {FormatTableName(tableName)} target INNER JOIN {tempInsertedWithIdentity} ins ON {joinOnStr} + DROP TABLE {tempTableName};"; } - - return tableName; } - - private static (string identityInsertOn, string identityInsertOff, SqlBulkCopyOptions bulkCopyOptions) - GetIdentityInsertOptions(bool identityInsert, string tableName) - => identityInsert - ? ($"SET IDENTITY_INSERT {FormatTableName(tableName)} ON", - $"SET IDENTITY_INSERT {FormatTableName(tableName)} OFF", SqlBulkCopyOptions.KeepIdentity) - : (string.Empty, string.Empty, SqlBulkCopyOptions.Default); -} +} \ No newline at end of file diff --git a/tests/Dapper.Bulk.Tests/CustomColumnNameTests.cs b/tests/Dapper.Bulk.Tests/CustomColumnNameTests.cs index 0c9c44e..1c57cf6 100644 --- a/tests/Dapper.Bulk.Tests/CustomColumnNameTests.cs +++ b/tests/Dapper.Bulk.Tests/CustomColumnNameTests.cs @@ -29,24 +29,6 @@ private class CustomColumnName public int Ignored { get; set; } } - [Fact] - public void InsertBulk() - { - var data = new List(); - for (var i = 0; i < 10; i++) - { - data.Add(new CustomColumnName { Name = Guid.NewGuid().ToString() , LongCol = i * 1000, IntCol = i}); - } - - using var connection = GetConnection(); - connection.Open(); - var inserted = connection.BulkInsertAndSelect(data).ToList(); - for (var i = 0; i < data.Count; i++) - { - IsValidInsert(inserted[i], data[i]); - } - } - [Fact] public void InsertSingle() { @@ -78,6 +60,24 @@ public void InsertSingleTransaction() IsValidInsert(inserted, item); } + [Fact] + public void InsertBulk() + { + var data = new List(); + for (var i = 0; i < 10; i++) + { + data.Add(new CustomColumnName { Name = Guid.NewGuid().ToString(), LongCol = i * 1000, IntCol = i }); + } + + using var connection = GetConnection(); + connection.Open(); + var inserted = connection.BulkInsertAndSelect(data).ToList(); + for (var i = 0; i < data.Count; i++) + { + IsValidInsert(inserted[i], data[i]); + } + } + private static void IsValidInsert(CustomColumnName inserted, CustomColumnName toBeInserted) { inserted.IdKey.Should().BePositive(); diff --git a/tests/Dapper.Bulk.Tests/GuidIdTests.cs b/tests/Dapper.Bulk.Tests/GuidIdTests.cs new file mode 100644 index 0000000..95b1b1d --- /dev/null +++ b/tests/Dapper.Bulk.Tests/GuidIdTests.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Dapper.Bulk.Tests; + +public class GuidIdTests : SqlServerTestSuite +{ + private List data; + private List ids = new(); + + public GuidIdTests() + { + data = new List(); + for (var i = 0; i < 10; i++) + { + var obj = new GuidIdentity + { + Id = Guid.NewGuid(), + Int_Col = i, + CreateDate = DateTime.UtcNow, + Name = i.ToString() + " user field", + }; + + data.Add(obj); + + ids.Add(obj.Id); + } + } + + [Fact] + public void InsertBulk() + { + using var connection = GetConnection(); + connection.Open(); + + connection.BulkInsert(data, null, 0, 30, true); + + var query = "SELECT * FROM GuidIdentity WHERE Id IN @Ids"; + var inserted = connection.Query(query, new { Ids = ids }); + + foreach (var item in inserted) + { + var refItem = data.FirstOrDefault(o => o.Id == item.Id); + IsValidInsert(item, refItem, true); + } + } + + [Fact] + public void InsertBulkAndSelect() + { + using var connection = GetConnection(); + connection.Open(); + + var inserted = connection.BulkInsertAndSelect(data, null, 0, 30, false).ToList(); + for (var i = 0; i < data.Count; i++) + { + IsValidInsert(inserted[i], data[i], false); + } + } + + private static void IsValidInsert(GuidIdentity inserted, GuidIdentity toBeInserted, bool checkIdentity = true) + { + if (checkIdentity) + inserted.Id.Should().Be(toBeInserted.Id); + else + inserted.Id.Should().NotBe(toBeInserted.Id); + + inserted.Int_Col.Should().Be(toBeInserted.Int_Col); + inserted.CreateDate.Should().Be(toBeInserted.CreateDate); + inserted.Name.Should().Be(toBeInserted.Name); + } + + [Table("GuidIdentity")] + public class GuidIdentity + { + [Key] + public Guid Id { get; set; } + + public int Int_Col { get; set; } + + public DateTime CreateDate { get; set; } + + public string Name { get; set; } + } +} diff --git a/tests/Dapper.Bulk.Tests/README.md b/tests/Dapper.Bulk.Tests/README.md index 61f4ac5..a5e0485 100644 --- a/tests/Dapper.Bulk.Tests/README.md +++ b/tests/Dapper.Bulk.Tests/README.md @@ -1,2 +1,2 @@ Before Running test, please create localDb DapperBulkTest -the connection string as : Server=(localdb)\\mssqllocaldb;Database=DapperBulkTest;Trusted_Connection=True;MultipleActiveResultSets=true; \ No newline at end of file +the connection string as : Server=localhost;Database=DapperBulkTest;Trusted_Connection=True;MultipleActiveResultSets=true; \ No newline at end of file diff --git a/tests/Dapper.Bulk.Tests/SqlServerTestSuite.cs b/tests/Dapper.Bulk.Tests/SqlServerTestSuite.cs index b673c5b..efa58f3 100644 --- a/tests/Dapper.Bulk.Tests/SqlServerTestSuite.cs +++ b/tests/Dapper.Bulk.Tests/SqlServerTestSuite.cs @@ -1,10 +1,10 @@ -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace Dapper.Bulk.Tests; public class SqlServerTestSuite { - private static readonly string ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=DapperBulkTest;Trusted_Connection=True;MultipleActiveResultSets=true;"; + private static readonly string ConnectionString = "Server=(localdb)\\mssqllocaldb;Database=DapperBulkTest;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true;"; public static SqlConnection GetConnection() => new(ConnectionString); @@ -16,72 +16,80 @@ static SqlServerTestSuite() connection.Open(); connection.Execute( $@"{DropTable("IdentityAndComputedTests")} - CREATE TABLE IdentityAndComputedTests - ( - [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [Name] NVARCHAR(100) NULL, - [CreateDate] DATETIME2 NOT NULL DEFAULT(GETDATE()) - );"); + CREATE TABLE IdentityAndComputedTests + ( + [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [Name] NVARCHAR(100) NULL, + [CreateDate] DATETIME2 NOT NULL DEFAULT(GETDATE()) + );"); connection.Execute( $@"{DropTable("NoIdentityTests")} - CREATE TABLE NoIdentityTests( - [ItemId] BIGINT NULL, - [Name] NVARCHAR(100) NULL - );"); + CREATE TABLE NoIdentityTests( + [ItemId] BIGINT NULL, + [Name] NVARCHAR(100) NULL + );"); connection.Execute( $@"{DropTable("EnumTests")} - CREATE TABLE EnumTests( - [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [IntEnum] INT NOT NULL, - [LongEnum] BIGINT NOT NULL - );"); + CREATE TABLE EnumTests( + [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [IntEnum] INT NOT NULL, + [LongEnum] BIGINT NOT NULL + );"); connection.Execute( $@"{DropTable("ByteArrays")} - CREATE TABLE ByteArrays( - [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [TestArray] varbinary(100) NOT NULL - );"); + CREATE TABLE ByteArrays( + [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [TestArray] varbinary(100) NOT NULL + );"); connection.Execute( $@"{DropTable("IdentityInsertEnabledTests")} - CREATE TABLE IdentityInsertEnabledTests - ( - [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [Name] NVARCHAR(100) NULL - );"); + CREATE TABLE IdentityInsertEnabledTests + ( + [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [Name] NVARCHAR(100) NULL + );"); connection.Execute( - $@"{DropTable("IdentityAndNotMappedTests")} - CREATE TABLE IdentityAndNotMappedTests - ( - [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [Name] NVARCHAR(100) NULL - );"); + $@"{DropTable("IdentityAndNotMappedTests")} + CREATE TABLE IdentityAndNotMappedTests + ( + [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [Name] NVARCHAR(100) NULL + );"); connection.Execute( - $@"{DropTable("CustomColumnNames")} - CREATE TABLE CustomColumnNames - ( - [Id_Key] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [Name_1] NVARCHAR(100) NULL, - [Int_Col] INT NOT NULL, - [Long_Col] BIGINT NOT NULL - );"); + $@"{DropTable("CustomColumnNames")} + CREATE TABLE CustomColumnNames + ( + [Id_Key] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [Name_1] NVARCHAR(100) NULL, + [Int_Col] INT NOT NULL, + [Long_Col] BIGINT NOT NULL + );"); connection.Execute( $@"{DropTable("10_Escapes")} - CREATE TABLE [10_Escapes]( - [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [10_Name] NVARCHAR(100) NULL - );"); + CREATE TABLE [10_Escapes]( + [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [10_Name] NVARCHAR(100) NULL + );"); + connection.Execute( + $@"{DropTable("GuidIdentity")} + CREATE TABLE [dbo].[GuidIdentity]( + [Id] [uniqueidentifier] ROWGUIDCOL DEFAULT NEWID() NOT NULL PRIMARY KEY, + [Int_Col] [int] NOT NULL, + [CreateDate] [datetime2](7) NULL, + [Name] [nvarchar](50) NULL + );"); var schemaName = "test"; connection.Execute( - $@"{DropTable($@"{schemaName}.10_Escapes")}"); + $@"{DropTable($@"{schemaName}.10_Escapes")}"); connection.Execute( $@"{DropSchema(schemaName)}"); @@ -91,27 +99,27 @@ [10_Name] NVARCHAR(100) NULL connection.Execute( $@"CREATE TABLE [{schemaName}].[10_Escapes]( - [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [10_Name] NVARCHAR(100) NULL - );"); + [Id] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [10_Name] NVARCHAR(100) NULL + );"); connection.Execute( $@"{DropTable("PE_TranslationPhrase")} - CREATE TABLE [PE_TranslationPhrase]( - [TranslationId] INT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [CultureName] NVARCHAR(100) NOT NULL, - [Phrase] NVARCHAR(100) NOT NULL, - [PhraseHash] uniqueidentifier NULL, - [RowAddedDateTime] DATETIME2 NOT NULL DEFAULT(GETDATE()) - );"); + CREATE TABLE [PE_TranslationPhrase]( + [TranslationId] INT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [CultureName] NVARCHAR(100) NOT NULL, + [Phrase] NVARCHAR(100) NOT NULL, + [PhraseHash] uniqueidentifier NULL, + [RowAddedDateTime] DATETIME2 NOT NULL DEFAULT(GETDATE()) + );"); connection.Execute( $@"{DropTable("IdentityAndWriteInsertTests")} - CREATE TABLE IdentityAndWriteInsertTests - ( - [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, - [Name] NVARCHAR(100) NULL, - [NotIgnored] INT NULL - );"); + CREATE TABLE IdentityAndWriteInsertTests + ( + [IdKey] BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY, + [Name] NVARCHAR(100) NULL, + [NotIgnored] INT NULL + );"); } }