Interview question
Import records in bounded EF Core batches
Processes a large import in chunks with fresh DbContexts, idempotent keys, cancellation, and bounded tracker growth.
TL;DR
Processes a large import in chunks with fresh DbContexts, idempotent keys, cancellation, and bounded tracker growth.
Batching, IDbContextFactory, change-tracker bounds, idempotency, partial progress, and relational testing.
Practice the problem like a real interview: restate, reason, implement, and test.
Input rows contain SourceRowId with a database unique constraint. Process rows in batches of 500. Each batch gets a fresh context and commits independently so memory and failure recovery are bounded. Existing source ids should be skipped.
A 50,000-row file uses roughly one batch of tracked entities at a time. If batch 40 fails, the first 39 remain committed and a rerun safely skips their source ids.
SourceRowId.I chunk the input, open a short-lived context for one chunk, load existing source ids with one query, add only missing rows, and save. The unique constraint remains authoritative under concurrent imports. Fresh contexts bound the tracker and make retry checkpoints clear.
public static async Task<int> ImportAsync(
IDbContextFactory<AppDbContext> contextFactory,
IEnumerable<ImportRow> rows,
CancellationToken cancellationToken)
{
var imported = 0;
foreach (var batch in rows.Chunk(500))
{
cancellationToken.ThrowIfCancellationRequested();
await using var db =
await contextFactory.CreateDbContextAsync(cancellationToken);
var sourceIds = batch
.Select(row => row.SourceRowId)
.Distinct()
.ToArray();
var existing = await db.ImportedRecords
.Where(record => sourceIds.Contains(record.SourceRowId))
.Select(record => record.SourceRowId)
.ToHashSetAsync(cancellationToken);
var newRecords = batch
.Where(row => !existing.Contains(row.SourceRowId))
.GroupBy(row => row.SourceRowId)
.Select(group => ImportedRecord.From(group.First()))
.ToList();
db.ImportedRecords.AddRange(newRecords);
await db.SaveChangesAsync(cancellationToken);
imported += newRecords.Count;
}
return imported;
}
Memory is bounded by one 500-row chunk plus one context's tracked entities. Database work becomes roughly two commands per batch in the uncontended case. Concurrent import races still need the unique constraint and a deliberate duplicate-conflict policy.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.