⚡ Events in Entity Framework ExtensionsCustomize bulk operations and set audit fields automatically

When working with bulk operations in Entity Framework Extensions, you often need to apply custom business logic instead of just saving data as-is.

With events, you can intercept the process and decide what should happen before or after the operation.

The most common scenario is setting audit fields such as CreatedDate and ModifiedDate automatically.

But events are not limited to auditing — you can also:

  • Validate data before processing
  • Modify column mappings dynamically
  • Change the destination table name at runtime
  • Log or clean up data after processing

Think of events as lifecycle hooks for your bulk methods. They keep your logic centralized, reusable, and consistent without duplicating code in multiple places.


🏷️ Types of Events

Entity Framework Extensions provides multiple events that let you customize bulk operations at different stages.

You can think of them as checkpoints during the lifecycle of a bulk method:

  • Pre events run before the operation is executed. They’re ideal for tasks like setting audit fields, validating data, or even swapping the destination table name.
  • Post events run after the operation is finished. They’re useful for cleanup, logging, adjusting in-memory entities, or chaining additional logic.

Here are the available events:

Event Pair Trigger Time Common Use Case
PreBulkInsert / PostBulkInsert Before / After BulkInsert Set CreatedDate / Log inserted rows
PreBulkUpdate / PostBulkUpdate Before / After BulkUpdate Set ModifiedDate / Trigger refresh, logging
PreBulkDelete / PostBulkDelete Before / After BulkDelete Implement soft deletes / Audit or cleanup
PreBulkMerge / PostBulkMerge Before / After BulkMerge Set CreatedDate & ModifiedDate / Track merged entities
PreBulkSynchronize / PostBulkSynchronize Before / After BulkSynchronize Manage audit fields / Monitor sync results
PreBulkSaveChanges / PostBulkSaveChanges Before / After BulkSaveChanges Apply rules via ChangeTracker / Log save outcome
PreBatchSaveChanges / PostBatchSaveChanges Before / After BatchSaveChanges Global batch rules / Batch completion tasks
PostConfiguration Right before bulk method executes Last-minute config like logging or mappings
BulkOperationExecuting / BulkOperationExecuted Before / After an operation inside a call Fine-grained control, adjust values / log results

⚠️ Warning: When using the IncludeGraph feature, the events PreBulkInsert, PreBulkUpdate, PreBulkMerge, PreBulkDelete, PreBulkSynchronize — and their corresponding Post events — are triggered only for root entities. They are not fired for the related entities included in the graph.


🏷️ PreBulkInsert and PostBulkInsert

The following example uses the PreBulkInsert event to set the CreatedDate property of each Customer:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBulkInsert = (ctx, obj) =>
{
    if (obj is IEnumerable<Customer> list) 
    {
        foreach (var customer in list)
            customer.CreatedDate = DateTime.Now;
    }
};

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: When using IncludeGraph, PreBulkInsert and PostBulkInsert are triggered only for root entities — not for related entities included in the graph.


🏷️ PreBulkUpdate and PostBulkUpdate

  • PreBulkUpdate runs before BulkUpdate. Often used to set ModifiedDate, validate data consistency, or enforce version numbers.
  • PostBulkUpdate runs after BulkUpdate. Useful for triggering domain events, clearing caches, or logging changes.

The following example uses the PreBulkUpdate event to set the ModifiedDate property of each Customer:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBulkUpdate = (ctx, obj) =>
{
    if (obj is IEnumerable<Customer> list) 
    {
        foreach (var customer in list)
            customer.ModifiedDate = DateTime.Now;
    }
};

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: When using IncludeGraph, PreBulkUpdate and PostBulkUpdate are triggered only for root entities — not for related entities included in the graph.


🏷️ PreBulkDelete and PostBulkDelete

  • PreBulkDelete runs before BulkDelete. Often used for soft deletes, such as setting an IsDeleted flag instead of removing rows.
  • PostBulkDelete runs after BulkDelete. Useful for logging, auditing, or archiving deletion results.

