DKNet.EfCore.Hooks
9.0.26
dotnet add package DKNet.EfCore.Hooks --version 9.0.26
NuGet\Install-Package DKNet.EfCore.Hooks -Version 9.0.26
<PackageReference Include="DKNet.EfCore.Hooks" Version="9.0.26" />
<PackageVersion Include="DKNet.EfCore.Hooks" Version="9.0.26" />
<PackageReference Include="DKNet.EfCore.Hooks" />
paket add DKNet.EfCore.Hooks --version 9.0.26
#r "nuget: DKNet.EfCore.Hooks, 9.0.26"
#:package DKNet.EfCore.Hooks@9.0.26
#addin nuget:?package=DKNet.EfCore.Hooks&version=9.0.26
#tool nuget:?package=DKNet.EfCore.Hooks&version=9.0.26
DKNet.EfCore.Hooks
Entity Framework Core lifecycle hooks system providing pre and post-save interceptors for implementing cross-cutting concerns like auditing, validation, caching, and event publishing. This package enables clean separation of business logic from data access concerns.
Features
- Lifecycle Hooks: Pre-save and post-save hooks for Entity Framework Core operations
- Snapshot Context: Track entity changes with before/after state comparison
- Async Support: Full async/await support for non-blocking hook execution
- Dependency Injection: Seamless integration with .NET dependency injection
- Multiple Hooks: Support for multiple hooks per DbContext with execution ordering
- Change Tracking: Access to entity state changes during save operations
- Error Handling: Robust error handling and hook execution management
- Performance Optimized: Efficient execution with minimal overhead
Supported Frameworks
- .NET 9.0+
- Entity Framework Core 9.0+
Installation
Install via NuGet Package Manager:
dotnet add package DKNet.EfCore.Hooks
Or via Package Manager Console:
Install-Package DKNet.EfCore.Hooks
Quick Start
Basic Hook Implementation
using DKNet.EfCore.Hooks;
using DKNet.EfCore.Extensions.Snapshots;
// Audit hook example
public class AuditHook : IBeforeSaveHookAsync
{
private readonly ICurrentUserService _currentUserService;
public AuditHook(ICurrentUserService currentUserService)
{
_currentUserService = currentUserService;
}
public Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
var currentUser = _currentUserService.UserId;
var now = DateTimeOffset.UtcNow;
foreach (var entry in context.Entries)
{
if (entry.Entity is IAuditedProperties auditedEntity)
{
switch (entry.State)
{
case EntityState.Added:
auditedEntity.CreatedBy = currentUser;
auditedEntity.CreatedOn = now;
break;
case EntityState.Modified:
auditedEntity.UpdatedBy = currentUser;
auditedEntity.UpdatedOn = now;
break;
}
}
}
return Task.CompletedTask;
}
}
// Event publishing hook
public class EventPublishingHook : IAfterSaveHookAsync
{
private readonly IEventPublisher _eventPublisher;
public EventPublishingHook(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
public async Task RunAfterSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
foreach (var entry in context.Entries)
{
if (entry.Entity is IEventEntity eventEntity)
{
var events = eventEntity.GetEvents();
foreach (var domainEvent in events)
{
await _eventPublisher.PublishAsync(domainEvent, cancellationToken);
}
eventEntity.ClearEvents();
}
}
}
}
Setup and Registration
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Customer> Customers { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}
// Configure services
public void ConfigureServices(IServiceCollection services)
{
// Register hooks
services.AddHook<AppDbContext, AuditHook>();
services.AddHook<AppDbContext, EventPublishingHook>();
services.AddHook<AppDbContext, ValidationHook>();
// Register hook dependencies
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<IEventPublisher, EventPublisher>();
// Add DbContext with hooks
services.AddDbContext<AppDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddHookInterceptor<AppDbContext>(provider);
});
}
Combined Hook Implementation
public class ComprehensiveHook : IHookAsync
{
private readonly ILogger<ComprehensiveHook> _logger;
private readonly IValidator _validator;
private readonly IEventPublisher _eventPublisher;
public ComprehensiveHook(
ILogger<ComprehensiveHook> logger,
IValidator validator,
IEventPublisher eventPublisher)
{
_logger = logger;
_validator = validator;
_eventPublisher = eventPublisher;
}
public async Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Running pre-save hooks for {EntityCount} entities", context.Entries.Count);
// Validation
foreach (var entry in context.Entries)
{
if (entry.State == EntityState.Added || entry.State == EntityState.Modified)
{
var validationResult = await _validator.ValidateAsync(entry.Entity, cancellationToken);
if (!validationResult.IsValid)
{
throw new ValidationException($"Validation failed for {entry.Entity.GetType().Name}: {validationResult.Errors}");
}
}
}
// Auto-set timestamps
foreach (var entry in context.Entries)
{
if (entry.Entity is ITimestampedEntity timestamped)
{
if (entry.State == EntityState.Added)
timestamped.CreatedAt = DateTimeOffset.UtcNow;
if (entry.State == EntityState.Modified)
timestamped.UpdatedAt = DateTimeOffset.UtcNow;
}
}
}
public async Task RunAfterSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Running post-save hooks for {EntityCount} entities", context.Entries.Count);
// Publish domain events
var events = new List<object>();
foreach (var entry in context.Entries)
{
if (entry.Entity is IEventEntity eventEntity)
{
events.AddRange(eventEntity.GetEvents());
eventEntity.ClearEvents();
}
}
foreach (var domainEvent in events)
{
await _eventPublisher.PublishAsync(domainEvent, cancellationToken);
}
// Cache invalidation
foreach (var entry in context.Entries)
{
if (entry.State == EntityState.Modified || entry.State == EntityState.Deleted)
{
// Invalidate cache for this entity type
await InvalidateCacheForEntityType(entry.Entity.GetType(), cancellationToken);
}
}
}
private async Task InvalidateCacheForEntityType(Type entityType, CancellationToken cancellationToken)
{
// Implementation depends on your caching strategy
_logger.LogDebug("Invalidating cache for entity type {EntityType}", entityType.Name);
await Task.CompletedTask;
}
}
Configuration
Multiple Hooks with Ordering
public class OrderedValidationHook : IBeforeSaveHookAsync
{
public int Order => 1; // Run first
public Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
// Validation logic
return Task.CompletedTask;
}
}
public class OrderedAuditHook : IBeforeSaveHookAsync
{
public int Order => 2; // Run after validation
public Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
// Audit logic
return Task.CompletedTask;
}
}
// Register in order
services.AddHook<AppDbContext, OrderedValidationHook>();
services.AddHook<AppDbContext, OrderedAuditHook>();
Conditional Hook Execution
public class ConditionalHook : IBeforeSaveHookAsync
{
private readonly IFeatureManager _featureManager;
public ConditionalHook(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
if (!await _featureManager.IsEnabledAsync("AuditLogging"))
return;
// Execute hook logic only when feature is enabled
foreach (var entry in context.Entries)
{
// Conditional audit logic
}
}
}
API Reference
Hook Interfaces
IHookBaseAsync
- Base interface for all hooksIBeforeSaveHookAsync
- Pre-save hook interfaceIAfterSaveHookAsync
- Post-save hook interfaceIHookAsync
- Combined pre and post-save hook interface
Setup Extensions
AddHook<TDbContext, THook>()
- Register hook for specific DbContextAddHookInterceptor<TDbContext>(IServiceProvider)
- Add hook interceptor to DbContext options
Snapshot Context
SnapshotContext.Entries
- Collection of entity change entriesSnapshotEntityEntry.Entity
- The tracked entitySnapshotEntityEntry.State
- Entity state (Added, Modified, Deleted, etc.)SnapshotEntityEntry.OriginalValues
- Original property values (for Modified entities)SnapshotEntityEntry.CurrentValues
- Current property values
Advanced Usage
Performance Monitoring Hook
public class PerformanceMonitoringHook : IHookAsync
{
private readonly ILogger<PerformanceMonitoringHook> _logger;
private readonly IMetrics _metrics;
private readonly Stopwatch _stopwatch = new();
public PerformanceMonitoringHook(ILogger<PerformanceMonitoringHook> logger, IMetrics metrics)
{
_logger = logger;
_metrics = metrics;
}
public Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
_stopwatch.Restart();
_logger.LogDebug("Starting save operation for {EntityCount} entities", context.Entries.Count);
_metrics.Counter("efcore.save_operations.started").Increment();
_metrics.Histogram("efcore.entities_per_save").Record(context.Entries.Count);
return Task.CompletedTask;
}
public Task RunAfterSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
_stopwatch.Stop();
var duration = _stopwatch.ElapsedMilliseconds;
_logger.LogDebug("Completed save operation in {Duration}ms for {EntityCount} entities",
duration, context.Entries.Count);
_metrics.Histogram("efcore.save_operations.duration").Record(duration);
_metrics.Counter("efcore.save_operations.completed").Increment();
if (duration > 5000) // Log slow operations
{
_logger.LogWarning("Slow save operation detected: {Duration}ms for {EntityCount} entities",
duration, context.Entries.Count);
}
return Task.CompletedTask;
}
}
Security and Authorization Hook
public class SecurityHook : IBeforeSaveHookAsync
{
private readonly ICurrentUserService _currentUser;
private readonly IAuthorizationService _authorizationService;
public SecurityHook(ICurrentUserService currentUser, IAuthorizationService authorizationService)
{
_currentUser = currentUser;
_authorizationService = authorizationService;
}
public async Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
foreach (var entry in context.Entries)
{
var entityType = entry.Entity.GetType().Name;
var operation = GetOperationType(entry.State);
var authResult = await _authorizationService.AuthorizeAsync(
_currentUser.Principal,
entry.Entity,
$"{entityType}.{operation}");
if (!authResult.Succeeded)
{
throw new UnauthorizedAccessException(
$"User {_currentUser.UserId} is not authorized to {operation} {entityType}");
}
// Row-level security for owned entities
if (entry.Entity is IOwnedEntity ownedEntity)
{
if (ownedEntity.OwnerId != _currentUser.UserId && !_currentUser.IsAdmin)
{
throw new UnauthorizedAccessException(
$"User {_currentUser.UserId} cannot access entity owned by {ownedEntity.OwnerId}");
}
}
}
}
private static string GetOperationType(EntityState state) => state switch
{
EntityState.Added => "Create",
EntityState.Modified => "Update",
EntityState.Deleted => "Delete",
_ => "Read"
};
}
Integration with External Systems
public class ExternalIntegrationHook : IAfterSaveHookAsync
{
private readonly ISearchIndexService _searchService;
private readonly INotificationService _notificationService;
private readonly ILogger<ExternalIntegrationHook> _logger;
public ExternalIntegrationHook(
ISearchIndexService searchService,
INotificationService notificationService,
ILogger<ExternalIntegrationHook> logger)
{
_searchService = searchService;
_notificationService = notificationService;
_logger = logger;
}
public async Task RunAfterSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
var searchUpdateTasks = new List<Task>();
var notificationTasks = new List<Task>();
foreach (var entry in context.Entries)
{
try
{
// Update search index
if (entry.Entity is ISearchable searchableEntity)
{
var task = entry.State switch
{
EntityState.Added or EntityState.Modified =>
_searchService.IndexAsync(searchableEntity, cancellationToken),
EntityState.Deleted =>
_searchService.RemoveAsync(searchableEntity.Id, cancellationToken),
_ => Task.CompletedTask
};
searchUpdateTasks.Add(task);
}
// Send notifications
if (entry.Entity is INotifiable notifiableEntity && entry.State == EntityState.Added)
{
var notificationTask = _notificationService.SendCreatedNotificationAsync(
notifiableEntity, cancellationToken);
notificationTasks.Add(notificationTask);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing external integration for {EntityType} {EntityId}",
entry.Entity.GetType().Name, GetEntityId(entry.Entity));
}
}
// Execute all tasks concurrently
await Task.WhenAll(searchUpdateTasks.Concat(notificationTasks));
}
private static object? GetEntityId(object entity)
{
return entity.GetType().GetProperty("Id")?.GetValue(entity);
}
}
Error Handling and Resilience
public class ResilientHook : IHookAsync
{
private readonly ILogger<ResilientHook> _logger;
private readonly IRetryPolicy _retryPolicy;
public ResilientHook(ILogger<ResilientHook> logger, IRetryPolicy retryPolicy)
{
_logger = logger;
_retryPolicy = retryPolicy;
}
public async Task RunBeforeSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
try
{
// Pre-save logic with retry
await _retryPolicy.ExecuteAsync(async () =>
{
await ProcessPreSaveLogic(context, cancellationToken);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Critical error in pre-save hook. Operation will be aborted.");
throw; // Re-throw to prevent save operation
}
}
public async Task RunAfterSaveAsync(SnapshotContext context, CancellationToken cancellationToken = default)
{
try
{
// Post-save logic with resilience (don't fail the main operation)
await _retryPolicy.ExecuteAsync(async () =>
{
await ProcessPostSaveLogic(context, cancellationToken);
});
}
catch (Exception ex)
{
// Log error but don't re-throw to avoid affecting the main save operation
_logger.LogError(ex, "Error in post-save hook. Main operation completed successfully.");
}
}
private Task ProcessPreSaveLogic(SnapshotContext context, CancellationToken cancellationToken)
{
// Critical pre-save operations
return Task.CompletedTask;
}
private Task ProcessPostSaveLogic(SnapshotContext context, CancellationToken cancellationToken)
{
// Non-critical post-save operations
return Task.CompletedTask;
}
}
Best Practices
- Separation of Concerns: Keep hooks focused on single responsibilities
- Error Handling: Use try-catch in post-save hooks to avoid affecting main operations
- Performance: Minimize processing time in pre-save hooks
- Async Operations: Use async/await for I/O operations
- Logging: Add comprehensive logging for debugging and monitoring
- Testing: Mock hook dependencies for unit testing
Performance Considerations
- Hook Execution Order: Critical hooks should run first
- Async Operations: Use Task.WhenAll for concurrent operations
- Database Calls: Minimize additional database calls in hooks
- Memory Usage: Be mindful of memory usage when processing large change sets
- Caching: Consider caching expensive operations within hook scope
Thread Safety
- Hook instances are scoped to the DbContext instance
- Concurrent access to shared resources requires proper synchronization
- Use thread-safe services and avoid shared mutable state
- Entity Framework Core change tracking is not thread-safe
Contributing
See the main CONTRIBUTING.md for guidelines on how to contribute to this project.
License
This project is licensed under the MIT License.
Related Packages
- DKNet.EfCore.Extensions - EF Core functionality extensions (includes SnapshotContext)
- DKNet.EfCore.Events - Domain event handling (uses hooks internally)
- DKNet.EfCore.Abstractions - Core entity abstractions
- DKNet.EfCore.DataAuthorization - Data authorization patterns
Part of the DKNet Framework - A comprehensive .NET framework for building modern, scalable applications.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- DKNet.EfCore.Extensions (>= 9.0.26)
- DKNet.Fw.Extensions (>= 9.0.26)
- Microsoft.EntityFrameworkCore (>= 9.0.9)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on DKNet.EfCore.Hooks:
Package | Downloads |
---|---|
DKNet.EfCore.DataAuthorization
Package Description |
|
DKNet.EfCore.Events
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
9.0.26 | 0 | 9/16/2025 |
9.0.25 | 14 | 9/15/2025 |
9.0.24 | 12 | 9/15/2025 |
9.0.23 | 101 | 9/6/2025 |
9.0.22 | 161 | 9/3/2025 |
9.0.21 | 131 | 9/1/2025 |
9.0.20 | 156 | 7/15/2025 |
9.0.19 | 146 | 7/14/2025 |
9.0.18 | 149 | 7/14/2025 |
9.0.17 | 147 | 7/14/2025 |
9.0.16 | 130 | 7/11/2025 |
9.0.15 | 130 | 7/11/2025 |
9.0.14 | 128 | 7/11/2025 |
9.0.13 | 133 | 7/11/2025 |
9.0.12 | 149 | 7/8/2025 |
9.0.11 | 147 | 7/8/2025 |
9.0.10 | 147 | 7/7/2025 |
9.0.9 | 145 | 7/2/2025 |
9.0.8 | 145 | 7/2/2025 |
9.0.7 | 158 | 7/1/2025 |
9.0.6 | 152 | 6/30/2025 |
9.0.5 | 150 | 6/24/2025 |
9.0.4 | 148 | 6/24/2025 |
9.0.3 | 150 | 6/23/2025 |
9.0.2 | 149 | 6/23/2025 |
9.0.1 | 154 | 6/23/2025 |