public static Task ExecuteAsync(DbContext context, Type type, IList <object> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken cancellationToken)
        {
            using (ActivitySources.StartExecuteActivity(operationType, entities.Count))
            {
                if (operationType != OperationType.Truncate && entities.Count == 0)
                {
                    return(Task.CompletedTask);
                }

                TableInfo tableInfo = TableInfo.CreateInstance(context, type, entities, operationType, bulkConfig);

                if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
                {
                    return(SqlBulkOperation.InsertAsync(context, type, entities, tableInfo, progress, cancellationToken));
                }
                else if (operationType == OperationType.Read)
                {
                    return(SqlBulkOperation.ReadAsync(context, type, entities, tableInfo, progress, cancellationToken));
                }
                else if (operationType == OperationType.Truncate)
                {
                    return(SqlBulkOperation.TruncateAsync(context, tableInfo, cancellationToken));
                }
                else
                {
                    return(SqlBulkOperation.MergeAsync(context, type, entities, tableInfo, operationType, progress, cancellationToken));
                }
            }
        }
        public static string GetSqlSetSegment <T>(DbContext context, T updateValues, List <string> updateColumns, List <SqlParameter> parameters) where T : class, new()
        {
            var    tableInfo        = TableInfo.CreateInstance <T>(context, new List <T>(), OperationType.Read, new BulkConfig());
            string sql              = string.Empty;
            Type   updateValuesType = typeof(T);
            var    defaultValues    = new T();

            foreach (var propertyNameColumnName in tableInfo.PropertyColumnNamesDict)
            {
                string propertyName           = propertyNameColumnName.Key;
                string columnName             = propertyNameColumnName.Value;
                var    property               = updateValuesType.GetProperty(propertyName);
                bool   isEnum                 = property.PropertyType.BaseType.Name == "Enum";
                var    propertyUpdateValue    = isEnum ? (int)property.GetValue(updateValues) : property.GetValue(updateValues);
                var    propertyDefaultValue   = property.GetValue(defaultValues);
                bool   isDifferentFromDefault = propertyUpdateValue?.ToString() != propertyDefaultValue?.ToString();
                if (isDifferentFromDefault || (updateColumns != null && updateColumns.Contains(propertyName)))
                {
                    sql += $"[{columnName}] = @{columnName}, ";
                    parameters.Add(new SqlParameter($"@{columnName}", propertyUpdateValue));
                }
            }
            if (String.IsNullOrEmpty(sql))
            {
                throw new InvalidOperationException("SET Columns not defined. If one or more columns should be updated to theirs default value use 'updateColumns' argument.");
            }
            sql = sql.Remove(sql.Length - 2, 2); // removes last excess comma and space: ", "
            return($"SET {sql}");
        }
Example #3
0
        public static void Execute <T>(DbContext context, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress) where T : class
        {
            if (operationType != OperationType.Truncate && entities.Count == 0)
            {
                return;
            }
            TableInfo tableInfo = TableInfo.CreateInstance(context, entities, operationType, bulkConfig);

            if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
            {
                SqlBulkOperation.Insert(context, entities, tableInfo, progress);
            }
            else if (operationType == OperationType.Read)
            {
                SqlBulkOperation.Read(context, entities, tableInfo, progress);
            }
            else if (operationType == OperationType.Truncate)
            {
                SqlBulkOperation.Truncate(context, tableInfo);
            }
            else
            {
                SqlBulkOperation.Merge(context, entities, tableInfo, operationType, progress);
            }
        }
Example #4
0
        public static Task ExecuteAsync <T>(DbContext context, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken cancellationToken) where T : class
        {
            if (operationType != OperationType.Truncate && entities.Count == 0)
            {
                return(Task.CompletedTask);
            }
            TableInfo tableInfo = TableInfo.CreateInstance(context, entities, operationType, bulkConfig);

            if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
            {
                return(SqlBulkOperation.InsertAsync(context, entities, tableInfo, progress, cancellationToken));
            }
            else if (operationType == OperationType.Read)
            {
                return(SqlBulkOperation.ReadAsync(context, entities, tableInfo, progress, cancellationToken));
            }
            else if (operationType == OperationType.Truncate)
            {
                return(SqlBulkOperation.TruncateAsync(context, tableInfo));
            }
            else
            {
                return(SqlBulkOperation.MergeAsync(context, entities, tableInfo, operationType, progress, cancellationToken));
            }
        }
        public BatchUpdateCreateBodyData(
            string baseSql,
            DbContext dbContext,
            IEnumerable <object> innerParameters,
            IQueryable query,
            Type rootType,
            string tableAlias,
            LambdaExpression updateExpression)
        {
            BaseSql      = baseSql;
            DatabaseType = SqlAdaptersMapping.GetDatabaseType(dbContext);
            DbContext    = dbContext;
            Query        = query;
            RootInstanceParameterName = updateExpression.Parameters?.First()?.Name;
            RootType          = rootType;
            TableAlias        = tableAlias;
            TableAliasesInUse = new List <string>();
            UpdateColumnsSql  = new StringBuilder();
            UpdateExpression  = updateExpression;

            _tableInfoBulkConfig = new BulkConfig();
            _tableInfoLookup     = new Dictionary <Type, TableInfo>();

            var tableInfo = TableInfo.CreateInstance(dbContext, rootType, Array.Empty <object>(), OperationType.Read, _tableInfoBulkConfig);

            _tableInfoLookup.Add(rootType, tableInfo);

            SqlParameters = new List <object>(innerParameters);

            foreach (Match match in BatchUtil.TableAliasPattern.Matches(baseSql))
            {
                TableAliasesInUse.Add(match.Groups[2].Value);
            }
        }
        public static void Execute(DbContext context, Type type, IList <object> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress)
        {
            using (ActivitySources.StartExecuteActivity(operationType, entities.Count))
            {
                if (operationType != OperationType.Truncate && entities.Count == 0)
                {
                    return;
                }

                TableInfo tableInfo = TableInfo.CreateInstance(context, type, entities, operationType, bulkConfig);

                if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
                {
                    SqlBulkOperation.Insert(context, type, entities, tableInfo, progress);
                }
                else if (operationType == OperationType.Read)
                {
                    SqlBulkOperation.Read(context, type, entities, tableInfo, progress);
                }
                else if (operationType == OperationType.Truncate)
                {
                    SqlBulkOperation.Truncate(context, tableInfo);
                }
                else
                {
                    SqlBulkOperation.Merge(context, type, entities, tableInfo, operationType, progress);
                }
            }
        }
Example #7
0
        /// <summary>
        /// get Update Sql
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static (string, List <SqlParameter>) GetSqlUpdate <T>(IQueryable <T> query, Expression <Func <T, bool> > expression) where T : class, new()
        {
            (string sql, string tableAlias) = GetBatchSql(query);
            var sb  = new StringBuilder();
            var sp  = new List <SqlParameter>();
            var dic = TableInfo.CreateInstance(GetDbContext(query), new List <T>(), OperationType.Read, new BulkConfig()).PropertyColumnNamesDict;

            CreateUpdateBody(dic, tableAlias, expression.Body, ref sb, ref sp);
            return($"UPDATE [{tableAlias}] SET {sb.ToString()} {sql}", sp);
        }
        /// <summary>
        /// get Update Sql
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static (string, List <SqlParameter>) GetSqlUpdate <T>(IQueryable <T> query, Expression <Func <T, T> > expression) where T : class
        {
            (string sql, string tableAlias, IEnumerable <SqlParameter> innerParameters) = GetBatchSql(query);
            var sqlColumns          = new StringBuilder();
            var sqlParameters       = new List <SqlParameter>(innerParameters);
            var columnNameValueDict = TableInfo.CreateInstance(GetDbContext(query), new List <T>(), OperationType.Read, new BulkConfig()).PropertyColumnNamesDict;

            CreateUpdateBody(columnNameValueDict, tableAlias, expression.Body, ref sqlColumns, ref sqlParameters);
            return($"UPDATE [{tableAlias}] SET {sqlColumns.ToString()} {sql}", sqlParameters);
        }
