From 2e557b39c4c2c0d08476cd79902da4a41195359e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:36:22 +0000 Subject: [PATCH 1/5] Initial plan From b636944a9324c3d0446cd1d942c8bb1cfb2bc426 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:49:16 +0000 Subject: [PATCH 2/5] Add comprehensive documentation with usage guide, getting started, and advanced features Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- README.md | 151 +++- docs/AdvancedFeatures.md | 829 ++++++++++++++++++++++ docs/GettingStarted.md | 422 ++++++++++++ docs/Usage.md | 1404 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 2745 insertions(+), 61 deletions(-) create mode 100644 docs/AdvancedFeatures.md create mode 100644 docs/GettingStarted.md diff --git a/README.md b/README.md index ad5fd8b..3202393 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,94 @@ ## Intro -[EntityFrameworkCore](https://github.com/dotnet/efcore) extensions +[EntityFrameworkCore](https://github.com/dotnet/efcore) extensions that provide a comprehensive set of tools and patterns to enhance your Entity Framework Core development experience. + +WeihanLi.EntityFramework offers: + +- **Repository Pattern** - Clean abstraction layer for data access +- **Unit of Work Pattern** - Transaction management across multiple repositories +- **Automatic Auditing** - Track all entity changes with flexible storage options +- **Auto-Update Features** - Automatic handling of CreatedAt/UpdatedAt timestamps and user tracking +- **Soft Delete** - Mark entities as deleted without physical removal +- **Database Extensions** - Convenient methods for bulk operations and queries +- **Database Functions** - SQL Server JSON operations and more + +## Quick Start + +### 1. Installation + +```bash +dotnet add package WeihanLi.EntityFramework +``` + +### 2. Basic Setup + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +// Add DbContext +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Add WeihanLi.EntityFramework services +builder.Services.AddEFRepository(); +builder.Services.AddEFAutoUpdateInterceptor(); +builder.Services.AddEFAutoAudit(auditBuilder => +{ + auditBuilder.WithUserIdProvider() + .WithStore(); +}); + +var app = builder.Build(); +``` + +### 3. Define Your Entities + +```csharp +public class Product : IEntityWithCreatedUpdatedAt, ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + + // Auto-update properties + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // Soft delete property + public bool IsDeleted { get; set; } +} +``` + +### 4. Use Repository Pattern + +```csharp +public class ProductService +{ + private readonly IEFRepository _repository; + + public ProductService(IEFRepository repository) + { + _repository = repository; + } + + public async Task CreateProductAsync(string name, decimal price) + { + var product = new Product { Name = name, Price = price }; + return await _repository.InsertAsync(product); + // CreatedAt/UpdatedAt automatically set, audit record created + } + + public async Task> GetActiveProductsAsync() + { + return await _repository.GetListAsync( + queryBuilder => queryBuilder.WithPredict(p => p.Price > 0) + ); + // Soft deleted products automatically filtered out + } +} +``` ## Package Release Notes @@ -33,36 +120,58 @@ See Releases/PRs for details ## Features -- Repository - - - `EFRepository` - - `EFRepositoryGenerator` +### 🏗️ Repository Pattern +- `IEFRepository` - Generic repository interface +- `EFRepository` - Full-featured repository implementation +- `EFRepositoryGenerator` - Dynamic repository creation +- **Query Builder** - Fluent API for complex queries +- **Bulk Operations** - Efficient batch updates and deletes -- UoW - - - `EFUnitOfWork` +### 🔄 Unit of Work Pattern +- `IEFUnitOfWork` - Transaction management +- **Multi-Repository Transactions** - Coordinate changes across entities +- **Rollback Support** - Automatic error handling -- DbFunctions - - - `JsonValue` implement `JSON_VALUE` for SqlServer 2016 and above +### 📋 Comprehensive Auditing +- **Automatic Change Tracking** - Monitor all entity modifications +- **Flexible Storage** - Database, file, console, or custom stores +- **Property Enrichment** - Add custom metadata to audit records +- **User Tracking** - Capture who made changes +- **Configurable Filtering** - Include/exclude entities and properties -- Audit +### ⚡ Auto-Update Features +- **Timestamp Management** - Automatic CreatedAt/UpdatedAt handling +- **User Tracking** - Automatic CreatedBy/UpdatedBy population +- **Soft Delete** - Mark entities as deleted without removal +- **Custom Auto-Update** - Define your own auto-update rules - - Auto auditing for entity changes - -- AutoUpdate +### 🔧 Database Extensions +- **Column Updates** - Update specific columns only +- **Bulk Operations** - Efficient mass updates +- **Query Helpers** - Get table/column names, check database type +- **Paging Support** - Built-in pagination for large datasets - - Soft delete for the specific entity - - Auto update CreatedAt/UpdatedAt/CreatedBy/UpdatedBy +### 🗄️ Database Functions +- **JSON Support** - `JSON_VALUE` for SQL Server 2016+ +- **SQL Server Functions** - Enhanced querying capabilities -- Extensions +## Documentation - - Update specific column(s) `Update` - - Update without specific column(s) `UpdateWithout` +🚀 **[Getting Started Guide](docs/GettingStarted.md)** - Step-by-step setup instructions for new users + +📖 **[Complete Usage Guide](docs/Usage.md)** - Comprehensive documentation with examples for all features + +📋 **[Release Notes](docs/ReleaseNotes.md)** - Version history and breaking changes + +🔧 **[Sample Project](samples/WeihanLi.EntityFramework.Sample/)** - Working examples and demonstrations ## Support -Feel free to try and [create issues](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) if you have any questions or integration issues +💡 **Questions?** Check out the [Usage Guide](docs/Usage.md) for detailed examples + +🐛 **Found a bug?** [Create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps + +💬 **Need help?** Feel free to [start a discussion](https://github.com/WeihanLi/WeihanLi.EntityFramework/discussions) or create an issue ## Usage diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md new file mode 100644 index 0000000..80d34b2 --- /dev/null +++ b/docs/AdvancedFeatures.md @@ -0,0 +1,829 @@ +# Advanced Features Guide + +This guide covers advanced features and patterns for WeihanLi.EntityFramework. + +## Table of Contents + +- [Custom Interceptors](#custom-interceptors) +- [Advanced Audit Configuration](#advanced-audit-configuration) +- [Performance Optimization](#performance-optimization) +- [Testing Strategies](#testing-strategies) +- [Custom Query Filters](#custom-query-filters) +- [Bulk Operations](#bulk-operations) +- [Integration Patterns](#integration-patterns) + +## Custom Interceptors + +### Creating Custom Auto-Update Logic + +```csharp +public class CustomAutoUpdateInterceptor : SaveChangesInterceptor +{ + private readonly ILogger _logger; + private readonly IUserIdProvider _userIdProvider; + + public CustomAutoUpdateInterceptor( + ILogger logger, + IUserIdProvider userIdProvider) + { + _logger = logger; + _userIdProvider = userIdProvider; + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + if (eventData.Context != null) + { + HandleCustomUpdates(eventData.Context); + } + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void HandleCustomUpdates(DbContext context) + { + var entries = context.ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); + + foreach (var entry in entries) + { + // Custom business logic + if (entry.Entity is IVersionedEntity versionedEntity) + { + if (entry.State == EntityState.Added) + { + versionedEntity.Version = 1; + } + else if (entry.State == EntityState.Modified) + { + versionedEntity.Version++; + } + } + + // Log entity changes + if (entry.Entity is IAuditableEntity auditableEntity) + { + _logger.LogInformation("Entity {EntityType} with ID {EntityId} is being {Action}", + entry.Entity.GetType().Name, + GetEntityId(entry), + entry.State); + } + } + } + + private object? GetEntityId(EntityEntry entry) + { + var keyProperty = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey()); + return keyProperty?.CurrentValue; + } +} + +// Supporting interfaces +public interface IVersionedEntity +{ + int Version { get; set; } +} + +public interface IAuditableEntity +{ + // Marker interface for entities that should be logged +} +``` + +### Conditional Interceptors + +```csharp +public class ConditionalAuditInterceptor : SaveChangesInterceptor +{ + private readonly IAuditConfig _auditConfig; + private readonly IServiceProvider _serviceProvider; + + public ConditionalAuditInterceptor( + IAuditConfig auditConfig, + IServiceProvider serviceProvider) + { + _auditConfig = auditConfig; + _serviceProvider = serviceProvider; + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + // Only audit in production environment + if (_auditConfig.IsEnabled && IsProductionEnvironment()) + { + var auditInterceptor = _serviceProvider.GetRequiredService(); + return auditInterceptor.SavingChangesAsync(eventData, result, cancellationToken); + } + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private bool IsProductionEnvironment() + { + var environment = _serviceProvider.GetService(); + return environment?.IsProduction() == true; + } +} +``` + +## Advanced Audit Configuration + +### Custom Audit Property Enricher + +```csharp +public class CustomAuditPropertyEnricher : IAuditPropertyEnricher +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IConfiguration _configuration; + + public CustomAuditPropertyEnricher( + IHttpContextAccessor httpContextAccessor, + IConfiguration configuration) + { + _httpContextAccessor = httpContextAccessor; + _configuration = configuration; + } + + public void Enrich(AuditEntry auditEntry) + { + var httpContext = _httpContextAccessor.HttpContext; + + if (httpContext != null) + { + // Add request information + auditEntry.ExtraProperties["UserAgent"] = httpContext.Request.Headers["User-Agent"].ToString(); + auditEntry.ExtraProperties["IpAddress"] = httpContext.Connection.RemoteIpAddress?.ToString(); + auditEntry.ExtraProperties["RequestId"] = httpContext.TraceIdentifier; + + // Add correlation ID if available + if (httpContext.Request.Headers.ContainsKey("X-Correlation-ID")) + { + auditEntry.ExtraProperties["CorrelationId"] = + httpContext.Request.Headers["X-Correlation-ID"].ToString(); + } + } + + // Add application-specific information + auditEntry.ExtraProperties["ApplicationVersion"] = _configuration["ApplicationVersion"]; + auditEntry.ExtraProperties["Environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + auditEntry.ExtraProperties["ServerName"] = Environment.MachineName; + } +} +``` + +### Multi-Store Audit Configuration + +```csharp +public class MultiStoreAuditStore : IAuditStore +{ + private readonly List _stores; + private readonly ILogger _logger; + + public MultiStoreAuditStore( + IEnumerable stores, + ILogger logger) + { + _stores = stores.ToList(); + _logger = logger; + } + + public async Task Save(ICollection auditEntries) + { + var tasks = _stores.Select(async store => + { + try + { + await store.Save(auditEntries); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save audit entries to store {StoreType}", + store.GetType().Name); + } + }); + + await Task.WhenAll(tasks); + } +} + +// Configuration +services.AddEFAutoAudit(builder => +{ + builder + .WithUserIdProvider() + .WithPropertyEnricher() + .WithStore() // Primary store + .WithStore() // Secondary store for analytics + .WithStore(); // Backup store +}); + +// Register multi-store +services.AddSingleton(); +``` + +## Performance Optimization + +### Optimized Repository Patterns + +```csharp +public class OptimizedProductService +{ + private readonly IEFRepository _repository; + private readonly IMemoryCache _cache; + + public OptimizedProductService( + IEFRepository repository, + IMemoryCache cache) + { + _repository = repository; + _cache = cache; + } + + // Use projection to reduce data transfer + public async Task> GetProductSummariesAsync() + { + return await _repository.GetResultAsync( + p => new ProductSummary + { + Id = p.Id, + Name = p.Name, + Price = p.Price, + CategoryName = p.Category.Name + }, + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithInclude(p => p.Category) + ); + } + + // Cached frequently accessed data + public async Task GetProductWithCacheAsync(int id) + { + var cacheKey = $"product_{id}"; + + if (_cache.TryGetValue(cacheKey, out Product? cachedProduct)) + { + return cachedProduct; + } + + var product = await _repository.FindAsync(id); + + if (product != null) + { + _cache.Set(cacheKey, product, TimeSpan.FromMinutes(15)); + } + + return product; + } + + // Bulk operations for better performance + public async Task BulkActivateProductsAsync(List productIds) + { + return await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.IsActive, true), + queryBuilder => queryBuilder.WithPredict(p => productIds.Contains(p.Id)) + ); + } + + // Streaming for large datasets + public async IAsyncEnumerable StreamActiveProductsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var query = _repository.Query( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Id)) + ); + + await foreach (var product in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return product; + } + } +} + +public class ProductSummary +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public string CategoryName { get; set; } = string.Empty; +} +``` + +### Connection and Query Optimization + +```csharp +// Configure DbContext for optimal performance +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString, sqlOptions => + { + sqlOptions.CommandTimeout(30); + sqlOptions.EnableRetryOnFailure(3); + }) + .EnableSensitiveDataLogging(isDevelopment) + .EnableDetailedErrors(isDevelopment) + .ConfigureWarnings(warnings => + { + warnings.Ignore(CoreEventId.FirstWithoutOrderByAndFilterWarning); + }); +}); + +// Connection pooling +services.AddDbContextPool(options => +{ + options.UseSqlServer(connectionString); +}, poolSize: 128); +``` + +## Testing Strategies + +### Unit Testing with Mocked Repositories + +```csharp +public class ProductServiceTests +{ + private readonly Mock> _mockRepository; + private readonly Mock _mockCache; + private readonly ProductService _service; + + public ProductServiceTests() + { + _mockRepository = new Mock>(); + _mockCache = new Mock(); + _service = new ProductService(_mockRepository.Object, _mockCache.Object); + } + + [Fact] + public async Task GetActiveProductsAsync_ShouldReturnOnlyActiveProducts() + { + // Arrange + var activeProducts = new List + { + new() { Id = 1, Name = "Product 1", IsActive = true }, + new() { Id = 2, Name = "Product 2", IsActive = true } + }; + + _mockRepository + .Setup(r => r.GetListAsync(It.IsAny>>())) + .ReturnsAsync(activeProducts); + + // Act + var result = await _service.GetActiveProductsAsync(); + + // Assert + Assert.Equal(2, result.Count); + Assert.All(result, p => Assert.True(p.IsActive)); + + _mockRepository.Verify( + r => r.GetListAsync(It.IsAny>>()), + Times.Once); + } + + [Fact] + public async Task CreateProductAsync_ShouldSetCorrectProperties() + { + // Arrange + var productName = "New Product"; + var productPrice = 99.99m; + var expectedProduct = new Product + { + Id = 1, + Name = productName, + Price = productPrice, + IsActive = true + }; + + _mockRepository + .Setup(r => r.InsertAsync(It.IsAny())) + .ReturnsAsync(expectedProduct); + + // Act + var result = await _service.CreateProductAsync(productName, productPrice); + + // Assert + Assert.Equal(expectedProduct.Id, result.Id); + Assert.Equal(productName, result.Name); + Assert.Equal(productPrice, result.Price); + Assert.True(result.IsActive); + + _mockRepository.Verify( + r => r.InsertAsync(It.Is(p => + p.Name == productName && + p.Price == productPrice && + p.IsActive)), + Times.Once); + } +} +``` + +### Integration Testing with In-Memory Database + +```csharp +public class ProductServiceIntegrationTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + + public ProductServiceIntegrationTests(WebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task CreateAndRetrieveProduct_ShouldWorkEndToEnd() + { + // Arrange + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var productService = scope.ServiceProvider.GetRequiredService(); + + await context.Database.EnsureCreatedAsync(); + + // Act + var createdProduct = await productService.CreateProductAsync("Test Product", 50.00m); + var retrievedProduct = await productService.GetProductByIdAsync(createdProduct.Id); + + // Assert + Assert.NotNull(retrievedProduct); + Assert.Equal("Test Product", retrievedProduct.Name); + Assert.Equal(50.00m, retrievedProduct.Price); + Assert.True(retrievedProduct.IsActive); + Assert.True(retrievedProduct.CreatedAt > DateTimeOffset.MinValue); + } +} + +// Test-specific configuration +public class TestStartup : Startup +{ + public TestStartup(IConfiguration configuration) : base(configuration) { } + + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + + // Replace real database with in-memory + services.Remove(services.Single(d => d.ServiceType == typeof(DbContextOptions))); + services.AddDbContext(options => + { + options.UseInMemoryDatabase("TestDatabase"); + }); + + // Use test user provider + services.AddSingleton(new StaticUserIdProvider("TestUser")); + } +} +``` + +## Custom Query Filters + +### Dynamic Query Filters + +```csharp +public class TenantAwareRepository : IEFRepository + where TEntity : class, ITenantEntity +{ + private readonly IEFRepository _baseRepository; + private readonly ITenantProvider _tenantProvider; + + public TenantAwareRepository( + IEFRepository baseRepository, + ITenantProvider tenantProvider) + { + _baseRepository = baseRepository; + _tenantProvider = tenantProvider; + } + + public async Task> GetListAsync( + Action>? queryBuilderAction = null) + { + return await _baseRepository.GetListAsync(queryBuilder => + { + // Always apply tenant filter + queryBuilder.WithPredict(e => e.TenantId == _tenantProvider.GetCurrentTenantId()); + + // Apply additional filters + queryBuilderAction?.Invoke(queryBuilder); + }); + } + + // Implement other methods with tenant filtering... +} + +public interface ITenantEntity +{ + string TenantId { get; set; } +} + +public interface ITenantProvider +{ + string GetCurrentTenantId(); +} +``` + +### Security-Based Filters + +```csharp +public class SecureProductRepository : IProductRepository +{ + private readonly IEFRepository _repository; + private readonly ICurrentUserService _currentUserService; + + public SecureProductRepository( + IEFRepository repository, + ICurrentUserService currentUserService) + { + _repository = repository; + _currentUserService = currentUserService; + } + + public async Task> GetAccessibleProductsAsync() + { + var currentUser = await _currentUserService.GetCurrentUserAsync(); + + return await _repository.GetListAsync(queryBuilder => + { + if (currentUser.Role == UserRole.Admin) + { + // Admins see all products + queryBuilder.WithPredict(p => true); + } + else if (currentUser.Role == UserRole.Manager) + { + // Managers see products in their department + queryBuilder.WithPredict(p => p.DepartmentId == currentUser.DepartmentId); + } + else + { + // Regular users see only active products they created + queryBuilder.WithPredict(p => + p.IsActive && p.CreatedBy == currentUser.Id); + } + }); + } +} +``` + +## Bulk Operations + +### Advanced Bulk Processing + +```csharp +public class BulkOperationService +{ + private readonly IEFRepository _productRepository; + private readonly ILogger _logger; + + public BulkOperationService( + IEFRepository productRepository, + ILogger logger) + { + _productRepository = productRepository; + _logger = logger; + } + + public async Task BulkUpdatePricesAsync( + Dictionary priceUpdates, + CancellationToken cancellationToken = default) + { + var result = new BulkOperationResult(); + const int batchSize = 1000; + + var batches = priceUpdates + .Select((item, index) => new { item, index }) + .GroupBy(x => x.index / batchSize) + .Select(g => g.Select(x => x.item).ToList()); + + foreach (var batch in batches) + { + try + { + var productIds = batch.Select(b => b.Key).ToList(); + + foreach (var update in batch) + { + var updated = await _productRepository.UpdateAsync( + setters => setters.SetProperty(p => p.Price, update.Value), + queryBuilder => queryBuilder.WithPredict(p => p.Id == update.Key) + ); + + if (updated > 0) + { + result.SuccessCount++; + } + else + { + result.FailedIds.Add(update.Key); + result.FailureCount++; + } + } + + _logger.LogInformation("Processed batch of {Count} price updates", batch.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to process batch of price updates"); + result.FailedIds.AddRange(batch.Select(b => b.Key)); + result.FailureCount += batch.Count; + } + + if (cancellationToken.IsCancellationRequested) + { + break; + } + } + + return result; + } + + public async Task BulkArchiveOldProductsAsync(DateTime cutoffDate) + { + return await _productRepository.UpdateAsync( + setters => setters + .SetProperty(p => p.IsActive, false) + .SetProperty(p => p.ArchivedAt, DateTime.UtcNow), + queryBuilder => queryBuilder.WithPredict(p => + p.IsActive && p.CreatedAt < cutoffDate) + ); + } +} + +public class BulkOperationResult +{ + public int SuccessCount { get; set; } + public int FailureCount { get; set; } + public List FailedIds { get; set; } = new(); + + public bool HasFailures => FailureCount > 0; + public double SuccessRate => + SuccessCount + FailureCount > 0 + ? (double)SuccessCount / (SuccessCount + FailureCount) + : 0; +} +``` + +## Integration Patterns + +### Event-Driven Architecture + +```csharp +public class EventDrivenProductService +{ + private readonly IEFRepository _repository; + private readonly IEventPublisher _eventPublisher; + + public EventDrivenProductService( + IEFRepository repository, + IEventPublisher eventPublisher) + { + _repository = repository; + _eventPublisher = eventPublisher; + } + + public async Task CreateProductAsync(CreateProductCommand command) + { + var product = new Product + { + Name = command.Name, + Price = command.Price, + CategoryId = command.CategoryId + }; + + var createdProduct = await _repository.InsertAsync(product); + + // Publish domain event + await _eventPublisher.PublishAsync(new ProductCreatedEvent + { + ProductId = createdProduct.Id, + Name = createdProduct.Name, + Price = createdProduct.Price, + CreatedAt = createdProduct.CreatedAt, + CreatedBy = createdProduct.CreatedBy + }); + + return createdProduct; + } +} + +public class ProductCreatedEvent +{ + public int ProductId { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public string CreatedBy { get; set; } = string.Empty; +} + +// Event handler for side effects +public class ProductCreatedEventHandler : IEventHandler +{ + private readonly IEmailService _emailService; + private readonly ICacheService _cacheService; + + public ProductCreatedEventHandler( + IEmailService emailService, + ICacheService cacheService) + { + _emailService = emailService; + _cacheService = cacheService; + } + + public async Task HandleAsync(ProductCreatedEvent @event) + { + // Send notification + await _emailService.SendProductCreatedNotificationAsync(@event); + + // Invalidate cache + await _cacheService.InvalidateAsync("products_*"); + + // Update search index, analytics, etc. + } +} +``` + +### CQRS Pattern Integration + +```csharp +// Command side - uses repositories for writes +public class UpdateProductCommandHandler +{ + private readonly IEFRepository _repository; + private readonly IEventStore _eventStore; + + public UpdateProductCommandHandler( + IEFRepository repository, + IEventStore eventStore) + { + _repository = repository; + _eventStore = eventStore; + } + + public async Task HandleAsync(UpdateProductCommand command) + { + var product = await _repository.FindAsync(command.ProductId); + if (product == null) + { + return UpdateProductResult.NotFound(); + } + + // Business logic + var oldPrice = product.Price; + product.Name = command.Name; + product.Price = command.Price; + + await _repository.UpdateAsync(product); + + // Store event + if (oldPrice != command.Price) + { + await _eventStore.AppendAsync(new ProductPriceChangedEvent + { + ProductId = product.Id, + OldPrice = oldPrice, + NewPrice = command.Price, + ChangedAt = DateTime.UtcNow, + ChangedBy = command.UserId + }); + } + + return UpdateProductResult.Success(); + } +} + +// Query side - uses raw EF for reads +public class ProductQueryService +{ + private readonly AppDbContext _context; + + public ProductQueryService(AppDbContext context) + { + _context = context; + } + + public async Task GetProductDetailsAsync(int productId) + { + return await _context.Products + .Where(p => p.Id == productId) + .Select(p => new ProductDetailsView + { + Id = p.Id, + Name = p.Name, + Price = p.Price, + CategoryName = p.Category.Name, + CreatedAt = p.CreatedAt, + UpdatedAt = p.UpdatedAt, + ReviewCount = p.Reviews.Count(), + AverageRating = p.Reviews.Average(r => r.Rating) + }) + .FirstOrDefaultAsync(); + } +} +``` + +These advanced patterns help you build robust, scalable applications while leveraging the full power of WeihanLi.EntityFramework. \ No newline at end of file diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 0000000..06f9ffd --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,422 @@ +# Getting Started with WeihanLi.EntityFramework + +This guide will help you get up and running with WeihanLi.EntityFramework quickly. + +## Prerequisites + +- .NET 8.0 or later +- Entity Framework Core 8.0 or later +- A supported database provider (SQL Server, SQLite, PostgreSQL, etc.) + +## Step 1: Installation + +Add the package to your project: + +```bash +dotnet add package WeihanLi.EntityFramework +``` + +## Step 2: Define Your Entities + +Create your entity classes and implement the desired interfaces for automatic features: + +```csharp +using WeihanLi.EntityFramework; + +// Basic entity with auto-update timestamps +public class Product : IEntityWithCreatedUpdatedAt +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public bool IsActive { get; set; } = true; + + // These will be automatically managed + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } +} + +// Entity with soft delete capability +public class Category : ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + + // Soft delete property + public bool IsDeleted { get; set; } +} + +// Entity with full audit trail +public class Order : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +{ + public int Id { get; set; } + public int CustomerId { get; set; } + public decimal TotalAmount { get; set; } + public OrderStatus Status { get; set; } + + // Auto-managed timestamp fields + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // Auto-managed user tracking fields + public string CreatedBy { get; set; } = string.Empty; + public string UpdatedBy { get; set; } = string.Empty; + + // Navigation properties + public List Items { get; set; } = new(); +} + +public enum OrderStatus +{ + Pending, + Processing, + Shipped, + Delivered, + Cancelled +} +``` + +## Step 3: Configure Your DbContext + +Set up your DbContext with the necessary configurations: + +```csharp +using Microsoft.EntityFrameworkCore; +using WeihanLi.EntityFramework.Audit; + +public class AppDbContext : AuditDbContext +{ + public AppDbContext(DbContextOptions options, IServiceProvider serviceProvider) + : base(options, serviceProvider) + { + } + + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderItems { get; set; } + public DbSet AuditRecords { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure Product entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(200); + entity.Property(e => e.Price).HasPrecision(18, 2); + entity.HasIndex(e => e.Name); + }); + + // Configure Category with soft delete filter + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(100); + entity.HasQueryFilter(c => !c.IsDeleted); // Global soft delete filter + }); + + // Configure Order entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.TotalAmount).HasPrecision(18, 2); + entity.HasMany(e => e.Items) + .WithOne() + .HasForeignKey("OrderId"); + }); + } +} +``` + +## Step 4: Configure Services + +Set up dependency injection in your `Program.cs`: + +```csharp +using WeihanLi.EntityFramework; +using WeihanLi.EntityFramework.Audit; + +var builder = WebApplication.CreateBuilder(args); + +// Add Entity Framework +builder.Services.AddDbContext((provider, options) => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) + .AddInterceptors( + provider.GetRequiredService(), + provider.GetRequiredService() + ); +}); + +// Add WeihanLi.EntityFramework services +builder.Services.AddEFRepository(); +builder.Services.AddEFAutoUpdateInterceptor(); + +// Configure user ID provider for audit and auto-update +builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); + +// Configure audit system +builder.Services.AddEFAutoAudit(auditBuilder => +{ + auditBuilder + .WithUserIdProvider() + .EnrichWithProperty("ApplicationName", "MyApplication") + .EnrichWithProperty("MachineName", Environment.MachineName) + .WithStore() // Store audit records in database + .IgnoreEntity(); // Don't audit the audit records themselves +}); + +var app = builder.Build(); + +// Ensure database is created (for development) +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); +} + +app.Run(); +``` + +## Step 5: Create a User ID Provider + +Implement a user ID provider to track who makes changes: + +```csharp +using System.Security.Claims; +using WeihanLi.Common.Services; + +public class HttpContextUserIdProvider : IUserIdProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextUserIdProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetUserId() + { + var user = _httpContextAccessor.HttpContext?.User; + + // Try to get user ID from claims + var userId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? user?.FindFirst("sub")?.Value + ?? user?.Identity?.Name; + + return userId ?? "Anonymous"; + } +} +``` + +## Step 6: Create Your First Service + +Create a service that uses the repository pattern: + +```csharp +using WeihanLi.EntityFramework; + +public class ProductService +{ + private readonly IEFRepository _productRepository; + private readonly IEFUnitOfWork _unitOfWork; + + public ProductService( + IEFRepository productRepository, + IEFUnitOfWork unitOfWork) + { + _productRepository = productRepository; + _unitOfWork = unitOfWork; + } + + public async Task CreateProductAsync(string name, decimal price) + { + var product = new Product + { + Name = name, + Price = price, + IsActive = true + // CreatedAt and UpdatedAt will be set automatically + }; + + return await _productRepository.InsertAsync(product); + } + + public async Task GetProductByIdAsync(int id) + { + return await _productRepository.FindAsync(id); + } + + public async Task> GetActiveProductsAsync() + { + return await _productRepository.GetListAsync( + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + public async Task UpdateProductPriceAsync(int productId, decimal newPrice) + { + var product = await _productRepository.FindAsync(productId); + if (product == null) return false; + + product.Price = newPrice; + // UpdatedAt will be set automatically + + var result = await _productRepository.UpdateAsync(product); + return result > 0; + } + + public async Task DeactivateProductAsync(int productId) + { + // Use bulk update for efficiency + var result = await _productRepository.UpdateAsync( + setters => setters.SetProperty(p => p.IsActive, false), + queryBuilder => queryBuilder.WithPredict(p => p.Id == productId) + ); + + return result > 0; + } + + public async Task> GetProductsPagedAsync(int page, int pageSize) + { + return await _productRepository.GetPagedListAsync( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Name)), + page, + pageSize + ); + } +} +``` + +## Step 7: Create a Controller (ASP.NET Core) + +```csharp +using Microsoft.AspNetCore.Mvc; + +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly ProductService _productService; + + public ProductsController(ProductService productService) + { + _productService = productService; + } + + [HttpGet] + public async Task>> GetProducts( + int page = 1, + int pageSize = 20) + { + var products = await _productService.GetProductsPagedAsync(page, pageSize); + return Ok(products); + } + + [HttpGet("{id}")] + public async Task> GetProduct(int id) + { + var product = await _productService.GetProductByIdAsync(id); + if (product == null) + { + return NotFound(); + } + return Ok(product); + } + + [HttpPost] + public async Task> CreateProduct(CreateProductRequest request) + { + var product = await _productService.CreateProductAsync(request.Name, request.Price); + return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product); + } + + [HttpPut("{id}/price")] + public async Task UpdatePrice(int id, UpdatePriceRequest request) + { + var success = await _productService.UpdateProductPriceAsync(id, request.Price); + if (!success) + { + return NotFound(); + } + return NoContent(); + } +} + +public class CreateProductRequest +{ + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } +} + +public class UpdatePriceRequest +{ + public decimal Price { get; set; } +} +``` + +## What Happens Automatically + +When you run this setup, WeihanLi.EntityFramework will automatically: + +1. **Set timestamps**: `CreatedAt` when inserting, `UpdatedAt` when updating +2. **Track users**: `CreatedBy` and `UpdatedBy` using your user ID provider +3. **Create audit records**: Every change is logged with full details +4. **Apply soft delete filters**: Soft-deleted entities are excluded from queries +5. **Handle transactions**: Unit of Work ensures data consistency + +## Next Steps + +- 📖 Read the [Complete Usage Guide](Usage.md) for advanced features +- 🔍 Explore the [sample project](../samples/WeihanLi.EntityFramework.Sample/) for more examples +- 🛠️ Check out bulk operations, advanced querying, and custom audit stores +- 📋 Review [Release Notes](ReleaseNotes.md) for version-specific information + +## Common Patterns + +### Repository with Unit of Work + +```csharp +public async Task ProcessOrderAsync(CreateOrderRequest request) +{ + var orderRepo = _unitOfWork.GetRepository(); + var productRepo = _unitOfWork.GetRepository(); + + // Create order + var order = await orderRepo.InsertAsync(new Order + { + CustomerId = request.CustomerId, + TotalAmount = request.Items.Sum(i => i.Price * i.Quantity) + }); + + // Add order items and update inventory + foreach (var item in request.Items) + { + await orderRepo.InsertAsync(new OrderItem + { + OrderId = order.Id, + ProductId = item.ProductId, + Quantity = item.Quantity, + Price = item.Price + }); + + // Update product inventory (example) + await productRepo.UpdateAsync( + setters => setters.SetProperty(p => p.Stock, p => p.Stock - item.Quantity), + queryBuilder => queryBuilder.WithPredict(p => p.Id == item.ProductId) + ); + } + + // Commit all changes in a single transaction + await _unitOfWork.CommitAsync(); +} +``` + +This gets you started with the core features. The library handles the complexity while giving you clean, testable code! \ No newline at end of file diff --git a/docs/Usage.md b/docs/Usage.md index 360bc2a..6620dcc 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -1,98 +1,469 @@ -# Usage +# WeihanLi.EntityFramework Usage Guide ## Installation To install the `WeihanLi.EntityFramework` package, you can use the NuGet Package Manager, .NET CLI, or PackageReference in your project file. -**.NET CLI** +### .NET CLI -``` +```bash dotnet add package WeihanLi.EntityFramework ``` -## Configuration +### Package Manager Console + +```powershell +Install-Package WeihanLi.EntityFramework +``` + +### PackageReference + +```xml + +``` + +> **Version Compatibility** +> - For EF 8 and above: use 8.x or above major-version matched versions +> - For EF 7: use 3.x +> - For EF Core 5/6: use 2.x +> - For EF Core 3.x: use 1.5.0 above, and 2.0.0 below +> - For EF Core 2.x: use 1.4.x and below + +## Quick Start + +### Basic Configuration + +Configure the services in your `Startup.cs` or `Program.cs` file: + +```csharp +// Program.cs (minimal hosting model) +var builder = WebApplication.CreateBuilder(args); + +// Add DbContext +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Add WeihanLi.EntityFramework services +builder.Services.AddEFRepository(); +builder.Services.AddEFAutoUpdateInterceptor(); +builder.Services.AddEFAutoAudit(builder => +{ + builder.WithUserIdProvider() + .EnrichWithProperty("MachineName", Environment.MachineName) + .WithStore(); +}); + +var app = builder.Build(); +``` -To configure the `WeihanLi.EntityFramework` package, you need to add the necessary services to your `IServiceCollection` in the `Startup.cs` or `Program.cs` file. +### Complete Configuration Example ```csharp public void ConfigureServices(IServiceCollection services) { - services.AddDbContext(options => - options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + // Configure DbContext with interceptors + services.AddDbContext((provider, options) => + { + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")) + .AddInterceptors( + provider.GetRequiredService(), + provider.GetRequiredService() + ); + }); + // Add repository services services.AddEFRepository(); + + // Add auto-update interceptor for automatic CreatedAt/UpdatedAt handling services.AddEFAutoUpdateInterceptor(); + + // Configure audit system services.AddEFAutoAudit(builder => { builder.WithUserIdProvider() .EnrichWithProperty("MachineName", Environment.MachineName) - .WithStore(); + .EnrichWithProperty("ApplicationName", "MyApp") + .WithStore() + .IgnoreEntity() + .IgnoreProperty("CreatedAt"); }); } ``` -## Examples +``` + +### Custom User ID Provider + +```csharp +public class CustomUserIdProvider : IUserIdProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetUserId() + { + return _httpContextAccessor.HttpContext?.User?.Identity?.Name ?? "System"; + } +} +``` + +## Features and Examples + +### 1. Repository Pattern -### Repository Pattern +The `WeihanLi.EntityFramework` package provides a clean repository pattern implementation for Entity Framework Core. -The `WeihanLi.EntityFramework` package provides a repository pattern implementation for Entity Framework Core. +#### Basic Repository Usage ```csharp -public class MyService +public class ProductService { - private readonly IEFRepository _repository; + private readonly IEFRepository _repository; - public MyService(IEFRepository repository) + public ProductService(IEFRepository repository) { _repository = repository; } - public async Task GetEntityByIdAsync(int id) + // Find by primary key + public async Task GetProductByIdAsync(int id) { return await _repository.FindAsync(id); } - public async Task AddEntityAsync(MyEntity entity) + // Insert single entity + public async Task CreateProductAsync(Product product) + { + return await _repository.InsertAsync(product); + } + + // Insert multiple entities + public async Task CreateProductsAsync(List products) + { + return await _repository.InsertAsync(products); + } + + // Update entity + public async Task UpdateProductAsync(Product product) + { + return await _repository.UpdateAsync(product); + } + + // Update specific columns only + public async Task UpdateProductNameAsync(int id, string newName) + { + return await _repository.UpdateAsync( + new Product { Id = id, Name = newName }, + p => p.Name // Only update Name property + ); + } + + // Update without specific columns + public async Task UpdateProductWithoutTimestampAsync(Product product) + { + return await _repository.UpdateWithoutAsync(product, p => p.UpdatedAt); + } + + // Delete by primary key + public async Task DeleteProductAsync(int id) + { + return await _repository.DeleteAsync(id); + } + + // Delete entity + public async Task DeleteProductAsync(Product product) + { + return await _repository.DeleteAsync(product); + } +} +``` + +#### Advanced Query Operations + +```csharp +public class ProductQueryService +{ + private readonly IEFRepository _repository; + + public ProductQueryService(IEFRepository repository) + { + _repository = repository; + } + + // Get first product with condition + public async Task GetFirstActiveProductAsync() { - await _repository.InsertAsync(entity); + return await _repository.FirstOrDefaultAsync( + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); } - public async Task UpdateEntityAsync(MyEntity entity) + // Get products with paging + public async Task> GetProductsPagedAsync(int page, int size) { - await _repository.UpdateAsync(entity); + return await _repository.GetPagedListAsync( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderByDescending(p => p.CreatedAt)), + page, + size + ); } - public async Task DeleteEntityAsync(int id) + // Get products with custom projection + public async Task> GetProductSummariesAsync() { - await _repository.DeleteAsync(id); + return await _repository.GetResultAsync( + p => new ProductDto + { + Id = p.Id, + Name = p.Name, + Price = p.Price + }, + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Count products + public async Task GetActiveProductCountAsync() + { + return await _repository.CountAsync( + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Check if any products exist + public async Task HasExpensiveProductsAsync() + { + return await _repository.AnyAsync( + queryBuilder => queryBuilder.WithPredict(p => p.Price > 1000) + ); + } + + // Raw query access + public IQueryable GetProductsQuery() + { + return _repository.Query( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Name)) + ); + } +} +``` + +#### Bulk Operations + +```csharp +public class ProductBulkService +{ + private readonly IEFRepository _repository; + + public ProductBulkService(IEFRepository repository) + { + _repository = repository; + } + + // Bulk update using expression + public async Task UpdatePricesAsync(decimal multiplier) + { + return await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.Price, p => p.Price * multiplier), + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Bulk delete with condition + public async Task DeleteInactiveProductsAsync() + { + return await _repository.DeleteAsync( + queryBuilder => queryBuilder.WithPredict(p => !p.IsActive) + ); } } ``` -### Unit of Work Pattern +### 2. Unit of Work Pattern + +The Unit of Work pattern helps manage transactions and ensures data consistency across multiple repository operations. -The `WeihanLi.EntityFramework` package also provides a unit of work pattern implementation for Entity Framework Core. +#### Basic Unit of Work Usage ```csharp -public class MyService +public class OrderService { private readonly IEFUnitOfWork _unitOfWork; - public MyService(IEFUnitOfWork unitOfWork) + public OrderService(IEFUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } - public async Task SaveChangesAsync() + public async Task CreateOrderWithItemsAsync(Order order, List items) { + // Get repositories through unit of work + var orderRepo = _unitOfWork.GetRepository(); + var itemRepo = _unitOfWork.GetRepository(); + + // All operations within the same transaction + var createdOrder = await orderRepo.InsertAsync(order); + + foreach (var item in items) + { + item.OrderId = createdOrder.Id; + await itemRepo.InsertAsync(item); + } + + // Commit all changes in a single transaction await _unitOfWork.CommitAsync(); + + return createdOrder; + } + + public async Task UpdateOrderStatusAsync(int orderId, OrderStatus newStatus) + { + var orderRepo = _unitOfWork.GetRepository(); + var auditRepo = _unitOfWork.GetRepository(); + + var order = await orderRepo.FindAsync(orderId); + if (order != null) + { + var oldStatus = order.Status; + order.Status = newStatus; + + await orderRepo.UpdateAsync(order); + + // Add audit record + await auditRepo.InsertAsync(new OrderAudit + { + OrderId = orderId, + OldStatus = oldStatus, + NewStatus = newStatus, + ChangedAt = DateTime.UtcNow + }); + + await _unitOfWork.CommitAsync(); + } + } + + public async Task TransferProductsBetweenWarehousesAsync( + int fromWarehouseId, + int toWarehouseId, + List transfers) + { + var inventoryRepo = _unitOfWork.GetRepository(); + var transferRepo = _unitOfWork.GetRepository(); + + foreach (var transfer in transfers) + { + // Reduce inventory in source warehouse + await inventoryRepo.UpdateAsync( + setters => setters.SetProperty(i => i.Quantity, i => i.Quantity - transfer.Quantity), + queryBuilder => queryBuilder.WithPredict(i => + i.WarehouseId == fromWarehouseId && i.ProductId == transfer.ProductId) + ); + + // Increase inventory in destination warehouse + await inventoryRepo.UpdateAsync( + setters => setters.SetProperty(i => i.Quantity, i => i.Quantity + transfer.Quantity), + queryBuilder => queryBuilder.WithPredict(i => + i.WarehouseId == toWarehouseId && i.ProductId == transfer.ProductId) + ); + + // Record the transfer + transfer.TransferDate = DateTime.UtcNow; + await transferRepo.InsertAsync(transfer); + } + + await _unitOfWork.CommitAsync(); + } +} +``` + +#### Advanced Unit of Work with Error Handling + +```csharp +public class AdvancedOrderService +{ + private readonly IEFUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public AdvancedOrderService( + IEFUnitOfWork unitOfWork, + ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task ProcessComplexOrderAsync(ComplexOrder complexOrder) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + // Step 1: Create order + var orderRepo = _unitOfWork.GetRepository(); + var order = await orderRepo.InsertAsync(complexOrder.Order); + + // Step 2: Process payment + var paymentRepo = _unitOfWork.GetRepository(); + var payment = await paymentRepo.InsertAsync(new Payment + { + OrderId = order.Id, + Amount = complexOrder.TotalAmount, + PaymentMethod = complexOrder.PaymentMethod + }); + + // Step 3: Update inventory + var inventoryRepo = _unitOfWork.GetRepository(); + foreach (var item in complexOrder.Items) + { + var inventoryItem = await inventoryRepo.FirstOrDefaultAsync( + qb => qb.WithPredict(i => i.ProductId == item.ProductId) + ); + + if (inventoryItem == null || inventoryItem.Quantity < item.Quantity) + { + throw new InvalidOperationException($"Insufficient inventory for product {item.ProductId}"); + } + + inventoryItem.Quantity -= item.Quantity; + await inventoryRepo.UpdateAsync(inventoryItem); + } + + // Step 4: Send notifications (simulated) + await SendOrderConfirmationAsync(order.Id); + + await _unitOfWork.CommitAsync(); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing complex order"); + await _unitOfWork.RollbackAsync(); + return false; + } + } + + private async Task SendOrderConfirmationAsync(int orderId) + { + // Simulate notification service + await Task.Delay(100); + _logger.LogInformation($"Order confirmation sent for order {orderId}"); } } ``` -### Audit +### 3. Audit System -The `WeihanLi.EntityFramework` package provides an audit feature to track changes in your entities. +The audit system automatically tracks changes to your entities, providing a complete history of data modifications. + +#### Setting up Audit DbContext ```csharp public class MyDbContext : AuditDbContext @@ -102,35 +473,988 @@ public class MyDbContext : AuditDbContext { } - public DbSet MyEntities { get; set; } + public DbSet Products { get; set; } + public DbSet Orders { get; set; } + public DbSet AuditRecords { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure your entities + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(200); + }); + } } ``` -### Soft Delete +#### Audit Configuration Options + +```csharp +// Basic audit configuration +services.AddEFAutoAudit(builder => +{ + builder.WithUserIdProvider() + .WithStore(); +}); + +// Advanced audit configuration +services.AddEFAutoAudit(builder => +{ + builder + // Configure user ID provider + .WithUserIdProvider() + + // Add custom properties to audit records + .EnrichWithProperty("ApplicationName", "MyApplication") + .EnrichWithProperty("MachineName", Environment.MachineName) + .EnrichWithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version?.ToString()) + + // Configure storage + .WithStore() // Console output + .WithStore() // File storage + .WithStore() // Database storage + + // Ignore specific entities + .IgnoreEntity() + .IgnoreEntity() + + // Ignore specific properties + .IgnoreProperty(p => p.InternalNotes) + .IgnoreProperty("Password") // Ignore by property name + .IgnoreProperty("CreatedAt") + + // Include unmodified properties (default is false) + .WithUnModifiedProperty() + + // Configure property value formatting + .WithPropertyValueFormatter(); +}); +``` + +#### Custom Audit Store + +```csharp +public class CustomAuditStore : IAuditStore +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public CustomAuditStore(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public async Task Save(ICollection auditEntries) + { + foreach (var entry in auditEntries) + { + _logger.LogInformation("Audit: {Operation} on {EntityType} with Id {EntityId} by {UserId}", + entry.OperationType, + entry.EntityType, + entry.EntityId, + entry.UserId); + + // Custom storage logic (e.g., send to external system) + await SendToExternalAuditSystemAsync(entry); + } + } + + private async Task SendToExternalAuditSystemAsync(AuditEntry entry) + { + // Implement custom audit storage logic + await Task.CompletedTask; + } +} +``` -The `WeihanLi.EntityFramework` package provides a soft delete feature to mark entities as deleted without actually removing them from the database. +#### Using Audit Interceptor Manually ```csharp -public class MyEntity : ISoftDeleteEntityWithDeleted +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); + +// Register the interceptor +services.AddSingleton(); +``` + +#### Querying Audit Records + +```csharp +public class AuditQueryService +{ + private readonly IEFRepository _auditRepository; + + public AuditQueryService(IEFRepository auditRepository) + { + _auditRepository = auditRepository; + } + + public async Task> GetUserActionsAsync(string userId, DateTime from, DateTime to) + { + return await _auditRepository.GetListAsync( + queryBuilder => queryBuilder + .WithPredict(a => a.UserId == userId && a.DateTime >= from && a.DateTime <= to) + .WithOrderBy(q => q.OrderByDescending(a => a.DateTime)) + ); + } + + public async Task> GetEntityHistoryAsync(string entityType, string entityId) + { + return await _auditRepository.GetListAsync( + queryBuilder => queryBuilder + .WithPredict(a => a.EntityType == entityType && a.EntityId == entityId) + .WithOrderBy(q => q.OrderBy(a => a.DateTime)) + ); + } + + public async Task> GetDailyOperationStatsAsync(DateTime date) + { + var records = await _auditRepository.GetListAsync( + queryBuilder => queryBuilder.WithPredict(a => a.DateTime.Date == date.Date) + ); + + return records.GroupBy(r => r.OperationType.ToString()) + .ToDictionary(g => g.Key, g => g.Count()); + } +} +``` + +### 4. Soft Delete + +The soft delete feature marks entities as deleted without actually removing them from the database, allowing for data recovery and audit trails. + +#### Entity Configuration for Soft Delete + +```csharp +// Option 1: Using ISoftDeleteEntityWithDeleted interface +public class Product : ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public bool IsDeleted { get; set; } // Required by interface +} + +// Option 2: Using ISoftDeleteEntity interface (with custom deleted property name) +public class Category : ISoftDeleteEntity +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public bool Deleted { get; set; } // Custom property name +} + +// Option 3: Custom soft delete implementation +public class CustomSoftDeleteEntity { public int Id { get; set; } - public string Name { get; set; } - public bool IsDeleted { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime? DeletedAt { get; set; } + public bool IsActive => DeletedAt == null; } ``` -### Auto Update +#### DbContext Configuration for Soft Delete -The `WeihanLi.EntityFramework` package provides an auto update feature to automatically update certain properties, such as `CreatedAt`, `UpdatedAt`, `CreatedBy`, and `UpdatedBy`. +```csharp +public class SoftDeleteDbContext : DbContext +{ + public SoftDeleteDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Configure global query filter for soft delete + modelBuilder.Entity() + .HasQueryFilter(p => !p.IsDeleted); + + modelBuilder.Entity() + .HasQueryFilter(c => !c.Deleted); + + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Add the soft delete interceptor + optionsBuilder.AddInterceptors(new SoftDeleteInterceptor()); + base.OnConfiguring(optionsBuilder); + } +} +``` + +#### Using Soft Delete with Services ```csharp -public class MyEntity : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +// Configure soft delete in Startup.cs +services.AddSingleton(); +services.AddEFAutoUpdateInterceptor(); + +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); +``` + +#### Soft Delete Operations + +```csharp +public class ProductService +{ + private readonly IEFRepository _repository; + private readonly SoftDeleteDbContext _context; + + public ProductService( + IEFRepository repository, + SoftDeleteDbContext context) + { + _repository = repository; + _context = context; + } + + // Regular delete - will soft delete the entity + public async Task DeleteProductAsync(int productId) + { + return await _repository.DeleteAsync(productId); + } + + // Get active products (soft deleted items are filtered out automatically) + public async Task> GetActiveProductsAsync() + { + return await _repository.GetListAsync(); + } + + // Get all products including soft deleted ones + public async Task> GetAllProductsIncludingDeletedAsync() + { + return await _context.Products + .IgnoreQueryFilters() // Ignore the soft delete filter + .ToListAsync(); + } + + // Get only soft deleted products + public async Task> GetDeletedProductsAsync() + { + return await _context.Products + .IgnoreQueryFilters() + .Where(p => p.IsDeleted) + .ToListAsync(); + } + + // Restore a soft deleted product + public async Task RestoreProductAsync(int productId) + { + var product = await _context.Products + .IgnoreQueryFilters() + .FirstOrDefaultAsync(p => p.Id == productId && p.IsDeleted); + + if (product != null) + { + product.IsDeleted = false; + return await _context.SaveChangesAsync(); + } + + return 0; + } + + // Permanently delete a soft deleted product + public async Task PermanentlyDeleteProductAsync(int productId) + { + var product = await _context.Products + .IgnoreQueryFilters() + .FirstOrDefaultAsync(p => p.Id == productId && p.IsDeleted); + + if (product != null) + { + _context.Products.Remove(product); + return await _context.SaveChangesAsync(); + } + + return 0; + } + + // Bulk soft delete + public async Task BulkDeleteInactiveProductsAsync() + { + return await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.IsDeleted, true), + queryBuilder => queryBuilder.WithPredict(p => !p.IsActive) + ); + } +} +``` + +### 5. Auto Update Features + +The auto-update system automatically manages timestamp and user fields when entities are created or modified. + +#### Entity Interfaces for Auto Update + +```csharp +// For automatic CreatedAt/UpdatedAt timestamps +public class Product : IEntityWithCreatedUpdatedAt +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public DateTimeOffset CreatedAt { get; set; } // Automatically set on insert + public DateTimeOffset UpdatedAt { get; set; } // Automatically updated on modify +} + +// For automatic CreatedBy/UpdatedBy user tracking +public class Order : IEntityWithCreatedUpdatedBy { public int Id { get; set; } - public string Name { get; set; } + public decimal TotalAmount { get; set; } + public string CreatedBy { get; set; } = string.Empty; // Automatically set on insert + public string UpdatedBy { get; set; } = string.Empty; // Automatically updated on modify +} + +// Combined timestamp and user tracking +public class Customer : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + + // Timestamp fields public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } - public string CreatedBy { get; set; } - public string UpdatedBy { get; set; } + + // User tracking fields + public string CreatedBy { get; set; } = string.Empty; + public string UpdatedBy { get; set; } = string.Empty; +} + +// Custom auto-update entity +public class BlogPost +{ + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + + [AutoUpdate(AutoUpdateOperation.Insert)] + public DateTime PublishedAt { get; set; } + + [AutoUpdate(AutoUpdateOperation.Update)] + public DateTime LastModified { get; set; } + + [AutoUpdate(AutoUpdateOperation.Insert | AutoUpdateOperation.Update)] + public string ModifiedBy { get; set; } = string.Empty; +} +``` + +#### Configuration + +```csharp +// Register auto-update services +services.AddSingleton(); +services.AddEFAutoUpdateInterceptor(); + +// Configure DbContext with auto-update interceptor +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); +``` + +#### Custom User ID Provider Examples + +```csharp +// HTTP Context based user provider +public class HttpContextUserIdProvider : IUserIdProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextUserIdProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetUserId() + { + return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? "Anonymous"; + } +} + +// Environment based user provider +public class EnvironmentUserIdProvider : IUserIdProvider +{ + public string GetUserId() + { + return Environment.UserName; + } +} + +// Static user provider for testing +public class StaticUserIdProvider : IUserIdProvider +{ + private readonly string _userId; + + public StaticUserIdProvider(string userId) + { + _userId = userId; + } + + public string GetUserId() => _userId; +} +``` + +#### Auto Update in Action + +```csharp +public class CustomerService +{ + private readonly IEFRepository _repository; + + public CustomerService(IEFRepository repository) + { + _repository = repository; + } + + public async Task CreateCustomerAsync(string name, string email) + { + var customer = new Customer + { + Name = name, + Email = email + // CreatedAt, CreatedBy will be set automatically + }; + + return await _repository.InsertAsync(customer); + } + + public async Task UpdateCustomerEmailAsync(int customerId, string newEmail) + { + var customer = await _repository.FindAsync(customerId); + if (customer != null) + { + customer.Email = newEmail; + // UpdatedAt, UpdatedBy will be set automatically + return await _repository.UpdateAsync(customer); + } + return 0; + } +} +``` + +#### Manual Auto Update Control + +```csharp +public class ManualAutoUpdateService +{ + private readonly MyDbContext _context; + + public ManualAutoUpdateService(MyDbContext context) + { + _context = context; + } + + public async Task UpdateWithCustomTimestampAsync(int customerId, string newName) + { + var customer = await _context.Customers.FindAsync(customerId); + if (customer != null) + { + customer.Name = newName; + customer.UpdatedAt = DateTimeOffset.Now.AddHours(-1); // Custom timestamp + + // Disable auto-update for this save operation + _context.ChangeTracker.Entries() + .Where(e => e.Entity.Id == customerId) + .ForEach(e => e.Property(nameof(Customer.UpdatedAt)).IsModified = false); + + await _context.SaveChangesAsync(); + } + } +} +``` + +### 6. Database Extensions + +The package provides useful database extensions for common operations. + +#### Update Extensions + +```csharp +public class ProductUpdateService +{ + private readonly MyDbContext _context; + + public ProductUpdateService(MyDbContext context) + { + _context = context; + } + + // Update specific columns using DbContext extension + public async Task UpdateProductPricesAsync(decimal priceMultiplier) + { + return await _context.Update( + setters => setters.SetProperty(p => p.Price, p => p.Price * priceMultiplier), + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Update without specific columns + public async Task UpdateProductWithoutTimestampAsync(Product product) + { + return await _context.UpdateWithout(product, p => p.UpdatedAt); + } + + // Bulk update with dictionary + public async Task BulkUpdateProductStatusAsync(List productIds, bool isActive) + { + return await _context.Products + .Where(p => productIds.Contains(p.Id)) + .ExecuteUpdateAsync(setters => setters.SetProperty(p => p.IsActive, isActive)); + } } ``` + +#### Query Extensions + +```csharp +public class ProductQueryExtensions +{ + private readonly MyDbContext _context; + + public ProductQueryExtensions(MyDbContext context) + { + _context = context; + } + + // Get table name + public string GetProductTableName() + { + return _context.GetTableName(); + } + + // Get column name + public string GetProductNameColumnName() + { + return _context.GetColumnName(p => p.Name); + } + + // Check if using relational database + public bool IsUsingRelationalDatabase() + { + return _context.Database.IsRelational(); + } + + // Paged list extension + public async Task> GetProductsPagedAsync(int page, int size) + { + var query = _context.Products.Where(p => p.IsActive); + return await query.ToPagedListAsync(page, size); + } +} +``` + +### 7. Database Functions + +The package provides database-specific functions for enhanced querying capabilities. + +#### JSON Functions (SQL Server) + +```csharp +public class JsonQueryService +{ + private readonly MyDbContext _context; + + public JsonQueryService(MyDbContext context) + { + _context = context; + } + + // Using JSON_VALUE function for SQL Server + public async Task> GetProductsByJsonPropertyAsync(string propertyValue) + { + return await _context.Products + .Where(p => DbFunctions.JsonValue(p.JsonData, "$.PropertyName") == propertyValue) + .ToListAsync(); + } + + // Example with complex JSON queries + public async Task> GetProductsWithJsonFiltersAsync() + { + return await _context.Products + .Where(p => + DbFunctions.JsonValue(p.Metadata, "$.category") == "Electronics" && + DbFunctions.JsonValue(p.Metadata, "$.inStock") == "true") + .ToListAsync(); + } +} + +// Entity with JSON column +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string JsonData { get; set; } = string.Empty; // JSON column + public string Metadata { get; set; } = string.Empty; // Another JSON column +} +``` + +### 8. Repository Generator + +The repository generator automatically creates repository instances for your entities. + +#### Configuration + +```csharp +// Register repository generator +services.AddEFRepositoryGenerator(); + +// Register specific repositories +services.AddEFRepository(); +``` + +#### Using Repository Generator + +```csharp +public class MultiRepositoryService +{ + private readonly IEFRepositoryGenerator _repositoryGenerator; + + public MultiRepositoryService(IEFRepositoryGenerator repositoryGenerator) + { + _repositoryGenerator = repositoryGenerator; + } + + public async Task ProcessOrderAsync(Order order) + { + // Get repositories dynamically + var orderRepo = _repositoryGenerator.GetRepository(); + var customerRepo = _repositoryGenerator.GetRepository(); + var productRepo = _repositoryGenerator.GetRepository(); + + // Validate customer + var customer = await customerRepo.FindAsync(order.CustomerId); + if (customer == null) + { + throw new InvalidOperationException("Customer not found"); + } + + // Validate products + foreach (var item in order.Items) + { + var product = await productRepo.FindAsync(item.ProductId); + if (product == null) + { + throw new InvalidOperationException($"Product {item.ProductId} not found"); + } + } + + // Save order + await orderRepo.InsertAsync(order); + } +} +``` + +### 9. Advanced Query Building + +The package provides a fluent query builder for complex scenarios. + +#### Query Builder Examples + +```csharp +public class AdvancedQueryService +{ + private readonly IEFRepository _repository; + + public AdvancedQueryService(IEFRepository repository) + { + _repository = repository; + } + + // Complex filtering with query builder + public async Task> GetFilteredProductsAsync(ProductFilter filter) + { + return await _repository.GetListAsync(queryBuilder => + { + // Base query + queryBuilder.WithPredict(p => p.IsActive); + + // Conditional filters + if (!string.IsNullOrEmpty(filter.Name)) + { + queryBuilder.WithPredict(p => p.Name.Contains(filter.Name)); + } + + if (filter.MinPrice.HasValue) + { + queryBuilder.WithPredict(p => p.Price >= filter.MinPrice.Value); + } + + if (filter.MaxPrice.HasValue) + { + queryBuilder.WithPredict(p => p.Price <= filter.MaxPrice.Value); + } + + if (filter.CategoryIds?.Any() == true) + { + queryBuilder.WithPredict(p => filter.CategoryIds.Contains(p.CategoryId)); + } + + // Ordering + switch (filter.SortBy?.ToLower()) + { + case "name": + queryBuilder.WithOrderBy(q => filter.SortDescending + ? q.OrderByDescending(p => p.Name) + : q.OrderBy(p => p.Name)); + break; + case "price": + queryBuilder.WithOrderBy(q => filter.SortDescending + ? q.OrderByDescending(p => p.Price) + : q.OrderBy(p => p.Price)); + break; + default: + queryBuilder.WithOrderBy(q => q.OrderByDescending(p => p.CreatedAt)); + break; + } + + // Include related data + if (filter.IncludeCategory) + { + queryBuilder.WithInclude(p => p.Category); + } + + if (filter.IncludeReviews) + { + queryBuilder.WithInclude(p => p.Reviews); + } + }); + } + + // Dynamic query building + public async Task> GetDynamicResultsAsync( + Expression> selector, + List>> predicates, + Expression>? orderBy = null, + bool descending = false) + { + return await _repository.GetResultAsync(selector, queryBuilder => + { + // Apply all predicates + foreach (var predicate in predicates) + { + queryBuilder.WithPredict(predicate); + } + + // Apply ordering + if (orderBy != null) + { + queryBuilder.WithOrderBy(q => descending + ? q.OrderByDescending(orderBy) + : q.OrderBy(orderBy)); + } + }); + } +} + +public class ProductFilter +{ + public string? Name { get; set; } + public decimal? MinPrice { get; set; } + public decimal? MaxPrice { get; set; } + public List? CategoryIds { get; set; } + public string? SortBy { get; set; } + public bool SortDescending { get; set; } + public bool IncludeCategory { get; set; } + public bool IncludeReviews { get; set; } +} +``` + +## Best Practices + +### 1. Performance Considerations + +```csharp +// Use projection for large datasets +var productSummaries = await _repository.GetResultAsync( + p => new { p.Id, p.Name, p.Price }, + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) +); + +// Use paging for large result sets +var pagedProducts = await _repository.GetPagedListAsync( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Name)), + pageNumber: 1, + pageSize: 50 +); + +// Use bulk operations for multiple updates +await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.IsActive, false), + queryBuilder => queryBuilder.WithPredict(p => p.LastSoldDate < DateTime.Now.AddYears(-1)) +); +``` + +### 2. Error Handling + +```csharp +public class SafeProductService +{ + private readonly IEFRepository _repository; + private readonly ILogger _logger; + + public SafeProductService( + IEFRepository repository, + ILogger logger) + { + _repository = repository; + _logger = logger; + } + + public async Task SafeGetProductAsync(int id) + { + try + { + return await _repository.FindAsync(id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving product {ProductId}", id); + return null; + } + } + + public async Task SafeUpdateProductAsync(Product product) + { + try + { + var result = await _repository.UpdateAsync(product); + return result > 0; + } + catch (DbUpdateConcurrencyException ex) + { + _logger.LogWarning(ex, "Concurrency conflict updating product {ProductId}", product.Id); + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating product {ProductId}", product.Id); + return false; + } + } +} +``` + +### 3. Testing + +```csharp +public class ProductServiceTests +{ + private readonly Mock> _mockRepository; + private readonly ProductService _service; + + public ProductServiceTests() + { + _mockRepository = new Mock>(); + _service = new ProductService(_mockRepository.Object); + } + + [Fact] + public async Task GetProductAsync_ShouldReturnProduct_WhenProductExists() + { + // Arrange + var productId = 1; + var expectedProduct = new Product { Id = productId, Name = "Test Product" }; + _mockRepository.Setup(r => r.FindAsync(productId)) + .ReturnsAsync(expectedProduct); + + // Act + var result = await _service.GetProductAsync(productId); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedProduct.Id, result.Id); + Assert.Equal(expectedProduct.Name, result.Name); + } +} +``` + +## Troubleshooting + +### Common Issues + +#### 1. Interceptor Not Working + +**Problem**: Auto-update or audit interceptors are not being triggered. + +**Solution**: Ensure interceptors are properly registered and added to DbContext: + +```csharp +// Correct registration +services.AddEFAutoUpdateInterceptor(); +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); +``` + +#### 2. Soft Delete Filter Not Applied + +**Problem**: Soft deleted entities are still appearing in queries. + +**Solution**: Ensure global query filters are configured: + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.Entity() + .HasQueryFilter(p => !p.IsDeleted); +} +``` + +#### 3. Repository Not Found + +**Problem**: `IEFRepository` cannot be resolved. + +**Solution**: Register repository services: + +```csharp +services.AddEFRepository(); +``` + +#### 4. Audit Records Not Saved + +**Problem**: Audit entries are created but not persisted. + +**Solution**: Ensure audit store is properly configured: + +```csharp +services.AddEFAutoAudit(builder => +{ + builder.WithStore(); // Make sure store is configured +}); +``` + +### Performance Tips + +1. **Use projections** for read-only operations to reduce data transfer +2. **Implement paging** for large datasets +3. **Use bulk operations** for multiple entity updates +4. **Configure appropriate indexes** for frequently queried properties +5. **Consider using `AsNoTracking()`** for read-only queries + +### Migration Notes + +When upgrading between major versions, review the [Release Notes](ReleaseNotes.md) for breaking changes and migration guidance. From 00016fed7c8903ff1c969284c0d82d7291cdeeb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:50:28 +0000 Subject: [PATCH 3/5] Finalize documentation with navigation links and complete structure Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- README.md | 2 ++ docs/AdvancedFeatures.md | 8 +++++++- docs/GettingStarted.md | 1 + docs/Usage.md | 6 ++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3202393..43d376e 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ See Releases/PRs for details 📖 **[Complete Usage Guide](docs/Usage.md)** - Comprehensive documentation with examples for all features +⚡ **[Advanced Features Guide](docs/AdvancedFeatures.md)** - Custom interceptors, performance optimization, and integration patterns + 📋 **[Release Notes](docs/ReleaseNotes.md)** - Version history and breaking changes 🔧 **[Sample Project](samples/WeihanLi.EntityFramework.Sample/)** - Working examples and demonstrations diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md index 80d34b2..fe27b5c 100644 --- a/docs/AdvancedFeatures.md +++ b/docs/AdvancedFeatures.md @@ -826,4 +826,10 @@ public class ProductQueryService } ``` -These advanced patterns help you build robust, scalable applications while leveraging the full power of WeihanLi.EntityFramework. \ No newline at end of file +These advanced patterns help you build robust, scalable applications while leveraging the full power of WeihanLi.EntityFramework. + +## Related Documentation + +- 🚀 [Getting Started Guide](GettingStarted.md) - New to WeihanLi.EntityFramework? Start here +- 📖 [Complete Usage Guide](Usage.md) - Comprehensive documentation with examples for all features +- 📋 [Release Notes](ReleaseNotes.md) - Version history and breaking changes \ No newline at end of file diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 06f9ffd..4fead75 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -375,6 +375,7 @@ When you run this setup, WeihanLi.EntityFramework will automatically: ## Next Steps - 📖 Read the [Complete Usage Guide](Usage.md) for advanced features +- ⚡ Explore [Advanced Features Guide](AdvancedFeatures.md) for custom interceptors and optimization - 🔍 Explore the [sample project](../samples/WeihanLi.EntityFramework.Sample/) for more examples - 🛠️ Check out bulk operations, advanced querying, and custom audit stores - 📋 Review [Release Notes](ReleaseNotes.md) for version-specific information diff --git a/docs/Usage.md b/docs/Usage.md index 6620dcc..10a1ac4 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -1458,3 +1458,9 @@ services.AddEFAutoAudit(builder => ### Migration Notes When upgrading between major versions, review the [Release Notes](ReleaseNotes.md) for breaking changes and migration guidance. + +## Related Documentation + +- 🚀 [Getting Started Guide](GettingStarted.md) - New to WeihanLi.EntityFramework? Start here +- ⚡ [Advanced Features Guide](AdvancedFeatures.md) - Custom interceptors, performance optimization, and integration patterns +- 📋 [Release Notes](ReleaseNotes.md) - Version history and breaking changes From c5e99b8f659901fa3c99c257e4f01c4fd24664c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 02:00:48 +0000 Subject: [PATCH 4/5] Remove discussion reference from README as discussions are not enabled Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 43d376e..771355e 100644 --- a/README.md +++ b/README.md @@ -171,9 +171,7 @@ See Releases/PRs for details 💡 **Questions?** Check out the [Usage Guide](docs/Usage.md) for detailed examples -🐛 **Found a bug?** [Create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps - -💬 **Need help?** Feel free to [start a discussion](https://github.com/WeihanLi/WeihanLi.EntityFramework/discussions) or create an issue +🐛 **Found a bug or need help?** Feel free to [create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps ## Usage From 63777107f14b3a50a90ef69ebf7fbd35c84409bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 02:07:50 +0000 Subject: [PATCH 5/5] Remove non-existent property value formatter from audit documentation Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- docs/Usage.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/Usage.md b/docs/Usage.md index 10a1ac4..63475eb 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -528,10 +528,7 @@ services.AddEFAutoAudit(builder => .IgnoreProperty("CreatedAt") // Include unmodified properties (default is false) - .WithUnModifiedProperty() - - // Configure property value formatting - .WithPropertyValueFormatter(); + .WithUnModifiedProperty(); }); ```