diff --git a/CascadeBulkInsert.Tests/App.config b/CascadeBulkInsert.Tests/App.config
new file mode 100644
index 0000000..e4247c7
--- /dev/null
+++ b/CascadeBulkInsert.Tests/App.config
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/AppReadme/Compare-NET-Objects-Help.chm b/CascadeBulkInsert.Tests/AppReadme/Compare-NET-Objects-Help.chm
new file mode 100644
index 0000000..7a6ba6c
Binary files /dev/null and b/CascadeBulkInsert.Tests/AppReadme/Compare-NET-Objects-Help.chm differ
diff --git a/CascadeBulkInsert.Tests/BulkInsertTest.cs b/CascadeBulkInsert.Tests/BulkInsertTest.cs
new file mode 100644
index 0000000..c00ab18
--- /dev/null
+++ b/CascadeBulkInsert.Tests/BulkInsertTest.cs
@@ -0,0 +1,30 @@
+using Altairis.Samples.Data.Northwind.CodeFirst;
+using EntityFramework.Metadata.Extensions;
+using Xunit;
+
+namespace EF.BulkInsert.Cascade.Tests
+{
+ public class BulkInsertTest
+ {
+ public BulkInsertTest()
+ {
+ }
+
+ [Fact]
+ public void StuctualTest()
+ {
+ using (var context = new NorthwindDbContext())
+ {
+ var propertyMaps = context.Db().Properties;
+// var columns = propertyMaps.Select(o => new
+// {
+// o.ColumnName,
+// GetValue = o.IsDiscriminator
+// ? x => typeof(T).Name
+// : ExpressHelper.GetPropGetter(o.PropertyName).Compile()
+// }).ToArray();
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/DataTableExtension.cs b/CascadeBulkInsert.Tests/DataTableExtension.cs
new file mode 100644
index 0000000..b5ca429
--- /dev/null
+++ b/CascadeBulkInsert.Tests/DataTableExtension.cs
@@ -0,0 +1,18 @@
+using System.Data;
+using FluentAssertions.Execution;
+using KellermanSoftware.CompareNetObjects;
+
+namespace EF.BulkInsert.Cascade.Tests
+{
+ public static class DataTableExtension
+ {
+ public static void ShouldBeSame(this DataTable t1, DataTable t2)
+ {
+ var comparisonResult = new CompareLogic().Compare(t1, t2);
+ if (!comparisonResult.AreEqual)
+ {
+ throw new AssertionFailedException(comparisonResult.DifferencesString);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/EF.BulkInsert.CascadeTests.csproj b/CascadeBulkInsert.Tests/EF.BulkInsert.CascadeTests.csproj
new file mode 100644
index 0000000..0c8df2c
--- /dev/null
+++ b/CascadeBulkInsert.Tests/EF.BulkInsert.CascadeTests.csproj
@@ -0,0 +1,132 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {AD8F025C-8968-4B1A-97AD-29950905A3A2}
+ Library
+ Properties
+ EF.BulkInsert.Cascade.Tests
+ EF.BulkInsert.Cascade.Tests
+ v4.5.2
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\Altairis.Samples.Data.Northwind.CodeFirst.1.0.0\lib\net45\Altairis.Samples.Data.Northwind.CodeFirst.dll
+
+
+ ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll
+ True
+
+
+ ..\packages\EntityFramework.Metadata.1.0.2.0\lib\net45\EntityFramework.Metadata.dll
+
+
+ ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll
+ True
+
+
+ ..\packages\FluentAssertions.4.18.0\lib\net45\FluentAssertions.dll
+ True
+
+
+ ..\packages\FluentAssertions.4.18.0\lib\net45\FluentAssertions.Core.dll
+ True
+
+
+ ..\packages\CompareNETObjects.3.06.0.0\lib\net452\KellermanSoftware.Compare-NET-Objects.dll
+ True
+
+
+
+
+
+
+ ..\packages\Microsoft.SqlServer.Compact.4.0.8854.1\lib\net40\System.Data.SqlServerCe.dll
+ True
+
+
+ ..\packages\Microsoft.SqlServer.Compact.4.0.8854.1\lib\net40\System.Data.SqlServerCe.Entity.dll
+ True
+
+
+
+
+ ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll
+ True
+
+
+ ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll
+ True
+
+
+ ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll
+ True
+
+
+ ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {555355a8-8ad2-4b0b-9f31-6966e0e79037}
+ EF.BulkInsert.Cascade
+
+
+
+
+ Always
+
+
+
+
+
+ if not exist "$(TargetDir)x86" md "$(TargetDir)x86"
+ xcopy /s /y "$(SolutionDir)packages\Microsoft.SqlServer.Compact.4.0.8854.1\NativeBinaries\x86\*.*" "$(TargetDir)x86"
+ if not exist "$(TargetDir)amd64" md "$(TargetDir)amd64"
+ xcopy /s /y "$(SolutionDir)packages\Microsoft.SqlServer.Compact.4.0.8854.1\NativeBinaries\amd64\*.*" "$(TargetDir)amd64"
+
+
+
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/MetaDataTests/DataTableTests.cs b/CascadeBulkInsert.Tests/MetaDataTests/DataTableTests.cs
new file mode 100644
index 0000000..fb1c818
--- /dev/null
+++ b/CascadeBulkInsert.Tests/MetaDataTests/DataTableTests.cs
@@ -0,0 +1,78 @@
+using System.Data;
+using EF.BulkInsert.Cascade.Tests.TestContext;
+using Xunit;
+
+namespace EF.BulkInsert.Cascade.Tests.MetaDataTests
+{
+ [Collection(nameof(DbContextCollection))]
+ public class DataTableTests
+ {
+ private readonly TableNameTestContext _context;
+
+ public DataTableTests(TableNameTestContext context)
+ {
+ _context = context;
+ }
+
+
+
+ [Fact]
+ public void StuctualTest()
+ {
+ var dataTable = new DataTable();
+ dataTable.Columns.AddRange(new[]
+ {
+ new DataColumn(nameof(BasicEntity.Id),typeof(long)),
+ new DataColumn(nameof(BasicEntity.Name),typeof(string)),
+ });
+ var dataRow = dataTable.Rows.Add();
+ dataRow[nameof(BasicEntity.Id)] = 1;
+ dataRow[nameof(BasicEntity.Name)] = "Test";
+ _context.GetDataReader(new[]{new BasicEntity {Id = 1,Name = "Test"}, }).ShouldBeSame(dataTable);
+ }
+/*
+ [Fact]
+ public void BasicInheritanceTest()
+ {
+ var columnInfos = _context.GetColumns().ToArray();
+ columnInfos.ShouldAllBeEquivalentTo(
+ new IColumnInfo[]
+ {
+ new ColumnInfo(nameof(InheritanceA.Id)),
+ new ColumnInfo(nameof(InheritanceA.Name)),
+ new ColumnInfo(nameof(InheritanceA.ColumnA)),
+ new ColumnInfoDescriptor(),
+ });
+ }
+
+ [Fact]
+ public void InheritanceTest()
+ {
+ _context.GetColumns().ShouldAllBeEquivalentTo(
+ new IColumnInfo[]
+ {
+ new ColumnInfo(nameof(InheritanceB.Id)),
+ new ColumnInfo(nameof(InheritanceB.Name)),
+ });
+ }
+
+
+ [Fact]
+ public void Inheritance2Test()
+ {
+ var columnInfos = _context.GetColumns().ToArray();
+ columnInfos.ShouldAllBeEquivalentTo(
+ new IColumnInfo[]
+ {
+ new ColumnInfo(nameof(InheritanceAa.Id)),
+ new ColumnInfo(nameof(InheritanceAa.Name)),
+ new ColumnInfo(nameof(InheritanceAa.ColumnA)),
+ new ColumnInfo(nameof(InheritanceAa.ColumnAa)),
+ new ColumnInfoDescriptor(),
+ });
+ }
+*/
+
+ }
+
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/MetaDataTests/DbContextCollection.cs b/CascadeBulkInsert.Tests/MetaDataTests/DbContextCollection.cs
new file mode 100644
index 0000000..b4c2fa6
--- /dev/null
+++ b/CascadeBulkInsert.Tests/MetaDataTests/DbContextCollection.cs
@@ -0,0 +1,10 @@
+using EF.BulkInsert.Cascade.Tests.TestContext;
+using Xunit;
+
+namespace EF.BulkInsert.Cascade.Tests.MetaDataTests
+{
+ [CollectionDefinition(nameof(DbContextCollection))]
+ public class DbContextCollection : ICollectionFixture
+ {
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/Properties/AssemblyInfo.cs b/CascadeBulkInsert.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..371703f
--- /dev/null
+++ b/CascadeBulkInsert.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("CascadeBulkInsert.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("CascadeBulkInsert.Tests")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("ad8f025c-8968-4b1a-97ad-29950905a3a2")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/CascadeBulkInsert.Tests/TestContext/BasicEntity.cs b/CascadeBulkInsert.Tests/TestContext/BasicEntity.cs
new file mode 100644
index 0000000..05d45f9
--- /dev/null
+++ b/CascadeBulkInsert.Tests/TestContext/BasicEntity.cs
@@ -0,0 +1,8 @@
+namespace EF.BulkInsert.Cascade.Tests.TestContext
+{
+ public class BasicEntity
+ {
+ public long Id { get; set; }
+ public string Name { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/TestContext/InheritanceBase.cs b/CascadeBulkInsert.Tests/TestContext/InheritanceBase.cs
new file mode 100644
index 0000000..53af559
--- /dev/null
+++ b/CascadeBulkInsert.Tests/TestContext/InheritanceBase.cs
@@ -0,0 +1,23 @@
+namespace EF.BulkInsert.Cascade.Tests.TestContext
+{
+ public abstract class InheritanceBase
+ {
+ public long Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class InheritanceA : InheritanceBase
+ {
+ public string ColumnA { get; set; }
+ }
+
+ public class InheritanceAa : InheritanceA
+ {
+ public string ColumnAa { get; set; }
+ }
+
+
+ public class InheritanceB : InheritanceBase
+ {
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/TestContext/TableNameSpecifiedEntity.cs b/CascadeBulkInsert.Tests/TestContext/TableNameSpecifiedEntity.cs
new file mode 100644
index 0000000..464a548
--- /dev/null
+++ b/CascadeBulkInsert.Tests/TestContext/TableNameSpecifiedEntity.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace EF.BulkInsert.Cascade.Tests.TestContext
+{
+ [Table("TestTableName")]
+ public class TableNameSpecifiedEntity
+ {
+ public long Id { get; set; }
+ public string Name { get; set; }
+
+ public ComplexType ComplexType { get; set; }
+ }
+
+ [ComplexType]
+ public class ComplexType
+ {
+ public string ComplexColumn { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/TestContext/TestContext.cs b/CascadeBulkInsert.Tests/TestContext/TestContext.cs
new file mode 100644
index 0000000..ef9a9f8
--- /dev/null
+++ b/CascadeBulkInsert.Tests/TestContext/TestContext.cs
@@ -0,0 +1,19 @@
+using System.Data.Entity;
+
+namespace EF.BulkInsert.Cascade.Tests.TestContext
+{
+ public class TableNameTestContext : DbContext
+ {
+ public TableNameTestContext()
+ {
+ Database.SetInitializer(null);
+ }
+
+ public IDbSet BasicEntities { get; set; }
+ public IDbSet TableNameSpecifiedEntities { get; set; }
+
+ public IDbSet InheritanceAs { get; set; }
+
+ public IDbSet InheritanceBs { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert.Tests/packages.config b/CascadeBulkInsert.Tests/packages.config
new file mode 100644
index 0000000..06b672a
--- /dev/null
+++ b/CascadeBulkInsert.Tests/packages.config
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CascadeBulkInsert/App.config b/CascadeBulkInsert/App.config
new file mode 100644
index 0000000..2fb423e
--- /dev/null
+++ b/CascadeBulkInsert/App.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CascadeBulkInsert/Cascade.cs b/CascadeBulkInsert/Cascade.cs
new file mode 100644
index 0000000..1139c46
--- /dev/null
+++ b/CascadeBulkInsert/Cascade.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Linq;
+using System.Linq.Expressions;
+using EF.BulkInsert.Cascade.Helpers;
+using EntityFramework.Metadata.Extensions;
+using JetBrains.Annotations;
+
+namespace EF.BulkInsert.Cascade
+{
+ public class Cascade : ICascade where TDestination : class
+ {
+ private readonly ICascade[] _cascades;
+ private readonly Expression>> _extractor;
+ private readonly string _propertyName;
+
+ public Cascade([NotNull] Expression>> extractor,
+ [NotNull] Expression> idGetter,
+ params ICascade[] cascades)
+ : this(extractor, ExpressHelper.GetPath(idGetter), cascades)
+ {
+ }
+
+ public Cascade([NotNull] Expression>> extractor,
+ [NotNull] Expression> idGetter,
+ params ICascade[] cascades)
+ : this(extractor, ExpressHelper.GetPath(idGetter), cascades)
+ {
+ }
+
+ public Cascade([NotNull] Expression>> extractor,
+ params ICascade[] cascades)
+ : this(extractor, (string) null, cascades)
+ {
+ }
+
+
+ private Cascade(
+ [NotNull] Expression>> extractor,
+ string propertyName,
+ params ICascade[] cascades)
+ {
+ _extractor = extractor;
+ _propertyName = propertyName;
+ _cascades = cascades;
+ }
+
+ public void InnerInsert(IEnumerable source, DbContext context, DbContextTransaction transaction)
+ {
+ if (source == null)
+ {
+ return;
+ }
+ var extractor = _extractor.Compile();
+ var s = source.Where(o => extractor(o) != null).ToArray();
+ if (!s.Any())
+ {
+ return;
+ }
+ var fkSetter = GetSetter(context);
+ var idGetter = context.GetPkGetter();
+ foreach (var parent in s)
+ {
+ foreach (var child in extractor(parent))
+ {
+ fkSetter.Invoke(child, idGetter(parent));
+ }
+ }
+ context.BulkInsertCascade(transaction, s.SelectMany(extractor).Distinct().ToArray(), _cascades);
+ }
+
+ private Action GetSetter(DbContext context)
+ {
+ var propertyName = _propertyName ?? context.Db()
+ .Properties.Single(o => o.NavigationProperty?.Type == typeof(TSource))
+ .PropertyName;
+ return ExpressHelper.GetPropSetter(propertyName).Compile();
+ }
+
+ public bool IsCascadeBeforeInsert => false;
+
+ protected bool Equals(Cascade other)
+ {
+ return Equals(_extractor, other._extractor) && string.Equals(_propertyName, other._propertyName) &&
+ Equals(_cascades, other._cascades);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ {
+ return false;
+ }
+ if (ReferenceEquals(this, obj))
+ {
+ return true;
+ }
+ if (obj.GetType() != GetType())
+ {
+ return false;
+ }
+ return Equals((Cascade) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = _extractor?.GetHashCode() ?? 0;
+ hashCode = (hashCode * 397) ^ (_propertyName?.GetHashCode() ?? 0);
+ hashCode = (hashCode * 397) ^ (_cascades?.GetHashCode() ?? 0);
+ return hashCode;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/CascadeReverse.cs b/CascadeBulkInsert/CascadeReverse.cs
new file mode 100644
index 0000000..69a95e5
--- /dev/null
+++ b/CascadeBulkInsert/CascadeReverse.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Entity;
+using System.Linq;
+using System.Linq.Expressions;
+using EF.BulkInsert.Cascade.Helpers;
+using EntityFramework.Metadata.Extensions;
+using JetBrains.Annotations;
+
+namespace EF.BulkInsert.Cascade
+{
+ public class CascadeReverse : ICascade where TDestination : class
+ {
+ [NotNull]
+ private readonly ICascade[] _cascades;
+ [NotNull]
+ private readonly Expression> _extractor;
+
+ private CascadeReverse([NotNull] Expression> extractor,[NotNull] params ICascade[] cascades)
+ {
+ _extractor = extractor;
+ _cascades = cascades;
+ }
+
+ public void InnerInsert(IEnumerable source, DbContext context, DbContextTransaction transaction)
+ {
+ if (source == null)
+ {
+ return;
+ }
+ var extractor = _extractor.Compile();
+ var s = source.Where(o => extractor(o) != null).ToArray();
+ if (!s.Any())
+ {
+ return;
+ }
+ var pkGetter = context.GetPkGetter();
+ var destinations = s.Select(extractor).Distinct().ToArray();
+ context.BulkInsertCascade(transaction, destinations, _cascades);
+ var fkSetter = GetSetter(context);
+ foreach (var destination in s)
+ {
+ var parrentId = pkGetter(extractor(destination));
+ fkSetter.Invoke(destination, parrentId);
+ }
+ }
+
+ public bool IsCascadeBeforeInsert => true;
+
+ private Action GetSetter(DbContext context)
+ {
+ var path = ExpressHelper.GetPath(_extractor);
+ var propertyName = context.Db().Properties.Single(o => o.NavigationProperty?.PropertyName == path).PropertyName;
+ return ExpressHelper.GetPropSetter(propertyName).Compile();
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/ContextExtender.cs b/CascadeBulkInsert/ContextExtender.cs
new file mode 100644
index 0000000..c8e2b32
--- /dev/null
+++ b/CascadeBulkInsert/ContextExtender.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Entity;
+using System.Data.SqlClient;
+using System.Linq;
+using EF.BulkInsert.Cascade.Helpers;
+using EntityFramework.Metadata;
+using EntityFramework.Metadata.Extensions;
+
+namespace EF.BulkInsert.Cascade
+{
+ public static class ContextExtender
+ {
+ private const int BulkCopyTimeout = 10 * 60;
+
+ ///
+ /// retrieve Ids for every Entity from
+ ///
+ /// Entity type
+ /// Database Context
+ /// List of entities which should be inserted into database
+ public static void RetrieveIds(this DbContext context, TEntity[] entities)
+ where TEntity : class
+ {
+ var tableName = context.Db().TableName;
+ var entitiesLength = entities.Length;
+ var minId = context.Database.SqlQuery(
+ $@"
+declare
+ @Maxid bigint;
+begin
+ select @Maxid=IDENT_CURRENT ('{tableName}');
+ set @Maxid =@Maxid+{entitiesLength};
+ DBCC CHECKIDENT ('{tableName}',Reseed,@Maxid);
+ select @Maxid;
+end;
+").First() - entitiesLength;
+ var pkSetter = context.GetPkSetter();
+ foreach (var entity in entities)
+ {
+ pkSetter(entity, ++minId);
+ }
+ }
+
+ ///
+ /// Inserts in Bulk operation
+ ///
+ /// Entity type
+ /// Database Context
+ /// Open transaction
+ /// List of entities which should be inserted into database
+ /// True, when id should be inserted ,false when id should be generated
+ public static void BulkInsert(this DbContext context, DbContextTransaction transaction, IEnumerable entities,
+ bool keepIdentity = false)
+ {
+ var underlyingTransaction = (SqlTransaction)transaction.UnderlyingTransaction;
+
+ using (var sqlBulkCopy = new SqlBulkCopy(underlyingTransaction.Connection,
+ keepIdentity ? SqlBulkCopyOptions.KeepIdentity : SqlBulkCopyOptions.Default, underlyingTransaction))
+ {
+ sqlBulkCopy.BulkCopyTimeout = BulkCopyTimeout;
+ sqlBulkCopy.DestinationTableName = context.Db().TableName;
+ var table = context.GetDataReader(entities);
+ foreach (var column in table.Columns.OfType().Select(o => o.ColumnName))
+ {
+ sqlBulkCopy.ColumnMappings.Add(column, column);
+ }
+ sqlBulkCopy.WriteToServer(table);
+ }
+ }
+
+ public static DataTable GetDataReader(this DbContext context, IEnumerable entities)
+ {
+ var propertyMaps = context.Db().Properties;
+ var table = propertyMaps.CreateTable();
+ var columns = propertyMaps.Select(o => new
+ {
+ o.ColumnName,
+ GetValue = o.IsDiscriminator
+ ? x => typeof(T).Name
+ : ExpressHelper.GetPropGetter(o.PropertyName).Compile()
+ }).ToArray();
+ foreach (var entity in entities)
+ {
+ var dataRow = table.Rows.Add();
+ foreach (var column in columns)
+ {
+ dataRow[column.ColumnName] = column.GetValue(entity) ?? DBNull.Value;
+ }
+ }
+ return table;
+ }
+
+ private static DataTable CreateTable(this IEnumerable columns)
+ {
+ var result = new DataTable();
+ foreach (var column in columns)
+ {
+ var propertyType = column.Type;
+ result.Columns.Add(column.PropertyName,
+ propertyType.IsNullable() ? propertyType.GetGenericArguments().First() : propertyType);
+ }
+ return result;
+ }
+
+ public static void BulkInsertWithIdGeneration(this DbContext context, DbContextTransaction transaction, IList forSave)
+ where T : class
+ {
+ if (forSave == null)
+ {
+ return;
+ }
+ var idGetter = context.GetPkGetter();
+ Func isNew = o => idGetter(o) == default(long);
+ var newObjects = forSave.Where(isNew).ToArray();
+ if (!newObjects.Any())
+ {
+ return;
+ }
+ context.RetrieveIds(newObjects);
+ context.BulkInsert(transaction, newObjects, true);
+ }
+
+ public static void BulkInsertCascade(this DbContext context, DbContextTransaction transaction, IList forSave,
+ params ICascade[] cascades) where T : class
+ {
+ if (forSave == null || !forSave.Any())
+ {
+ return;
+ }
+ foreach (var cascade in cascades.Where(o => o.IsCascadeBeforeInsert))
+ {
+ cascade.InnerInsert(forSave, context, transaction);
+ }
+
+ if (cascades.Any())
+ {
+ context.BulkInsertWithIdGeneration(transaction, forSave);
+ }
+ else
+ {
+ context.BulkInsertWithIdGeneration(transaction, forSave);
+ }
+ foreach (var cascade in cascades.Where(o => !o.IsCascadeBeforeInsert))
+ {
+ cascade.InnerInsert(forSave, context, transaction);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/EF.BulkInsert.Cascade.csproj b/CascadeBulkInsert/EF.BulkInsert.Cascade.csproj
new file mode 100644
index 0000000..fd41f3e
--- /dev/null
+++ b/CascadeBulkInsert/EF.BulkInsert.Cascade.csproj
@@ -0,0 +1,77 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {555355A8-8AD2-4B0B-9F31-6966E0E79037}
+ Library
+ Properties
+ EF.BulkInsert.Cascade
+ EF.BulkInsert.Cascade
+ v4.5.2
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll
+ True
+
+
+ ..\packages\EntityFramework.Metadata.1.0.2.0\lib\net45\EntityFramework.Metadata.dll
+ True
+
+
+ ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll
+ True
+
+
+ ..\packages\JetBrains.Annotations.10.2.1\lib\net\JetBrains.Annotations.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CascadeBulkInsert/Helpers/ExpressHelper.cs b/CascadeBulkInsert/Helpers/ExpressHelper.cs
new file mode 100644
index 0000000..4518f95
--- /dev/null
+++ b/CascadeBulkInsert/Helpers/ExpressHelper.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace EF.BulkInsert.Cascade.Helpers
+{
+ ///
+ /// Helper used for parsing expressions
+ ///
+ internal static class ExpressHelper
+ {
+ ///
+ /// Creates property value getter from the property name
+ ///
+ /// Type containing the property
+ /// Path to the property
+ /// Property value getter expression
+ internal static Expression> GetPropGetter(string path)
+ {
+ var paramExpression = Expression.Parameter(typeof(TObject), "value");
+ var expressionTree = path.Split('.').Aggregate(paramExpression, Expression.Property);
+
+ var body = Expression.Convert(expressionTree, typeof(object));
+ return Expression.Lambda>(body, paramExpression);
+ }
+
+ ///
+ /// Creates property value getter from the property name
+ ///
+ /// Type containing the property
+ /// Type of the property
+ /// Name of the property
+ /// Property value getter expression
+ ///
+ internal static Expression> GetPropGetter(string propertyName)
+ {
+ ParameterExpression paramExpression = Expression.Parameter(typeof(TObject), "value");
+
+ var propertyGetterExpression = Expression.Property(paramExpression, propertyName);
+ return Expression.Lambda>(propertyGetterExpression, paramExpression);
+ }
+
+
+ internal static Expression> GetPropSetter(string propertyName)
+ {
+ var paramObject = Expression.Parameter(typeof(TObject), "o");
+ var paramValue = Expression.Parameter(typeof(TProperty), "value");
+ var property = Expression.Property(paramObject, propertyName);
+ var assign = Expression.Assign(property, paramValue);
+ return Expression.Lambda>(assign, paramObject, paramValue);
+ }
+
+ ///
+ /// Gets path for expression
+ ///
+ /// Type
+ /// Property type
+ /// Property expression
+ /// string representation of path to property
+ internal static string GetPath(Expression> exp)
+ {
+ return string.Join(".", GetItemsInPath(exp).Reverse());
+ }
+
+ private static IEnumerable GetItemsInPath(Expression> exp)
+ {
+ if (exp == null)
+ {
+ yield break;
+ }
+ var memberExp = FindMemberExpression(exp.Body);
+ while (memberExp != null)
+ {
+ yield return memberExp.Member.Name;
+ memberExp = FindMemberExpression(memberExp.Expression);
+ }
+ }
+
+
+ private static bool IsConversion(this Expression exp)
+ {
+ return exp.NodeType == ExpressionType.Convert || exp.NodeType == ExpressionType.ConvertChecked;
+ }
+
+ private static MemberExpression FindMemberExpression(this Expression exp)
+ {
+ if (exp is MemberExpression)
+ {
+ return (MemberExpression)exp;
+ }
+ if (IsConversion(exp) && exp is UnaryExpression)
+ {
+ return ((UnaryExpression)exp).Operand as MemberExpression;
+ }
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/Helpers/MetadataExtender.cs b/CascadeBulkInsert/Helpers/MetadataExtender.cs
new file mode 100644
index 0000000..0a62c20
--- /dev/null
+++ b/CascadeBulkInsert/Helpers/MetadataExtender.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Data.Entity;
+using System.Linq;
+using EntityFramework.Metadata.Extensions;
+
+namespace EF.BulkInsert.Cascade.Helpers
+{
+ internal static class MetadataExtender
+ {
+ public static Action GetPkSetter(this DbContext context)
+ {
+ var primaryKey = context.Db().Pks.Single();
+ if (primaryKey.Type != typeof(long))
+ {
+ throw new NotSupportedException("Just long type primary key are supported");
+ }
+ return ExpressHelper.GetPropSetter(primaryKey.PropertyName).Compile();
+ }
+
+ public static Func GetPkGetter(this DbContext context)
+ {
+ var primaryKey = context.Db().Pks.Single();
+ if (primaryKey.Type != typeof(long))
+ {
+ throw new NotSupportedException("Just long type primary key are supported");
+ }
+ return ExpressHelper.GetPropGetter(primaryKey.PropertyName).Compile();
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/Helpers/TypeExtender.cs b/CascadeBulkInsert/Helpers/TypeExtender.cs
new file mode 100644
index 0000000..865813c
--- /dev/null
+++ b/CascadeBulkInsert/Helpers/TypeExtender.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace EF.BulkInsert.Cascade.Helpers
+{
+ internal static class TypeExtender
+ {
+ internal static bool IsNullable(this Type type)
+ {
+ return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/ICascade.cs b/CascadeBulkInsert/ICascade.cs
new file mode 100644
index 0000000..6561e7b
--- /dev/null
+++ b/CascadeBulkInsert/ICascade.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using System.Data.Entity;
+
+namespace EF.BulkInsert.Cascade
+{
+ public interface ICascade
+ {
+ bool IsCascadeBeforeInsert { get; }
+
+ void InnerInsert(IEnumerable source, DbContext context, DbContextTransaction transaction);
+ }
+}
\ No newline at end of file
diff --git a/CascadeBulkInsert/Properties/AssemblyInfo.cs b/CascadeBulkInsert/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..c6a322f
--- /dev/null
+++ b/CascadeBulkInsert/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("CascadeBulkInsert")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("CascadeBulkInsert")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("555355a8-8ad2-4b0b-9f31-6966e0e79037")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/CascadeBulkInsert/packages.config b/CascadeBulkInsert/packages.config
new file mode 100644
index 0000000..69d5974
--- /dev/null
+++ b/CascadeBulkInsert/packages.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/EF.BulkInsert.Cascade.sln b/EF.BulkInsert.Cascade.sln
new file mode 100644
index 0000000..336a03f
--- /dev/null
+++ b/EF.BulkInsert.Cascade.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.16
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF.BulkInsert.Cascade", "CascadeBulkInsert\EF.BulkInsert.Cascade.csproj", "{555355A8-8AD2-4B0B-9F31-6966E0E79037}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF.BulkInsert.CascadeTests", "CascadeBulkInsert.Tests\EF.BulkInsert.CascadeTests.csproj", "{AD8F025C-8968-4B1A-97AD-29950905A3A2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {555355A8-8AD2-4B0B-9F31-6966E0E79037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {555355A8-8AD2-4B0B-9F31-6966E0E79037}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {555355A8-8AD2-4B0B-9F31-6966E0E79037}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {555355A8-8AD2-4B0B-9F31-6966E0E79037}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD8F025C-8968-4B1A-97AD-29950905A3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD8F025C-8968-4B1A-97AD-29950905A3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD8F025C-8968-4B1A-97AD-29950905A3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD8F025C-8968-4B1A-97AD-29950905A3A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6DC1841B-861B-43A4-AB00-53A66254B63B}
+ EndGlobalSection
+EndGlobal