Example #9
0
        public static void Execute <T>(DbContext context, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress) where T : class
        {
            TableInfo tableInfo = TableInfo.CreateInstance(context, entities, operationType, bulkConfig);

            if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
            {
                SqlBulkOperation.Insert <T>(context, entities, tableInfo, progress);
            }
            else
            {
                SqlBulkOperation.Merge <T>(context, entities, tableInfo, operationType, progress);
            }
        }
Example #10
0
        public static string GetSqlSetSegment <T>(DbContext context, T updateValues, List <string> updateColumns, List <object> parameters) where T : class, new()
        {
            var    tableInfo        = TableInfo.CreateInstance <T>(context, new List <T>(), OperationType.Read, new BulkConfig());
            string sql              = string.Empty;
            Type   updateValuesType = typeof(T);
            var    defaultValues    = new T();

            foreach (var propertyNameColumnName in tableInfo.PropertyColumnNamesDict)
            {
                string       propertyName = propertyNameColumnName.Key;
                string       columnName   = propertyNameColumnName.Value;
                var          pArray       = propertyName.Split(new char[] { '.' });
                Type         lastType     = updateValuesType;
                PropertyInfo property     = lastType.GetProperty(pArray[0]);
                if (property != null)
                {
                    object propertyUpdateValue  = property.GetValue(updateValues);
                    object propertyDefaultValue = property.GetValue(defaultValues);
                    for (int i = 1; i < pArray.Length; i++)
                    {
                        lastType            = property.PropertyType;
                        property            = lastType.GetProperty(pArray[i]);
                        propertyUpdateValue = propertyUpdateValue != null?property.GetValue(propertyUpdateValue) : propertyUpdateValue;

                        var lastDefaultValues = lastType.Assembly.CreateInstance(lastType.FullName);
                        propertyDefaultValue = property.GetValue(lastDefaultValues);
                    }

                    if (tableInfo.ConvertibleProperties.ContainsKey(columnName))
                    {
                        propertyUpdateValue = tableInfo.ConvertibleProperties[columnName].ConvertToProvider.Invoke(propertyUpdateValue);
                    }

                    bool isDifferentFromDefault = propertyUpdateValue != null && propertyUpdateValue?.ToString() != propertyDefaultValue?.ToString();
                    if (isDifferentFromDefault || (updateColumns != null && updateColumns.Contains(propertyName)))
                    {
                        sql += $"[{columnName}] = @{columnName}, ";
                        propertyUpdateValue = propertyUpdateValue ?? DBNull.Value;
                        parameters.Add(new SqlParameter($"@{columnName}", propertyUpdateValue));
                    }
                }
            }
            if (String.IsNullOrEmpty(sql))
            {
                throw new InvalidOperationException("SET Columns not defined. If one or more columns should be updated to theirs default value use 'updateColumns' argument.");
            }
            sql = sql.Remove(sql.Length - 2, 2); // removes last excess comma and space: ", "
            return($"SET {sql}");
        }
        public TableInfo GetTableInfoForType(Type typeToLookup)
        {
            if (_tableInfoLookup.TryGetValue(typeToLookup, out var tableInfo))
            {
                return(tableInfo);
            }

            tableInfo = TableInfo.CreateInstance(DbContext, typeToLookup, Array.Empty <object>(), OperationType.Read, _tableInfoBulkConfig);
            if (tableInfo != null)
            {
                _tableInfoLookup.Add(typeToLookup, tableInfo);
            }

            return(tableInfo);
        }