The following example uses the PreBulkDelete event to mark each Customer as deleted (IsDeleted = true). The list is then cleared, so the BulkDelete method will not physically remove rows — it will only update them:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBulkDelete = (ctx, obj) => 
{
    if (obj is IEnumerable<Customer> list)
    {
        foreach (var customer in list)
            customer.IsDeleted = true;

        ctx.BulkUpdate(list);
        ((List<Customer>)obj).Clear(); // Prevents physical delete
    }
};

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: When using IncludeGraph, PreBulkDelete and PostBulkDelete are triggered only for root entities — not for related entities included in the graph.


🏷️ PreBulkMerge and PostBulkMerge

  • PreBulkMerge runs before BulkMerge. Useful for setting both CreatedDate and ModifiedDate, depending on whether the entity is new or existing.
  • PostBulkMerge runs after BulkMerge. Often used to track which entities were merged or to log synchronization results.

The following example uses the PreBulkMerge event to set the CreatedDate for new customers and the ModifiedDate for existing ones:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBulkMerge = (ctx, obj) =>
{
    if (obj is IEnumerable<Customer> list)
    {
        foreach (var customer in list)
        {
            if (customer.CustomerID == 0)
                customer.CreatedDate = DateTime.Now;
            else
                customer.ModifiedDate = DateTime.Now;
        }
    }
};

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: When using IncludeGraph, PreBulkMerge and PostBulkMerge are triggered only for root entities — not for related entities included in the graph.


🏷️ PreBulkSynchronize and PostBulkSynchronize

  • PreBulkSynchronize runs before BulkSynchronize. Commonly used to set audit fields when syncing external data.
  • PostBulkSynchronize runs after BulkSynchronize. Useful for reporting, monitoring, or verifying synchronization results.

The following example uses the PreBulkSynchronize event to set the CreatedDate for new customers and the ModifiedDate for existing ones:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBulkSynchronize = (ctx, obj) =>
{
    if (obj is IEnumerable<Customer> list)
    {
        foreach (var customer in list)
        {
            if (customer.CustomerID == 0)
                customer.CreatedDate = DateTime.Now;
            else
                customer.ModifiedDate = DateTime.Now;
        }
    }
};

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: When using IncludeGraph, PreBulkSynchronize and PostBulkSynchronize are triggered only for root entities — not for related entities included in the graph.


🏷️ PreBulkSaveChanges and PostBulkSaveChanges

  • PreBulkSaveChanges runs before BulkSaveChanges. Useful for applying rules across all tracked entities.
  • PostBulkSaveChanges runs after BulkSaveChanges. Often used for logging or refreshing the in-memory state.

The following example uses the PreBulkSaveChanges event to set the CreatedDate for new customers and the ModifiedDate for existing customers:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBulkSaveChanges = ctx =>
{
    foreach (var change in ctx.ChangeTracker.Entries().ToList())
    {
        if (change.State == EntityState.Added)
            change.CurrentValues["CreatedDate"] = DateTime.Now;
        else if (change.State == EntityState.Modified)
            change.CurrentValues["ModifiedDate"] = DateTime.Now;
    }
};

Online Example (EF Core) | Online Example (EF6)


🏷️ PreBatchSaveChanges and PostBatchSaveChanges

  • PreBatchSaveChanges runs before BatchSaveChanges. Useful for enforcing global rules across batch operations.
  • PostBatchSaveChanges runs after BatchSaveChanges. Often used for logging or handling batch completion tasks.

The following example uses the PreBatchSaveChanges event to set the CreatedDate for new customers and the ModifiedDate for existing customers:

// @nuget: Z.EntityFramework.Extensions
using Z.EntityFramework.Extensions;

EntityFrameworkManager.PreBatchSaveChanges = ctx =>
{
    foreach (var change in ctx.ChangeTracker.Entries().ToList())
    {
        if (change.State == EntityState.Added)
            change.CurrentValues["CreatedDate"] = DateTime.Now;
        else if (change.State == EntityState.Modified)
            change.CurrentValues["ModifiedDate"] = DateTime.Now;
    }
};

Online Example (EF6)

