WeihanLi.EntityFramework Usage Guide
August 2, 2025 ยท View on GitHub
Installation
To install the WeihanLi.EntityFramework package, you can use the NuGet Package Manager, .NET CLI, or PackageReference in your project file.
.NET CLI
dotnet add package WeihanLi.EntityFramework
Package Manager Console
Install-Package WeihanLi.EntityFramework
PackageReference
<PackageReference Include="WeihanLi.EntityFramework" Version="8.0.0" />
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:
// Program.cs (minimal hosting model)
var builder = WebApplication.CreateBuilder(args);
// Add DbContext
builder.Services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add WeihanLi.EntityFramework services
builder.Services.AddEFRepository();
builder.Services.AddEFAutoUpdateInterceptor();
builder.Services.AddEFAutoAudit(builder =>
{
builder.WithUserIdProvider<CustomUserIdProvider>()
.EnrichWithProperty("MachineName", Environment.MachineName)
.WithStore<AuditConsoleStore>();
});
var app = builder.Build();
Complete Configuration Example
public void ConfigureServices(IServiceCollection services)
{
// Configure DbContext with interceptors
services.AddDbContext<MyDbContext>((provider, options) =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
.AddInterceptors(
provider.GetRequiredService<AutoUpdateInterceptor>(),
provider.GetRequiredService<AuditInterceptor>()
);
});
// Add repository services
services.AddEFRepository();
// Add auto-update interceptor for automatic CreatedAt/UpdatedAt handling
services.AddEFAutoUpdateInterceptor();
// Configure audit system
services.AddEFAutoAudit(builder =>
{
builder.WithUserIdProvider<CustomUserIdProvider>()
.EnrichWithProperty("MachineName", Environment.MachineName)
.EnrichWithProperty("ApplicationName", "MyApp")
.WithStore<AuditDatabaseStore>()
.IgnoreEntity<AuditRecord>()
.IgnoreProperty("CreatedAt");
});
}
### 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
The WeihanLi.EntityFramework package provides a clean repository pattern implementation for Entity Framework Core.
Basic Repository Usage
public class ProductService
{
private readonly IEFRepository<MyDbContext, Product> _repository;
public ProductService(IEFRepository<MyDbContext, Product> repository)
{
_repository = repository;
}
// Find by primary key
public async Task<Product?> GetProductByIdAsync(int id)
{
return await _repository.FindAsync(id);
}
// Insert single entity
public async Task<Product> CreateProductAsync(Product product)
{
return await _repository.InsertAsync(product);
}
// Insert multiple entities
public async Task<int> CreateProductsAsync(List<Product> products)
{
return await _repository.InsertAsync(products);
}
// Update entity
public async Task<int> UpdateProductAsync(Product product)
{
return await _repository.UpdateAsync(product);
}
// Update specific columns only
public async Task<int> 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<int> UpdateProductWithoutTimestampAsync(Product product)
{
return await _repository.UpdateWithoutAsync(product, p => p.UpdatedAt);
}
// Delete by primary key
public async Task<int> DeleteProductAsync(int id)
{
return await _repository.DeleteAsync(id);
}
// Delete entity
public async Task<int> DeleteProductAsync(Product product)
{
return await _repository.DeleteAsync(product);
}
}
Advanced Query Operations
public class ProductQueryService
{
private readonly IEFRepository<MyDbContext, Product> _repository;
public ProductQueryService(IEFRepository<MyDbContext, Product> repository)
{
_repository = repository;
}
// Get first product with condition
public async Task<Product?> GetFirstActiveProductAsync()
{
return await _repository.FirstOrDefaultAsync(
queryBuilder => queryBuilder.WithPredict(p => p.IsActive)
);
}
// Get products with paging
public async Task<IPagedListModel<Product>> GetProductsPagedAsync(int page, int size)
{
return await _repository.GetPagedListAsync(
queryBuilder => queryBuilder
.WithPredict(p => p.IsActive)
.WithOrderBy(q => q.OrderByDescending(p => p.CreatedAt)),
page,
size
);
}
// Get products with custom projection
public async Task<List<ProductDto>> GetProductSummariesAsync()
{
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<int> GetActiveProductCountAsync()
{
return await _repository.CountAsync(
queryBuilder => queryBuilder.WithPredict(p => p.IsActive)
);
}
// Check if any products exist
public async Task<bool> HasExpensiveProductsAsync()
{
return await _repository.AnyAsync(
queryBuilder => queryBuilder.WithPredict(p => p.Price > 1000)
);
}
// Raw query access
public IQueryable<Product> GetProductsQuery()
{
return _repository.Query(
queryBuilder => queryBuilder
.WithPredict(p => p.IsActive)
.WithOrderBy(q => q.OrderBy(p => p.Name))
);
}
}
Bulk Operations
public class ProductBulkService
{
private readonly IEFRepository<MyDbContext, Product> _repository;
public ProductBulkService(IEFRepository<MyDbContext, Product> repository)
{
_repository = repository;
}
// Bulk update using expression
public async Task<int> 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<int> DeleteInactiveProductsAsync()
{
return await _repository.DeleteAsync(
queryBuilder => queryBuilder.WithPredict(p => !p.IsActive)
);
}
}
2. Unit of Work Pattern
The Unit of Work pattern helps manage transactions and ensures data consistency across multiple repository operations.
Basic Unit of Work Usage
public class OrderService
{
private readonly IEFUnitOfWork<MyDbContext> _unitOfWork;
public OrderService(IEFUnitOfWork<MyDbContext> unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<Order> CreateOrderWithItemsAsync(Order order, List<OrderItem> items)
{
// Get repositories through unit of work
var orderRepo = _unitOfWork.GetRepository<Order>();
var itemRepo = _unitOfWork.GetRepository<OrderItem>();
// 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<Order>();
var auditRepo = _unitOfWork.GetRepository<OrderAudit>();
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<ProductTransfer> transfers)
{
var inventoryRepo = _unitOfWork.GetRepository<InventoryItem>();
var transferRepo = _unitOfWork.GetRepository<ProductTransfer>();
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
public class AdvancedOrderService
{
private readonly IEFUnitOfWork<MyDbContext> _unitOfWork;
private readonly ILogger<AdvancedOrderService> _logger;
public AdvancedOrderService(
IEFUnitOfWork<MyDbContext> unitOfWork,
ILogger<AdvancedOrderService> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task<bool> ProcessComplexOrderAsync(ComplexOrder complexOrder)
{
try
{
await _unitOfWork.BeginTransactionAsync();
// Step 1: Create order
var orderRepo = _unitOfWork.GetRepository<Order>();
var order = await orderRepo.InsertAsync(complexOrder.Order);
// Step 2: Process payment
var paymentRepo = _unitOfWork.GetRepository<Payment>();
var payment = await paymentRepo.InsertAsync(new Payment
{
OrderId = order.Id,
Amount = complexOrder.TotalAmount,
PaymentMethod = complexOrder.PaymentMethod
});
// Step 3: Update inventory
var inventoryRepo = _unitOfWork.GetRepository<InventoryItem>();
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}");
}
}
3. Audit System
The audit system automatically tracks changes to your entities, providing a complete history of data modifications.
Setting up Audit DbContext
public class MyDbContext : AuditDbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options, IServiceProvider serviceProvider)
: base(options, serviceProvider)
{
}
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<AuditRecord> AuditRecords { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure your entities
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
});
}
}
Audit Configuration Options
// Basic audit configuration
services.AddEFAutoAudit(builder =>
{
builder.WithUserIdProvider<CustomUserIdProvider>()
.WithStore<AuditDatabaseStore>();
});
// Advanced audit configuration
services.AddEFAutoAudit(builder =>
{
builder
// Configure user ID provider
.WithUserIdProvider<CustomUserIdProvider>()
// Add custom properties to audit records
.EnrichWithProperty("ApplicationName", "MyApplication")
.EnrichWithProperty("MachineName", Environment.MachineName)
.EnrichWithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version?.ToString())
// Configure storage
.WithStore<AuditConsoleStore>() // Console output
.WithStore<AuditFileStore>() // File storage
.WithStore<AuditDatabaseStore>() // Database storage
// Ignore specific entities
.IgnoreEntity<AuditRecord>()
.IgnoreEntity<SystemLog>()
// Ignore specific properties
.IgnoreProperty<Product>(p => p.InternalNotes)
.IgnoreProperty("Password") // Ignore by property name
.IgnoreProperty("CreatedAt")
// Include unmodified properties (default is false)
.WithUnModifiedProperty();
});
Custom Audit Store
public class CustomAuditStore : IAuditStore
{
private readonly ILogger<CustomAuditStore> _logger;
private readonly IServiceProvider _serviceProvider;
public CustomAuditStore(ILogger<CustomAuditStore> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public async Task Save(ICollection<AuditEntry> 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;
}
}
Using Audit Interceptor Manually
services.AddDbContext<MyDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<AuditInterceptor>());
});
// Register the interceptor
services.AddSingleton<AuditInterceptor>();
Querying Audit Records
public class AuditQueryService
{
private readonly IEFRepository<MyDbContext, AuditRecord> _auditRepository;
public AuditQueryService(IEFRepository<MyDbContext, AuditRecord> auditRepository)
{
_auditRepository = auditRepository;
}
public async Task<List<AuditRecord>> 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<List<AuditRecord>> 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<Dictionary<string, int>> 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
// 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; } = string.Empty;
public DateTime? DeletedAt { get; set; }
public bool IsActive => DeletedAt == null;
}
DbContext Configuration for Soft Delete
public class SoftDeleteDbContext : DbContext
{
public SoftDeleteDbContext(DbContextOptions<SoftDeleteDbContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure global query filter for soft delete
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.IsDeleted);
modelBuilder.Entity<Category>()
.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
// Configure soft delete in Startup.cs
services.AddSingleton<IUserIdProvider, EnvironmentUserIdProvider>();
services.AddEFAutoUpdateInterceptor();
services.AddDbContext<SoftDeleteDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<AutoUpdateInterceptor>());
});
Soft Delete Operations
public class ProductService
{
private readonly IEFRepository<SoftDeleteDbContext, Product> _repository;
private readonly SoftDeleteDbContext _context;
public ProductService(
IEFRepository<SoftDeleteDbContext, Product> repository,
SoftDeleteDbContext context)
{
_repository = repository;
_context = context;
}
// Regular delete - will soft delete the entity
public async Task<int> DeleteProductAsync(int productId)
{
return await _repository.DeleteAsync(productId);
}
// Get active products (soft deleted items are filtered out automatically)
public async Task<List<Product>> GetActiveProductsAsync()
{
return await _repository.GetListAsync();
}
// Get all products including soft deleted ones
public async Task<List<Product>> GetAllProductsIncludingDeletedAsync()
{
return await _context.Products
.IgnoreQueryFilters() // Ignore the soft delete filter
.ToListAsync();
}
// Get only soft deleted products
public async Task<List<Product>> GetDeletedProductsAsync()
{
return await _context.Products
.IgnoreQueryFilters()
.Where(p => p.IsDeleted)
.ToListAsync();
}
// Restore a soft deleted product
public async Task<int> 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<int> 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<int> 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
// 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 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; }
// 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
// Register auto-update services
services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
services.AddEFAutoUpdateInterceptor();
// Configure DbContext with auto-update interceptor
services.AddDbContext<MyDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<AutoUpdateInterceptor>());
});
Custom User ID Provider Examples
// 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
public class CustomerService
{
private readonly IEFRepository<MyDbContext, Customer> _repository;
public CustomerService(IEFRepository<MyDbContext, Customer> repository)
{
_repository = repository;
}
public async Task<Customer> 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<int> 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
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<Customer>()
.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
public class ProductUpdateService
{
private readonly MyDbContext _context;
public ProductUpdateService(MyDbContext context)
{
_context = context;
}
// Update specific columns using DbContext extension
public async Task<int> UpdateProductPricesAsync(decimal priceMultiplier)
{
return await _context.Update<Product>(
setters => setters.SetProperty(p => p.Price, p => p.Price * priceMultiplier),
queryBuilder => queryBuilder.WithPredict(p => p.IsActive)
);
}
// Update without specific columns
public async Task<int> UpdateProductWithoutTimestampAsync(Product product)
{
return await _context.UpdateWithout(product, p => p.UpdatedAt);
}
// Bulk update with dictionary
public async Task<int> BulkUpdateProductStatusAsync(List<int> productIds, bool isActive)
{
return await _context.Products
.Where(p => productIds.Contains(p.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(p => p.IsActive, isActive));
}
}
Query Extensions
public class ProductQueryExtensions
{
private readonly MyDbContext _context;
public ProductQueryExtensions(MyDbContext context)
{
_context = context;
}
// Get table name
public string GetProductTableName()
{
return _context.GetTableName<Product>();
}
// Get column name
public string GetProductNameColumnName()
{
return _context.GetColumnName<Product>(p => p.Name);
}
// Check if using relational database
public bool IsUsingRelationalDatabase()
{
return _context.Database.IsRelational();
}
// Paged list extension
public async Task<IPagedListModel<Product>> 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)
public class JsonQueryService
{
private readonly MyDbContext _context;
public JsonQueryService(MyDbContext context)
{
_context = context;
}
// Using JSON_VALUE function for SQL Server
public async Task<List<Product>> GetProductsByJsonPropertyAsync(string propertyValue)
{
return await _context.Products
.Where(p => DbFunctions.JsonValue(p.JsonData, "$.PropertyName") == propertyValue)
.ToListAsync();
}
// Example with complex JSON queries
public async Task<List<Product>> 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
// Register repository generator
services.AddEFRepositoryGenerator();
// Register specific repositories
services.AddEFRepository<MyDbContext>();
Using Repository Generator
public class MultiRepositoryService
{
private readonly IEFRepositoryGenerator<MyDbContext> _repositoryGenerator;
public MultiRepositoryService(IEFRepositoryGenerator<MyDbContext> repositoryGenerator)
{
_repositoryGenerator = repositoryGenerator;
}
public async Task ProcessOrderAsync(Order order)
{
// Get repositories dynamically
var orderRepo = _repositoryGenerator.GetRepository<Order>();
var customerRepo = _repositoryGenerator.GetRepository<Customer>();
var productRepo = _repositoryGenerator.GetRepository<Product>();
// 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
public class AdvancedQueryService
{
private readonly IEFRepository<MyDbContext, Product> _repository;
public AdvancedQueryService(IEFRepository<MyDbContext, Product> repository)
{
_repository = repository;
}
// Complex filtering with query builder
public async Task<List<Product>> 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<List<TResult>> GetDynamicResultsAsync<TResult>(
Expression<Func<Product, TResult>> selector,
List<Expression<Func<Product, bool>>> predicates,
Expression<Func<Product, object>>? 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<int>? 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
// 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
public class SafeProductService
{
private readonly IEFRepository<MyDbContext, Product> _repository;
private readonly ILogger<SafeProductService> _logger;
public SafeProductService(
IEFRepository<MyDbContext, Product> repository,
ILogger<SafeProductService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<Product?> 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<bool> 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
public class ProductServiceTests
{
private readonly Mock<IEFRepository<TestDbContext, Product>> _mockRepository;
private readonly ProductService _service;
public ProductServiceTests()
{
_mockRepository = new Mock<IEFRepository<TestDbContext, Product>>();
_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:
// Correct registration
services.AddEFAutoUpdateInterceptor();
services.AddDbContext<MyDbContext>((provider, options) =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(provider.GetRequiredService<AutoUpdateInterceptor>());
});
2. Soft Delete Filter Not Applied
Problem: Soft deleted entities are still appearing in queries.
Solution: Ensure global query filters are configured:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.IsDeleted);
}
3. Repository Not Found
Problem: IEFRepository<TDbContext, TEntity> cannot be resolved.
Solution: Register repository services:
services.AddEFRepository();
4. Audit Records Not Saved
Problem: Audit entries are created but not persisted.
Solution: Ensure audit store is properly configured:
services.AddEFAutoAudit(builder =>
{
builder.WithStore<AuditDatabaseStore>(); // Make sure store is configured
});
Performance Tips
- Use projections for read-only operations to reduce data transfer
- Implement paging for large datasets
- Use bulk operations for multiple entity updates
- Configure appropriate indexes for frequently queried properties
- Consider using
AsNoTracking()for read-only queries
Migration Notes
When upgrading between major versions, review the Release Notes for breaking changes and migration guidance.
Related Documentation
- ๐ Getting Started Guide - New to WeihanLi.EntityFramework? Start here
- โก Advanced Features Guide - Custom interceptors, performance optimization, and integration patterns
- ๐ Release Notes - Version history and breaking changes