Example #12
0
        private static (string, List <object>) GetSqlUpdate <T>(IQueryable query, DbContext context, Type type, Expression <Func <T, T> > expression) where T : class
        {
            (string sql, string tableAlias, string tableAliasSufixAs, string topStatement, string leadingComments, IEnumerable <object> innerParameters) = GetBatchSql(query, context, isUpdate: true);
            var sqlColumns          = new StringBuilder();
            var sqlParameters       = new List <object>(innerParameters);
            var columnNameValueDict = TableInfo.CreateInstance(GetDbContext(query), type, new List <object>(), OperationType.Read, new BulkConfig()).PropertyColumnNamesDict;
            var dbType = GetDatabaseType(context);

            CreateUpdateBody(columnNameValueDict, tableAlias, expression.Body, dbType, ref sqlColumns, ref sqlParameters);

            sqlParameters = ReloadSqlParameters(context, sqlParameters); // Sqlite requires SqliteParameters
            sqlColumns    = (GetDatabaseType(context) == DbServer.SqlServer) ? sqlColumns : sqlColumns.Replace($"[{tableAlias}].", "");

            var resultQuery = $"{leadingComments}UPDATE {topStatement}{tableAlias}{tableAliasSufixAs} SET {sqlColumns} {sql}";

            return(resultQuery, sqlParameters);
        }
Example #13
0
        public static void Execute <T>(DbContext context, Type type, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress) where T : class
        {
            type ??= typeof(T);
            using (ActivitySources.StartExecuteActivity(operationType, entities.Count))
            {
                if (entities.Count == 0 &&
                    operationType != OperationType.InsertOrUpdateOrDelete &&
                    operationType != OperationType.Truncate &&
                    operationType != OperationType.SaveChanges &&
                    (bulkConfig == null || bulkConfig.CustomSourceTableName == null))
                {
                    return;
                }

                if (operationType == OperationType.SaveChanges)
                {
                    DbContextBulkTransactionSaveChanges.SaveChanges(context, bulkConfig, progress);
                    return;
                }
                else if (bulkConfig?.IncludeGraph == true)
                {
                    DbContextBulkTransactionGraphUtil.ExecuteWithGraph(context, entities, operationType, bulkConfig, progress);
                }
                else
                {
                    TableInfo tableInfo = TableInfo.CreateInstance(context, type, entities, operationType, bulkConfig);

                    if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity && tableInfo.BulkConfig.CustomSourceTableName == null)
                    {
                        SqlBulkOperation.Insert(context, type, entities, tableInfo, progress);
                    }
                    else if (operationType == OperationType.Read)
                    {
                        SqlBulkOperation.Read(context, type, entities, tableInfo, progress);
                    }
                    else if (operationType == OperationType.Truncate)
                    {
                        SqlBulkOperation.Truncate(context, tableInfo);
                    }
                    else
                    {
                        SqlBulkOperation.Merge(context, type, entities, tableInfo, operationType, progress);
                    }
                }
            }
        }
Example #14
0
        public static Task ExecuteAsync <T>(DbContext context, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress) where T : class
        {
            TableInfo tableInfo = TableInfo.CreateInstance(context, entities, operationType, bulkConfig);

            if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
            {
                return(SqlBulkOperation.InsertAsync(context, entities, tableInfo, progress));
            }
            else if (operationType == OperationType.Read)
            {
                return(SqlBulkOperation.ReadAsync(context, entities, tableInfo, progress));
            }
            else
            {
                return(SqlBulkOperation.MergeAsync(context, entities, tableInfo, operationType, progress));
            }
        }