⚠️ Warning: BatchSaveChanges is available only in EF6. It is not supported in EF Core.


🏷️ PostConfiguration

The PostConfiguration event runs right before a bulk method executes, after all options have been configured.

It’s a great place to apply last-minute adjustments, such as enabling logging, applying filters, or tuning options dynamically.

The following example uses the PostConfiguration event to set a custom FormulaInsert on the Description column:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

context.BulkInsert(list, options =>
{
    options.PostConfiguration = ops =>
    {
        ops.ColumnMappings
           .Find(x => x.SourceName == "Description")
           .FormulaInsert = "'any valid sql statement'";
    };
});

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: There is no PreConfiguration event. Configuration can only be adjusted at the PostConfiguration stage.


🏷️ BulkOperationExecuting and BulkOperationExecuted

  • BulkOperationExecuting runs before an individual bulk operation inside a call. Great for tweaking values or applying validations.
  • BulkOperationExecuted runs after an individual bulk operation. Useful for marking entities, updating in-memory state, or logging results.

The following example demonstrates how to use both events:

// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var list = new List<Customer>() 
{ 
	new Customer()	{ Name ="Customer_A", IsActive = true},
	new Customer()	{ Name ="Customer_B", IsActive = true},
	new Customer()	{ Name ="Customer_C", IsActive = true}
};
context.Customers.AddRange(list);

context.BulkSaveChanges(options =>
{
    options.BulkOperationExecuting = op =>
    {
        list.ForEach(x => 
        { 
            x.Description = "Before Execution";
            x.IsActive = false;
        });
    };

    options.BulkOperationExecuted = op =>
    {
        list.ForEach(x => 
        { 
            x.Description = "After Execution";
            x.IsActive = true;
        });
    };
});

Online Example (EF Core) | Online Example (EF6)

⚠️ Warning: These events run per bulk operation, not per entity. If a call executes multiple operations (for example, one insert and one update batch), both BulkOperationExecuting and BulkOperationExecuted will fire for each operation.


🔎 Comparison at a Glance

Event Pair Trigger Time Common Use Case
PreBulkInsert / PostBulkInsert Before / After BulkInsert Set CreatedDate / Log inserted rows
PreBulkUpdate / PostBulkUpdate Before / After BulkUpdate Set ModifiedDate / Trigger refresh, logging
PreBulkDelete / PostBulkDelete Before / After BulkDelete Soft deletes / Audit deletions
PreBulkMerge / PostBulkMerge Before / After BulkMerge Set CreatedDate & ModifiedDate / Track merged entities
PreBulkSynchronize / PostBulkSynchronize Before / After BulkSynchronize Audit fields for sync / Monitor results
PreBulkSaveChanges / PostBulkSaveChanges Before / After BulkSaveChanges Apply rules via ChangeTracker / Log save outcome
PreBatchSaveChanges / PostBatchSaveChanges Before / After BatchSaveChanges Global batch rules / Batch completion tasks
PostConfiguration Right before bulk method executes Configure logging or options
BulkOperationExecuting / BulkOperationExecuted Before / After an operation inside a call Adjust entity values / Log results

📌 When to Use

Events are useful when you want to:

  • Automatically set CreatedDate and ModifiedDate.
  • Apply soft delete rules instead of physical deletes.
  • Log database commands and SQL queries for debugging or auditing.
  • Enforce validation or business rules globally before any data hits the database.
  • Run cleanup or reporting after bulk operations finish.

⭐ Why It’s Useful

With events you can:

  • Automate audit field management without repeating code.
  • Ensure consistent rules across bulk operations.
  • Centralize logic in one place, reducing duplication and errors.
  • Add flexible, custom behaviors before and after the operation.


🏁 Conclusion

Events in Entity Framework Extensions give you powerful hooks into the lifecycle of bulk operations.

  • Use Pre events to adjust or validate data before saving.
  • Use Post events to log, monitor, or adjust state after saving.
  • Use operation-specific events when you need fine-grained control inside a single call.

🎯 The result: cleaner code, consistent rules, and fewer surprises in your database.


Last updated: 2025-08-20
Author: