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