Skip to content

Commit 41e70e6

Browse files
committed
Bulk insert/update support temp table entities as source (require table name override)
1 parent 4e16ec8 commit 41e70e6

File tree

15 files changed

+534
-36
lines changed

15 files changed

+534
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Metadata;
3+
using Thinktecture.Internal;
4+
5+
namespace Thinktecture.EntityFrameworkCore.BulkOperations.Internal;
6+
7+
/// <summary>
8+
/// This is an internal API.
9+
/// </summary>
10+
public static class BulkOperationModelExtensions
11+
{
12+
/// <summary>
13+
/// Resolves the entity type, table name, and schema for the given type <typeparamref name="T"/>.
14+
/// Falls back to temp table entity lookup if the type is not found as a regular entity.
15+
/// When a temp table entity is found, the caller must provide <paramref name="tableNameOverride"/>.
16+
/// </summary>
17+
public static (IEntityType EntityType, string TableName, string? Schema) GetEntityType<T>(
18+
this IModel model,
19+
string? tableNameOverride,
20+
string? schemaOverride)
21+
where T : class
22+
{
23+
var entityType = model.FindEntityType(typeof(T));
24+
25+
if (entityType is null)
26+
{
27+
entityType = model.FindEntityType(EntityNameProvider.GetTempTableName(typeof(T)));
28+
29+
if (entityType is null)
30+
throw new EntityTypeNotFoundException(typeof(T));
31+
32+
if (tableNameOverride is null)
33+
throw new InvalidOperationException($"The entity type for '{typeof(T).ShortDisplayName()}' is configured as a temp table entity and does not have a regular table mapping. Provide the target table name via the 'TableName' property in options.");
34+
}
35+
36+
var tableName = tableNameOverride
37+
?? entityType.GetTableName()
38+
?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
39+
var schema = schemaOverride ?? entityType.GetSchema();
40+
41+
return (entityType, tableName, schema);
42+
}
43+
}

src/Thinktecture.EntityFrameworkCore.PostgreSQL/EntityFrameworkCore/BulkOperations/NpgsqlBulkOperationExecutor.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ public Task<int> BulkInsertAsync<T>(
101101
CancellationToken cancellationToken = default)
102102
where T : class
103103
{
104-
var entityType = _ctx.Model.GetEntityType(typeof(T));
105-
var tableName = options.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
106-
var schema = options.Schema ?? entityType.GetSchema();
104+
var (entityType, tableName, schema) = _ctx.Model.GetEntityType<T>(options.TableName, options.Schema);
107105

108106
return BulkInsertAsync(entityType, entities, schema, tableName, options, NpgsqlBulkOperationContextFactoryForEntities.Instance, cancellationToken);
109107
}
@@ -525,9 +523,7 @@ public async Task<int> BulkUpdateAsync<T>(
525523
private async Task<int> BulkUpdateAsync<T>(IEnumerable<T> entities, NpgsqlBulkUpdateOptions options, CancellationToken cancellationToken)
526524
where T : class
527525
{
528-
var entityType = _ctx.Model.GetEntityType(typeof(T));
529-
var tableName = options.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
530-
var schema = options.Schema ?? entityType.GetSchema();
526+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, options.TableName, options.Schema);
531527
var propertiesForUpdate = options.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, true);
532528

533529
if (propertiesForUpdate.Count == 0)
@@ -560,9 +556,7 @@ public async Task<int> BulkInsertOrUpdateAsync<T>(
560556
private async Task<int> BulkInsertOrUpdateAsync<T>(IEnumerable<T> entities, NpgsqlBulkInsertOrUpdateOptions options, CancellationToken cancellationToken)
561557
where T : class
562558
{
563-
var entityType = _ctx.Model.GetEntityType(typeof(T));
564-
var tableName = options.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
565-
var schema = options.Schema ?? entityType.GetSchema();
559+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, options.TableName, options.Schema);
566560
var propertiesForInsert = options.PropertiesToInsert.DeterminePropertiesForInsert(entityType, true);
567561
var propertiesForUpdate = options.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, true);
568562