Example #15
0
        public static async Task ExecuteAsync <T>(DbContext context, Type type, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken cancellationToken = default) where T : class
        {
            type ??= typeof(T);
            using (ActivitySources.StartExecuteActivity(operationType, entities.Count))
            {
                if (entities.Count == 0 && operationType != OperationType.InsertOrUpdateOrDelete && operationType != OperationType.Truncate && operationType != OperationType.SaveChanges)
                {
                    return;
                }

                if (operationType == OperationType.SaveChanges)
                {
                    await DbContextBulkTransactionSaveChanges.SaveChangesAsync(context, bulkConfig, progress, cancellationToken);
                }
                else if (bulkConfig?.IncludeGraph == true)
                {
                    await DbContextBulkTransactionGraphUtil.ExecuteWithGraphAsync(context, entities, operationType, bulkConfig, progress, cancellationToken);
                }
                else
                {
                    TableInfo tableInfo = TableInfo.CreateInstance(context, type, entities, operationType, bulkConfig);

                    if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
                    {
                        await SqlBulkOperation.InsertAsync(context, type, entities, tableInfo, progress, cancellationToken);
                    }
                    else if (operationType == OperationType.Read)
                    {
                        await SqlBulkOperation.ReadAsync(context, type, entities, tableInfo, progress, cancellationToken);
                    }
                    else if (operationType == OperationType.Truncate)
                    {
                        await SqlBulkOperation.TruncateAsync(context, tableInfo, cancellationToken);
                    }
                    else
                    {
                        await SqlBulkOperation.MergeAsync(context, type, entities, tableInfo, operationType, progress, cancellationToken);
                    }
                }
            }
        }
Example #16
0
        public static string GetSqlSetSegment <T>(DbContext context, T updateValues, List <string> updateColumns) where T : class, new()
        {
            var    tableInfo        = TableInfo.CreateInstance <T>(context, new List <T>(), OperationType.Read, new BulkConfig());
            string sql              = string.Empty;
            Type   updateValuesType = typeof(T);
            var    defaultValues    = new T();

            foreach (var propertyColumn in tableInfo.PropertyColumnNamesDict)
            {
                var  property               = updateValuesType.GetProperty(propertyColumn.Key);
                var  propertyUpdateValue    = property.GetValue(updateValues);
                var  propertyDefaultValue   = property.GetValue(defaultValues);
                bool isDifferentFromDefault = propertyUpdateValue?.ToString() != propertyDefaultValue?.ToString();
                if (isDifferentFromDefault || (updateColumns != null && updateColumns.Contains(propertyColumn.Key)))
                {
                    sql += propertyColumn.Value + " = '" + propertyUpdateValue + "'" + ", ";
                }
            }
            sql = sql.Remove(sql.Length - 2, 2); // removes last excess comma and space: ", "
            return($"SET {sql}");
        }
        /// <summary>
        /// get Update Sql
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static (string, List <object>) GetSqlUpdate <T>(IQueryable <T> query, DbContext context, Expression <Func <T, T> > expression, Dictionary <string, object> parametersDict) where T : class
        {
            (string sql, string tableAlias, string tableAliasSufixAs, IEnumerable <object> innerParameters) = GetBatchSql(query, context, isUpdate: true);
            var sqlColumns  = new StringBuilder();
            int paramsIndex = 0;

            if (parametersDict != null)
            {
                innerParameters = parametersDict.Select(a => new SqlParameter($"@__{a.Key}_{paramsIndex++}", a.Value));
            }
            var sqlParameters       = new List <object>(innerParameters);
            var columnNameValueDict = TableInfo.CreateInstance(context, new List <T>(), OperationType.Read, new BulkConfig()).PropertyColumnNamesDict;
            var dbType = GetDatabaseType(context);

            CreateUpdateBody(columnNameValueDict, tableAlias, expression.Body, dbType, ref sqlColumns, ref sqlParameters);

            sqlParameters = ReloadSqlParameters(context, sqlParameters); // Sqlite requires SqliteParameters
            sqlColumns    = (GetDatabaseType(context) == DbServer.SqlServer) ? sqlColumns : sqlColumns.Replace($"[{tableAlias}].", "");

            var resultQuery = $"UPDATE {tableAlias}{tableAliasSufixAs} SET {sqlColumns} {sql}";

            return(resultQuery, sqlParameters);
        }
        public static async Task ExecuteAsync(DbContext context, Type type, IList <object> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken cancellationToken)
        {
            using (ActivitySources.StartExecuteActivity(operationType, entities.Count))
            {
                if (operationType != OperationType.Truncate && entities.Count == 0)
                {
                    return;
                }

                if (bulkConfig?.IncludeGraph == true)
                {
                    await DbContextBulkTransactionGraphUtil.ExecuteWithGraphAsync(context, entities, operationType, bulkConfig, progress, cancellationToken);
                }
                else
                {
                    TableInfo tableInfo = TableInfo.CreateInstance(context, type, entities, operationType, bulkConfig);

                    if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
                    {
                        await SqlBulkOperation.InsertAsync(context, type, entities, tableInfo, progress, cancellationToken);
                    }
                    else if (operationType == OperationType.Read)
                    {
                        await SqlBulkOperation.ReadAsync(context, type, entities, tableInfo, progress, cancellationToken);
                    }
                    else if (operationType == OperationType.Truncate)
                    {
                        await SqlBulkOperation.TruncateAsync(context, tableInfo, cancellationToken);
                    }
                    else
                    {
                        await SqlBulkOperation.MergeAsync(context, type, entities, tableInfo, operationType, progress, cancellationToken);
                    }
                }
            }
        }
        public static void Execute <T>(DbContext context, IList <T> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress) where T : class
        {
            using (ActivitySources.StartExecuteActivity(operationType, entities.Count))
            {
                if (operationType != OperationType.Truncate && entities.Count == 0)
                {
                    return;
                }

                if (bulkConfig?.IncludeGraph == true)
                {
                    DbContextBulkTransactionGraphUtil.ExecuteWithGraph(context, entities, operationType, bulkConfig, progress);
                }
                else
                {
                    TableInfo tableInfo = TableInfo.CreateInstance(context, entities, operationType, bulkConfig);

                    if (operationType == OperationType.Insert && !tableInfo.BulkConfig.SetOutputIdentity)
                    {
                        SqlBulkOperation.Insert(context, entities, tableInfo, progress);
                    }
                    else if (operationType == OperationType.Read)
                    {
                        SqlBulkOperation.Read(context, entities, tableInfo, progress);
                    }
                    else if (operationType == OperationType.Truncate)
                    {
                        SqlBulkOperation.Truncate(context, tableInfo);
                    }
                    else
                    {
                        SqlBulkOperation.Merge(context, entities, tableInfo, operationType, progress);
                    }
                }
            }
        }
