DKNet is an enterprise-grade .NET library collection focused on advanced EF Core extensions, dynamic predicate building, and the Specification pattern. It provides production-ready tools for building robust, type-safe, and testable data access layers, including dynamic LINQ support, LinqKit integration. Designed for modern cloud-native applications, DKNet enforces strict code quality, async best practices, and full documentation for all public APIs. Enterprise-grade .NET library suite for modern application development, featuring advanced EF Core extensions (dynamic predicates, specifications, LinqKit), robust Domain-Driven Design (DDD) patterns, and domain event support. DKNet empowers scalable, maintainable, and testable solutions with type-safe validation, async/await, XML documentation, and high code quality standards. Ideal for cloud-native, microservices, and enterprise architectures.
$ dotnet add package DKNet.EfCore.HooksEntity 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.
Install via NuGet Package Manager:
dotnet add package DKNet.EfCore.HooksOr via Package Manager Console:
Install-Package DKNet.EfCore.Hooksusing 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();
}
}
}
}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);
});
}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;
}
}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>();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
}
}
}IHookBaseAsync - Base interface for all hooksIBeforeSaveHookAsync - Pre-save hook interfaceIAfterSaveHookAsync - Post-save hook interfaceIHookAsync - Combined pre and post-save hook interfaceAddHook<TDbContext, THook>() - Register hook for specific DbContextAddHookInterceptor<TDbContext>(IServiceProvider) - Add hook interceptor to DbContext optionsSnapshotContext.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 valuespublic 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;
}
}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"
};
}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);
}
}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;
}
}See the main CONTRIBUTING.md for guidelines on how to contribute to this project.
This project is licensed under the MIT License.
Part of the DKNet Framework - A comprehensive .NET framework for building modern, scalable applications.