Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added fluent WithIdentityInsert() method in order to deactivate and reactivate Identity insert in an sql database. Added a test project using TestContainers #468

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Paillave.EntityFrameworkCoreExtension.ContextMetadata;
using Paillave.EntityFrameworkCoreExtension.Core;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Paillave.EntityFrameworkCoreExtension.ContextMetadata;
using Paillave.EntityFrameworkCoreExtension.Core;

namespace Paillave.EntityFrameworkCoreExtension.BulkSave
{
Expand Down Expand Up @@ -108,29 +106,31 @@ public BulkSaveEngineBase(DbContext context, params Expression<Func<T, object>>[
// dbComputedProperties = allProperties.Where(i => i.GetValueGenerationStrategy() != null).ToList();
// computedProperties = allProperties.Where(i => (i.ValueGenerated & ValueGenerated.OnAddOrUpdate) != ValueGenerated.Never).ToList();
// allProperties[0].GetDefaultValueSql()
dbComputedProperties = allProperties.Where(i => (i.ValueGenerated & ValueGenerated.OnAddOrUpdate) != ValueGenerated.Never).ToList();
notPivotComputedProperties = dbComputedProperties.Except(_propertiesForPivotSet.SelectMany(i => i)).ToList();
dbComputedProperties = allProperties.Where(i => i.GetComputedColumnSql() != null).ToList();

defaultValuesProperties = allProperties.Where(i => i.GetDefaultValueSql() != null).ToList();

_propertiesToBulkLoad = allProperties.Except(notPivotComputedProperties).ToList();
_propertiesToBulkLoad = allProperties
.Except(dbComputedProperties)
.ToList();

_propertiesToInsert = allProperties
.Except(dbComputedProperties)
//.Except(new[] { identityProperty })
.ToList();

_propertiesToUpdate = allProperties
// .Except(_propertiesForPivot)
.Except(dbComputedProperties)
//.Except(new[] { identityProperty })
.ToList();

_propertiesToGetAfterSetInTarget = dbComputedProperties
//.Union(new[] { identityProperty })
.Union(defaultValuesProperties)
.Distinct(new LambdaEqualityComparer<IProperty, string>(i => i.GetColumnName(StoreObject)))
.ToList();
}
public void Save(IList<T> entities, CancellationToken cancellationToken, bool doNotUpdateIfExists = false, bool insertOnly = false)
public void Save(IList<T> entities, CancellationToken cancellationToken, bool doNotUpdateIfExists = false, bool insertOnly = false, bool identityInsert = false)
{
if (entities.Count == 0) return;
var previousAutoDetect = _context.ChangeTracker.AutoDetectChangesEnabled;
Expand All @@ -150,9 +150,19 @@ public void Save(IList<T> entities, CancellationToken cancellationToken, bool do
contextQuery.CreateOutputStagingTable();
outputStagingTableCreated = true;
if (insertOnly)
contextQuery.InsertFromStaging();
{
if (identityInsert)
{
contextQuery.InsertFromStagingWithIdentityInsert();
}
else
{
contextQuery.InsertFromStaging();
}
}
else
contextQuery.MergeFromStaging(doNotUpdateIfExists);

if (this.ShouldIndexStagingTable(entities.Count()))
contextQuery.IndexOutputStagingTable();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
{
public static class DbContextBulkExtensions
{
public static void BulkSave<T>(this DbContext context, IList<T> entities, CancellationToken cancellationToken, Expression<Func<T, object>> pivotKey = null, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class

Check warning on line 13 in src/Paillave.EntityFrameworkCoreExtension/BulkSave/DbContextBulkExtensions.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
BulkSaveEngineBase<T> bulkSaveEngine = new BulkSaveEngine<T>(context, pivotKey);
bulkSaveEngine.Save(entities, cancellationToken, doNotUpdateIfExists, insertOnly);
}
public static void BulkSave<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>>[] pivotKeys, CancellationToken cancellationToken, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
public static void BulkSave<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>>[] pivotKeys, CancellationToken cancellationToken, bool doNotUpdateIfExists = false, bool insertOnly = false, bool identityInsert = false ) where T : class
{
BulkSaveEngineBase<T> bulkSaveEngine = new BulkSaveEngine<T>(context, pivotKeys);
bulkSaveEngine.Save(entities, cancellationToken, doNotUpdateIfExists, insertOnly);
bulkSaveEngine.Save(entities, cancellationToken, doNotUpdateIfExists, insertOnly, identityInsert );
}
public static void BulkUpdate<TEntity, TSource>(this DbContext context, IList<TSource> sources, Expression<Func<TSource, TEntity>> updateKey, Expression<Func<TSource, TEntity>> updateValues) where TEntity : class
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public SaveContextQueryBase(DbContext context, string schema, string table, List
/// </summary>
public abstract void MergeFromStaging(bool doNotUpdateIfExists = false);
public abstract void InsertFromStaging();
public abstract void InsertFromStagingWithIdentityInsert();
public abstract void IndexStagingTable(List<IProperty> propertiesForPivot);
public abstract void IndexOutputStagingTable();
public abstract DataTable GetOutputStaging();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
// using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Paillave.EntityFrameworkCoreExtension.BulkSave.SqlServer
{
Expand All @@ -30,7 +26,7 @@ public override int CreateStagingTable()
=> this.Context.Database.ExecuteSqlRaw(this.CreateStagingTableSql());

protected virtual string CreateStagingTableSql()
=> $@"SELECT TOP 0 { string.Join(",", PropertiesToBulkLoad.Select(i => $"T.[{i.GetColumnName(base.StoreObject)}]")) }, 0 as [{TempColumnNumOrderName}]
=> $@"SELECT TOP 0 {string.Join(",", PropertiesToBulkLoad.Select(i => $"T.[{i.GetColumnName(base.StoreObject)}]"))}, 0 as [{TempColumnNumOrderName}]
INTO {SqlStagingTableName} FROM {SqlTargetTable} AS T
LEFT JOIN {SqlTargetTable} AS Source ON 1 = 0 option(recompile);";

Expand Down Expand Up @@ -124,6 +120,13 @@ public override void MergeFromStaging(bool doNotUpdateIfExists = false)
public override void InsertFromStaging()
=> this.Context.Database.ExecuteSqlRaw(this.InsertFromStagingSql());

public override void InsertFromStagingWithIdentityInsert()
{
Context.Database.ExecuteSqlRaw($"Set Identity_Insert {SqlTargetTable} On;");
InsertFromStaging();
Context.Database.ExecuteSqlRaw($"Set Identity_Insert {SqlTargetTable} Off;");
}

private string CreateEqualityConditionSql(string aliasLeft, string aliasRight, IProperty property)
{
string regularEquality = $"{aliasLeft}.[{property.GetColumnName(base.StoreObject)}] = {aliasRight}.[{property.GetColumnName(base.StoreObject)}]";
Expand Down
39 changes: 39 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/BloggingContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Paillave.Etl.EntityFrameworkCore.Tests.Entities;

namespace Paillave.Etl.EntityFrameworkCore.Tests
{
public class BloggingContext : DbContext
{
private readonly string _connectionString;

public BloggingContext(string connectionString)
{
_connectionString = connectionString;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}

public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>(entity =>
{
entity.HasKey(e => e.PostId);
entity.Property(e => e.PostId).ValueGeneratedNever();
entity.Property(e => e.Name).HasMaxLength(20);
});

modelBuilder.Entity<Blog>(entity =>
{
entity.HasKey(e => e.BlogId);
entity.Property(e => e.BlogId).ValueGeneratedOnAdd().UseIdentityColumn(1);
entity.Property(e => e.Name).HasMaxLength(20);
});
}
}
}
77 changes: 77 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/BulkSaveTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Paillave.Etl.EntityFrameworkCore.Tests.Entities;
using Paillave.Etl.Reactive.Operators;

namespace Paillave.Etl.EntityFrameworkCore.Tests
{
public class BulkSaveTests : SqlDatabaseTests
{
public BulkSaveTests(MsSqlDatabaseFixture fixture) : base(fixture)
{ }

[Fact]
public async Task GivenTableWithoutIdentityColumn_WhenEfCoreSave_ThenItemsSaved()
{
// arange
var executionOptions = new ExecutionOptions<int>
{
Resolver = new SimpleDependencyResolver()
.Register<DbContext>(Context)
};

var streamProcessRunner = StreamProcessRunner.Create((ISingleStream<int> configStream) => configStream
.CrossApply("generate ids", i => Enumerable.Range(1, i))
.EfCoreSave("insert", builder => builder.Entity(i => new Post()
{
PostId = i,
Name = $"Post {i}",
}).InsertOnly()), "GivenTableWithoutIdentityColumn_WhenEfCoreSave_ThenItemsSaved");


// act
await streamProcessRunner.ExecuteAsync(3, executionOptions);


//assert
var posts = await Context.Posts.ToListAsync();
posts.Should().BeEquivalentTo(new[]
{
new Post() { PostId = 1, Name = "Post 1", },
new Post() { PostId = 2, Name = "Post 2", },
new Post() { PostId = 3, Name = "Post 3", }
});
}

[Fact]
public async Task GivenTableWithIdentityColumn_WhenEfCoreSave_ThenItemsSaved()
{
// arange
var executionOptions = new ExecutionOptions<int>
{
Resolver = new SimpleDependencyResolver()
.Register<DbContext>(Context)
};

var streamProcessRunner = StreamProcessRunner.Create((ISingleStream<int> configStream) => configStream
.CrossApply("generate ids", i => Enumerable.Range(1, i))
.EfCoreSave("insert", builder => builder.Entity(i => new Blog()
{
BlogId = i,
Name = $"Blog {i}",
}).InsertOnly().WithIdentityInsert()), "GivenTableWithIdentityColumn_WhenEfCoreSave_ThenItemsSaved");


// act
await streamProcessRunner.ExecuteAsync(3, executionOptions);


//assert
var blogs = await Context.Blogs.ToListAsync();
blogs.Should().BeEquivalentTo(new[]
{
new Blog() { BlogId = 1, Name = "Blog 1", },
new Blog() { BlogId = 2, Name = "Blog 2", },
new Blog() { BlogId = 3, Name = "Blog 3", }
});
}
}
}
7 changes: 7 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/Entities/Blog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Paillave.Etl.EntityFrameworkCore.Tests.Entities;

public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
}
7 changes: 7 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/Entities/Post.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Paillave.Etl.EntityFrameworkCore.Tests.Entities;

public class Post
{
public int PostId { get; set; }
public string Name { get; set; }
}
27 changes: 27 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/MsSqlDatabaseFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Data.SqlClient;
using Testcontainers.MsSql;

namespace Paillave.Etl.EntityFrameworkCore.Tests
{
public class MsSqlDatabaseFixture : IAsyncLifetime
{
private readonly MsSqlContainer _dbContainer;

public MsSqlDatabaseFixture()
{
_dbContainer = new MsSqlBuilder().Build();
}

public Task InitializeAsync() => _dbContainer.StartAsync();


public Task DisposeAsync() => _dbContainer.DisposeAsync().AsTask();

public string GenerateDatabaseConnectionString()
{
var builder = new SqlConnectionStringBuilder(_dbContainer.GetConnectionString());
builder.InitialCatalog = $"Test_{Guid.NewGuid().ToString("N")}";
return builder.ConnectionString;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Testcontainers.MsSql" Version="3.6.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Paillave.Etl.EntityFrameworkCore\Paillave.Etl.EntityFrameworkCore.csproj" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/SqlDatabaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Paillave.Etl.EntityFrameworkCore.Tests
{
public class SqlDatabaseTests : IClassFixture<MsSqlDatabaseFixture>, IAsyncLifetime
{
protected readonly BloggingContext Context;

protected SqlDatabaseTests(MsSqlDatabaseFixture fixture)
{
Context = new BloggingContext(fixture.GenerateDatabaseConnectionString());
}

public Task InitializeAsync() => Context.Database.EnsureCreatedAsync();

public async Task DisposeAsync()
{
await Context.Database.EnsureDeletedAsync();
await Context.DisposeAsync();
}
}
}
4 changes: 4 additions & 0 deletions src/Paillave.Etl.EntityFrameworkCore.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
global using FluentAssertions;
global using Microsoft.EntityFrameworkCore;
global using Paillave.Etl.Core;
global using Xunit;
Loading
Loading