Skip to main content
The Intent.EntityFrameworkCore.BulkOperations module adds high-performance bulk operation support to Entity Framework Core repositories using the EFCore.BulkExtensions library.

Overview

Bulk operations significantly improve performance when working with large datasets by reducing the number of database round trips. This module adds extension methods to repositories for:
  • Bulk Insert
  • Bulk Update
  • Bulk Delete
  • Bulk Insert or Update (Upsert)
  • Bulk Read

Installation

Intent.EntityFrameworkCore.BulkOperations

Benefits

Compared to standard EF Core operations:
  • 10-50x faster for inserts
  • 5-25x faster for updates
  • 3-15x faster for deletes
  • Reduced memory consumption
  • Optimal for batch processing

Usage

Bulk Insert

Bulk Insert
public class CustomerImportService
{
    private readonly ICustomerRepository _repository;

    public async Task ImportCustomersAsync(List<Customer> customers)
    {
        // Standard way (slow for large datasets)
        foreach (var customer in customers)
        {
            _repository.Add(customer);
        }
        await _repository.UnitOfWork.SaveChangesAsync();

        // Bulk way (much faster)
        await _repository.BulkInsertAsync(customers);
    }
}

Bulk Update

Bulk Update
public async Task UpdateCustomerStatusAsync(List<Guid> customerIds, CustomerStatus newStatus)
{
    var customers = await _repository.FindByIdsAsync(customerIds.ToArray());
    
    foreach (var customer in customers)
    {
        customer.UpdateStatus(newStatus);
    }

    // Bulk update
    await _repository.BulkUpdateAsync(customers);
}

Bulk Delete

Bulk Delete
public async Task DeleteInactiveCustomersAsync(DateTime inactiveSince)
{
    var inactiveCustomers = await _repository.FindAllAsync(
        x => x.LastActivityDate < inactiveSince
    );

    // Bulk delete
    await _repository.BulkDeleteAsync(inactiveCustomers);
}

Bulk Insert or Update (Upsert)

Upsert
public async Task SyncCustomersAsync(List<Customer> customers)
{
    // Insert new customers or update existing ones
    await _repository.BulkInsertOrUpdateAsync(customers);
}

Bulk Read

Bulk Read
public async Task<List<Customer>> GetCustomersByIdsAsync(List<Guid> ids)
{
    // Efficiently read multiple entities by ID
    return await _repository.BulkReadAsync(x => ids.Contains(x.Id));
}

Configuration Options

Bulk operations can be configured with various options:
Configuration
var bulkConfig = new BulkConfig
{
    BatchSize = 1000,
    BulkCopyTimeout = 300,
    PreserveInsertOrder = true,
    SetOutputIdentity = true,
    UseTempDB = true
};

await _repository.BulkInsertAsync(customers, bulkConfig);
Number of entities processed in each batch.
  • Larger batches = fewer round trips but more memory
  • Smaller batches = more round trips but less memory
Default: 2000
Timeout in seconds for bulk copy operations.Increase for very large datasets.Default: 30
Maintains the order of entities during insert.Useful when insert order matters for foreign key relationships.Default: true
Sets the identity values for inserted entities.When true, generated IDs are populated back into the entity objects.Default: false
Uses temporary database tables for staging.Can improve performance for very large datasets.Default: false

Performance Comparison

Standard vs Bulk Insert

Performance Test
// Test with 10,000 customers
var customers = GenerateTestCustomers(10000);

// Standard approach: ~15 seconds
var stopwatch = Stopwatch.StartNew();
foreach (var customer in customers)
{
    _repository.Add(customer);
}
await _repository.UnitOfWork.SaveChangesAsync();
stopwatch.Stop();
Console.WriteLine($"Standard Insert: {stopwatch.ElapsedMilliseconds}ms");

// Bulk approach: ~500 milliseconds
stopwatch.Restart();
await _repository.BulkInsertAsync(customers);
stopwatch.Stop();
Console.WriteLine($"Bulk Insert: {stopwatch.ElapsedMilliseconds}ms");

Advanced Scenarios

Conditional Upsert

Conditional Upsert
var bulkConfig = new BulkConfig
{
    UpdateByProperties = new List<string> { nameof(Customer.Email) },
    PropertiesToInclude = new List<string> 
    { 
        nameof(Customer.Name), 
        nameof(Customer.Email),
        nameof(Customer.Phone)
    }
};

await _repository.BulkInsertOrUpdateAsync(customers, bulkConfig);

Bulk Delete with Predicate

Bulk Delete with Predicate
// Delete without loading entities into memory
await _repository.BulkDeleteAsync(x => x.IsDeleted && x.DeletedDate < DateTime.UtcNow.AddYears(-1));

Transaction Support

Transaction
using var transaction = await _dbContext.Database.BeginTransactionAsync();

try
{
    await _customerRepository.BulkInsertAsync(customers);
    await _orderRepository.BulkInsertAsync(orders);
    
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Database Provider Support

Bulk operations are supported on:
  • ✅ SQL Server
  • ✅ PostgreSQL
  • ✅ MySQL
  • ✅ SQLite
  • ❌ In-Memory (falls back to standard operations)
  • ❌ Cosmos DB (not applicable)

NuGet Dependencies

This module automatically adds:
  • EFCore.BulkExtensions - Core bulk operations library
  • Database-specific extensions based on your provider

Best Practices

Use for Large Batches

Bulk operations shine with 1,000+ entities. For smaller batches, standard EF Core may be sufficient.

Consider Memory

Loading large datasets into memory can cause issues. Use batch processing for very large operations.

Disable Tracking

Use AsNoTracking() when querying data for bulk operations to reduce memory overhead.

Test Performance

Always benchmark bulk operations in your specific environment with realistic data volumes.

Limitations

  • Triggers and computed columns may not fire during bulk operations
  • Some database features (like temporal tables) may require special handling
  • Identity insert requires explicit configuration
  • Navigation properties are not automatically processed

Additional Resources

Build docs developers (and LLMs) love