src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutor.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ public Task<int> BulkInsertAsync<T>(
9999
CancellationToken cancellationToken = default)
100100
where T : class
101101
{
102-
var entityType = _ctx.Model.GetEntityType(typeof(T));
103-
var tableName = options.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
104-
var schema = options.Schema ?? entityType.GetSchema();
102+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, options.TableName, options.Schema);
105103

106104
return BulkInsertAsync(entityType, entities, schema, tableName, options, SqlServerBulkOperationContextFactoryForEntities.Instance, cancellationToken);
107105
}
@@ -480,9 +478,7 @@ public async Task<int> BulkUpdateAsync<T>(
480478
private async Task<int> BulkUpdateAsync<T>(IEnumerable<T> entities, SqlServerBulkUpdateOptions options, CancellationToken cancellationToken)
481479
where T : class
482480
{
483-
var entityType = _ctx.Model.GetEntityType(typeof(T));
484-
var tableName = options.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
485-
var schema = options.Schema ?? entityType.GetSchema();
481+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, options.TableName, options.Schema);
486482
var propertiesForUpdate = options.PropertiesToUpdate.DeterminePropertiesForUpdate(entityType, true);
487483

488484
if (propertiesForUpdate.Count == 0)
@@ -515,9 +511,7 @@ public async Task<int> BulkInsertOrUpdateAsync<T>(
515511
private async Task<int> BulkInsertOrUpdateAsync<T>(IEnumerable<T> entities, SqlServerBulkInsertOrUpdateOptions options, CancellationToken cancellationToken)
516512
where T : class
517513
{
518-
var entityType = _ctx.Model.GetEntityType(typeof(T));
519-
var tableName = options.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
520-
var schema = options.Schema ?? entityType.GetSchema();
514+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, options.TableName, options.Schema);
521515
var propertiesForInsert = options.PropertiesToInsert.DeterminePropertiesForInsert(entityType, true);
522516
var propertiesForUpdate = options.PropertiesToUpdate.DeterminePropertiesForInsert(entityType, true);
523517

src/Thinktecture.EntityFrameworkCore.Sqlite.Core/EntityFrameworkCore/BulkOperations/SqliteBulkOperationExecutor.cs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,10 @@ public async Task<int> BulkInsertAsync<T>(
103103
ArgumentNullException.ThrowIfNull(entities);
104104
ArgumentNullException.ThrowIfNull(options);
105105

106-
var entityType = _ctx.Model.GetEntityType(typeof(T));
107-
108106
if (options is not SqliteBulkInsertOptions sqliteOptions)
109107
sqliteOptions = new SqliteBulkInsertOptions(options);
110108

111-
var tableName = sqliteOptions.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
112-
var schema = sqliteOptions.Schema ?? entityType.GetSchema();
109+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, sqliteOptions.TableName, sqliteOptions.Schema);
113110

114111
return await BulkInsertAsync(entityType,
115112
entities,
@@ -147,13 +144,10 @@ public async Task<int> BulkUpdateAsync<T>(
147144
ArgumentNullException.ThrowIfNull(entities);
148145
ArgumentNullException.ThrowIfNull(options);
149146

150-
var entityType = _ctx.Model.GetEntityType(typeof(T));
151-
152147
if (options is not SqliteBulkUpdateOptions sqliteOptions)
153148
sqliteOptions = new SqliteBulkUpdateOptions(options);
154149

155-
var tableName = sqliteOptions.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
156-
var schema = sqliteOptions.Schema ?? entityType.GetSchema();
150+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, sqliteOptions.TableName, sqliteOptions.Schema);
157151
var ctx = new BulkUpdateContext(_ctx,
158152
_ctx.GetService<IEntityDataReaderFactory>(),
159153
(SqliteConnection)_ctx.Database.GetDbConnection(),
@@ -177,9 +171,7 @@ public async Task<int> BulkInsertOrUpdateAsync<T>(
177171
if (!(options is ISqliteBulkInsertOrUpdateOptions sqliteOptions))
178172
sqliteOptions = new SqliteBulkInsertOrUpdateOptions(options);
179173

180-
var entityType = _ctx.Model.GetEntityType(typeof(T));
181-
var tableName = sqliteOptions.TableName ?? entityType.GetTableName() ?? throw new InvalidOperationException($"The entity '{entityType.Name}' has no table name.");
182-
var schema = sqliteOptions.Schema ?? entityType.GetSchema();
174+
var (entityType, tableName, schema) = BulkOperationModelExtensions.GetEntityType<T>(_ctx.Model, sqliteOptions.TableName, sqliteOptions.Schema);
183175
var ctx = new BulkInsertOrUpdateContext(_ctx,
184176
_ctx.GetService<IEntityDataReaderFactory>(),
185177
(SqliteConnection)_ctx.Database.GetDbConnection(),

tests/Thinktecture.EntityFrameworkCore.PostgreSQL.Tests/EntityFrameworkCore/BulkOperations/NpgsqlBulkOperationExecutorTests/BulkInsertAsync.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ public BulkInsertAsync(ITestOutputHelper testOutputHelper, NpgsqlFixture npgsqlF
1515
}
1616

1717
[Fact]
18-
public async Task Should_throw_when_trying_to_insert_into_pure_temp_table_entity()
18+
public async Task Should_throw_when_trying_to_insert_temp_table_entity_without_table_name()
1919
{
2020
ConfigureModel = builder => builder.ConfigureTempTable<int>();
2121

2222
await SUT.Invoking(sut => sut.BulkInsertAsync(new List<TempTable<int>> { new(0) }, new NpgsqlBulkInsertOptions()))
23-
.Should().ThrowAsync<ArgumentException>()
24-
.WithMessage("The provided type 'TempTable<int>' is not part of the provided Entity Framework model. (Parameter 'type')");
23+
.Should().ThrowAsync<InvalidOperationException>()
24+
.WithMessage("*configured as a temp table entity*Provide the target table name*");
2525
}
2626

2727
[Fact]
@@ -839,4 +839,29 @@ public async Task Should_insert_entity_with_null_jsonb_column()
839839
loadedEntity.JsonbColumn.Should().BeNull();
840840
loadedEntity.JsonColumn.Should().BeNull();
841841
}
842+
843+
[Fact]
844+
public async Task Should_insert_temp_table_entity_when_table_name_is_provided()
845+
{
846+
ConfigureModel = builder => builder.ConfigureTempTableEntity<TestEntityTempTable>(false, TestEntityTempTable.Configure);
847+
848+
var entity = new TestEntityTempTable
849+
{
850+
Id = new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"),
851+
Name = "Name",
852+
RequiredName = "RequiredName",
853+
Count = 42
854+
};
855+
856+
var affectedRows = await SUT.BulkInsertAsync(new[] { entity }, new NpgsqlBulkInsertOptions { TableName = "TestEntities", Schema = Schema });
857+
858+
affectedRows.Should().Be(1);
859+
860+
var loadedEntities = await AssertDbContext.TestEntities.ToListAsync();
861+
loadedEntities.Should().HaveCount(1);
862+
loadedEntities[0].Id.Should().Be(new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"));
863+
loadedEntities[0].Name.Should().Be("Name");
864+
loadedEntities[0].RequiredName.Should().Be("RequiredName");
865+
loadedEntities[0].Count.Should().Be(42);
866+
}
842867
}

tests/Thinktecture.EntityFrameworkCore.PostgreSQL.Tests/EntityFrameworkCore/BulkOperations/NpgsqlBulkOperationExecutorTests/BulkInsertOrUpdateAsync.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Thinktecture.EntityFrameworkCore.TempTables;
23
using Thinktecture.TestDatabaseContext;
34

45
// ReSharper disable InconsistentNaming
@@ -507,4 +508,58 @@ public async Task Should_insert_or_update_entity_with_jsonb_and_json_columns()
507508
loadedEntities[1].JsonbColumn.Should().Be("""{"key": "new"}""");
508509
loadedEntities[1].JsonColumn.Should().Be("""{"key": "new"}""");
509510
}
511+
512+
[Fact]
513+
public async Task Should_throw_when_trying_to_insert_or_update_temp_table_entity_without_table_name()
514+
{
515+
ConfigureModel = builder => builder.ConfigureTempTable<int>();
516+
517+
await SUT.Invoking(sut => sut.BulkInsertOrUpdateAsync(new List<TempTable<int>> { new(0) }, new NpgsqlBulkInsertOrUpdateOptions()))
518+
.Should().ThrowAsync<InvalidOperationException>()
519+
.WithMessage("*configured as a temp table entity*Provide the target table name*");
520+
}
521+
522+
[Fact]
523+
public async Task Should_insert_or_update_temp_table_entity_when_table_name_is_provided()
524+
{
525+
ConfigureModel = builder => builder.ConfigureTempTableEntity<TestEntityTempTable>(false, TestEntityTempTable.Configure);
526+
527+
var existingEntity = new TestEntity
528+
{
529+
Id = new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"),
530+
Name = "Name",
531+
RequiredName = "RequiredName",
532+
Count = 42
533+
};
534+
ArrangeDbContext.Add(existingEntity);
535+
await ArrangeDbContext.SaveChangesAsync();
536+
537+
var updatedEntity = new TestEntityTempTable
538+
{
539+
Id = new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"),
540+
Name = "UpdatedName",
541+
RequiredName = "RequiredName",
542+
Count = 99
543+
};
544+
var newEntity = new TestEntityTempTable
545+
{
546+
Id = new Guid("8AF163D7-D316-4B2D-A62F-6326A80C8BEE"),
547+
Name = "NewName",
548+
RequiredName = "RequiredName",
549+
Count = 1
550+
};
551+
552+
var affectedRows = await SUT.BulkInsertOrUpdateAsync(new[] { updatedEntity, newEntity }, new NpgsqlBulkInsertOrUpdateOptions { TableName = "TestEntities", Schema = Schema });
553+
554+
affectedRows.Should().Be(2);
555+
556+
var loadedEntities = await AssertDbContext.TestEntities.OrderBy(e => e.Count).ToListAsync();
557+
loadedEntities.Should().HaveCount(2);
558+
loadedEntities[0].Id.Should().Be(new Guid("8AF163D7-D316-4B2D-A62F-6326A80C8BEE"));
559+
loadedEntities[0].Name.Should().Be("NewName");
560+
loadedEntities[0].Count.Should().Be(1);
561+
loadedEntities[1].Id.Should().Be(new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"));
562+
loadedEntities[1].Name.Should().Be("UpdatedName");
563+
loadedEntities[1].Count.Should().Be(99);
564+
}
510565
}

tests/Thinktecture.EntityFrameworkCore.PostgreSQL.Tests/EntityFrameworkCore/BulkOperations/NpgsqlBulkOperationExecutorTests/BulkUpdateAsync.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Thinktecture.EntityFrameworkCore.TempTables;
23
using Thinktecture.TestDatabaseContext;
34

45
// ReSharper disable InconsistentNaming
@@ -366,4 +367,48 @@ public async Task Should_update_entity_with_jsonb_and_json_columns()
366367
loadedEntity.JsonbColumn.Should().Be("""{"key": "updated"}""");
367368
loadedEntity.JsonColumn.Should().Be("""{"key": "updated"}""");
368369
}
370+
371+
[Fact]
372+
public async Task Should_throw_when_trying_to_update_temp_table_entity_without_table_name()
373+
{
374+
ConfigureModel = builder => builder.ConfigureTempTable<int>();
375+
376+
await SUT.Invoking(sut => sut.BulkUpdateAsync(new List<TempTable<int>> { new(0) }, new NpgsqlBulkUpdateOptions()))
377+
.Should().ThrowAsync<InvalidOperationException>()
378+
.WithMessage("*configured as a temp table entity*Provide the target table name*");
379+
}
380+
381+
[Fact]
382+
public async Task Should_update_temp_table_entity_when_table_name_is_provided()
383+
{
384+
ConfigureModel = builder => builder.ConfigureTempTableEntity<TestEntityTempTable>(false, TestEntityTempTable.Configure);
385+
386+
var entity = new TestEntity
387+
{
388+
Id = new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"),
389+
Name = "Name",
390+
RequiredName = "RequiredName",
391+
Count = 42
392+
};
393+
ArrangeDbContext.Add(entity);
394+
await ArrangeDbContext.SaveChangesAsync();
395+
396+
var tempEntity = new TestEntityTempTable
397+
{
398+
Id = new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"),
399+
Name = "UpdatedName",
400+
RequiredName = "RequiredName",
401+
Count = 99
402+
};
403+
404+
var affectedRows = await SUT.BulkUpdateAsync(new[] { tempEntity }, new NpgsqlBulkUpdateOptions { TableName = "TestEntities", Schema = Schema });
405+
406+
affectedRows.Should().Be(1);
407+
408+
var loadedEntities = await AssertDbContext.TestEntities.ToListAsync();
409+
loadedEntities.Should().HaveCount(1);
410+
loadedEntities[0].Id.Should().Be(new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"));
411+
loadedEntities[0].Name.Should().Be("UpdatedName");
412+
loadedEntities[0].Count.Should().Be(99);
413+
}
369414
}

tests/Thinktecture.EntityFrameworkCore.SqlServer.Tests/EntityFrameworkCore/BulkOperations/SqlServerBulkOperationExecutorTests/BulkInsertAsync.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ public BulkInsertAsync(ITestOutputHelper testOutputHelper, SqlServerFixture sqlS
1818
}
1919

2020
[Fact]
21-
public async Task Should_throw_when_trying_to_insert_into_pure_temp_table_entity()
21+
public async Task Should_throw_when_trying_to_insert_temp_table_entity_without_table_name()
2222
{
2323
ConfigureModel = builder => builder.ConfigureTempTable<int>();
2424

2525
await SUT.Invoking(sut => sut.BulkInsertAsync(new List<TempTable<int>> { new(0) }, new SqlServerBulkInsertOptions()))
26-
.Should().ThrowAsync<ArgumentException>()
27-
.WithMessage("The provided type 'TempTable<int>' is not part of the provided Entity Framework model. (Parameter 'type')");
26+
.Should().ThrowAsync<InvalidOperationException>()
27+
.WithMessage("*configured as a temp table entity*Provide the target table name*");
2828
}
2929

3030
[Fact]
@@ -789,4 +789,36 @@ await ActDbContext.Database.ExecuteSqlRawAsync($"""
789789
await ActDbContext.Database.ExecuteSqlRawAsync($"DROP TABLE IF EXISTS [{Schema}].[TestEntities_BulkInsertRedirect]");
790790
}
791791
}
792+
793+
[Fact]
794+
public async Task Should_insert_temp_table_entity_when_table_name_is_provided()
795+
{
796+
ConfigureModel = builder => builder.ConfigureTempTableEntity<TestEntityTempTable>(false, TestEntityTempTable.Configure);
797+
798+
var entity = new TestEntityTempTable
799+
{
800+
Id = new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"),
801+
Name = "Name",
802+
RequiredName = "RequiredName",
803+
Count = 42
804+
};
805+
806+
var affectedRows = await SUT.BulkInsertAsync(new[] { entity }, new SqlServerBulkInsertOptions { TableName = "TestEntities" });
807+
808+
affectedRows.Should().Be(1);
809+
810+
var loadedEntities = await AssertDbContext.TestEntities.ToListAsync();
811+
loadedEntities.Should().HaveCount(1);
812+
loadedEntities[0].Id.Should().Be(new Guid("40B5CA93-5C02-48AD-B8A1-12BC13313866"));
813+
loadedEntities[0].Name.Should().Be("Name");
814+
loadedEntities[0].RequiredName.Should().Be("RequiredName");
815+
loadedEntities[0].Count.Should().Be(42);
816+
}
817+
818+
[Fact]
819+
public async Task Should_throw_when_type_is_not_in_model()
820+
{
821+
await SUT.Invoking(sut => sut.BulkInsertAsync(new List<TestEntityTempTable> { new() }, new SqlServerBulkInsertOptions()))
822+
.Should().ThrowAsync<EntityTypeNotFoundException>();
823+
}
792824
}

0 commit comments

Comments
 (0)