Example #20
0
        public static string GetSqlSetSegment(DbContext context, Type updateValuesType, object updateValues, List <string> updateColumns, List <object> parameters)
        {
            var tableInfo = TableInfo.CreateInstance(context, updateValuesType, new List <object>(), OperationType.Read, new BulkConfig());

            return(GetSqlSetSegment(context, tableInfo, updateValuesType, updateValues, Activator.CreateInstance(updateValuesType), updateColumns, parameters));
        }
Example #21
0
        public static string GetSqlSetSegment <T>(DbContext context, T updateValues, List <string> updateColumns, List <object> parameters) where T : class, new()
        {
            var tableInfo = TableInfo.CreateInstance <T>(context, new List <T>(), OperationType.Read, new BulkConfig());

            return(GetSqlSetSegment(context, tableInfo, typeof(T), updateValues, new T(), updateColumns, parameters));
        }
Example #22
0
        private static async Task ExecuteWithGraphAsync_Impl(DbContext context, IEnumerable <object> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken?cancellationToken, bool isAsync = true)
        {
            if (operationType != OperationType.Insert &&
                operationType != OperationType.InsertOrUpdate &&
                operationType != OperationType.InsertOrUpdateDelete &&
                operationType != OperationType.Update)
            {
                throw new InvalidBulkConfigException($"{nameof(BulkConfig)}.{nameof(BulkConfig.IncludeGraph)} only supports Insert or Update operations.");
            }

            // Sqlite bulk merge adapter does not support multiple objects of the same type with a zero value primary key
            if (SqlAdaptersMapping.GetDatabaseType(context) == DbServer.Sqlite)
            {
                throw new NotSupportedException("Sqlite is not currently supported due to its BulkInsert implementation.");
            }

            // If this is set to false, won't be able to propogate new primary keys to the relationships
            bulkConfig.SetOutputIdentity = true;

            // If this is set to false, wont' be able to support some code first model types as EFCore uses shadow properties when a relationship's foreign keys arent explicitly defined
            bulkConfig.EnableShadowProperties = true;

            var rootGraphItems = GraphUtil.GetOrderedGraph(context, entities);

            if (rootGraphItems == null)
            {
                return;
            }

            // Inserting an entity graph must be done within a transaction otherwise the database could end up in a bad state
            var hasExistingTransaction = context.Database.CurrentTransaction != null;
            var transaction            = context.Database.CurrentTransaction ?? (isAsync ? await context.Database.BeginTransactionAsync() : context.Database.BeginTransaction());

            try
            {
                foreach (var actionGraphItem in rootGraphItems)
                {
                    var entitiesToAction = GetUniqueEntities(context, actionGraphItem).ToList();
                    var tableInfo        = TableInfo.CreateInstance(context, actionGraphItem.EntityClrType, entitiesToAction, operationType, bulkConfig);

                    if (isAsync)
                    {
                        await SqlBulkOperation.MergeAsync(context, actionGraphItem.EntityClrType, entitiesToAction, tableInfo, operationType, progress, cancellationToken.Value);
                    }
                    else
                    {
                        SqlBulkOperation.Merge(context, actionGraphItem.EntityClrType, entitiesToAction, tableInfo, operationType, progress);
                    }

                    // Loop through the dependants and update their foreign keys with the PK values of the just inserted / merged entities
                    foreach (var graphEntity in actionGraphItem.Entities)
                    {
                        var entity       = graphEntity.Entity;
                        var parentEntity = graphEntity.ParentEntity;

                        // If the parent entity is null its the root type of the object graph.
                        if (parentEntity is null)
                        {
                            foreach (var navigation in actionGraphItem.Relationships)
                            {
                                // If this relationship requires the parents value to exist
                                if (navigation.ParentNavigation.IsDependentToPrincipal() == false)
                                {
                                    foreach (var navGraphEntity in navigation.Entities)
                                    {
                                        if (navGraphEntity.ParentEntity != entity)
                                        {
                                            continue;
                                        }

                                        SetForeignKeyForRelationship(context, navigation.ParentNavigation,
                                                                     navGraphEntity.Entity, entity);
                                    }
                                }
                            }
                        }
                        else
                        {
                            var navigation = actionGraphItem.ParentNavigation;

                            if (navigation.IsDependentToPrincipal())
                            {
                                SetForeignKeyForRelationship(context, navigation,
                                                             dependent: parentEntity,
                                                             principal: entity);
                            }
                            else
                            {
                                SetForeignKeyForRelationship(context, navigation,
                                                             dependent: entity,
                                                             principal: parentEntity);
                            }
                        }
                    }
                }

                if (hasExistingTransaction == false)
                {
                    if (isAsync)
                    {
                        await transaction.CommitAsync();
                    }
                    else
                    {
                        transaction.Commit();
                    }
                }
            }
            finally
            {
                if (hasExistingTransaction == false)
                {
                    if (isAsync)
                    {
                        await transaction.DisposeAsync();
                    }
                    else
                    {
                        transaction.Dispose();
                    }
                }
            }
        }
        private static async Task ExecuteWithGraphAsync(DbContext context, IEnumerable <object> entities, OperationType operationType, BulkConfig bulkConfig, Action <decimal> progress, CancellationToken cancellationToken, bool isAsync)
        {
            if (operationType != OperationType.Insert &&
                operationType != OperationType.InsertOrUpdate &&
                operationType != OperationType.InsertOrUpdateDelete &&
                operationType != OperationType.Update)
            {
                throw new InvalidBulkConfigException($"{nameof(BulkConfig)}.{nameof(BulkConfig.IncludeGraph)} only supports Insert or Update operations.");
            }

            // Sqlite bulk merge adapter does not support multiple objects of the same type with a zero value primary key
            if (SqlAdaptersMapping.GetDatabaseType(context) == DbServer.Sqlite)
            {
                throw new NotSupportedException("Sqlite is not currently supported due to its BulkInsert implementation.");
            }

            bulkConfig.PreserveInsertOrder = true; // Required for SetOutputIdentity ('true' is default but here explicitly assigned again in case it was changed to 'false' in BulkConfing)
            bulkConfig.SetOutputIdentity   = true; // If this is set to false, won't be able to propogate new primary keys to the relationships

            // If this is set to false, wont' be able to support some code first model types as EFCore uses shadow properties when a relationship's foreign keys arent explicitly defined
            bulkConfig.EnableShadowProperties = true;

            var graphNodes = GraphUtil.GetTopologicallySortedGraph(context, entities);

            if (graphNodes == null)
            {
                return;
            }

            // Inserting an entity graph must be done within a transaction otherwise the database could end up in a bad state
            var hasExistingTransaction = context.Database.CurrentTransaction != null;
            var transaction            = context.Database.CurrentTransaction ?? (isAsync ? await context.Database.BeginTransactionAsync() : context.Database.BeginTransaction());

            try
            {
                // Group the graph nodes by entity type so we can merge them into the database in batches, in the correct order of dependency (topological order)
                var graphNodesGroupedByType = graphNodes.GroupBy(y => y.Entity.GetType());

                foreach (var graphNodeGroup in graphNodesGroupedByType)
                {
                    // It is possible the object graph contains duplicate entities (by primary key) but the entities are different object instances in memory.
                    // This an happen when deserializing a nested JSON tree for example. So filter out the duplicates.
                    var entitiesToAction = GetUniqueEntities(context, graphNodeGroup.Select(y => y.Entity)).ToList();
                    var entityClrType    = graphNodeGroup.Key;
                    var tableInfo        = TableInfo.CreateInstance(context, entityClrType, entitiesToAction, operationType, bulkConfig);

                    if (isAsync)
                    {
                        await SqlBulkOperation.MergeAsync(context, entityClrType, entitiesToAction, tableInfo, operationType, progress, cancellationToken);
                    }
                    else
                    {
                        SqlBulkOperation.Merge(context, entityClrType, entitiesToAction, tableInfo, operationType, progress);
                    }

                    // Set the foreign keys for dependents so they may be inserted on the next loop
                    var dependentsOfSameType = SetForeignKeysForDependentsAndYieldSameTypeDependents(context, entityClrType, graphNodeGroup).ToList();

                    // If there are any dependents of the same type (parent child relationship), then save those dependent entities again to commit the fk values
                    if (dependentsOfSameType.Any())
                    {
                        var dependentTableInfo = TableInfo.CreateInstance(context, entityClrType, dependentsOfSameType, operationType, bulkConfig);

                        if (isAsync)
                        {
                            await SqlBulkOperation.MergeAsync(context, entityClrType, dependentsOfSameType, dependentTableInfo, operationType, progress, cancellationToken);
                        }
                        else
                        {
                            SqlBulkOperation.Merge(context, entityClrType, dependentsOfSameType, dependentTableInfo, operationType, progress);
                        }
                    }
                }

                if (hasExistingTransaction == false)
                {
                    if (isAsync)
                    {
                        await transaction.CommitAsync();
                    }
                    else
                    {
                        transaction.Commit();
                    }
                }
            }
            finally
            {
                if (hasExistingTransaction == false)
                {
                    if (isAsync)
                    {
                        await transaction.DisposeAsync();
                    }
                    else
                    {
                        transaction.Dispose();
                    }
                }
            }
        }