Large Data CRUD in .NET: Batching Techniques to Avoid Out of Memory

BatchHelper

public static class BatchHelper
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        using var e = source.GetEnumerator();
        while (e.MoveNext())
            yield return GetBatch(e, size);
    }


    private static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, int size)
    {
        do yield return e.Current;
        while (--size > 0 && e.MoveNext());
    }
}


Batched Create (Insert)

public async Task BulkInsertAsync(IEnumerable<MyModel> data, MyDbContext dbContext)
{
    foreach (var batch in data.Batch(1000))
    {
        dbContext.MyModels.AddRange(batch);
        await dbContext.SaveChangesAsync();
    }
}


Batched Update

public async Task BulkUpdateAsync(IEnumerable<MyModel> updatedData, MyDbContext dbContext)
{
    foreach (var batch in updatedData.Batch(1000))
    {
        dbContext.MyModels.UpdateRange(batch);
        await dbContext.SaveChangesAsync();
    }
}


⚠️ If you're using detached objects or want to attach only modified values, you'll need to Attach() them and set their state to Modified.

// Alternative: attach manually
foreach (var item in batch)
{
    dbContext.MyModels.Attach(item);
    dbContext.Entry(item).State = EntityState.Modified;
}


Batched Delete

public async Task BulkDeleteAsync(IEnumerable<int> idsToDelete, MyDbContext dbContext)
{
    foreach (var batch in idsToDelete.Batch(1000))
    {
        var records = await dbContext.MyModels
            .Where(m => batch.Contains(m.Id))
            .ToListAsync();


        dbContext.MyModels.RemoveRange(records);
        await dbContext.SaveChangesAsync();
    }
}