Skip to content

Commit 03e3619

Browse files
CopilotPhenX
andauthored
Add EF Core-style logging for bulk insert operations (#103)
* Initial plan * Add logging similar to EF Core: BulkInsertExecuted and ExecutedDbCommand log messages Agent-Logs-Url: https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/sessions/481b78be-69b9-450e-b8b4-3c7176838952 Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> * Apply review fixes for logging and resource disposal Agent-Logs-Url: https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/sessions/50f8adc9-2589-4b71-a87e-14db3ec99af9 Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> * Update docs and Copilot instructions for logging changes Agent-Logs-Url: https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/sessions/056ca3a1-0d57-424f-a270-daf8457b20bd Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
1 parent e5f31d1 commit 03e3619

13 files changed

Lines changed: 257 additions & 10 deletions

File tree

.github/copilot-instructions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@
88
- Do not include "issue #N", "triangulate", "hypothesis", or similar investigation language in
99
inline code comments or block comments inside method bodies. Such context belongs in the pull
1010
request description, not in the source code.
11+
12+
## Documentation and tests
13+
14+
- Always update documentation when public APIs change.
15+
- Always update `README.md` when notable behavior or features are added or modified.
16+
- Always add or update tests for behavior changes.

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,25 @@ await dbContext.ExecuteBulkInsertAsync(entities, o =>
123123
await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);
124124
```
125125

126+
### Logging
127+
128+
Bulk insert operations emit EF Core-style logs when a logger factory is configured on the `DbContext` options:
129+
130+
```csharp
131+
services.AddDbContext<MyDbContext>(options =>
132+
{
133+
options
134+
.UseSqlite(connectionString)
135+
.UseLoggerFactory(loggerFactory)
136+
.UseBulkInsertSqlite();
137+
});
138+
```
139+
140+
Log events:
141+
142+
* `1004` (`Information`): bulk insert completion with elapsed time and destination table.
143+
* `1005` (`Debug`): auxiliary SQL commands executed by the library (such as temp-table DDL).
144+
126145
### Conflict resolution / merge / upsert
127146

128147
Conflict resolution works by specifying columns that should be used to detect conflicts and the action to take when

docs/documentation.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ await dbContext.ExecuteBulkInsertAsync(entities, o =>
8484
await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities);
8585
```
8686

87+
## Logging
88+
89+
Bulk insert operations emit EF Core-style logs when a logger factory is configured:
90+
91+
```csharp
92+
services.AddDbContext<MyDbContext>(options =>
93+
{
94+
options
95+
.UseSqlite(connectionString)
96+
.UseLoggerFactory(loggerFactory)
97+
.UseBulkInsertSqlite();
98+
});
99+
```
100+
101+
Log events:
102+
103+
* `1004` (`Information`): bulk insert completion with elapsed time and destination table.
104+
* `1005` (`Debug`): auxiliary SQL commands executed by the library (for example temp-table SQL).
105+
87106
### Conflict resolution / merge / upsert
88107

89108
Conflict resolution works by specifying columns that should be used to detect conflicts and the action to take when

src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
1313

1414
[UsedImplicitly]
15-
internal class MySqlBulkInsertProvider(ILogger<MySqlBulkInsertProvider> logger) : BulkInsertProviderBase<MySqlServerDialectBuilder, MySqlBulkInsertOptions>(logger)
15+
internal class MySqlBulkInsertProvider(ILoggerFactory? loggerFactory) : BulkInsertProviderBase<MySqlServerDialectBuilder, MySqlBulkInsertOptions>(loggerFactory)
1616
{
1717
//language=sql
1818
/// <inheritdoc />

src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
namespace PhenX.EntityFrameworkCore.BulkInsert.Oracle;
1212

1313
[UsedImplicitly]
14-
internal class OracleBulkInsertProvider(ILogger<OracleBulkInsertProvider>? logger) : BulkInsertProviderBase<OracleDialectBuilder, OracleBulkInsertOptions>(logger)
14+
internal class OracleBulkInsertProvider(ILoggerFactory? loggerFactory) : BulkInsertProviderBase<OracleDialectBuilder, OracleBulkInsertOptions>(loggerFactory)
1515
{
1616
/// <inheritdoc />
1717
protected override string BulkInsertId => "ROWID";

src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
1616

1717
[UsedImplicitly]
18-
internal class PostgreSqlBulkInsertProvider(ILogger<PostgreSqlBulkInsertProvider>? logger) : BulkInsertProviderBase<PostgreSqlDialectBuilder, PostgreSqlBulkInsertOptions>(logger)
18+
internal class PostgreSqlBulkInsertProvider(ILoggerFactory? loggerFactory) : BulkInsertProviderBase<PostgreSqlDialectBuilder, PostgreSqlBulkInsertOptions>(loggerFactory)
1919
{
2020
//language=sql
2121
/// <inheritdoc />

src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;
1111

1212
[UsedImplicitly]
13-
internal class SqlServerBulkInsertProvider(ILogger<SqlServerBulkInsertProvider>? logger) : BulkInsertProviderBase<SqlServerDialectBuilder, SqlServerBulkInsertOptions>(logger)
13+
internal class SqlServerBulkInsertProvider(ILoggerFactory? loggerFactory) : BulkInsertProviderBase<SqlServerDialectBuilder, SqlServerBulkInsertOptions>(loggerFactory)
1414
{
1515
//language=sql
1616
/// <inheritdoc />

src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace PhenX.EntityFrameworkCore.BulkInsert.Sqlite;
1414

1515
[UsedImplicitly]
16-
internal class SqliteBulkInsertProvider(ILogger<SqliteBulkInsertProvider>? logger) : BulkInsertProviderBase<SqliteDialectBuilder, BulkInsertOptions>(logger)
16+
internal class SqliteBulkInsertProvider(ILoggerFactory? loggerFactory) : BulkInsertProviderBase<SqliteDialectBuilder, BulkInsertOptions>(loggerFactory)
1717
{
1818
private const int MaxParams = 1000;
1919

src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertOptionsExtension.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public DbContextOptionsExtensionInfo Info
1616

1717
public void ApplyServices(IServiceCollection services)
1818
{
19-
services.TryAddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
20-
services.AddSingleton<IBulkInsertProvider, TProvider>();
19+
services.TryAddSingleton<ILoggerFactory, NullLoggerFactory>();
20+
services.AddScoped<IBulkInsertProvider, TProvider>();
2121
}
2222

2323
public void Validate(IDbContextOptions options)

src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics;
12
using System.Runtime.CompilerServices;
23

34
using Microsoft.EntityFrameworkCore;
@@ -11,10 +12,12 @@
1112

1213
namespace PhenX.EntityFrameworkCore.BulkInsert;
1314

14-
internal abstract class BulkInsertProviderBase<TDialect, TOptions>(ILogger? logger) : BulkInsertProviderUntyped<TDialect, TOptions>
15+
internal abstract class BulkInsertProviderBase<TDialect, TOptions>(ILoggerFactory? loggerFactory) : BulkInsertProviderUntyped<TDialect, TOptions>
1516
where TDialect : SqlDialectBuilder, new()
1617
where TOptions : BulkInsertOptions, new()
1718
{
19+
private ILogger? _logger;
20+
private ILogger? logger => _logger ??= loggerFactory?.CreateLogger(GetType());
1821
protected virtual string BulkInsertId => "_bulk_insert_id";
1922

2023
protected abstract string AddTableCopyBulkInsertId { get; }
@@ -144,7 +147,15 @@ private async Task<string> PerformBulkInsertAsync<T>(
144147
activity?.AddTag("tempTable", tempTableRequired);
145148
activity?.AddTag("synchronous", sync);
146149

150+
var sw = Stopwatch.StartNew();
147151
await BulkInsert(sync, context, tableInfo, entities, tableName, columns, options, ctk);
152+
sw.Stop();
153+
154+
if (logger != null)
155+
{
156+
Log.BulkInsertExecuted(logger, sw.ElapsedMilliseconds, tableName);
157+
}
158+
148159
return tableName;
149160
}
150161

@@ -254,16 +265,17 @@ protected virtual Task DropTempTableAsync(bool sync, DbContext dbContext, string
254265
return Task.CompletedTask;
255266
}
256267

257-
protected static async Task ExecuteAsync(
268+
protected async Task ExecuteAsync(
258269
bool sync,
259270
DbContext context,
260271
string query,
261272
CancellationToken ctk)
262273
{
263-
var command = context.Database.GetDbConnection().CreateCommand();
274+
using var command = context.Database.GetDbConnection().CreateCommand();
264275
command.Transaction = context.Database.CurrentTransaction!.GetDbTransaction();
265276
command.CommandText = query;
266277

278+
var sw = Stopwatch.StartNew();
267279
if (sync)
268280
{
269281
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
@@ -273,5 +285,11 @@ protected static async Task ExecuteAsync(
273285
{
274286
await command.ExecuteNonQueryAsync(ctk);
275287
}
288+
sw.Stop();
289+
290+
if (logger != null)
291+
{
292+
Log.ExecutedDbCommand(logger, sw.ElapsedMilliseconds, query);
293+
}
276294
}
277295
}

0 commit comments

Comments
 (0)