diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index abeeaab..4085678 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,6 +1,6 @@
{
"name": "CodeSpace",
- "image": "mcr.microsoft.com/dotnet/sdk:8.0",
+ "image": "mcr.microsoft.com/dotnet/sdk:10.0-preview",
// Install needed extensions
"extensions": [
"ms-dotnettools.csharp",
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 7da0570..4d14067 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -12,7 +12,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: dotnet info
run: dotnet --info
- name: build
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
index fcc9930..e0fae1d 100644
--- a/.github/workflows/dotnet-format.yml
+++ b/.github/workflows/dotnet-format.yml
@@ -10,7 +10,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: build
run: dotnet build
- name: format
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f86d080..6fbea4d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,7 +12,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Build
shell: pwsh
run: .\build.ps1 --stable=true
diff --git a/Directory.Build.props b/Directory.Build.props
index 6cb96de..4ef292b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,13 +2,12 @@
- net9.0
+ net10.0
preview
enable
enable
- 1.0.75
- 9.0.3
-
+ 1.0.84
+ 10.0.0
true
true
true
diff --git a/README.md b/README.md
index 00aa2ba..771355e 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,33 +120,59 @@ 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
+
+β‘ **[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
## 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 or need help?** Feel free to [create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps
+
+## Usage
+
+For detailed usage instructions, please refer to the [Usage Documentation](docs/Usage.md).
diff --git a/WeihanLi.EntityFramework.sln b/WeihanLi.EntityFramework.sln
deleted file mode 100644
index adfa9ac..0000000
--- a/WeihanLi.EntityFramework.sln
+++ /dev/null
@@ -1,91 +0,0 @@
-ο»Ώ
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29409.12
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeihanLi.EntityFramework", "src\WeihanLi.EntityFramework\WeihanLi.EntityFramework.csproj", "{0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40B7673F-5ACD-42E4-AE04-6C3AEEBC8546}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{46AED92E-94FC-409A-9CFB-C9CD4E59717D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeihanLi.EntityFramework.Sample", "samples\WeihanLi.EntityFramework.Sample\WeihanLi.EntityFramework.Sample.csproj", "{0E013C55-6D52-4B68-B188-460B97DB1E48}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeihanLi.EntityFramework.Test", "test\WeihanLi.EntityFramework.Test\WeihanLi.EntityFramework.Test.csproj", "{9DF6D464-63A7-487C-9763-4A212ADDFC4B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A1756F80-F25E-4ADC-A079-048FFCA5DBA6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeihanLi.EntityFramework.SourceGenerator", "src\WeihanLi.EntityFramework.SourceGenerator\WeihanLi.EntityFramework.SourceGenerator.csproj", "{010A98BE-1871-4E6F-8129-7A1C44C73423}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Debug|x64.Build.0 = Debug|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Debug|x86.Build.0 = Debug|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Release|Any CPU.Build.0 = Release|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Release|x64.ActiveCfg = Release|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Release|x64.Build.0 = Release|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Release|x86.ActiveCfg = Release|Any CPU
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A}.Release|x86.Build.0 = Release|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Debug|x64.Build.0 = Debug|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Debug|x86.Build.0 = Debug|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Release|Any CPU.Build.0 = Release|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Release|x64.ActiveCfg = Release|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Release|x64.Build.0 = Release|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Release|x86.ActiveCfg = Release|Any CPU
- {0E013C55-6D52-4B68-B188-460B97DB1E48}.Release|x86.Build.0 = Release|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Debug|x64.Build.0 = Debug|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Debug|x86.Build.0 = Debug|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Release|x64.ActiveCfg = Release|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Release|x64.Build.0 = Release|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Release|x86.ActiveCfg = Release|Any CPU
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B}.Release|x86.Build.0 = Release|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Debug|x64.ActiveCfg = Debug|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Debug|x64.Build.0 = Debug|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Debug|x86.ActiveCfg = Debug|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Debug|x86.Build.0 = Debug|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Release|Any CPU.Build.0 = Release|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Release|x64.ActiveCfg = Release|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Release|x64.Build.0 = Release|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Release|x86.ActiveCfg = Release|Any CPU
- {010A98BE-1871-4E6F-8129-7A1C44C73423}.Release|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {0BB1C3F3-50C6-4924-894C-0A9A7BA4E54A} = {40B7673F-5ACD-42E4-AE04-6C3AEEBC8546}
- {0E013C55-6D52-4B68-B188-460B97DB1E48} = {46AED92E-94FC-409A-9CFB-C9CD4E59717D}
- {9DF6D464-63A7-487C-9763-4A212ADDFC4B} = {A1756F80-F25E-4ADC-A079-048FFCA5DBA6}
- {010A98BE-1871-4E6F-8129-7A1C44C73423} = {40B7673F-5ACD-42E4-AE04-6C3AEEBC8546}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {201A8B4F-A16D-44B7-BB29-F9B9CFC2A467}
- EndGlobalSection
-EndGlobal
diff --git a/WeihanLi.EntityFramework.slnx b/WeihanLi.EntityFramework.slnx
new file mode 100644
index 0000000..43a0a82
--- /dev/null
+++ b/WeihanLi.EntityFramework.slnx
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index f7edf08..cd3bcd7 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -11,7 +11,7 @@ steps:
displayName: 'Use .NET sdk'
inputs:
packageType: sdk
- version: 9.0.x
+ version: 10.0.x
includePreviewVersions: true
- script: dotnet --info
diff --git a/build.ps1 b/build.ps1
index fe9674a..2f32d77 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,7 +1,7 @@
[string]$SCRIPT = '.\build\build.cs'
# Install dotnet tool
-dotnet tool update --global dotnet-execute
+dotnet tool install --global dotnet-execute
Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN
diff --git a/build.sh b/build.sh
index ed717f1..8d287c2 100644
--- a/build.sh
+++ b/build.sh
@@ -2,7 +2,7 @@
SCRIPT='./build/build.cs'
# Install tool
-dotnet tool update --global dotnet-execute
+dotnet tool install --global dotnet-execute
export PATH="$PATH:$HOME/.dotnet/tools"
echo "dotnet-exec $SCRIPT --args=$@"
diff --git a/build/build.cs b/build/build.cs
index a54aab1..6cdc849 100644
--- a/build/build.cs
+++ b/build/build.cs
@@ -1,115 +1,14 @@
-var target = CommandLineParser.Val("target", args, "Default");
-var apiKey = CommandLineParser.Val("apiKey", args);
-var stable = CommandLineParser.BooleanVal("stable", args);
-var noPush = CommandLineParser.BooleanVal("noPush", args);
-var branchName = EnvHelper.Val("BUILD_SOURCEBRANCHNAME", "local");
-
-var solutionPath = "./WeihanLi.EntityFramework.sln";
+var solutionPath = "./WeihanLi.EntityFramework.slnx";
string[] srcProjects = [
"./src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj"
];
string[] testProjects = [ "./test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj" ];
-await new BuildProcessBuilder()
- .WithSetup(() =>
- {
- // cleanup artifacts
- if (Directory.Exists("./artifacts/packages"))
- Directory.Delete("./artifacts/packages", true);
-
- // args
- Console.WriteLine("Arguments");
- Console.WriteLine($" {args.StringJoin(" ")}");
- })
- .WithTaskExecuting(task => Console.WriteLine($@"===== Task {task.Name} {task.Description} executing ======"))
- .WithTaskExecuted(task => Console.WriteLine($@"===== Task {task.Name} {task.Description} executed ======"))
- .WithTask("hello", b => b.WithExecution(() => Console.WriteLine("Hello dotnet-exec build")))
- .WithTask("build", b =>
- {
- b.WithDescription("dotnet build")
- .WithExecution(() => ExecuteCommandAsync($"dotnet build {solutionPath}"))
- ;
- })
- .WithTask("test", b =>
+await DotNetPackageBuildProcess
+ .Create(options =>
{
- b.WithDescription("dotnet test")
- .WithDependency("build")
- .WithExecution(async () =>
- {
- foreach (var project in testProjects)
- {
- await ExecuteCommandAsync($"dotnet test --collect:\"XPlat Code Coverage;Format=cobertura,opencover;ExcludeByAttribute=ExcludeFromCodeCoverage,Obsolete,GeneratedCode,CompilerGeneratedAttribute\" {project}");
- }
- })
- ;
+ options.SolutionPath = solutionPath;
+ options.SrcProjects = srcProjects;
+ options.TestProjects = testProjects;
})
- .WithTask("pack", b => b.WithDescription("dotnet pack")
- .WithDependency("build")
- .WithExecution(async () =>
- {
- if (stable || branchName == "master")
- {
- foreach (var project in srcProjects)
- {
- await ExecuteCommandAsync($"dotnet pack {project} -o ./artifacts/packages");
- }
- }
- else
- {
- var suffix = $"preview-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
- foreach (var project in srcProjects)
- {
- await ExecuteCommandAsync($"dotnet pack {project} -o ./artifacts/packages --version-suffix {suffix}");
- }
- }
-
- if (noPush)
- {
- Console.WriteLine("Skip push there's noPush specified");
- return;
- }
-
- if (string.IsNullOrEmpty(apiKey))
- {
- // try to get apiKey from environment variable
- apiKey = Environment.GetEnvironmentVariable("NuGet__ApiKey");
-
- if (string.IsNullOrEmpty(apiKey))
- {
- Console.WriteLine("Skip push since there's no apiKey found");
- return;
- }
- }
-
- if (branchName != "master" && branchName != "preview")
- {
- Console.WriteLine($"Skip push since branch name {branchName} not support push packages");
- return;
- }
-
- // push nuget packages
- foreach (var file in Directory.GetFiles("./artifacts/packages/", "*.nupkg"))
- {
- await ExecuteCommandAsync($"dotnet nuget push {file} -k {apiKey} --skip-duplicate", [new("$NuGet__ApiKey", apiKey)]);
- }
- }))
- .WithTask("Default", b => b.WithDependency("hello").WithDependency("pack"))
- .Build()
- .ExecuteAsync(target, ApplicationHelper.ExitToken);
-
-async Task ExecuteCommandAsync(string commandText, KeyValuePair[]? replacements = null)
-{
- var commandTextWithReplacements = commandText;
- if (replacements is { Length: > 0})
- {
- foreach (var item in replacements)
- {
- commandTextWithReplacements = commandTextWithReplacements.Replace(item.Value, item.Key);
- }
- }
- Console.WriteLine($"Executing command: \n {commandTextWithReplacements}");
- Console.WriteLine();
- var result = await CommandExecutor.ExecuteCommandAndOutputAsync(commandText);
- result.EnsureSuccessExitCode();
- Console.WriteLine();
-}
+ .ExecuteAsync(args);
diff --git a/build/version.props b/build/version.props
index 94ca4d1..8cd2d0d 100644
--- a/build/version.props
+++ b/build/version.props
@@ -1,7 +1,7 @@
- 9
- 3
+ 10
+ 0
0
0
$(VersionMajor).$(VersionMinor).$(VersionPatch)
diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md
new file mode 100644
index 0000000..fe27b5c
--- /dev/null
+++ b/docs/AdvancedFeatures.md
@@ -0,0 +1,835 @@
+# 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.
+
+## 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
new file mode 100644
index 0000000..4fead75
--- /dev/null
+++ b/docs/GettingStarted.md
@@ -0,0 +1,423 @@
+# 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 [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
+
+## 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
new file mode 100644
index 0000000..63475eb
--- /dev/null
+++ b/docs/Usage.md
@@ -0,0 +1,1463 @@
+# 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
+
+```bash
+dotnet add package WeihanLi.EntityFramework
+```
+
+### 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();
+```
+
+### Complete Configuration Example
+
+```csharp
+public void ConfigureServices(IServiceCollection services)
+{
+ // 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)
+ .EnrichWithProperty("ApplicationName", "MyApp")
+ .WithStore()
+ .IgnoreEntity()
+ .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
+
+```csharp
+public class ProductService
+{
+ private readonly IEFRepository _repository;
+
+ public ProductService(IEFRepository repository)
+ {
+ _repository = repository;
+ }
+
+ // Find by primary key
+ public async Task GetProductByIdAsync(int id)
+ {
+ return await _repository.FindAsync(id);
+ }
+
+ // 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()
+ {
+ return await _repository.FirstOrDefaultAsync(
+ queryBuilder => queryBuilder.WithPredict(p => p.IsActive)
+ );
+ }
+
+ // Get products with paging
+ public async Task> 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> 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 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)
+ );
+ }
+}
+```
+
+### 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
+
+```csharp
+public class OrderService
+{
+ private readonly IEFUnitOfWork _unitOfWork;
+
+ public OrderService(IEFUnitOfWork unitOfWork)
+ {
+ _unitOfWork = unitOfWork;
+ }
+
+ 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}");
+ }
+}
+```
+
+### 3. Audit System
+
+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
+{
+ public MyDbContext(DbContextOptions options, IServiceProvider serviceProvider)
+ : base(options, serviceProvider)
+ {
+ }
+
+ 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);
+ });
+ }
+}
+```
+
+#### 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();
+});
+```
+
+#### 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;
+ }
+}
+```
+
+#### Using Audit Interceptor Manually
+
+```csharp
+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; } = string.Empty;
+ public DateTime? DeletedAt { get; set; }
+ public bool IsActive => DeletedAt == null;
+}
+```
+
+#### DbContext Configuration for Soft Delete
+
+```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
+// 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 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
+
+```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