From 92bb337ffc3aa89920d10759e1f8aafc08b1e2e2 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 21 Mar 2025 21:49:33 +0800 Subject: [PATCH 01/32] feat: update dotnet 10 --- Directory.Build.props | 6 +++--- src/WeihanLi.EntityFramework/EFRepository.cs | 4 ++-- src/WeihanLi.EntityFramework/IEFRepository.cs | 4 ++-- .../WeihanLi.EntityFramework.csproj | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6cb96de..a55e3e2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,12 +2,12 @@ - net9.0 + net10.0 preview enable enable - 1.0.75 - 9.0.3 + 1.0.76 + 10.0.0-preview.2.25163.8 true true diff --git a/src/WeihanLi.EntityFramework/EFRepository.cs b/src/WeihanLi.EntityFramework/EFRepository.cs index 6a5266c..b671332 100644 --- a/src/WeihanLi.EntityFramework/EFRepository.cs +++ b/src/WeihanLi.EntityFramework/EFRepository.cs @@ -260,7 +260,7 @@ public virtual int Update(Expression> whereExpression, IDict return DbContext.SaveChanges(); } - public int Update(Expression, SetPropertyCalls>> setExpression, + public int Update(Action> setExpression, Action>? queryBuilderAction = null) { var queryBuilder = new EFRepositoryQueryBuilder(DbContext.Set()); @@ -268,7 +268,7 @@ public int Update(Expression, SetPropertyCalls UpdateAsync(Expression, SetPropertyCalls>> setExpression, + public Task UpdateAsync(Action> setExpression, Action>? queryBuilderAction = null, CancellationToken cancellationToken = default) { diff --git a/src/WeihanLi.EntityFramework/IEFRepository.cs b/src/WeihanLi.EntityFramework/IEFRepository.cs index 38ace54..a00b098 100644 --- a/src/WeihanLi.EntityFramework/IEFRepository.cs +++ b/src/WeihanLi.EntityFramework/IEFRepository.cs @@ -32,10 +32,10 @@ public interface IEFRepository : IRepository /// the entity founded, if not found, null returned ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken); - int Update(Expression, SetPropertyCalls>> setExpression, Action>? queryBuilderAction = null); + int Update(Action> setExpression, Action>? queryBuilderAction = null); Task UpdateAsync( - Expression, SetPropertyCalls>> setExpression, + Action> setExpression, Action>? queryBuilderAction = null, CancellationToken cancellationToken = default); diff --git a/src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj b/src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj index 3024a2b..2e852fc 100644 --- a/src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj +++ b/src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj @@ -1,6 +1,6 @@ ο»Ώ - net8.0 + net10.0 WeihanLi.EntityFramework WeihanLi.EntityFramework https://avatars3.githubusercontent.com/u/7604648 From 5456cbb56bbae6429a67d54c231ef2ff848eaaef Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 21 Mar 2025 21:51:06 +0800 Subject: [PATCH 02/32] feat: update CI dotnet sdk to dotnet 10 --- .devcontainer/devcontainer.json | 2 +- .github/workflows/default.yml | 2 +- .github/workflows/dotnet-format.yml | 2 +- .github/workflows/release.yml | 2 +- azure-pipelines.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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/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 From 9958636280ae86608e70cdbc12b978e806a17e4b Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 21 Mar 2025 21:57:43 +0800 Subject: [PATCH 03/32] refactor: fix generator reference and warnings --- .../DbContextInterceptorSamples.cs | 4 ++-- .../WeihanLi.EntityFramework.Sample.csproj | 2 +- .../EFExtensionsGenerator.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/WeihanLi.EntityFramework.Sample/DbContextInterceptorSamples.cs b/samples/WeihanLi.EntityFramework.Sample/DbContextInterceptorSamples.cs index a12ba9a..7875380 100644 --- a/samples/WeihanLi.EntityFramework.Sample/DbContextInterceptorSamples.cs +++ b/samples/WeihanLi.EntityFramework.Sample/DbContextInterceptorSamples.cs @@ -48,13 +48,13 @@ private static async Task InterceptorTest2() file sealed class FileTestDbContext(DbContextOptions options) : DbContext(options) { - public DbSet Entities { get; set; } + public DbSet Entities { get; set; } = null!; } file sealed class TestEntity { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } } file sealed class SavingInterceptor : SaveChangesInterceptor diff --git a/samples/WeihanLi.EntityFramework.Sample/WeihanLi.EntityFramework.Sample.csproj b/samples/WeihanLi.EntityFramework.Sample/WeihanLi.EntityFramework.Sample.csproj index de091d0..f09bff1 100644 --- a/samples/WeihanLi.EntityFramework.Sample/WeihanLi.EntityFramework.Sample.csproj +++ b/samples/WeihanLi.EntityFramework.Sample/WeihanLi.EntityFramework.Sample.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/WeihanLi.EntityFramework.SourceGenerator/EFExtensionsGenerator.cs b/src/WeihanLi.EntityFramework.SourceGenerator/EFExtensionsGenerator.cs index acb4a9e..e9b9647 100644 --- a/src/WeihanLi.EntityFramework.SourceGenerator/EFExtensionsGenerator.cs +++ b/src/WeihanLi.EntityFramework.SourceGenerator/EFExtensionsGenerator.cs @@ -13,7 +13,7 @@ public sealed class EFExtensionsGenerator : IIncrementalGenerator namespace WeihanLi.EntityFramework { [AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class EFExtensionsAttribute; + public sealed class EFExtensionsAttribute; } """; @@ -39,7 +39,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var dbContextName = dbContextDeclaration!.Identifier.Text; var repositoryNamespace = dbContextDeclaration.Parent!.GetNamespace(); var generatedCode = GenerateRepositoryCode(dbContextName, repositoryNamespace, templates); - spc.AddSource($"{dbContextName}Repository.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); + // spc.AddSource($"{dbContextName}Repository.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); }); } From 8bfb585e0648fcd2baee3d84471594bddf17bc41 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 21 Mar 2025 22:30:07 +0800 Subject: [PATCH 04/32] refactor: use xunit v3 --- test/WeihanLi.EntityFramework.Test/EFRepositoryTest.cs | 3 +-- test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs | 5 ++--- .../WeihanLi.EntityFramework.Test.csproj | 7 +++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/WeihanLi.EntityFramework.Test/EFRepositoryTest.cs b/test/WeihanLi.EntityFramework.Test/EFRepositoryTest.cs index a494e45..94f1906 100644 --- a/test/WeihanLi.EntityFramework.Test/EFRepositoryTest.cs +++ b/test/WeihanLi.EntityFramework.Test/EFRepositoryTest.cs @@ -3,7 +3,6 @@ using WeihanLi.Common; using WeihanLi.Common.Helpers; using Xunit; -using Xunit.Abstractions; namespace WeihanLi.EntityFramework.Test; @@ -40,7 +39,7 @@ public void InsertTest() [Fact] public async Task InsertAsyncTest() { - using (await _lock.LockAsync()) + using (await _lock.LockAsync(cancellationToken: TestContext.Current.CancellationToken)) { await DependencyResolver.TryInvokeAsync>(async repo => { diff --git a/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs b/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs index 59e5f03..140f6e0 100644 --- a/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs +++ b/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using WeihanLi.Common.Data; using Xunit; -using Xunit.Abstractions; namespace WeihanLi.EntityFramework.Test; @@ -193,13 +192,13 @@ await repo.InsertAsync(new TestEntity() await uow.CommitAsync(); - var committedCount = await repository.CountAsync(); + var committedCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(committedCount, beforeCount + 1); entity = await repository.DbContext.FindAsync(3); Assert.Equal(new string('3', 6), entity.Name); - entity = await repository.DbContext.FindAsync(new object[] { 4 }, CancellationToken.None); + entity = await repository.DbContext.FindAsync(new object[] { 4 }, cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(new string('4', 6), entity.Name); Assert.Equal(1, await Repository.DeleteAsync(1)); diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index e92d721..f710371 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -3,14 +3,17 @@ true false + exe disable + true + true - - + + From 08e6d618e57075cf949456e2ef71ae85abb883b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 21 Mar 2025 14:30:53 +0000 Subject: [PATCH 05/32] Automated dotnet-format update from commit 8bfb585e0648fcd2baee3d84471594bddf17bc41 on refs/heads/dev --- .../EFUnitOfWorkTest.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs b/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs index 140f6e0..a50aa26 100644 --- a/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs +++ b/test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs @@ -25,7 +25,7 @@ public void TransactionTest() IServiceScope scope1 = null; try { - _semaphore.Wait(); + _semaphore.Wait(TestContext.Current.CancellationToken); scope1 = Services.CreateScope(); var repository = scope1.ServiceProvider.GetRequiredService>(); @@ -115,7 +115,7 @@ public async Task TransactionAsyncTest() IServiceScope scope1 = null; try { - await _semaphore.WaitAsync(); + await _semaphore.WaitAsync(TestContext.Current.CancellationToken); scope1 = Services.CreateScope(); var repository = scope1.ServiceProvider.GetRequiredService>() .GetRepository(); @@ -150,7 +150,7 @@ await repository.InsertAsync(new[] CreatedAt = DateTime.UtcNow, Name = "xss3", } - }); + }, TestContext.Current.CancellationToken); using (var scope = Services.CreateScope()) { @@ -159,10 +159,10 @@ await repo.InsertAsync(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "xxxxxx" - }); + }, TestContext.Current.CancellationToken); } - var beforeCount = await repository.CountAsync(); + var beforeCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); using var uow = repository.GetUnitOfWork(); uow.DbContext.Update(new TestEntity() { @@ -187,15 +187,15 @@ await repo.InsertAsync(new TestEntity() Name = "xyy1", }); - var beforeCommitCount = await repository.CountAsync(); + var beforeCommitCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(beforeCount, beforeCommitCount); - await uow.CommitAsync(); + await uow.CommitAsync(TestContext.Current.CancellationToken); var committedCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(committedCount, beforeCount + 1); - entity = await repository.DbContext.FindAsync(3); + entity = await repository.DbContext.FindAsync(new object[] { 3 }, TestContext.Current.CancellationToken); Assert.Equal(new string('3', 6), entity.Name); entity = await repository.DbContext.FindAsync(new object[] { 4 }, cancellationToken: TestContext.Current.CancellationToken); @@ -218,7 +218,7 @@ public void RollbackTest() { try { - _semaphore.Wait(); + _semaphore.Wait(TestContext.Current.CancellationToken); using (var scope = Services.CreateScope()) { @@ -257,7 +257,7 @@ public async Task RollbackAsyncTest() { try { - await _semaphore.WaitAsync(); + await _semaphore.WaitAsync(TestContext.Current.CancellationToken); using (var scope = Services.CreateScope()) { @@ -267,7 +267,7 @@ public async Task RollbackAsyncTest() .Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "saa" }); unitOfWork.DbContext.TestEntities .Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "saa" }); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(TestContext.Current.CancellationToken); } using (var scope = Services.CreateScope()) { @@ -279,7 +279,7 @@ public async Task RollbackAsyncTest() unitOfWork.DbContext.TestEntities.Add(new TestEntity() { Name = "xxx", CreatedAt = DateTime.UtcNow }); unitOfWork.DbContext.TestEntities.Add(new TestEntity() { Name = "xxx", CreatedAt = DateTime.UtcNow }); - await unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(TestContext.Current.CancellationToken); var count2 = unitOfWork.DbContext.TestEntities.Count(); Assert.Equal(count, count2); @@ -301,7 +301,7 @@ public void HybridTest() } try { - _semaphore.Wait(); + _semaphore.Wait(TestContext.Current.CancellationToken); using (var scope = Services.CreateScope()) { From fb9e01ac476f05ed36f50d7e9e95b8da1dc89ae0 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 21 Mar 2025 22:58:46 +0800 Subject: [PATCH 06/32] Update version.props --- build/version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From b726768c1456b7440d043942f779e5651b10c750 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Mon, 24 Mar 2025 10:24:25 +0800 Subject: [PATCH 07/32] Update WeihanLi.EntityFramework.Test.csproj --- .../WeihanLi.EntityFramework.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index f710371..737d3c4 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -5,7 +5,7 @@ false exe disable - true + true From 0f3a26375450cf418450ef81a413c11c2ce89567 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Wed, 2 Apr 2025 22:47:38 +0800 Subject: [PATCH 08/32] Add usage documentation Related to #74 --- README.md | 4 ++ docs/Usage.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 docs/Usage.md diff --git a/README.md b/README.md index 00aa2ba..ad5fd8b 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,7 @@ See Releases/PRs for details ## Support Feel free to try and [create issues](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) if you have any questions or integration issues + +## Usage + +For detailed usage instructions, please refer to the [Usage Documentation](docs/Usage.md). diff --git a/docs/Usage.md b/docs/Usage.md new file mode 100644 index 0000000..360bc2a --- /dev/null +++ b/docs/Usage.md @@ -0,0 +1,136 @@ +# Usage + +## 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 +``` + +## Configuration + +To configure the `WeihanLi.EntityFramework` package, you need to add the necessary services to your `IServiceCollection` in the `Startup.cs` or `Program.cs` file. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + + services.AddEFRepository(); + services.AddEFAutoUpdateInterceptor(); + services.AddEFAutoAudit(builder => + { + builder.WithUserIdProvider() + .EnrichWithProperty("MachineName", Environment.MachineName) + .WithStore(); + }); +} +``` + +## Examples + +### Repository Pattern + +The `WeihanLi.EntityFramework` package provides a repository pattern implementation for Entity Framework Core. + +```csharp +public class MyService +{ + private readonly IEFRepository _repository; + + public MyService(IEFRepository repository) + { + _repository = repository; + } + + public async Task GetEntityByIdAsync(int id) + { + return await _repository.FindAsync(id); + } + + public async Task AddEntityAsync(MyEntity entity) + { + await _repository.InsertAsync(entity); + } + + public async Task UpdateEntityAsync(MyEntity entity) + { + await _repository.UpdateAsync(entity); + } + + public async Task DeleteEntityAsync(int id) + { + await _repository.DeleteAsync(id); + } +} +``` + +### Unit of Work Pattern + +The `WeihanLi.EntityFramework` package also provides a unit of work pattern implementation for Entity Framework Core. + +```csharp +public class MyService +{ + private readonly IEFUnitOfWork _unitOfWork; + + public MyService(IEFUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task SaveChangesAsync() + { + await _unitOfWork.CommitAsync(); + } +} +``` + +### Audit + +The `WeihanLi.EntityFramework` package provides an audit feature to track changes in your entities. + +```csharp +public class MyDbContext : AuditDbContext +{ + public MyDbContext(DbContextOptions options, IServiceProvider serviceProvider) + : base(options, serviceProvider) + { + } + + public DbSet MyEntities { get; set; } +} +``` + +### Soft Delete + +The `WeihanLi.EntityFramework` package provides a soft delete feature to mark entities as deleted without actually removing them from the database. + +```csharp +public class MyEntity : ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } + public bool IsDeleted { get; set; } +} +``` + +### Auto Update + +The `WeihanLi.EntityFramework` package provides an auto update feature to automatically update certain properties, such as `CreatedAt`, `UpdatedAt`, `CreatedBy`, and `UpdatedBy`. + +```csharp +public class MyEntity : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +{ + public int Id { get; set; } + public string Name { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + public string CreatedBy { get; set; } + public string UpdatedBy { get; set; } +} +``` From e82a64364d7fe8ba90a0e8c78ef9052ca4c11be5 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 11 Apr 2025 10:55:45 +0800 Subject: [PATCH 09/32] Update Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a55e3e2..4230a4a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ enable enable 1.0.76 - 10.0.0-preview.2.25163.8 + 10.0.0-preview.3.25171.6 true true From 0b3b76e64d24d5d1bc3bcb99b588902c302dfb44 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Tue, 15 Apr 2025 16:07:58 +0800 Subject: [PATCH 10/32] Update Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4230a4a..b96a009 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ preview enable enable - 1.0.76 + 1.0.77 10.0.0-preview.3.25171.6 true From a8a9c25ef5c2790e19adc3a3f0b357d4f3cf78ad Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Wed, 11 Jun 2025 13:28:53 +0800 Subject: [PATCH 11/32] Update Directory.Build.props --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b96a009..17b98ba 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,8 +6,8 @@ preview enable enable - 1.0.77 - 10.0.0-preview.3.25171.6 + 1.0.79-preview-20250610-235418 + 10.0.0-preview.5.25277.114 true true From 1a60a2e86ba839e64d20dc4f7b21e51d930219bf Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Tue, 17 Jun 2025 11:18:14 +0800 Subject: [PATCH 12/32] Update Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 17b98ba..9ba444a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ preview enable enable - 1.0.79-preview-20250610-235418 + 1.0.79 10.0.0-preview.5.25277.114 true From 3c042790f9a808baf3d5833634ea6747525266ec Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Wed, 16 Jul 2025 07:56:21 +0800 Subject: [PATCH 13/32] feat: bump dependencies --- Directory.Build.props | 4 ++-- .../WeihanLi.EntityFramework.Test.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9ba444a..3038510 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,8 +6,8 @@ preview enable enable - 1.0.79 - 10.0.0-preview.5.25277.114 + 1.0.80 + 10.0.0-preview.6.25358.103 true true diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index 737d3c4..414b2a4 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -12,8 +12,8 @@ - - + + From d70cc9773b2255e106d1067e389f659ae2dd51c2 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Fri, 25 Jul 2025 23:45:36 +0800 Subject: [PATCH 14/32] feat: use slnx --- WeihanLi.EntityFramework.sln | 91 ----------------------------------- WeihanLi.EntityFramework.slnx | 12 +++++ build/build.cs | 2 +- 3 files changed, 13 insertions(+), 92 deletions(-) delete mode 100644 WeihanLi.EntityFramework.sln create mode 100644 WeihanLi.EntityFramework.slnx 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/build/build.cs b/build/build.cs index a54aab1..e0fa3d9 100644 --- a/build/build.cs +++ b/build/build.cs @@ -4,7 +4,7 @@ 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" ]; From 6f46a3a7bee88972be7ade89dd984b5113d5cee0 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Sat, 2 Aug 2025 08:53:57 +0800 Subject: [PATCH 15/32] feat: support ignore named query filter fixes #79 --- .../Program.cs | 10 +++++-- .../TestDbContext.cs | 9 ++++++ .../EFRepositoryQueryBuilder.cs | 29 +++++++++++++++++-- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/samples/WeihanLi.EntityFramework.Sample/Program.cs b/samples/WeihanLi.EntityFramework.Sample/Program.cs index f879aed..1c45fa1 100644 --- a/samples/WeihanLi.EntityFramework.Sample/Program.cs +++ b/samples/WeihanLi.EntityFramework.Sample/Program.cs @@ -21,10 +21,10 @@ public static class Program public static async Task Main(string[] args) { // SoftDeleteTest(); - // RepositoryTest(); + RepositoryTest(); // AutoAuditTest(); - await DbContextInterceptorSamples.RunAsync(); + // await DbContextInterceptorSamples.RunAsync(); Console.WriteLine("completed"); Console.ReadLine(); @@ -212,7 +212,7 @@ private static void RepositoryTest() var conn = db.Database.GetDbConnection(); try { - conn.Execute($@"TRUNCATE TABLE {tableName}"); + conn.Execute($"TRUNCATE TABLE {tableName}"); } catch { @@ -236,6 +236,10 @@ private static void RepositoryTest() { "Extra", "12345"} }); + var list = repo.Query(q => q.IgnoreQueryFilters(["not-null"])) + .ToArray(); + Console.WriteLine(list.Length); + repo.Update(x => x.SetProperty(_ => _.Extra, _ => "{}"), q => q.IgnoreQueryFilters()); var abc = db.TestEntities.AsNoTracking().ToArray(); diff --git a/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs b/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs index 9b4457d..2fc81c9 100644 --- a/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs +++ b/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs @@ -11,6 +11,15 @@ public TestDbContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity() + // .HasQueryFilter("one-month-ago", t => t.CreatedAt > DateTime.Now.AddMonths(-1)) + .HasQueryFilter("not-null", t => t.Extra != null) + ; + } + public DbSet TestEntities { get; set; } = null!; } diff --git a/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs b/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs index b08e00f..248068f 100644 --- a/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs +++ b/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs @@ -1,8 +1,5 @@ ο»Ώusing Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using WeihanLi.Common; @@ -55,6 +52,28 @@ public EFRepositoryQueryBuilder IgnoreQueryFilters(bool ignoreQueryFilt _ignoreQueryFilters = ignoreQueryFilters; return this; } + + private readonly HashSet _queryFiltersToIgnore = new(); + + public EFRepositoryQueryBuilder IgnoreQueryFilters(IReadOnlyCollection queryFilters, bool ignoreQueryFilters = true) + { + ArgumentNullException.ThrowIfNull(queryFilters); + if (ignoreQueryFilters) + { + foreach (var queryFilter in queryFilters) + { + _queryFiltersToIgnore.Add(queryFilter); + } + } + else + { + foreach (var queryFilter in queryFilters) + { + _queryFiltersToIgnore.Remove(queryFilter); + } + } + return this; + } private int _count; @@ -83,6 +102,10 @@ public IQueryable Build() { query = query.IgnoreQueryFilters(); } + else if (_queryFiltersToIgnore.Count > 0) + { + query = query.IgnoreQueryFilters(_queryFiltersToIgnore); + } if (_whereExpression.Count > 0) { foreach (var expression in _whereExpression) From 92cce93dc6b73c03a22529b850148ad11f36c6e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 00:54:39 +0000 Subject: [PATCH 16/32] Automated dotnet-format update from commit 6f46a3a7bee88972be7ade89dd984b5113d5cee0 on refs/heads/dev --- src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs b/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs index 248068f..8a5b434 100644 --- a/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs +++ b/src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs @@ -52,9 +52,9 @@ public EFRepositoryQueryBuilder IgnoreQueryFilters(bool ignoreQueryFilt _ignoreQueryFilters = ignoreQueryFilters; return this; } - + private readonly HashSet _queryFiltersToIgnore = new(); - + public EFRepositoryQueryBuilder IgnoreQueryFilters(IReadOnlyCollection queryFilters, bool ignoreQueryFilters = true) { ArgumentNullException.ThrowIfNull(queryFilters); From b5d6d080674c76bb51b9cddc98a7d22da8c14873 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Sat, 2 Aug 2025 09:26:58 +0800 Subject: [PATCH 17/32] update sample --- samples/WeihanLi.EntityFramework.Sample/Program.cs | 14 +++++++++----- .../TestDbContext.cs | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/samples/WeihanLi.EntityFramework.Sample/Program.cs b/samples/WeihanLi.EntityFramework.Sample/Program.cs index 1c45fa1..314c16c 100644 --- a/samples/WeihanLi.EntityFramework.Sample/Program.cs +++ b/samples/WeihanLi.EntityFramework.Sample/Program.cs @@ -236,15 +236,19 @@ private static void RepositoryTest() { "Extra", "12345"} }); - var list = repo.Query(q => q.IgnoreQueryFilters(["not-null"])) - .ToArray(); - Console.WriteLine(list.Length); - repo.Update(x => x.SetProperty(_ => _.Extra, _ => "{}"), q => q.IgnoreQueryFilters()); var abc = db.TestEntities.AsNoTracking().ToArray(); Console.WriteLine($"{string.Join(Environment.NewLine, abc.Select(_ => _.ToJson()))}"); - + + var entities = repo.Query(q => q.IgnoreQueryFilters(["not-null"])) + .ToArray(); + Console.WriteLine(entities.Length); + + entities = repo.Query(q => q.IgnoreQueryFilters()) + .ToArray(); + Console.WriteLine(entities.Length); + var data = repo.Query(q => q.WithPredictIf(f => f.Id > 0, false)).ToArray(); Console.WriteLine(JsonSerializer.Serialize(data)); diff --git a/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs b/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs index 2fc81c9..a7e0335 100644 --- a/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs +++ b/samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs @@ -16,6 +16,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); modelBuilder.Entity() // .HasQueryFilter("one-month-ago", t => t.CreatedAt > DateTime.Now.AddMonths(-1)) + .HasQueryFilter("valid-id", t => t.Id > 0) .HasQueryFilter("not-null", t => t.Extra != null) ; } From f0156ca0b8b633645d34c382af7d1cd9d98260b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 01:28:58 +0000 Subject: [PATCH 18/32] Automated dotnet-format update from commit 2cbfc919e7af0ec09019b96fb6dfca8909f000c3 on refs/heads/dev --- samples/WeihanLi.EntityFramework.Sample/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/WeihanLi.EntityFramework.Sample/Program.cs b/samples/WeihanLi.EntityFramework.Sample/Program.cs index 314c16c..117e3cf 100644 --- a/samples/WeihanLi.EntityFramework.Sample/Program.cs +++ b/samples/WeihanLi.EntityFramework.Sample/Program.cs @@ -240,15 +240,15 @@ private static void RepositoryTest() var abc = db.TestEntities.AsNoTracking().ToArray(); Console.WriteLine($"{string.Join(Environment.NewLine, abc.Select(_ => _.ToJson()))}"); - + var entities = repo.Query(q => q.IgnoreQueryFilters(["not-null"])) .ToArray(); Console.WriteLine(entities.Length); - + entities = repo.Query(q => q.IgnoreQueryFilters()) .ToArray(); Console.WriteLine(entities.Length); - + var data = repo.Query(q => q.WithPredictIf(f => f.Id > 0, false)).ToArray(); Console.WriteLine(JsonSerializer.Serialize(data)); From dd9d97c6c79eb34a2fa52582f0806ba366f43254 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Sat, 2 Aug 2025 09:32:17 +0800 Subject: [PATCH 19/32] feat: bump dependencies --- .../WeihanLi.EntityFramework.SourceGenerator.csproj | 4 ++-- .../WeihanLi.EntityFramework.Test.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WeihanLi.EntityFramework.SourceGenerator/WeihanLi.EntityFramework.SourceGenerator.csproj b/src/WeihanLi.EntityFramework.SourceGenerator/WeihanLi.EntityFramework.SourceGenerator.csproj index 86b9662..c3ecf63 100644 --- a/src/WeihanLi.EntityFramework.SourceGenerator/WeihanLi.EntityFramework.SourceGenerator.csproj +++ b/src/WeihanLi.EntityFramework.SourceGenerator/WeihanLi.EntityFramework.SourceGenerator.csproj @@ -20,8 +20,8 @@ - - + + diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index 414b2a4..e400c7f 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -13,7 +13,7 @@ - + From 2e557b39c4c2c0d08476cd79902da4a41195359e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:36:22 +0000 Subject: [PATCH 20/32] Initial plan From b636944a9324c3d0446cd1d942c8bb1cfb2bc426 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:49:16 +0000 Subject: [PATCH 21/32] Add comprehensive documentation with usage guide, getting started, and advanced features Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- README.md | 151 +++- docs/AdvancedFeatures.md | 829 ++++++++++++++++++++++ docs/GettingStarted.md | 422 ++++++++++++ docs/Usage.md | 1404 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 2745 insertions(+), 61 deletions(-) create mode 100644 docs/AdvancedFeatures.md create mode 100644 docs/GettingStarted.md diff --git a/README.md b/README.md index ad5fd8b..3202393 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,94 @@ ## Intro -[EntityFrameworkCore](https://github.com/dotnet/efcore) extensions +[EntityFrameworkCore](https://github.com/dotnet/efcore) extensions that provide a comprehensive set of tools and patterns to enhance your Entity Framework Core development experience. + +WeihanLi.EntityFramework offers: + +- **Repository Pattern** - Clean abstraction layer for data access +- **Unit of Work Pattern** - Transaction management across multiple repositories +- **Automatic Auditing** - Track all entity changes with flexible storage options +- **Auto-Update Features** - Automatic handling of CreatedAt/UpdatedAt timestamps and user tracking +- **Soft Delete** - Mark entities as deleted without physical removal +- **Database Extensions** - Convenient methods for bulk operations and queries +- **Database Functions** - SQL Server JSON operations and more + +## Quick Start + +### 1. Installation + +```bash +dotnet add package WeihanLi.EntityFramework +``` + +### 2. Basic Setup + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +// Add DbContext +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Add WeihanLi.EntityFramework services +builder.Services.AddEFRepository(); +builder.Services.AddEFAutoUpdateInterceptor(); +builder.Services.AddEFAutoAudit(auditBuilder => +{ + auditBuilder.WithUserIdProvider() + .WithStore(); +}); + +var app = builder.Build(); +``` + +### 3. Define Your Entities + +```csharp +public class Product : IEntityWithCreatedUpdatedAt, ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + + // Auto-update properties + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // Soft delete property + public bool IsDeleted { get; set; } +} +``` + +### 4. Use Repository Pattern + +```csharp +public class ProductService +{ + private readonly IEFRepository _repository; + + public ProductService(IEFRepository repository) + { + _repository = repository; + } + + public async Task CreateProductAsync(string name, decimal price) + { + var product = new Product { Name = name, Price = price }; + return await _repository.InsertAsync(product); + // CreatedAt/UpdatedAt automatically set, audit record created + } + + public async Task> GetActiveProductsAsync() + { + return await _repository.GetListAsync( + queryBuilder => queryBuilder.WithPredict(p => p.Price > 0) + ); + // Soft deleted products automatically filtered out + } +} +``` ## Package Release Notes @@ -33,36 +120,58 @@ See Releases/PRs for details ## Features -- Repository - - - `EFRepository` - - `EFRepositoryGenerator` +### πŸ—οΈ Repository Pattern +- `IEFRepository` - Generic repository interface +- `EFRepository` - Full-featured repository implementation +- `EFRepositoryGenerator` - Dynamic repository creation +- **Query Builder** - Fluent API for complex queries +- **Bulk Operations** - Efficient batch updates and deletes -- UoW - - - `EFUnitOfWork` +### πŸ”„ Unit of Work Pattern +- `IEFUnitOfWork` - Transaction management +- **Multi-Repository Transactions** - Coordinate changes across entities +- **Rollback Support** - Automatic error handling -- DbFunctions - - - `JsonValue` implement `JSON_VALUE` for SqlServer 2016 and above +### πŸ“‹ Comprehensive Auditing +- **Automatic Change Tracking** - Monitor all entity modifications +- **Flexible Storage** - Database, file, console, or custom stores +- **Property Enrichment** - Add custom metadata to audit records +- **User Tracking** - Capture who made changes +- **Configurable Filtering** - Include/exclude entities and properties -- Audit +### ⚑ Auto-Update Features +- **Timestamp Management** - Automatic CreatedAt/UpdatedAt handling +- **User Tracking** - Automatic CreatedBy/UpdatedBy population +- **Soft Delete** - Mark entities as deleted without removal +- **Custom Auto-Update** - Define your own auto-update rules - - Auto auditing for entity changes - -- AutoUpdate +### πŸ”§ Database Extensions +- **Column Updates** - Update specific columns only +- **Bulk Operations** - Efficient mass updates +- **Query Helpers** - Get table/column names, check database type +- **Paging Support** - Built-in pagination for large datasets - - Soft delete for the specific entity - - Auto update CreatedAt/UpdatedAt/CreatedBy/UpdatedBy +### πŸ—„οΈ Database Functions +- **JSON Support** - `JSON_VALUE` for SQL Server 2016+ +- **SQL Server Functions** - Enhanced querying capabilities -- Extensions +## Documentation - - Update specific column(s) `Update` - - Update without specific column(s) `UpdateWithout` +πŸš€ **[Getting Started Guide](docs/GettingStarted.md)** - Step-by-step setup instructions for new users + +πŸ“– **[Complete Usage Guide](docs/Usage.md)** - Comprehensive documentation with examples for all features + +πŸ“‹ **[Release Notes](docs/ReleaseNotes.md)** - Version history and breaking changes + +πŸ”§ **[Sample Project](samples/WeihanLi.EntityFramework.Sample/)** - Working examples and demonstrations ## Support -Feel free to try and [create issues](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) if you have any questions or integration issues +πŸ’‘ **Questions?** Check out the [Usage Guide](docs/Usage.md) for detailed examples + +πŸ› **Found a bug?** [Create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps + +πŸ’¬ **Need help?** Feel free to [start a discussion](https://github.com/WeihanLi/WeihanLi.EntityFramework/discussions) or create an issue ## Usage diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md new file mode 100644 index 0000000..80d34b2 --- /dev/null +++ b/docs/AdvancedFeatures.md @@ -0,0 +1,829 @@ +# Advanced Features Guide + +This guide covers advanced features and patterns for WeihanLi.EntityFramework. + +## Table of Contents + +- [Custom Interceptors](#custom-interceptors) +- [Advanced Audit Configuration](#advanced-audit-configuration) +- [Performance Optimization](#performance-optimization) +- [Testing Strategies](#testing-strategies) +- [Custom Query Filters](#custom-query-filters) +- [Bulk Operations](#bulk-operations) +- [Integration Patterns](#integration-patterns) + +## Custom Interceptors + +### Creating Custom Auto-Update Logic + +```csharp +public class CustomAutoUpdateInterceptor : SaveChangesInterceptor +{ + private readonly ILogger _logger; + private readonly IUserIdProvider _userIdProvider; + + public CustomAutoUpdateInterceptor( + ILogger logger, + IUserIdProvider userIdProvider) + { + _logger = logger; + _userIdProvider = userIdProvider; + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + if (eventData.Context != null) + { + HandleCustomUpdates(eventData.Context); + } + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void HandleCustomUpdates(DbContext context) + { + var entries = context.ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified); + + foreach (var entry in entries) + { + // Custom business logic + if (entry.Entity is IVersionedEntity versionedEntity) + { + if (entry.State == EntityState.Added) + { + versionedEntity.Version = 1; + } + else if (entry.State == EntityState.Modified) + { + versionedEntity.Version++; + } + } + + // Log entity changes + if (entry.Entity is IAuditableEntity auditableEntity) + { + _logger.LogInformation("Entity {EntityType} with ID {EntityId} is being {Action}", + entry.Entity.GetType().Name, + GetEntityId(entry), + entry.State); + } + } + } + + private object? GetEntityId(EntityEntry entry) + { + var keyProperty = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey()); + return keyProperty?.CurrentValue; + } +} + +// Supporting interfaces +public interface IVersionedEntity +{ + int Version { get; set; } +} + +public interface IAuditableEntity +{ + // Marker interface for entities that should be logged +} +``` + +### Conditional Interceptors + +```csharp +public class ConditionalAuditInterceptor : SaveChangesInterceptor +{ + private readonly IAuditConfig _auditConfig; + private readonly IServiceProvider _serviceProvider; + + public ConditionalAuditInterceptor( + IAuditConfig auditConfig, + IServiceProvider serviceProvider) + { + _auditConfig = auditConfig; + _serviceProvider = serviceProvider; + } + + public override ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + // Only audit in production environment + if (_auditConfig.IsEnabled && IsProductionEnvironment()) + { + var auditInterceptor = _serviceProvider.GetRequiredService(); + return auditInterceptor.SavingChangesAsync(eventData, result, cancellationToken); + } + + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private bool IsProductionEnvironment() + { + var environment = _serviceProvider.GetService(); + return environment?.IsProduction() == true; + } +} +``` + +## Advanced Audit Configuration + +### Custom Audit Property Enricher + +```csharp +public class CustomAuditPropertyEnricher : IAuditPropertyEnricher +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IConfiguration _configuration; + + public CustomAuditPropertyEnricher( + IHttpContextAccessor httpContextAccessor, + IConfiguration configuration) + { + _httpContextAccessor = httpContextAccessor; + _configuration = configuration; + } + + public void Enrich(AuditEntry auditEntry) + { + var httpContext = _httpContextAccessor.HttpContext; + + if (httpContext != null) + { + // Add request information + auditEntry.ExtraProperties["UserAgent"] = httpContext.Request.Headers["User-Agent"].ToString(); + auditEntry.ExtraProperties["IpAddress"] = httpContext.Connection.RemoteIpAddress?.ToString(); + auditEntry.ExtraProperties["RequestId"] = httpContext.TraceIdentifier; + + // Add correlation ID if available + if (httpContext.Request.Headers.ContainsKey("X-Correlation-ID")) + { + auditEntry.ExtraProperties["CorrelationId"] = + httpContext.Request.Headers["X-Correlation-ID"].ToString(); + } + } + + // Add application-specific information + auditEntry.ExtraProperties["ApplicationVersion"] = _configuration["ApplicationVersion"]; + auditEntry.ExtraProperties["Environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + auditEntry.ExtraProperties["ServerName"] = Environment.MachineName; + } +} +``` + +### Multi-Store Audit Configuration + +```csharp +public class MultiStoreAuditStore : IAuditStore +{ + private readonly List _stores; + private readonly ILogger _logger; + + public MultiStoreAuditStore( + IEnumerable stores, + ILogger logger) + { + _stores = stores.ToList(); + _logger = logger; + } + + public async Task Save(ICollection auditEntries) + { + var tasks = _stores.Select(async store => + { + try + { + await store.Save(auditEntries); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save audit entries to store {StoreType}", + store.GetType().Name); + } + }); + + await Task.WhenAll(tasks); + } +} + +// Configuration +services.AddEFAutoAudit(builder => +{ + builder + .WithUserIdProvider() + .WithPropertyEnricher() + .WithStore() // Primary store + .WithStore() // Secondary store for analytics + .WithStore(); // Backup store +}); + +// Register multi-store +services.AddSingleton(); +``` + +## Performance Optimization + +### Optimized Repository Patterns + +```csharp +public class OptimizedProductService +{ + private readonly IEFRepository _repository; + private readonly IMemoryCache _cache; + + public OptimizedProductService( + IEFRepository repository, + IMemoryCache cache) + { + _repository = repository; + _cache = cache; + } + + // Use projection to reduce data transfer + public async Task> GetProductSummariesAsync() + { + return await _repository.GetResultAsync( + p => new ProductSummary + { + Id = p.Id, + Name = p.Name, + Price = p.Price, + CategoryName = p.Category.Name + }, + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithInclude(p => p.Category) + ); + } + + // Cached frequently accessed data + public async Task GetProductWithCacheAsync(int id) + { + var cacheKey = $"product_{id}"; + + if (_cache.TryGetValue(cacheKey, out Product? cachedProduct)) + { + return cachedProduct; + } + + var product = await _repository.FindAsync(id); + + if (product != null) + { + _cache.Set(cacheKey, product, TimeSpan.FromMinutes(15)); + } + + return product; + } + + // Bulk operations for better performance + public async Task BulkActivateProductsAsync(List productIds) + { + return await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.IsActive, true), + queryBuilder => queryBuilder.WithPredict(p => productIds.Contains(p.Id)) + ); + } + + // Streaming for large datasets + public async IAsyncEnumerable StreamActiveProductsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var query = _repository.Query( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Id)) + ); + + await foreach (var product in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return product; + } + } +} + +public class ProductSummary +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public string CategoryName { get; set; } = string.Empty; +} +``` + +### Connection and Query Optimization + +```csharp +// Configure DbContext for optimal performance +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString, sqlOptions => + { + sqlOptions.CommandTimeout(30); + sqlOptions.EnableRetryOnFailure(3); + }) + .EnableSensitiveDataLogging(isDevelopment) + .EnableDetailedErrors(isDevelopment) + .ConfigureWarnings(warnings => + { + warnings.Ignore(CoreEventId.FirstWithoutOrderByAndFilterWarning); + }); +}); + +// Connection pooling +services.AddDbContextPool(options => +{ + options.UseSqlServer(connectionString); +}, poolSize: 128); +``` + +## Testing Strategies + +### Unit Testing with Mocked Repositories + +```csharp +public class ProductServiceTests +{ + private readonly Mock> _mockRepository; + private readonly Mock _mockCache; + private readonly ProductService _service; + + public ProductServiceTests() + { + _mockRepository = new Mock>(); + _mockCache = new Mock(); + _service = new ProductService(_mockRepository.Object, _mockCache.Object); + } + + [Fact] + public async Task GetActiveProductsAsync_ShouldReturnOnlyActiveProducts() + { + // Arrange + var activeProducts = new List + { + new() { Id = 1, Name = "Product 1", IsActive = true }, + new() { Id = 2, Name = "Product 2", IsActive = true } + }; + + _mockRepository + .Setup(r => r.GetListAsync(It.IsAny>>())) + .ReturnsAsync(activeProducts); + + // Act + var result = await _service.GetActiveProductsAsync(); + + // Assert + Assert.Equal(2, result.Count); + Assert.All(result, p => Assert.True(p.IsActive)); + + _mockRepository.Verify( + r => r.GetListAsync(It.IsAny>>()), + Times.Once); + } + + [Fact] + public async Task CreateProductAsync_ShouldSetCorrectProperties() + { + // Arrange + var productName = "New Product"; + var productPrice = 99.99m; + var expectedProduct = new Product + { + Id = 1, + Name = productName, + Price = productPrice, + IsActive = true + }; + + _mockRepository + .Setup(r => r.InsertAsync(It.IsAny())) + .ReturnsAsync(expectedProduct); + + // Act + var result = await _service.CreateProductAsync(productName, productPrice); + + // Assert + Assert.Equal(expectedProduct.Id, result.Id); + Assert.Equal(productName, result.Name); + Assert.Equal(productPrice, result.Price); + Assert.True(result.IsActive); + + _mockRepository.Verify( + r => r.InsertAsync(It.Is(p => + p.Name == productName && + p.Price == productPrice && + p.IsActive)), + Times.Once); + } +} +``` + +### Integration Testing with In-Memory Database + +```csharp +public class ProductServiceIntegrationTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + + public ProductServiceIntegrationTests(WebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task CreateAndRetrieveProduct_ShouldWorkEndToEnd() + { + // Arrange + using var scope = _factory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var productService = scope.ServiceProvider.GetRequiredService(); + + await context.Database.EnsureCreatedAsync(); + + // Act + var createdProduct = await productService.CreateProductAsync("Test Product", 50.00m); + var retrievedProduct = await productService.GetProductByIdAsync(createdProduct.Id); + + // Assert + Assert.NotNull(retrievedProduct); + Assert.Equal("Test Product", retrievedProduct.Name); + Assert.Equal(50.00m, retrievedProduct.Price); + Assert.True(retrievedProduct.IsActive); + Assert.True(retrievedProduct.CreatedAt > DateTimeOffset.MinValue); + } +} + +// Test-specific configuration +public class TestStartup : Startup +{ + public TestStartup(IConfiguration configuration) : base(configuration) { } + + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + + // Replace real database with in-memory + services.Remove(services.Single(d => d.ServiceType == typeof(DbContextOptions))); + services.AddDbContext(options => + { + options.UseInMemoryDatabase("TestDatabase"); + }); + + // Use test user provider + services.AddSingleton(new StaticUserIdProvider("TestUser")); + } +} +``` + +## Custom Query Filters + +### Dynamic Query Filters + +```csharp +public class TenantAwareRepository : IEFRepository + where TEntity : class, ITenantEntity +{ + private readonly IEFRepository _baseRepository; + private readonly ITenantProvider _tenantProvider; + + public TenantAwareRepository( + IEFRepository baseRepository, + ITenantProvider tenantProvider) + { + _baseRepository = baseRepository; + _tenantProvider = tenantProvider; + } + + public async Task> GetListAsync( + Action>? queryBuilderAction = null) + { + return await _baseRepository.GetListAsync(queryBuilder => + { + // Always apply tenant filter + queryBuilder.WithPredict(e => e.TenantId == _tenantProvider.GetCurrentTenantId()); + + // Apply additional filters + queryBuilderAction?.Invoke(queryBuilder); + }); + } + + // Implement other methods with tenant filtering... +} + +public interface ITenantEntity +{ + string TenantId { get; set; } +} + +public interface ITenantProvider +{ + string GetCurrentTenantId(); +} +``` + +### Security-Based Filters + +```csharp +public class SecureProductRepository : IProductRepository +{ + private readonly IEFRepository _repository; + private readonly ICurrentUserService _currentUserService; + + public SecureProductRepository( + IEFRepository repository, + ICurrentUserService currentUserService) + { + _repository = repository; + _currentUserService = currentUserService; + } + + public async Task> GetAccessibleProductsAsync() + { + var currentUser = await _currentUserService.GetCurrentUserAsync(); + + return await _repository.GetListAsync(queryBuilder => + { + if (currentUser.Role == UserRole.Admin) + { + // Admins see all products + queryBuilder.WithPredict(p => true); + } + else if (currentUser.Role == UserRole.Manager) + { + // Managers see products in their department + queryBuilder.WithPredict(p => p.DepartmentId == currentUser.DepartmentId); + } + else + { + // Regular users see only active products they created + queryBuilder.WithPredict(p => + p.IsActive && p.CreatedBy == currentUser.Id); + } + }); + } +} +``` + +## Bulk Operations + +### Advanced Bulk Processing + +```csharp +public class BulkOperationService +{ + private readonly IEFRepository _productRepository; + private readonly ILogger _logger; + + public BulkOperationService( + IEFRepository productRepository, + ILogger logger) + { + _productRepository = productRepository; + _logger = logger; + } + + public async Task BulkUpdatePricesAsync( + Dictionary priceUpdates, + CancellationToken cancellationToken = default) + { + var result = new BulkOperationResult(); + const int batchSize = 1000; + + var batches = priceUpdates + .Select((item, index) => new { item, index }) + .GroupBy(x => x.index / batchSize) + .Select(g => g.Select(x => x.item).ToList()); + + foreach (var batch in batches) + { + try + { + var productIds = batch.Select(b => b.Key).ToList(); + + foreach (var update in batch) + { + var updated = await _productRepository.UpdateAsync( + setters => setters.SetProperty(p => p.Price, update.Value), + queryBuilder => queryBuilder.WithPredict(p => p.Id == update.Key) + ); + + if (updated > 0) + { + result.SuccessCount++; + } + else + { + result.FailedIds.Add(update.Key); + result.FailureCount++; + } + } + + _logger.LogInformation("Processed batch of {Count} price updates", batch.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to process batch of price updates"); + result.FailedIds.AddRange(batch.Select(b => b.Key)); + result.FailureCount += batch.Count; + } + + if (cancellationToken.IsCancellationRequested) + { + break; + } + } + + return result; + } + + public async Task BulkArchiveOldProductsAsync(DateTime cutoffDate) + { + return await _productRepository.UpdateAsync( + setters => setters + .SetProperty(p => p.IsActive, false) + .SetProperty(p => p.ArchivedAt, DateTime.UtcNow), + queryBuilder => queryBuilder.WithPredict(p => + p.IsActive && p.CreatedAt < cutoffDate) + ); + } +} + +public class BulkOperationResult +{ + public int SuccessCount { get; set; } + public int FailureCount { get; set; } + public List FailedIds { get; set; } = new(); + + public bool HasFailures => FailureCount > 0; + public double SuccessRate => + SuccessCount + FailureCount > 0 + ? (double)SuccessCount / (SuccessCount + FailureCount) + : 0; +} +``` + +## Integration Patterns + +### Event-Driven Architecture + +```csharp +public class EventDrivenProductService +{ + private readonly IEFRepository _repository; + private readonly IEventPublisher _eventPublisher; + + public EventDrivenProductService( + IEFRepository repository, + IEventPublisher eventPublisher) + { + _repository = repository; + _eventPublisher = eventPublisher; + } + + public async Task CreateProductAsync(CreateProductCommand command) + { + var product = new Product + { + Name = command.Name, + Price = command.Price, + CategoryId = command.CategoryId + }; + + var createdProduct = await _repository.InsertAsync(product); + + // Publish domain event + await _eventPublisher.PublishAsync(new ProductCreatedEvent + { + ProductId = createdProduct.Id, + Name = createdProduct.Name, + Price = createdProduct.Price, + CreatedAt = createdProduct.CreatedAt, + CreatedBy = createdProduct.CreatedBy + }); + + return createdProduct; + } +} + +public class ProductCreatedEvent +{ + public int ProductId { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public string CreatedBy { get; set; } = string.Empty; +} + +// Event handler for side effects +public class ProductCreatedEventHandler : IEventHandler +{ + private readonly IEmailService _emailService; + private readonly ICacheService _cacheService; + + public ProductCreatedEventHandler( + IEmailService emailService, + ICacheService cacheService) + { + _emailService = emailService; + _cacheService = cacheService; + } + + public async Task HandleAsync(ProductCreatedEvent @event) + { + // Send notification + await _emailService.SendProductCreatedNotificationAsync(@event); + + // Invalidate cache + await _cacheService.InvalidateAsync("products_*"); + + // Update search index, analytics, etc. + } +} +``` + +### CQRS Pattern Integration + +```csharp +// Command side - uses repositories for writes +public class UpdateProductCommandHandler +{ + private readonly IEFRepository _repository; + private readonly IEventStore _eventStore; + + public UpdateProductCommandHandler( + IEFRepository repository, + IEventStore eventStore) + { + _repository = repository; + _eventStore = eventStore; + } + + public async Task HandleAsync(UpdateProductCommand command) + { + var product = await _repository.FindAsync(command.ProductId); + if (product == null) + { + return UpdateProductResult.NotFound(); + } + + // Business logic + var oldPrice = product.Price; + product.Name = command.Name; + product.Price = command.Price; + + await _repository.UpdateAsync(product); + + // Store event + if (oldPrice != command.Price) + { + await _eventStore.AppendAsync(new ProductPriceChangedEvent + { + ProductId = product.Id, + OldPrice = oldPrice, + NewPrice = command.Price, + ChangedAt = DateTime.UtcNow, + ChangedBy = command.UserId + }); + } + + return UpdateProductResult.Success(); + } +} + +// Query side - uses raw EF for reads +public class ProductQueryService +{ + private readonly AppDbContext _context; + + public ProductQueryService(AppDbContext context) + { + _context = context; + } + + public async Task GetProductDetailsAsync(int productId) + { + return await _context.Products + .Where(p => p.Id == productId) + .Select(p => new ProductDetailsView + { + Id = p.Id, + Name = p.Name, + Price = p.Price, + CategoryName = p.Category.Name, + CreatedAt = p.CreatedAt, + UpdatedAt = p.UpdatedAt, + ReviewCount = p.Reviews.Count(), + AverageRating = p.Reviews.Average(r => r.Rating) + }) + .FirstOrDefaultAsync(); + } +} +``` + +These advanced patterns help you build robust, scalable applications while leveraging the full power of WeihanLi.EntityFramework. \ No newline at end of file diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 0000000..06f9ffd --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,422 @@ +# Getting Started with WeihanLi.EntityFramework + +This guide will help you get up and running with WeihanLi.EntityFramework quickly. + +## Prerequisites + +- .NET 8.0 or later +- Entity Framework Core 8.0 or later +- A supported database provider (SQL Server, SQLite, PostgreSQL, etc.) + +## Step 1: Installation + +Add the package to your project: + +```bash +dotnet add package WeihanLi.EntityFramework +``` + +## Step 2: Define Your Entities + +Create your entity classes and implement the desired interfaces for automatic features: + +```csharp +using WeihanLi.EntityFramework; + +// Basic entity with auto-update timestamps +public class Product : IEntityWithCreatedUpdatedAt +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public bool IsActive { get; set; } = true; + + // These will be automatically managed + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } +} + +// Entity with soft delete capability +public class Category : ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + + // Soft delete property + public bool IsDeleted { get; set; } +} + +// Entity with full audit trail +public class Order : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +{ + public int Id { get; set; } + public int CustomerId { get; set; } + public decimal TotalAmount { get; set; } + public OrderStatus Status { get; set; } + + // Auto-managed timestamp fields + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // Auto-managed user tracking fields + public string CreatedBy { get; set; } = string.Empty; + public string UpdatedBy { get; set; } = string.Empty; + + // Navigation properties + public List Items { get; set; } = new(); +} + +public enum OrderStatus +{ + Pending, + Processing, + Shipped, + Delivered, + Cancelled +} +``` + +## Step 3: Configure Your DbContext + +Set up your DbContext with the necessary configurations: + +```csharp +using Microsoft.EntityFrameworkCore; +using WeihanLi.EntityFramework.Audit; + +public class AppDbContext : AuditDbContext +{ + public AppDbContext(DbContextOptions options, IServiceProvider serviceProvider) + : base(options, serviceProvider) + { + } + + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderItems { get; set; } + public DbSet AuditRecords { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure Product entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(200); + entity.Property(e => e.Price).HasPrecision(18, 2); + entity.HasIndex(e => e.Name); + }); + + // Configure Category with soft delete filter + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(100); + entity.HasQueryFilter(c => !c.IsDeleted); // Global soft delete filter + }); + + // Configure Order entity + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.TotalAmount).HasPrecision(18, 2); + entity.HasMany(e => e.Items) + .WithOne() + .HasForeignKey("OrderId"); + }); + } +} +``` + +## Step 4: Configure Services + +Set up dependency injection in your `Program.cs`: + +```csharp +using WeihanLi.EntityFramework; +using WeihanLi.EntityFramework.Audit; + +var builder = WebApplication.CreateBuilder(args); + +// Add Entity Framework +builder.Services.AddDbContext((provider, options) => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) + .AddInterceptors( + provider.GetRequiredService(), + provider.GetRequiredService() + ); +}); + +// Add WeihanLi.EntityFramework services +builder.Services.AddEFRepository(); +builder.Services.AddEFAutoUpdateInterceptor(); + +// Configure user ID provider for audit and auto-update +builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); + +// Configure audit system +builder.Services.AddEFAutoAudit(auditBuilder => +{ + auditBuilder + .WithUserIdProvider() + .EnrichWithProperty("ApplicationName", "MyApplication") + .EnrichWithProperty("MachineName", Environment.MachineName) + .WithStore() // Store audit records in database + .IgnoreEntity(); // Don't audit the audit records themselves +}); + +var app = builder.Build(); + +// Ensure database is created (for development) +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); +} + +app.Run(); +``` + +## Step 5: Create a User ID Provider + +Implement a user ID provider to track who makes changes: + +```csharp +using System.Security.Claims; +using WeihanLi.Common.Services; + +public class HttpContextUserIdProvider : IUserIdProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextUserIdProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetUserId() + { + var user = _httpContextAccessor.HttpContext?.User; + + // Try to get user ID from claims + var userId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? user?.FindFirst("sub")?.Value + ?? user?.Identity?.Name; + + return userId ?? "Anonymous"; + } +} +``` + +## Step 6: Create Your First Service + +Create a service that uses the repository pattern: + +```csharp +using WeihanLi.EntityFramework; + +public class ProductService +{ + private readonly IEFRepository _productRepository; + private readonly IEFUnitOfWork _unitOfWork; + + public ProductService( + IEFRepository productRepository, + IEFUnitOfWork unitOfWork) + { + _productRepository = productRepository; + _unitOfWork = unitOfWork; + } + + public async Task CreateProductAsync(string name, decimal price) + { + var product = new Product + { + Name = name, + Price = price, + IsActive = true + // CreatedAt and UpdatedAt will be set automatically + }; + + return await _productRepository.InsertAsync(product); + } + + public async Task GetProductByIdAsync(int id) + { + return await _productRepository.FindAsync(id); + } + + public async Task> GetActiveProductsAsync() + { + return await _productRepository.GetListAsync( + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + public async Task UpdateProductPriceAsync(int productId, decimal newPrice) + { + var product = await _productRepository.FindAsync(productId); + if (product == null) return false; + + product.Price = newPrice; + // UpdatedAt will be set automatically + + var result = await _productRepository.UpdateAsync(product); + return result > 0; + } + + public async Task DeactivateProductAsync(int productId) + { + // Use bulk update for efficiency + var result = await _productRepository.UpdateAsync( + setters => setters.SetProperty(p => p.IsActive, false), + queryBuilder => queryBuilder.WithPredict(p => p.Id == productId) + ); + + return result > 0; + } + + public async Task> GetProductsPagedAsync(int page, int pageSize) + { + return await _productRepository.GetPagedListAsync( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Name)), + page, + pageSize + ); + } +} +``` + +## Step 7: Create a Controller (ASP.NET Core) + +```csharp +using Microsoft.AspNetCore.Mvc; + +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly ProductService _productService; + + public ProductsController(ProductService productService) + { + _productService = productService; + } + + [HttpGet] + public async Task>> GetProducts( + int page = 1, + int pageSize = 20) + { + var products = await _productService.GetProductsPagedAsync(page, pageSize); + return Ok(products); + } + + [HttpGet("{id}")] + public async Task> GetProduct(int id) + { + var product = await _productService.GetProductByIdAsync(id); + if (product == null) + { + return NotFound(); + } + return Ok(product); + } + + [HttpPost] + public async Task> CreateProduct(CreateProductRequest request) + { + var product = await _productService.CreateProductAsync(request.Name, request.Price); + return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product); + } + + [HttpPut("{id}/price")] + public async Task UpdatePrice(int id, UpdatePriceRequest request) + { + var success = await _productService.UpdateProductPriceAsync(id, request.Price); + if (!success) + { + return NotFound(); + } + return NoContent(); + } +} + +public class CreateProductRequest +{ + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } +} + +public class UpdatePriceRequest +{ + public decimal Price { get; set; } +} +``` + +## What Happens Automatically + +When you run this setup, WeihanLi.EntityFramework will automatically: + +1. **Set timestamps**: `CreatedAt` when inserting, `UpdatedAt` when updating +2. **Track users**: `CreatedBy` and `UpdatedBy` using your user ID provider +3. **Create audit records**: Every change is logged with full details +4. **Apply soft delete filters**: Soft-deleted entities are excluded from queries +5. **Handle transactions**: Unit of Work ensures data consistency + +## Next Steps + +- πŸ“– Read the [Complete Usage Guide](Usage.md) for advanced features +- πŸ” Explore the [sample project](../samples/WeihanLi.EntityFramework.Sample/) for more examples +- πŸ› οΈ Check out bulk operations, advanced querying, and custom audit stores +- πŸ“‹ Review [Release Notes](ReleaseNotes.md) for version-specific information + +## Common Patterns + +### Repository with Unit of Work + +```csharp +public async Task ProcessOrderAsync(CreateOrderRequest request) +{ + var orderRepo = _unitOfWork.GetRepository(); + var productRepo = _unitOfWork.GetRepository(); + + // Create order + var order = await orderRepo.InsertAsync(new Order + { + CustomerId = request.CustomerId, + TotalAmount = request.Items.Sum(i => i.Price * i.Quantity) + }); + + // Add order items and update inventory + foreach (var item in request.Items) + { + await orderRepo.InsertAsync(new OrderItem + { + OrderId = order.Id, + ProductId = item.ProductId, + Quantity = item.Quantity, + Price = item.Price + }); + + // Update product inventory (example) + await productRepo.UpdateAsync( + setters => setters.SetProperty(p => p.Stock, p => p.Stock - item.Quantity), + queryBuilder => queryBuilder.WithPredict(p => p.Id == item.ProductId) + ); + } + + // Commit all changes in a single transaction + await _unitOfWork.CommitAsync(); +} +``` + +This gets you started with the core features. The library handles the complexity while giving you clean, testable code! \ No newline at end of file diff --git a/docs/Usage.md b/docs/Usage.md index 360bc2a..6620dcc 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -1,98 +1,469 @@ -# Usage +# WeihanLi.EntityFramework Usage Guide ## Installation To install the `WeihanLi.EntityFramework` package, you can use the NuGet Package Manager, .NET CLI, or PackageReference in your project file. -**.NET CLI** +### .NET CLI -``` +```bash dotnet add package WeihanLi.EntityFramework ``` -## Configuration +### Package Manager Console + +```powershell +Install-Package WeihanLi.EntityFramework +``` + +### PackageReference + +```xml + +``` + +> **Version Compatibility** +> - For EF 8 and above: use 8.x or above major-version matched versions +> - For EF 7: use 3.x +> - For EF Core 5/6: use 2.x +> - For EF Core 3.x: use 1.5.0 above, and 2.0.0 below +> - For EF Core 2.x: use 1.4.x and below + +## Quick Start + +### Basic Configuration + +Configure the services in your `Startup.cs` or `Program.cs` file: + +```csharp +// Program.cs (minimal hosting model) +var builder = WebApplication.CreateBuilder(args); + +// Add DbContext +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Add WeihanLi.EntityFramework services +builder.Services.AddEFRepository(); +builder.Services.AddEFAutoUpdateInterceptor(); +builder.Services.AddEFAutoAudit(builder => +{ + builder.WithUserIdProvider() + .EnrichWithProperty("MachineName", Environment.MachineName) + .WithStore(); +}); + +var app = builder.Build(); +``` -To configure the `WeihanLi.EntityFramework` package, you need to add the necessary services to your `IServiceCollection` in the `Startup.cs` or `Program.cs` file. +### Complete Configuration Example ```csharp public void ConfigureServices(IServiceCollection services) { - services.AddDbContext(options => - options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + // Configure DbContext with interceptors + services.AddDbContext((provider, options) => + { + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")) + .AddInterceptors( + provider.GetRequiredService(), + provider.GetRequiredService() + ); + }); + // Add repository services services.AddEFRepository(); + + // Add auto-update interceptor for automatic CreatedAt/UpdatedAt handling services.AddEFAutoUpdateInterceptor(); + + // Configure audit system services.AddEFAutoAudit(builder => { builder.WithUserIdProvider() .EnrichWithProperty("MachineName", Environment.MachineName) - .WithStore(); + .EnrichWithProperty("ApplicationName", "MyApp") + .WithStore() + .IgnoreEntity() + .IgnoreProperty("CreatedAt"); }); } ``` -## Examples +``` + +### Custom User ID Provider + +```csharp +public class CustomUserIdProvider : IUserIdProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public CustomUserIdProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetUserId() + { + return _httpContextAccessor.HttpContext?.User?.Identity?.Name ?? "System"; + } +} +``` + +## Features and Examples + +### 1. Repository Pattern -### Repository Pattern +The `WeihanLi.EntityFramework` package provides a clean repository pattern implementation for Entity Framework Core. -The `WeihanLi.EntityFramework` package provides a repository pattern implementation for Entity Framework Core. +#### Basic Repository Usage ```csharp -public class MyService +public class ProductService { - private readonly IEFRepository _repository; + private readonly IEFRepository _repository; - public MyService(IEFRepository repository) + public ProductService(IEFRepository repository) { _repository = repository; } - public async Task GetEntityByIdAsync(int id) + // Find by primary key + public async Task GetProductByIdAsync(int id) { return await _repository.FindAsync(id); } - public async Task AddEntityAsync(MyEntity entity) + // Insert single entity + public async Task CreateProductAsync(Product product) + { + return await _repository.InsertAsync(product); + } + + // Insert multiple entities + public async Task CreateProductsAsync(List products) + { + return await _repository.InsertAsync(products); + } + + // Update entity + public async Task UpdateProductAsync(Product product) + { + return await _repository.UpdateAsync(product); + } + + // Update specific columns only + public async Task UpdateProductNameAsync(int id, string newName) + { + return await _repository.UpdateAsync( + new Product { Id = id, Name = newName }, + p => p.Name // Only update Name property + ); + } + + // Update without specific columns + public async Task UpdateProductWithoutTimestampAsync(Product product) + { + return await _repository.UpdateWithoutAsync(product, p => p.UpdatedAt); + } + + // Delete by primary key + public async Task DeleteProductAsync(int id) + { + return await _repository.DeleteAsync(id); + } + + // Delete entity + public async Task DeleteProductAsync(Product product) + { + return await _repository.DeleteAsync(product); + } +} +``` + +#### Advanced Query Operations + +```csharp +public class ProductQueryService +{ + private readonly IEFRepository _repository; + + public ProductQueryService(IEFRepository repository) + { + _repository = repository; + } + + // Get first product with condition + public async Task GetFirstActiveProductAsync() { - await _repository.InsertAsync(entity); + return await _repository.FirstOrDefaultAsync( + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); } - public async Task UpdateEntityAsync(MyEntity entity) + // Get products with paging + public async Task> GetProductsPagedAsync(int page, int size) { - await _repository.UpdateAsync(entity); + return await _repository.GetPagedListAsync( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderByDescending(p => p.CreatedAt)), + page, + size + ); } - public async Task DeleteEntityAsync(int id) + // Get products with custom projection + public async Task> GetProductSummariesAsync() { - await _repository.DeleteAsync(id); + return await _repository.GetResultAsync( + p => new ProductDto + { + Id = p.Id, + Name = p.Name, + Price = p.Price + }, + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Count products + public async Task GetActiveProductCountAsync() + { + return await _repository.CountAsync( + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Check if any products exist + public async Task HasExpensiveProductsAsync() + { + return await _repository.AnyAsync( + queryBuilder => queryBuilder.WithPredict(p => p.Price > 1000) + ); + } + + // Raw query access + public IQueryable GetProductsQuery() + { + return _repository.Query( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Name)) + ); + } +} +``` + +#### Bulk Operations + +```csharp +public class ProductBulkService +{ + private readonly IEFRepository _repository; + + public ProductBulkService(IEFRepository repository) + { + _repository = repository; + } + + // Bulk update using expression + public async Task UpdatePricesAsync(decimal multiplier) + { + return await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.Price, p => p.Price * multiplier), + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Bulk delete with condition + public async Task DeleteInactiveProductsAsync() + { + return await _repository.DeleteAsync( + queryBuilder => queryBuilder.WithPredict(p => !p.IsActive) + ); } } ``` -### Unit of Work Pattern +### 2. Unit of Work Pattern + +The Unit of Work pattern helps manage transactions and ensures data consistency across multiple repository operations. -The `WeihanLi.EntityFramework` package also provides a unit of work pattern implementation for Entity Framework Core. +#### Basic Unit of Work Usage ```csharp -public class MyService +public class OrderService { private readonly IEFUnitOfWork _unitOfWork; - public MyService(IEFUnitOfWork unitOfWork) + public OrderService(IEFUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } - public async Task SaveChangesAsync() + public async Task CreateOrderWithItemsAsync(Order order, List items) { + // Get repositories through unit of work + var orderRepo = _unitOfWork.GetRepository(); + var itemRepo = _unitOfWork.GetRepository(); + + // All operations within the same transaction + var createdOrder = await orderRepo.InsertAsync(order); + + foreach (var item in items) + { + item.OrderId = createdOrder.Id; + await itemRepo.InsertAsync(item); + } + + // Commit all changes in a single transaction await _unitOfWork.CommitAsync(); + + return createdOrder; + } + + public async Task UpdateOrderStatusAsync(int orderId, OrderStatus newStatus) + { + var orderRepo = _unitOfWork.GetRepository(); + var auditRepo = _unitOfWork.GetRepository(); + + var order = await orderRepo.FindAsync(orderId); + if (order != null) + { + var oldStatus = order.Status; + order.Status = newStatus; + + await orderRepo.UpdateAsync(order); + + // Add audit record + await auditRepo.InsertAsync(new OrderAudit + { + OrderId = orderId, + OldStatus = oldStatus, + NewStatus = newStatus, + ChangedAt = DateTime.UtcNow + }); + + await _unitOfWork.CommitAsync(); + } + } + + public async Task TransferProductsBetweenWarehousesAsync( + int fromWarehouseId, + int toWarehouseId, + List transfers) + { + var inventoryRepo = _unitOfWork.GetRepository(); + var transferRepo = _unitOfWork.GetRepository(); + + foreach (var transfer in transfers) + { + // Reduce inventory in source warehouse + await inventoryRepo.UpdateAsync( + setters => setters.SetProperty(i => i.Quantity, i => i.Quantity - transfer.Quantity), + queryBuilder => queryBuilder.WithPredict(i => + i.WarehouseId == fromWarehouseId && i.ProductId == transfer.ProductId) + ); + + // Increase inventory in destination warehouse + await inventoryRepo.UpdateAsync( + setters => setters.SetProperty(i => i.Quantity, i => i.Quantity + transfer.Quantity), + queryBuilder => queryBuilder.WithPredict(i => + i.WarehouseId == toWarehouseId && i.ProductId == transfer.ProductId) + ); + + // Record the transfer + transfer.TransferDate = DateTime.UtcNow; + await transferRepo.InsertAsync(transfer); + } + + await _unitOfWork.CommitAsync(); + } +} +``` + +#### Advanced Unit of Work with Error Handling + +```csharp +public class AdvancedOrderService +{ + private readonly IEFUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public AdvancedOrderService( + IEFUnitOfWork unitOfWork, + ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + public async Task ProcessComplexOrderAsync(ComplexOrder complexOrder) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + // Step 1: Create order + var orderRepo = _unitOfWork.GetRepository(); + var order = await orderRepo.InsertAsync(complexOrder.Order); + + // Step 2: Process payment + var paymentRepo = _unitOfWork.GetRepository(); + var payment = await paymentRepo.InsertAsync(new Payment + { + OrderId = order.Id, + Amount = complexOrder.TotalAmount, + PaymentMethod = complexOrder.PaymentMethod + }); + + // Step 3: Update inventory + var inventoryRepo = _unitOfWork.GetRepository(); + foreach (var item in complexOrder.Items) + { + var inventoryItem = await inventoryRepo.FirstOrDefaultAsync( + qb => qb.WithPredict(i => i.ProductId == item.ProductId) + ); + + if (inventoryItem == null || inventoryItem.Quantity < item.Quantity) + { + throw new InvalidOperationException($"Insufficient inventory for product {item.ProductId}"); + } + + inventoryItem.Quantity -= item.Quantity; + await inventoryRepo.UpdateAsync(inventoryItem); + } + + // Step 4: Send notifications (simulated) + await SendOrderConfirmationAsync(order.Id); + + await _unitOfWork.CommitAsync(); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing complex order"); + await _unitOfWork.RollbackAsync(); + return false; + } + } + + private async Task SendOrderConfirmationAsync(int orderId) + { + // Simulate notification service + await Task.Delay(100); + _logger.LogInformation($"Order confirmation sent for order {orderId}"); } } ``` -### Audit +### 3. Audit System -The `WeihanLi.EntityFramework` package provides an audit feature to track changes in your entities. +The audit system automatically tracks changes to your entities, providing a complete history of data modifications. + +#### Setting up Audit DbContext ```csharp public class MyDbContext : AuditDbContext @@ -102,35 +473,988 @@ public class MyDbContext : AuditDbContext { } - public DbSet MyEntities { get; set; } + public DbSet Products { get; set; } + public DbSet Orders { get; set; } + public DbSet AuditRecords { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Configure your entities + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).IsRequired().HasMaxLength(200); + }); + } } ``` -### Soft Delete +#### Audit Configuration Options + +```csharp +// Basic audit configuration +services.AddEFAutoAudit(builder => +{ + builder.WithUserIdProvider() + .WithStore(); +}); + +// Advanced audit configuration +services.AddEFAutoAudit(builder => +{ + builder + // Configure user ID provider + .WithUserIdProvider() + + // Add custom properties to audit records + .EnrichWithProperty("ApplicationName", "MyApplication") + .EnrichWithProperty("MachineName", Environment.MachineName) + .EnrichWithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version?.ToString()) + + // Configure storage + .WithStore() // Console output + .WithStore() // File storage + .WithStore() // Database storage + + // Ignore specific entities + .IgnoreEntity() + .IgnoreEntity() + + // Ignore specific properties + .IgnoreProperty(p => p.InternalNotes) + .IgnoreProperty("Password") // Ignore by property name + .IgnoreProperty("CreatedAt") + + // Include unmodified properties (default is false) + .WithUnModifiedProperty() + + // Configure property value formatting + .WithPropertyValueFormatter(); +}); +``` + +#### Custom Audit Store + +```csharp +public class CustomAuditStore : IAuditStore +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public CustomAuditStore(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public async Task Save(ICollection auditEntries) + { + foreach (var entry in auditEntries) + { + _logger.LogInformation("Audit: {Operation} on {EntityType} with Id {EntityId} by {UserId}", + entry.OperationType, + entry.EntityType, + entry.EntityId, + entry.UserId); + + // Custom storage logic (e.g., send to external system) + await SendToExternalAuditSystemAsync(entry); + } + } + + private async Task SendToExternalAuditSystemAsync(AuditEntry entry) + { + // Implement custom audit storage logic + await Task.CompletedTask; + } +} +``` -The `WeihanLi.EntityFramework` package provides a soft delete feature to mark entities as deleted without actually removing them from the database. +#### Using Audit Interceptor Manually ```csharp -public class MyEntity : ISoftDeleteEntityWithDeleted +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); + +// Register the interceptor +services.AddSingleton(); +``` + +#### Querying Audit Records + +```csharp +public class AuditQueryService +{ + private readonly IEFRepository _auditRepository; + + public AuditQueryService(IEFRepository auditRepository) + { + _auditRepository = auditRepository; + } + + public async Task> GetUserActionsAsync(string userId, DateTime from, DateTime to) + { + return await _auditRepository.GetListAsync( + queryBuilder => queryBuilder + .WithPredict(a => a.UserId == userId && a.DateTime >= from && a.DateTime <= to) + .WithOrderBy(q => q.OrderByDescending(a => a.DateTime)) + ); + } + + public async Task> GetEntityHistoryAsync(string entityType, string entityId) + { + return await _auditRepository.GetListAsync( + queryBuilder => queryBuilder + .WithPredict(a => a.EntityType == entityType && a.EntityId == entityId) + .WithOrderBy(q => q.OrderBy(a => a.DateTime)) + ); + } + + public async Task> GetDailyOperationStatsAsync(DateTime date) + { + var records = await _auditRepository.GetListAsync( + queryBuilder => queryBuilder.WithPredict(a => a.DateTime.Date == date.Date) + ); + + return records.GroupBy(r => r.OperationType.ToString()) + .ToDictionary(g => g.Key, g => g.Count()); + } +} +``` + +### 4. Soft Delete + +The soft delete feature marks entities as deleted without actually removing them from the database, allowing for data recovery and audit trails. + +#### Entity Configuration for Soft Delete + +```csharp +// Option 1: Using ISoftDeleteEntityWithDeleted interface +public class Product : ISoftDeleteEntityWithDeleted +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public bool IsDeleted { get; set; } // Required by interface +} + +// Option 2: Using ISoftDeleteEntity interface (with custom deleted property name) +public class Category : ISoftDeleteEntity +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public bool Deleted { get; set; } // Custom property name +} + +// Option 3: Custom soft delete implementation +public class CustomSoftDeleteEntity { public int Id { get; set; } - public string Name { get; set; } - public bool IsDeleted { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime? DeletedAt { get; set; } + public bool IsActive => DeletedAt == null; } ``` -### Auto Update +#### DbContext Configuration for Soft Delete -The `WeihanLi.EntityFramework` package provides an auto update feature to automatically update certain properties, such as `CreatedAt`, `UpdatedAt`, `CreatedBy`, and `UpdatedBy`. +```csharp +public class SoftDeleteDbContext : DbContext +{ + public SoftDeleteDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Configure global query filter for soft delete + modelBuilder.Entity() + .HasQueryFilter(p => !p.IsDeleted); + + modelBuilder.Entity() + .HasQueryFilter(c => !c.Deleted); + + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // Add the soft delete interceptor + optionsBuilder.AddInterceptors(new SoftDeleteInterceptor()); + base.OnConfiguring(optionsBuilder); + } +} +``` + +#### Using Soft Delete with Services ```csharp -public class MyEntity : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +// Configure soft delete in Startup.cs +services.AddSingleton(); +services.AddEFAutoUpdateInterceptor(); + +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); +``` + +#### Soft Delete Operations + +```csharp +public class ProductService +{ + private readonly IEFRepository _repository; + private readonly SoftDeleteDbContext _context; + + public ProductService( + IEFRepository repository, + SoftDeleteDbContext context) + { + _repository = repository; + _context = context; + } + + // Regular delete - will soft delete the entity + public async Task DeleteProductAsync(int productId) + { + return await _repository.DeleteAsync(productId); + } + + // Get active products (soft deleted items are filtered out automatically) + public async Task> GetActiveProductsAsync() + { + return await _repository.GetListAsync(); + } + + // Get all products including soft deleted ones + public async Task> GetAllProductsIncludingDeletedAsync() + { + return await _context.Products + .IgnoreQueryFilters() // Ignore the soft delete filter + .ToListAsync(); + } + + // Get only soft deleted products + public async Task> GetDeletedProductsAsync() + { + return await _context.Products + .IgnoreQueryFilters() + .Where(p => p.IsDeleted) + .ToListAsync(); + } + + // Restore a soft deleted product + public async Task RestoreProductAsync(int productId) + { + var product = await _context.Products + .IgnoreQueryFilters() + .FirstOrDefaultAsync(p => p.Id == productId && p.IsDeleted); + + if (product != null) + { + product.IsDeleted = false; + return await _context.SaveChangesAsync(); + } + + return 0; + } + + // Permanently delete a soft deleted product + public async Task PermanentlyDeleteProductAsync(int productId) + { + var product = await _context.Products + .IgnoreQueryFilters() + .FirstOrDefaultAsync(p => p.Id == productId && p.IsDeleted); + + if (product != null) + { + _context.Products.Remove(product); + return await _context.SaveChangesAsync(); + } + + return 0; + } + + // Bulk soft delete + public async Task BulkDeleteInactiveProductsAsync() + { + return await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.IsDeleted, true), + queryBuilder => queryBuilder.WithPredict(p => !p.IsActive) + ); + } +} +``` + +### 5. Auto Update Features + +The auto-update system automatically manages timestamp and user fields when entities are created or modified. + +#### Entity Interfaces for Auto Update + +```csharp +// For automatic CreatedAt/UpdatedAt timestamps +public class Product : IEntityWithCreatedUpdatedAt +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public DateTimeOffset CreatedAt { get; set; } // Automatically set on insert + public DateTimeOffset UpdatedAt { get; set; } // Automatically updated on modify +} + +// For automatic CreatedBy/UpdatedBy user tracking +public class Order : IEntityWithCreatedUpdatedBy { public int Id { get; set; } - public string Name { get; set; } + public decimal TotalAmount { get; set; } + public string CreatedBy { get; set; } = string.Empty; // Automatically set on insert + public string UpdatedBy { get; set; } = string.Empty; // Automatically updated on modify +} + +// Combined timestamp and user tracking +public class Customer : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + + // Timestamp fields public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } - public string CreatedBy { get; set; } - public string UpdatedBy { get; set; } + + // User tracking fields + public string CreatedBy { get; set; } = string.Empty; + public string UpdatedBy { get; set; } = string.Empty; +} + +// Custom auto-update entity +public class BlogPost +{ + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + + [AutoUpdate(AutoUpdateOperation.Insert)] + public DateTime PublishedAt { get; set; } + + [AutoUpdate(AutoUpdateOperation.Update)] + public DateTime LastModified { get; set; } + + [AutoUpdate(AutoUpdateOperation.Insert | AutoUpdateOperation.Update)] + public string ModifiedBy { get; set; } = string.Empty; +} +``` + +#### Configuration + +```csharp +// Register auto-update services +services.AddSingleton(); +services.AddEFAutoUpdateInterceptor(); + +// Configure DbContext with auto-update interceptor +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); +``` + +#### Custom User ID Provider Examples + +```csharp +// HTTP Context based user provider +public class HttpContextUserIdProvider : IUserIdProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextUserIdProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetUserId() + { + return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? "Anonymous"; + } +} + +// Environment based user provider +public class EnvironmentUserIdProvider : IUserIdProvider +{ + public string GetUserId() + { + return Environment.UserName; + } +} + +// Static user provider for testing +public class StaticUserIdProvider : IUserIdProvider +{ + private readonly string _userId; + + public StaticUserIdProvider(string userId) + { + _userId = userId; + } + + public string GetUserId() => _userId; +} +``` + +#### Auto Update in Action + +```csharp +public class CustomerService +{ + private readonly IEFRepository _repository; + + public CustomerService(IEFRepository repository) + { + _repository = repository; + } + + public async Task CreateCustomerAsync(string name, string email) + { + var customer = new Customer + { + Name = name, + Email = email + // CreatedAt, CreatedBy will be set automatically + }; + + return await _repository.InsertAsync(customer); + } + + public async Task UpdateCustomerEmailAsync(int customerId, string newEmail) + { + var customer = await _repository.FindAsync(customerId); + if (customer != null) + { + customer.Email = newEmail; + // UpdatedAt, UpdatedBy will be set automatically + return await _repository.UpdateAsync(customer); + } + return 0; + } +} +``` + +#### Manual Auto Update Control + +```csharp +public class ManualAutoUpdateService +{ + private readonly MyDbContext _context; + + public ManualAutoUpdateService(MyDbContext context) + { + _context = context; + } + + public async Task UpdateWithCustomTimestampAsync(int customerId, string newName) + { + var customer = await _context.Customers.FindAsync(customerId); + if (customer != null) + { + customer.Name = newName; + customer.UpdatedAt = DateTimeOffset.Now.AddHours(-1); // Custom timestamp + + // Disable auto-update for this save operation + _context.ChangeTracker.Entries() + .Where(e => e.Entity.Id == customerId) + .ForEach(e => e.Property(nameof(Customer.UpdatedAt)).IsModified = false); + + await _context.SaveChangesAsync(); + } + } +} +``` + +### 6. Database Extensions + +The package provides useful database extensions for common operations. + +#### Update Extensions + +```csharp +public class ProductUpdateService +{ + private readonly MyDbContext _context; + + public ProductUpdateService(MyDbContext context) + { + _context = context; + } + + // Update specific columns using DbContext extension + public async Task UpdateProductPricesAsync(decimal priceMultiplier) + { + return await _context.Update( + setters => setters.SetProperty(p => p.Price, p => p.Price * priceMultiplier), + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) + ); + } + + // Update without specific columns + public async Task UpdateProductWithoutTimestampAsync(Product product) + { + return await _context.UpdateWithout(product, p => p.UpdatedAt); + } + + // Bulk update with dictionary + public async Task BulkUpdateProductStatusAsync(List productIds, bool isActive) + { + return await _context.Products + .Where(p => productIds.Contains(p.Id)) + .ExecuteUpdateAsync(setters => setters.SetProperty(p => p.IsActive, isActive)); + } } ``` + +#### Query Extensions + +```csharp +public class ProductQueryExtensions +{ + private readonly MyDbContext _context; + + public ProductQueryExtensions(MyDbContext context) + { + _context = context; + } + + // Get table name + public string GetProductTableName() + { + return _context.GetTableName(); + } + + // Get column name + public string GetProductNameColumnName() + { + return _context.GetColumnName(p => p.Name); + } + + // Check if using relational database + public bool IsUsingRelationalDatabase() + { + return _context.Database.IsRelational(); + } + + // Paged list extension + public async Task> GetProductsPagedAsync(int page, int size) + { + var query = _context.Products.Where(p => p.IsActive); + return await query.ToPagedListAsync(page, size); + } +} +``` + +### 7. Database Functions + +The package provides database-specific functions for enhanced querying capabilities. + +#### JSON Functions (SQL Server) + +```csharp +public class JsonQueryService +{ + private readonly MyDbContext _context; + + public JsonQueryService(MyDbContext context) + { + _context = context; + } + + // Using JSON_VALUE function for SQL Server + public async Task> GetProductsByJsonPropertyAsync(string propertyValue) + { + return await _context.Products + .Where(p => DbFunctions.JsonValue(p.JsonData, "$.PropertyName") == propertyValue) + .ToListAsync(); + } + + // Example with complex JSON queries + public async Task> GetProductsWithJsonFiltersAsync() + { + return await _context.Products + .Where(p => + DbFunctions.JsonValue(p.Metadata, "$.category") == "Electronics" && + DbFunctions.JsonValue(p.Metadata, "$.inStock") == "true") + .ToListAsync(); + } +} + +// Entity with JSON column +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string JsonData { get; set; } = string.Empty; // JSON column + public string Metadata { get; set; } = string.Empty; // Another JSON column +} +``` + +### 8. Repository Generator + +The repository generator automatically creates repository instances for your entities. + +#### Configuration + +```csharp +// Register repository generator +services.AddEFRepositoryGenerator(); + +// Register specific repositories +services.AddEFRepository(); +``` + +#### Using Repository Generator + +```csharp +public class MultiRepositoryService +{ + private readonly IEFRepositoryGenerator _repositoryGenerator; + + public MultiRepositoryService(IEFRepositoryGenerator repositoryGenerator) + { + _repositoryGenerator = repositoryGenerator; + } + + public async Task ProcessOrderAsync(Order order) + { + // Get repositories dynamically + var orderRepo = _repositoryGenerator.GetRepository(); + var customerRepo = _repositoryGenerator.GetRepository(); + var productRepo = _repositoryGenerator.GetRepository(); + + // Validate customer + var customer = await customerRepo.FindAsync(order.CustomerId); + if (customer == null) + { + throw new InvalidOperationException("Customer not found"); + } + + // Validate products + foreach (var item in order.Items) + { + var product = await productRepo.FindAsync(item.ProductId); + if (product == null) + { + throw new InvalidOperationException($"Product {item.ProductId} not found"); + } + } + + // Save order + await orderRepo.InsertAsync(order); + } +} +``` + +### 9. Advanced Query Building + +The package provides a fluent query builder for complex scenarios. + +#### Query Builder Examples + +```csharp +public class AdvancedQueryService +{ + private readonly IEFRepository _repository; + + public AdvancedQueryService(IEFRepository repository) + { + _repository = repository; + } + + // Complex filtering with query builder + public async Task> GetFilteredProductsAsync(ProductFilter filter) + { + return await _repository.GetListAsync(queryBuilder => + { + // Base query + queryBuilder.WithPredict(p => p.IsActive); + + // Conditional filters + if (!string.IsNullOrEmpty(filter.Name)) + { + queryBuilder.WithPredict(p => p.Name.Contains(filter.Name)); + } + + if (filter.MinPrice.HasValue) + { + queryBuilder.WithPredict(p => p.Price >= filter.MinPrice.Value); + } + + if (filter.MaxPrice.HasValue) + { + queryBuilder.WithPredict(p => p.Price <= filter.MaxPrice.Value); + } + + if (filter.CategoryIds?.Any() == true) + { + queryBuilder.WithPredict(p => filter.CategoryIds.Contains(p.CategoryId)); + } + + // Ordering + switch (filter.SortBy?.ToLower()) + { + case "name": + queryBuilder.WithOrderBy(q => filter.SortDescending + ? q.OrderByDescending(p => p.Name) + : q.OrderBy(p => p.Name)); + break; + case "price": + queryBuilder.WithOrderBy(q => filter.SortDescending + ? q.OrderByDescending(p => p.Price) + : q.OrderBy(p => p.Price)); + break; + default: + queryBuilder.WithOrderBy(q => q.OrderByDescending(p => p.CreatedAt)); + break; + } + + // Include related data + if (filter.IncludeCategory) + { + queryBuilder.WithInclude(p => p.Category); + } + + if (filter.IncludeReviews) + { + queryBuilder.WithInclude(p => p.Reviews); + } + }); + } + + // Dynamic query building + public async Task> GetDynamicResultsAsync( + Expression> selector, + List>> predicates, + Expression>? orderBy = null, + bool descending = false) + { + return await _repository.GetResultAsync(selector, queryBuilder => + { + // Apply all predicates + foreach (var predicate in predicates) + { + queryBuilder.WithPredict(predicate); + } + + // Apply ordering + if (orderBy != null) + { + queryBuilder.WithOrderBy(q => descending + ? q.OrderByDescending(orderBy) + : q.OrderBy(orderBy)); + } + }); + } +} + +public class ProductFilter +{ + public string? Name { get; set; } + public decimal? MinPrice { get; set; } + public decimal? MaxPrice { get; set; } + public List? CategoryIds { get; set; } + public string? SortBy { get; set; } + public bool SortDescending { get; set; } + public bool IncludeCategory { get; set; } + public bool IncludeReviews { get; set; } +} +``` + +## Best Practices + +### 1. Performance Considerations + +```csharp +// Use projection for large datasets +var productSummaries = await _repository.GetResultAsync( + p => new { p.Id, p.Name, p.Price }, + queryBuilder => queryBuilder.WithPredict(p => p.IsActive) +); + +// Use paging for large result sets +var pagedProducts = await _repository.GetPagedListAsync( + queryBuilder => queryBuilder + .WithPredict(p => p.IsActive) + .WithOrderBy(q => q.OrderBy(p => p.Name)), + pageNumber: 1, + pageSize: 50 +); + +// Use bulk operations for multiple updates +await _repository.UpdateAsync( + setters => setters.SetProperty(p => p.IsActive, false), + queryBuilder => queryBuilder.WithPredict(p => p.LastSoldDate < DateTime.Now.AddYears(-1)) +); +``` + +### 2. Error Handling + +```csharp +public class SafeProductService +{ + private readonly IEFRepository _repository; + private readonly ILogger _logger; + + public SafeProductService( + IEFRepository repository, + ILogger logger) + { + _repository = repository; + _logger = logger; + } + + public async Task SafeGetProductAsync(int id) + { + try + { + return await _repository.FindAsync(id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving product {ProductId}", id); + return null; + } + } + + public async Task SafeUpdateProductAsync(Product product) + { + try + { + var result = await _repository.UpdateAsync(product); + return result > 0; + } + catch (DbUpdateConcurrencyException ex) + { + _logger.LogWarning(ex, "Concurrency conflict updating product {ProductId}", product.Id); + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating product {ProductId}", product.Id); + return false; + } + } +} +``` + +### 3. Testing + +```csharp +public class ProductServiceTests +{ + private readonly Mock> _mockRepository; + private readonly ProductService _service; + + public ProductServiceTests() + { + _mockRepository = new Mock>(); + _service = new ProductService(_mockRepository.Object); + } + + [Fact] + public async Task GetProductAsync_ShouldReturnProduct_WhenProductExists() + { + // Arrange + var productId = 1; + var expectedProduct = new Product { Id = productId, Name = "Test Product" }; + _mockRepository.Setup(r => r.FindAsync(productId)) + .ReturnsAsync(expectedProduct); + + // Act + var result = await _service.GetProductAsync(productId); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedProduct.Id, result.Id); + Assert.Equal(expectedProduct.Name, result.Name); + } +} +``` + +## Troubleshooting + +### Common Issues + +#### 1. Interceptor Not Working + +**Problem**: Auto-update or audit interceptors are not being triggered. + +**Solution**: Ensure interceptors are properly registered and added to DbContext: + +```csharp +// Correct registration +services.AddEFAutoUpdateInterceptor(); +services.AddDbContext((provider, options) => +{ + options.UseSqlServer(connectionString) + .AddInterceptors(provider.GetRequiredService()); +}); +``` + +#### 2. Soft Delete Filter Not Applied + +**Problem**: Soft deleted entities are still appearing in queries. + +**Solution**: Ensure global query filters are configured: + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.Entity() + .HasQueryFilter(p => !p.IsDeleted); +} +``` + +#### 3. Repository Not Found + +**Problem**: `IEFRepository` cannot be resolved. + +**Solution**: Register repository services: + +```csharp +services.AddEFRepository(); +``` + +#### 4. Audit Records Not Saved + +**Problem**: Audit entries are created but not persisted. + +**Solution**: Ensure audit store is properly configured: + +```csharp +services.AddEFAutoAudit(builder => +{ + builder.WithStore(); // Make sure store is configured +}); +``` + +### Performance Tips + +1. **Use projections** for read-only operations to reduce data transfer +2. **Implement paging** for large datasets +3. **Use bulk operations** for multiple entity updates +4. **Configure appropriate indexes** for frequently queried properties +5. **Consider using `AsNoTracking()`** for read-only queries + +### Migration Notes + +When upgrading between major versions, review the [Release Notes](ReleaseNotes.md) for breaking changes and migration guidance. From 00016fed7c8903ff1c969284c0d82d7291cdeeb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:50:28 +0000 Subject: [PATCH 22/32] Finalize documentation with navigation links and complete structure Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- README.md | 2 ++ docs/AdvancedFeatures.md | 8 +++++++- docs/GettingStarted.md | 1 + docs/Usage.md | 6 ++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3202393..43d376e 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ See Releases/PRs for details πŸ“– **[Complete Usage Guide](docs/Usage.md)** - Comprehensive documentation with examples for all features +⚑ **[Advanced Features Guide](docs/AdvancedFeatures.md)** - Custom interceptors, performance optimization, and integration patterns + πŸ“‹ **[Release Notes](docs/ReleaseNotes.md)** - Version history and breaking changes πŸ”§ **[Sample Project](samples/WeihanLi.EntityFramework.Sample/)** - Working examples and demonstrations diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md index 80d34b2..fe27b5c 100644 --- a/docs/AdvancedFeatures.md +++ b/docs/AdvancedFeatures.md @@ -826,4 +826,10 @@ public class ProductQueryService } ``` -These advanced patterns help you build robust, scalable applications while leveraging the full power of WeihanLi.EntityFramework. \ No newline at end of file +These advanced patterns help you build robust, scalable applications while leveraging the full power of WeihanLi.EntityFramework. + +## Related Documentation + +- πŸš€ [Getting Started Guide](GettingStarted.md) - New to WeihanLi.EntityFramework? Start here +- πŸ“– [Complete Usage Guide](Usage.md) - Comprehensive documentation with examples for all features +- πŸ“‹ [Release Notes](ReleaseNotes.md) - Version history and breaking changes \ No newline at end of file diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 06f9ffd..4fead75 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -375,6 +375,7 @@ When you run this setup, WeihanLi.EntityFramework will automatically: ## Next Steps - πŸ“– Read the [Complete Usage Guide](Usage.md) for advanced features +- ⚑ Explore [Advanced Features Guide](AdvancedFeatures.md) for custom interceptors and optimization - πŸ” Explore the [sample project](../samples/WeihanLi.EntityFramework.Sample/) for more examples - πŸ› οΈ Check out bulk operations, advanced querying, and custom audit stores - πŸ“‹ Review [Release Notes](ReleaseNotes.md) for version-specific information diff --git a/docs/Usage.md b/docs/Usage.md index 6620dcc..10a1ac4 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -1458,3 +1458,9 @@ services.AddEFAutoAudit(builder => ### Migration Notes When upgrading between major versions, review the [Release Notes](ReleaseNotes.md) for breaking changes and migration guidance. + +## Related Documentation + +- πŸš€ [Getting Started Guide](GettingStarted.md) - New to WeihanLi.EntityFramework? Start here +- ⚑ [Advanced Features Guide](AdvancedFeatures.md) - Custom interceptors, performance optimization, and integration patterns +- πŸ“‹ [Release Notes](ReleaseNotes.md) - Version history and breaking changes From c5e99b8f659901fa3c99c257e4f01c4fd24664c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 02:00:48 +0000 Subject: [PATCH 23/32] Remove discussion reference from README as discussions are not enabled Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 43d376e..771355e 100644 --- a/README.md +++ b/README.md @@ -171,9 +171,7 @@ See Releases/PRs for details πŸ’‘ **Questions?** Check out the [Usage Guide](docs/Usage.md) for detailed examples -πŸ› **Found a bug?** [Create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps - -πŸ’¬ **Need help?** Feel free to [start a discussion](https://github.com/WeihanLi/WeihanLi.EntityFramework/discussions) or create an issue +πŸ› **Found a bug or need help?** Feel free to [create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps ## Usage From 63777107f14b3a50a90ef69ebf7fbd35c84409bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 02:07:50 +0000 Subject: [PATCH 24/32] Remove non-existent property value formatter from audit documentation Co-authored-by: WeihanLi <7604648+WeihanLi@users.noreply.github.com> --- docs/Usage.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/Usage.md b/docs/Usage.md index 10a1ac4..63475eb 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -528,10 +528,7 @@ services.AddEFAutoAudit(builder => .IgnoreProperty("CreatedAt") // Include unmodified properties (default is false) - .WithUnModifiedProperty() - - // Configure property value formatting - .WithPropertyValueFormatter(); + .WithUnModifiedProperty(); }); ``` From e105b89e509d5b9f488a89a4128d4a55c829058f Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Sat, 16 Aug 2025 23:56:58 +0800 Subject: [PATCH 25/32] feat: bump dependencies --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 3038510..17252d5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,8 +6,8 @@ preview enable enable - 1.0.80 - 10.0.0-preview.6.25358.103 + 1.0.81 + 10.0.0-preview.7.25380.108 true true From 115ec840a88b7a11cb3d0f61e8e7e83e8be3ea59 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Sun, 17 Aug 2025 00:02:18 +0800 Subject: [PATCH 26/32] feat: bump xunit dependency --- .../WeihanLi.EntityFramework.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index e400c7f..ed8fc0e 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -13,7 +13,7 @@ - + From d2980bf6fc2692c13ff3ef95346f5f03345dc158 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Sun, 17 Aug 2025 00:09:31 +0800 Subject: [PATCH 27/32] build: simplify build scripts --- build.ps1 | 2 +- build.sh | 2 +- build/build.cs | 113 +++---------------------------------------------- 3 files changed, 8 insertions(+), 109 deletions(-) diff --git a/build.ps1 b/build.ps1 index fe9674a..ee2960a 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 --prerelease Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN diff --git a/build.sh b/build.sh index ed717f1..4fee4d3 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 --prerelease export PATH="$PATH:$HOME/.dotnet/tools" echo "dotnet-exec $SCRIPT --args=$@" diff --git a/build/build.cs b/build/build.cs index e0fa3d9..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.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); From 2768a0640539b6826c424ab41df756e6ca37d52b Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Tue, 19 Aug 2025 12:01:18 +0800 Subject: [PATCH 28/32] build: remove prerelease flag for dotnet-exec tool --- build.ps1 | 16 ++++++++-------- build.sh | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.ps1 b/build.ps1 index ee2960a..4ac8cdf 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,8 +1,8 @@ -[string]$SCRIPT = '.\build\build.cs' - -# Install dotnet tool -dotnet tool install --global dotnet-execute --prerelease - -Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN - -dotnet-exec $SCRIPT --args $ARGS +[string]$SCRIPT = '.\build\build.cs' + +# Install dotnet tool +dotnet tool install --global dotnet-execute + +Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN + +dotnet-exec $SCRIPT --args $ARGS diff --git a/build.sh b/build.sh index 4fee4d3..8d287c2 100644 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ SCRIPT='./build/build.cs' # Install tool -dotnet tool install --global dotnet-execute --prerelease +dotnet tool install --global dotnet-execute export PATH="$PATH:$HOME/.dotnet/tools" echo "dotnet-exec $SCRIPT --args=$@" From 0b1eecdb7aed50c2a19c59b282e66e7d9b4f29cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 04:01:58 +0000 Subject: [PATCH 29/32] Automated dotnet-format update from commit 2768a0640539b6826c424ab41df756e6ca37d52b on refs/heads/dev --- build.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.ps1 b/build.ps1 index 4ac8cdf..2f32d77 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,8 +1,8 @@ -[string]$SCRIPT = '.\build\build.cs' - -# Install dotnet tool -dotnet tool install --global dotnet-execute - -Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN - -dotnet-exec $SCRIPT --args $ARGS +[string]$SCRIPT = '.\build\build.cs' + +# Install dotnet tool +dotnet tool install --global dotnet-execute + +Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN + +dotnet-exec $SCRIPT --args $ARGS From 760816003d10c31b260db696ea1d855bd73fb31a Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 11 Sep 2025 08:17:42 +0800 Subject: [PATCH 30/32] Update CommonVersion and EFVersion in build props --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 17252d5..f51cf99 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,8 +6,8 @@ preview enable enable - 1.0.81 - 10.0.0-preview.7.25380.108 + 1.0.82 + 10.0.0-rc.1.25451.107 true true From 0e9b9cc37f85b378679404f88d396f6675651e62 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Tue, 11 Nov 2025 22:03:32 +0800 Subject: [PATCH 31/32] upgrade dependencies --- Directory.Build.props | 5 ++--- .../WeihanLi.EntityFramework.Test.csproj | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index f51cf99..d26e6fd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,9 +6,8 @@ preview enable enable - 1.0.82 - 10.0.0-rc.1.25451.107 - + 1.0.83 + 10.0.0 true true true diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index ed8fc0e..0aaed8c 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -12,8 +12,8 @@ - - + + From 5e90ca05aab7d191361e0babd1b286034e96d283 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Wed, 12 Nov 2025 11:31:24 +0800 Subject: [PATCH 32/32] bump dependencies --- Directory.Build.props | 2 +- .../WeihanLi.EntityFramework.Test.csproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d26e6fd..4ef292b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ preview enable enable - 1.0.83 + 1.0.84 10.0.0 true true diff --git a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj index 0aaed8c..bfcbe6e 100644 --- a/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj +++ b/test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj @@ -5,15 +5,15 @@ false exe disable - + true true - - + +