├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── default.yml │ ├── dotnet-format.yml │ └── release.yml ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── README.md ├── WeihanLi.EntityFramework.sln.DotSettings ├── WeihanLi.EntityFramework.slnx ├── azure-pipelines.yml ├── build.ps1 ├── build.sh ├── build ├── build.cs └── version.props ├── docs ├── AdvancedFeatures.md ├── GettingStarted.md ├── ReleaseNotes.md └── Usage.md ├── nuget.config ├── samples └── WeihanLi.EntityFramework.Sample │ ├── AutoAuditContext1.cs │ ├── DbContextInterceptorSamples.cs │ ├── Program.cs │ ├── SoftDeleteSampleContext.cs │ ├── TestDbContext.cs │ └── WeihanLi.EntityFramework.Sample.csproj ├── src ├── WeihanLi.EntityFramework.SourceGenerator │ ├── EFExtensionsGenerator.cs │ └── WeihanLi.EntityFramework.SourceGenerator.csproj └── WeihanLi.EntityFramework │ ├── Audit │ ├── AuditDbContext.cs │ ├── AuditEntry.cs │ ├── AuditExtensions.cs │ ├── AuditInterceptor.cs │ ├── AuditRecord.cs │ ├── AuditRecordsDbContext.cs │ ├── IAuditConfig.cs │ ├── IAuditPropertyEnricher.cs │ └── IAuditStore.cs │ ├── DbContextBase.cs │ ├── DbContextOptionsConfiguration.cs │ ├── DbFunctions.cs │ ├── EFExtensions.cs │ ├── EFInternalExtensions.cs │ ├── EFRepository.cs │ ├── EFRepositoryExtensions.cs │ ├── EFRepositoryFactory.cs │ ├── EFRepositoryFactoryExtensions.cs │ ├── EFRepositoryGenerator.cs │ ├── EFRepositoryGeneratorExtensions.cs │ ├── EFRepositoryGeneratorOptions.cs │ ├── EFRepositoryQueryBuilder.cs │ ├── EFUnitOfWork.cs │ ├── EFUnitOfWorkExtensions.cs │ ├── IEFRepository.cs │ ├── IEFRepositoryBuilder.cs │ ├── IEFRepositoryFactory.cs │ ├── IEFRepositoryGenerator.cs │ ├── IEFUnitOfWork.cs │ ├── IQueryablePageListExtensions.cs │ ├── Interceptors │ ├── AutoUpdateInterceptor.cs │ └── SoftDeleteInterceptor.cs │ ├── InternalHelper.cs │ ├── ServiceCollectionExtension.cs │ ├── Services │ ├── IEntitySavingHandler.cs │ ├── SoftDeleteSavingHandler.cs │ ├── UpdatedAtEntitySavingHandler.cs │ └── UpdatedByEntitySavingHandler.cs │ └── WeihanLi.EntityFramework.csproj └── test └── WeihanLi.EntityFramework.Test ├── EFExtensions.cs ├── EFExtensionsTest.cs ├── EFRepositoryTest.cs ├── EFTestBase.cs ├── EFTestFixture.cs ├── EFUnitOfWorkTest.cs ├── RelationalTest.cs ├── TestDbContext.cs ├── WeihanLi.EntityFramework.Test.csproj └── xunit.runner.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodeSpace", 3 | "image": "mcr.microsoft.com/dotnet/sdk:10.0-preview", 4 | // Install needed extensions 5 | "extensions": [ 6 | "ms-dotnettools.csharp", 7 | "davidanson.vscode-markdownlint" 8 | ] 9 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8-bom 16 | 17 | # Xml project files 18 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 19 | indent_size = 2 20 | 21 | # Xml config files 22 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 23 | indent_size = 2 24 | 25 | # JSON files 26 | [*.json] 27 | indent_size = 2 28 | 29 | # Dotnet code style settings: 30 | [*.{cs,vb}] 31 | # namespace style 32 | csharp_style_namespace_declarations=file_scoped:warning 33 | 34 | # Sort using and Import directives with System.* appearing first 35 | dotnet_sort_system_directives_first = false 36 | # Avoid "this." and "Me." if not necessary 37 | dotnet_style_qualification_for_field = false:suggestion 38 | dotnet_style_qualification_for_property = false:suggestion 39 | dotnet_style_qualification_for_method = false:suggestion 40 | dotnet_style_qualification_for_event = false:suggestion 41 | 42 | # Use language keywords instead of framework type names for type references 43 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 44 | dotnet_style_predefined_type_for_member_access = true:suggestion 45 | 46 | # Suggest more modern language features when available 47 | dotnet_style_object_initializer = true:suggestion 48 | dotnet_style_collection_initializer = true:suggestion 49 | dotnet_style_coalesce_expression = true:suggestion 50 | dotnet_style_null_propagation = true:suggestion 51 | dotnet_style_explicit_tuple_names = true:suggestion 52 | 53 | # CSharp code style settings: 54 | [*.cs] 55 | # Prefer "var" everywhere 56 | csharp_style_var_for_built_in_types = true:suggestion 57 | csharp_style_var_when_type_is_apparent = true:suggestion 58 | csharp_style_var_elsewhere = true:suggestion 59 | 60 | # Prefer method-like constructs to have a block body 61 | csharp_style_expression_bodied_methods = false:none 62 | csharp_style_expression_bodied_constructors = false:none 63 | csharp_style_expression_bodied_operators = false:none 64 | 65 | # Prefer property-like constructs to have an expression-body 66 | csharp_style_expression_bodied_properties = true:none 67 | csharp_style_expression_bodied_indexers = true:none 68 | csharp_style_expression_bodied_accessors = true:none 69 | 70 | # Suggest more modern language features when available 71 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 72 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 73 | csharp_style_inlined_variable_declaration = true:suggestion 74 | csharp_style_throw_expression = true:suggestion 75 | csharp_style_conditional_delegate_call = true:suggestion 76 | 77 | # Newline settings 78 | csharp_new_line_before_open_brace = all 79 | csharp_new_line_before_else = true 80 | csharp_new_line_before_catch = true 81 | csharp_new_line_before_finally = true 82 | csharp_new_line_before_members_in_object_initializers = true 83 | csharp_new_line_before_members_in_anonymous_types = true 84 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | *.sh text eol=lf 7 | *.ps1 text eol=crlf 8 | 9 | ############################################################################### 10 | # Set default behavior for command prompt diff. 11 | # 12 | # This is need for earlier builds of msysgit that does not have it on by 13 | # default for csharp files. 14 | # Note: This is only used by command line 15 | ############################################################################### 16 | #*.cs diff=csharp 17 | 18 | ############################################################################### 19 | # Set the merge driver for project and solution files 20 | # 21 | # Merging from the command prompt will add diff markers to the files if there 22 | # are conflicts (Merging from VS is not affected by the settings below, in VS 23 | # the diff markers are never inserted). Diff markers may cause the following 24 | # file extensions to fail to load in VS. An alternative would be to treat 25 | # these files as binary and thus will always conflict and require user 26 | # intervention with every merge. To do so, just uncomment the entries below 27 | ############################################################################### 28 | #*.sln merge=binary 29 | #*.csproj merge=binary 30 | #*.vbproj merge=binary 31 | #*.vcxproj merge=binary 32 | #*.vcproj merge=binary 33 | #*.dbproj merge=binary 34 | #*.fsproj merge=binary 35 | #*.lsproj merge=binary 36 | #*.wixproj merge=binary 37 | #*.modelproj merge=binary 38 | #*.sqlproj merge=binary 39 | #*.wwaproj merge=binary 40 | 41 | ############################################################################### 42 | # behavior for image files 43 | # 44 | # image files are treated as binary by default. 45 | ############################################################################### 46 | #*.jpg binary 47 | #*.png binary 48 | #*.gif binary 49 | 50 | ############################################################################### 51 | # diff behavior for common document formats 52 | # 53 | # Convert binary document formats to text before diffing them. This feature 54 | # is only available from the command line. Turn it on by uncommenting the 55 | # entries below. 56 | ############################################################################### 57 | #*.doc diff=astextplain 58 | #*.DOC diff=astextplain 59 | #*.docx diff=astextplain 60 | #*.DOCX diff=astextplain 61 | #*.dot diff=astextplain 62 | #*.DOT diff=astextplain 63 | #*.pdf diff=astextplain 64 | #*.PDF diff=astextplain 65 | #*.rtf diff=astextplain 66 | #*.RTF diff=astextplain 67 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: default 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup .NET SDK 13 | uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: 10.0.x 16 | - name: dotnet info 17 | run: dotnet --info 18 | - name: build 19 | run: bash build.sh 20 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-format.yml: -------------------------------------------------------------------------------- 1 | name: dotnet-format 2 | 3 | on: "push" 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Setup .NET SDK 11 | uses: actions/setup-dotnet@v4 12 | with: 13 | dotnet-version: 10.0.x 14 | - name: build 15 | run: dotnet build 16 | - name: format 17 | run: dotnet format 18 | - name: check for changes 19 | run: | 20 | if git diff --exit-code; then 21 | echo "has_changes=false" >> $GITHUB_ENV 22 | else 23 | echo "has_changes=true" >> $GITHUB_ENV 24 | fi 25 | - name: Commit and Push 26 | if: ${{ env.has_changes == 'true' }} 27 | shell: bash 28 | run: | 29 | # echo $GITHUB_REF_NAME 30 | # echo $GITHUB_SHA 31 | git config --local user.name "github-actions[bot]" 32 | git config --local user.email "weihanli@outlook.com" 33 | git add -u 34 | git commit -m "Automated dotnet-format update from commit ${GITHUB_SHA} on ${GITHUB_REF}" 35 | git log -1 36 | remote_repo="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 37 | git push "${remote_repo}" HEAD:${GITHUB_REF} 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ master ] 6 | jobs: 7 | build: 8 | name: Release 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup .NET SDK 13 | uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: 10.0.x 16 | - name: Build 17 | shell: pwsh 18 | run: .\build.ps1 --stable=true 19 | - name: Get Release Version 20 | shell: pwsh 21 | run: dotnet-exec https://github.com/OpenReservation/scripts/blob/main/build/export-gh-release-version.cs 22 | - name: create release 23 | shell: pwsh 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | run: | 27 | gh release create ${{ env.ReleaseVersion }} --generate-notes --target master (Get-Item ./artifacts/packages/*.nupkg) 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | tools/** 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | 331 | # sqlite db file 332 | *.db 333 | *.db-shm 334 | *.db-wal 335 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net10.0 6 | preview 7 | enable 8 | enable 9 | 1.0.82 10 | 10.0.0-rc.1.25451.107 11 | 12 | true 13 | true 14 | true 15 | snupkg 16 | 17 | WeihanLi 18 | WeihanLi 19 | WeihanLi.EntityFramework 20 | Apache-2.0 21 | https://github.com/WeihanLi/WeihanLi.EntityFramework 22 | git 23 | Copyright 2019-$([System.DateTime]::Now.Year) (c) WeihanLi 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeihanLi.EntityFramework 2 | 3 | [![WeihanLi.EntityFramework](https://img.shields.io/nuget/v/WeihanLi.EntityFramework.svg)](https://www.nuget.org/packages/WeihanLi.EntityFramework/) 4 | 5 | [![WeihanLi.EntityFramework Latest](https://img.shields.io/nuget/vpre/WeihanLi.EntityFramework)](https://www.nuget.org/packages/WeihanLi.EntityFramework/absoluteLatest) 6 | 7 | [![Pipeline Build Status](https://weihanli.visualstudio.com/Pipelines/_apis/build/status/WeihanLi.WeihanLi.EntityFramework?branchName=dev)](https://weihanli.visualstudio.com/Pipelines/_build/latest?definitionId=11&branchName=dev) 8 | 9 | ![Github Build Status](https://github.com/WeihanLi/WeihanLi.EntityFramework/workflows/default/badge.svg) 10 | 11 | ## Intro 12 | 13 | [EntityFrameworkCore](https://github.com/dotnet/efcore) extensions that provide a comprehensive set of tools and patterns to enhance your Entity Framework Core development experience. 14 | 15 | WeihanLi.EntityFramework offers: 16 | 17 | - **Repository Pattern** - Clean abstraction layer for data access 18 | - **Unit of Work Pattern** - Transaction management across multiple repositories 19 | - **Automatic Auditing** - Track all entity changes with flexible storage options 20 | - **Auto-Update Features** - Automatic handling of CreatedAt/UpdatedAt timestamps and user tracking 21 | - **Soft Delete** - Mark entities as deleted without physical removal 22 | - **Database Extensions** - Convenient methods for bulk operations and queries 23 | - **Database Functions** - SQL Server JSON operations and more 24 | 25 | ## Quick Start 26 | 27 | ### 1. Installation 28 | 29 | ```bash 30 | dotnet add package WeihanLi.EntityFramework 31 | ``` 32 | 33 | ### 2. Basic Setup 34 | 35 | ```csharp 36 | // Program.cs 37 | var builder = WebApplication.CreateBuilder(args); 38 | 39 | // Add DbContext 40 | builder.Services.AddDbContext(options => 41 | options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); 42 | 43 | // Add WeihanLi.EntityFramework services 44 | builder.Services.AddEFRepository(); 45 | builder.Services.AddEFAutoUpdateInterceptor(); 46 | builder.Services.AddEFAutoAudit(auditBuilder => 47 | { 48 | auditBuilder.WithUserIdProvider() 49 | .WithStore(); 50 | }); 51 | 52 | var app = builder.Build(); 53 | ``` 54 | 55 | ### 3. Define Your Entities 56 | 57 | ```csharp 58 | public class Product : IEntityWithCreatedUpdatedAt, ISoftDeleteEntityWithDeleted 59 | { 60 | public int Id { get; set; } 61 | public string Name { get; set; } = string.Empty; 62 | public decimal Price { get; set; } 63 | 64 | // Auto-update properties 65 | public DateTimeOffset CreatedAt { get; set; } 66 | public DateTimeOffset UpdatedAt { get; set; } 67 | 68 | // Soft delete property 69 | public bool IsDeleted { get; set; } 70 | } 71 | ``` 72 | 73 | ### 4. Use Repository Pattern 74 | 75 | ```csharp 76 | public class ProductService 77 | { 78 | private readonly IEFRepository _repository; 79 | 80 | public ProductService(IEFRepository repository) 81 | { 82 | _repository = repository; 83 | } 84 | 85 | public async Task CreateProductAsync(string name, decimal price) 86 | { 87 | var product = new Product { Name = name, Price = price }; 88 | return await _repository.InsertAsync(product); 89 | // CreatedAt/UpdatedAt automatically set, audit record created 90 | } 91 | 92 | public async Task> GetActiveProductsAsync() 93 | { 94 | return await _repository.GetListAsync( 95 | queryBuilder => queryBuilder.WithPredict(p => p.Price > 0) 96 | ); 97 | // Soft deleted products automatically filtered out 98 | } 99 | } 100 | ``` 101 | 102 | ## Package Release Notes 103 | 104 | See Releases/PRs for details 105 | 106 | - Releases: https://github.com/WeihanLi/WeihanLi.EntityFramework/releases 107 | - PRs: https://github.com/WeihanLi/WeihanLi.EntityFramework/pulls?q=is%3Apr+is%3Aclosed+is%3Amerged+base%3Amaster 108 | 109 | > Package Versions 110 | > 111 | > For EF 8 and above, use 8.x or above major-version matched versions 112 | > 113 | > For EF 7, use 3.x 114 | > 115 | > For EF Core 5/6, use 2.x 116 | > 117 | > For EF Core 3.x, use 1.5.0 above, and 2.0.0 below 118 | > 119 | > For EF Core 2.x , use 1.4.x and below 120 | 121 | ## Features 122 | 123 | ### 🏗️ Repository Pattern 124 | - `IEFRepository` - Generic repository interface 125 | - `EFRepository` - Full-featured repository implementation 126 | - `EFRepositoryGenerator` - Dynamic repository creation 127 | - **Query Builder** - Fluent API for complex queries 128 | - **Bulk Operations** - Efficient batch updates and deletes 129 | 130 | ### 🔄 Unit of Work Pattern 131 | - `IEFUnitOfWork` - Transaction management 132 | - **Multi-Repository Transactions** - Coordinate changes across entities 133 | - **Rollback Support** - Automatic error handling 134 | 135 | ### 📋 Comprehensive Auditing 136 | - **Automatic Change Tracking** - Monitor all entity modifications 137 | - **Flexible Storage** - Database, file, console, or custom stores 138 | - **Property Enrichment** - Add custom metadata to audit records 139 | - **User Tracking** - Capture who made changes 140 | - **Configurable Filtering** - Include/exclude entities and properties 141 | 142 | ### ⚡ Auto-Update Features 143 | - **Timestamp Management** - Automatic CreatedAt/UpdatedAt handling 144 | - **User Tracking** - Automatic CreatedBy/UpdatedBy population 145 | - **Soft Delete** - Mark entities as deleted without removal 146 | - **Custom Auto-Update** - Define your own auto-update rules 147 | 148 | ### 🔧 Database Extensions 149 | - **Column Updates** - Update specific columns only 150 | - **Bulk Operations** - Efficient mass updates 151 | - **Query Helpers** - Get table/column names, check database type 152 | - **Paging Support** - Built-in pagination for large datasets 153 | 154 | ### 🗄️ Database Functions 155 | - **JSON Support** - `JSON_VALUE` for SQL Server 2016+ 156 | - **SQL Server Functions** - Enhanced querying capabilities 157 | 158 | ## Documentation 159 | 160 | 🚀 **[Getting Started Guide](docs/GettingStarted.md)** - Step-by-step setup instructions for new users 161 | 162 | 📖 **[Complete Usage Guide](docs/Usage.md)** - Comprehensive documentation with examples for all features 163 | 164 | ⚡ **[Advanced Features Guide](docs/AdvancedFeatures.md)** - Custom interceptors, performance optimization, and integration patterns 165 | 166 | 📋 **[Release Notes](docs/ReleaseNotes.md)** - Version history and breaking changes 167 | 168 | 🔧 **[Sample Project](samples/WeihanLi.EntityFramework.Sample/)** - Working examples and demonstrations 169 | 170 | ## Support 171 | 172 | 💡 **Questions?** Check out the [Usage Guide](docs/Usage.md) for detailed examples 173 | 174 | 🐛 **Found a bug or need help?** Feel free to [create an issue](https://github.com/WeihanLi/WeihanLi.EntityFramework/issues/new) with reproduction steps 175 | 176 | ## Usage 177 | 178 | For detailed usage instructions, please refer to the [Usage Documentation](docs/Usage.md). 179 | -------------------------------------------------------------------------------- /WeihanLi.EntityFramework.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | EF 3 | True -------------------------------------------------------------------------------- /WeihanLi.EntityFramework.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - '*' # must quote since "*" is a YAML reserved character; we want a string 5 | 6 | pool: 7 | vmImage: 'windows-latest' 8 | 9 | steps: 10 | - task: UseDotNet@2 11 | displayName: 'Use .NET sdk' 12 | inputs: 13 | packageType: sdk 14 | version: 10.0.x 15 | includePreviewVersions: true 16 | 17 | - script: dotnet --info 18 | displayName: 'dotnet info' 19 | 20 | - powershell: ./build.ps1 21 | displayName: 'Powershell Script' 22 | env: 23 | Nuget__ApiKey: $(nugetApiKey) 24 | Nuget__SourceUrl: $(nugetSourceUrl) 25 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [string]$SCRIPT = '.\build\build.cs' 2 | 3 | # Install dotnet tool 4 | dotnet tool install --global dotnet-execute 5 | 6 | Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN 7 | 8 | dotnet-exec $SCRIPT --args $ARGS 9 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT='./build/build.cs' 3 | 4 | # Install tool 5 | dotnet tool install --global dotnet-execute 6 | export PATH="$PATH:$HOME/.dotnet/tools" 7 | 8 | echo "dotnet-exec $SCRIPT --args=$@" 9 | 10 | dotnet-exec $SCRIPT --args="$@" 11 | -------------------------------------------------------------------------------- /build/build.cs: -------------------------------------------------------------------------------- 1 | var solutionPath = "./WeihanLi.EntityFramework.slnx"; 2 | string[] srcProjects = [ 3 | "./src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj" 4 | ]; 5 | string[] testProjects = [ "./test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj" ]; 6 | 7 | await DotNetPackageBuildProcess 8 | .Create(options => 9 | { 10 | options.SolutionPath = solutionPath; 11 | options.SrcProjects = srcProjects; 12 | options.TestProjects = testProjects; 13 | }) 14 | .ExecuteAsync(args); 15 | -------------------------------------------------------------------------------- /build/version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 4 | 0 5 | 0 6 | 0 7 | $(VersionMajor).$(VersionMinor).$(VersionPatch) 8 | $(VersionMajor).$(VersionMinor).$(VersionPatch).$(VersionRevision) 9 | dev 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started with WeihanLi.EntityFramework 2 | 3 | This guide will help you get up and running with WeihanLi.EntityFramework quickly. 4 | 5 | ## Prerequisites 6 | 7 | - .NET 8.0 or later 8 | - Entity Framework Core 8.0 or later 9 | - A supported database provider (SQL Server, SQLite, PostgreSQL, etc.) 10 | 11 | ## Step 1: Installation 12 | 13 | Add the package to your project: 14 | 15 | ```bash 16 | dotnet add package WeihanLi.EntityFramework 17 | ``` 18 | 19 | ## Step 2: Define Your Entities 20 | 21 | Create your entity classes and implement the desired interfaces for automatic features: 22 | 23 | ```csharp 24 | using WeihanLi.EntityFramework; 25 | 26 | // Basic entity with auto-update timestamps 27 | public class Product : IEntityWithCreatedUpdatedAt 28 | { 29 | public int Id { get; set; } 30 | public string Name { get; set; } = string.Empty; 31 | public decimal Price { get; set; } 32 | public bool IsActive { get; set; } = true; 33 | 34 | // These will be automatically managed 35 | public DateTimeOffset CreatedAt { get; set; } 36 | public DateTimeOffset UpdatedAt { get; set; } 37 | } 38 | 39 | // Entity with soft delete capability 40 | public class Category : ISoftDeleteEntityWithDeleted 41 | { 42 | public int Id { get; set; } 43 | public string Name { get; set; } = string.Empty; 44 | public string Description { get; set; } = string.Empty; 45 | 46 | // Soft delete property 47 | public bool IsDeleted { get; set; } 48 | } 49 | 50 | // Entity with full audit trail 51 | public class Order : IEntityWithCreatedUpdatedAt, IEntityWithCreatedUpdatedBy 52 | { 53 | public int Id { get; set; } 54 | public int CustomerId { get; set; } 55 | public decimal TotalAmount { get; set; } 56 | public OrderStatus Status { get; set; } 57 | 58 | // Auto-managed timestamp fields 59 | public DateTimeOffset CreatedAt { get; set; } 60 | public DateTimeOffset UpdatedAt { get; set; } 61 | 62 | // Auto-managed user tracking fields 63 | public string CreatedBy { get; set; } = string.Empty; 64 | public string UpdatedBy { get; set; } = string.Empty; 65 | 66 | // Navigation properties 67 | public List Items { get; set; } = new(); 68 | } 69 | 70 | public enum OrderStatus 71 | { 72 | Pending, 73 | Processing, 74 | Shipped, 75 | Delivered, 76 | Cancelled 77 | } 78 | ``` 79 | 80 | ## Step 3: Configure Your DbContext 81 | 82 | Set up your DbContext with the necessary configurations: 83 | 84 | ```csharp 85 | using Microsoft.EntityFrameworkCore; 86 | using WeihanLi.EntityFramework.Audit; 87 | 88 | public class AppDbContext : AuditDbContext 89 | { 90 | public AppDbContext(DbContextOptions options, IServiceProvider serviceProvider) 91 | : base(options, serviceProvider) 92 | { 93 | } 94 | 95 | public DbSet Products { get; set; } 96 | public DbSet Categories { get; set; } 97 | public DbSet Orders { get; set; } 98 | public DbSet OrderItems { get; set; } 99 | public DbSet AuditRecords { get; set; } 100 | 101 | protected override void OnModelCreating(ModelBuilder modelBuilder) 102 | { 103 | base.OnModelCreating(modelBuilder); 104 | 105 | // Configure Product entity 106 | modelBuilder.Entity(entity => 107 | { 108 | entity.HasKey(e => e.Id); 109 | entity.Property(e => e.Name).IsRequired().HasMaxLength(200); 110 | entity.Property(e => e.Price).HasPrecision(18, 2); 111 | entity.HasIndex(e => e.Name); 112 | }); 113 | 114 | // Configure Category with soft delete filter 115 | modelBuilder.Entity(entity => 116 | { 117 | entity.HasKey(e => e.Id); 118 | entity.Property(e => e.Name).IsRequired().HasMaxLength(100); 119 | entity.HasQueryFilter(c => !c.IsDeleted); // Global soft delete filter 120 | }); 121 | 122 | // Configure Order entity 123 | modelBuilder.Entity(entity => 124 | { 125 | entity.HasKey(e => e.Id); 126 | entity.Property(e => e.TotalAmount).HasPrecision(18, 2); 127 | entity.HasMany(e => e.Items) 128 | .WithOne() 129 | .HasForeignKey("OrderId"); 130 | }); 131 | } 132 | } 133 | ``` 134 | 135 | ## Step 4: Configure Services 136 | 137 | Set up dependency injection in your `Program.cs`: 138 | 139 | ```csharp 140 | using WeihanLi.EntityFramework; 141 | using WeihanLi.EntityFramework.Audit; 142 | 143 | var builder = WebApplication.CreateBuilder(args); 144 | 145 | // Add Entity Framework 146 | builder.Services.AddDbContext((provider, options) => 147 | { 148 | options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) 149 | .AddInterceptors( 150 | provider.GetRequiredService(), 151 | provider.GetRequiredService() 152 | ); 153 | }); 154 | 155 | // Add WeihanLi.EntityFramework services 156 | builder.Services.AddEFRepository(); 157 | builder.Services.AddEFAutoUpdateInterceptor(); 158 | 159 | // Configure user ID provider for audit and auto-update 160 | builder.Services.AddHttpContextAccessor(); 161 | builder.Services.AddSingleton(); 162 | 163 | // Configure audit system 164 | builder.Services.AddEFAutoAudit(auditBuilder => 165 | { 166 | auditBuilder 167 | .WithUserIdProvider() 168 | .EnrichWithProperty("ApplicationName", "MyApplication") 169 | .EnrichWithProperty("MachineName", Environment.MachineName) 170 | .WithStore() // Store audit records in database 171 | .IgnoreEntity(); // Don't audit the audit records themselves 172 | }); 173 | 174 | var app = builder.Build(); 175 | 176 | // Ensure database is created (for development) 177 | using (var scope = app.Services.CreateScope()) 178 | { 179 | var context = scope.ServiceProvider.GetRequiredService(); 180 | context.Database.EnsureCreated(); 181 | } 182 | 183 | app.Run(); 184 | ``` 185 | 186 | ## Step 5: Create a User ID Provider 187 | 188 | Implement a user ID provider to track who makes changes: 189 | 190 | ```csharp 191 | using System.Security.Claims; 192 | using WeihanLi.Common.Services; 193 | 194 | public class HttpContextUserIdProvider : IUserIdProvider 195 | { 196 | private readonly IHttpContextAccessor _httpContextAccessor; 197 | 198 | public HttpContextUserIdProvider(IHttpContextAccessor httpContextAccessor) 199 | { 200 | _httpContextAccessor = httpContextAccessor; 201 | } 202 | 203 | public string GetUserId() 204 | { 205 | var user = _httpContextAccessor.HttpContext?.User; 206 | 207 | // Try to get user ID from claims 208 | var userId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value 209 | ?? user?.FindFirst("sub")?.Value 210 | ?? user?.Identity?.Name; 211 | 212 | return userId ?? "Anonymous"; 213 | } 214 | } 215 | ``` 216 | 217 | ## Step 6: Create Your First Service 218 | 219 | Create a service that uses the repository pattern: 220 | 221 | ```csharp 222 | using WeihanLi.EntityFramework; 223 | 224 | public class ProductService 225 | { 226 | private readonly IEFRepository _productRepository; 227 | private readonly IEFUnitOfWork _unitOfWork; 228 | 229 | public ProductService( 230 | IEFRepository productRepository, 231 | IEFUnitOfWork unitOfWork) 232 | { 233 | _productRepository = productRepository; 234 | _unitOfWork = unitOfWork; 235 | } 236 | 237 | public async Task CreateProductAsync(string name, decimal price) 238 | { 239 | var product = new Product 240 | { 241 | Name = name, 242 | Price = price, 243 | IsActive = true 244 | // CreatedAt and UpdatedAt will be set automatically 245 | }; 246 | 247 | return await _productRepository.InsertAsync(product); 248 | } 249 | 250 | public async Task GetProductByIdAsync(int id) 251 | { 252 | return await _productRepository.FindAsync(id); 253 | } 254 | 255 | public async Task> GetActiveProductsAsync() 256 | { 257 | return await _productRepository.GetListAsync( 258 | queryBuilder => queryBuilder.WithPredict(p => p.IsActive) 259 | ); 260 | } 261 | 262 | public async Task UpdateProductPriceAsync(int productId, decimal newPrice) 263 | { 264 | var product = await _productRepository.FindAsync(productId); 265 | if (product == null) return false; 266 | 267 | product.Price = newPrice; 268 | // UpdatedAt will be set automatically 269 | 270 | var result = await _productRepository.UpdateAsync(product); 271 | return result > 0; 272 | } 273 | 274 | public async Task DeactivateProductAsync(int productId) 275 | { 276 | // Use bulk update for efficiency 277 | var result = await _productRepository.UpdateAsync( 278 | setters => setters.SetProperty(p => p.IsActive, false), 279 | queryBuilder => queryBuilder.WithPredict(p => p.Id == productId) 280 | ); 281 | 282 | return result > 0; 283 | } 284 | 285 | public async Task> GetProductsPagedAsync(int page, int pageSize) 286 | { 287 | return await _productRepository.GetPagedListAsync( 288 | queryBuilder => queryBuilder 289 | .WithPredict(p => p.IsActive) 290 | .WithOrderBy(q => q.OrderBy(p => p.Name)), 291 | page, 292 | pageSize 293 | ); 294 | } 295 | } 296 | ``` 297 | 298 | ## Step 7: Create a Controller (ASP.NET Core) 299 | 300 | ```csharp 301 | using Microsoft.AspNetCore.Mvc; 302 | 303 | [ApiController] 304 | [Route("api/[controller]")] 305 | public class ProductsController : ControllerBase 306 | { 307 | private readonly ProductService _productService; 308 | 309 | public ProductsController(ProductService productService) 310 | { 311 | _productService = productService; 312 | } 313 | 314 | [HttpGet] 315 | public async Task>> GetProducts( 316 | int page = 1, 317 | int pageSize = 20) 318 | { 319 | var products = await _productService.GetProductsPagedAsync(page, pageSize); 320 | return Ok(products); 321 | } 322 | 323 | [HttpGet("{id}")] 324 | public async Task> GetProduct(int id) 325 | { 326 | var product = await _productService.GetProductByIdAsync(id); 327 | if (product == null) 328 | { 329 | return NotFound(); 330 | } 331 | return Ok(product); 332 | } 333 | 334 | [HttpPost] 335 | public async Task> CreateProduct(CreateProductRequest request) 336 | { 337 | var product = await _productService.CreateProductAsync(request.Name, request.Price); 338 | return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product); 339 | } 340 | 341 | [HttpPut("{id}/price")] 342 | public async Task UpdatePrice(int id, UpdatePriceRequest request) 343 | { 344 | var success = await _productService.UpdateProductPriceAsync(id, request.Price); 345 | if (!success) 346 | { 347 | return NotFound(); 348 | } 349 | return NoContent(); 350 | } 351 | } 352 | 353 | public class CreateProductRequest 354 | { 355 | public string Name { get; set; } = string.Empty; 356 | public decimal Price { get; set; } 357 | } 358 | 359 | public class UpdatePriceRequest 360 | { 361 | public decimal Price { get; set; } 362 | } 363 | ``` 364 | 365 | ## What Happens Automatically 366 | 367 | When you run this setup, WeihanLi.EntityFramework will automatically: 368 | 369 | 1. **Set timestamps**: `CreatedAt` when inserting, `UpdatedAt` when updating 370 | 2. **Track users**: `CreatedBy` and `UpdatedBy` using your user ID provider 371 | 3. **Create audit records**: Every change is logged with full details 372 | 4. **Apply soft delete filters**: Soft-deleted entities are excluded from queries 373 | 5. **Handle transactions**: Unit of Work ensures data consistency 374 | 375 | ## Next Steps 376 | 377 | - 📖 Read the [Complete Usage Guide](Usage.md) for advanced features 378 | - ⚡ Explore [Advanced Features Guide](AdvancedFeatures.md) for custom interceptors and optimization 379 | - 🔍 Explore the [sample project](../samples/WeihanLi.EntityFramework.Sample/) for more examples 380 | - 🛠️ Check out bulk operations, advanced querying, and custom audit stores 381 | - 📋 Review [Release Notes](ReleaseNotes.md) for version-specific information 382 | 383 | ## Common Patterns 384 | 385 | ### Repository with Unit of Work 386 | 387 | ```csharp 388 | public async Task ProcessOrderAsync(CreateOrderRequest request) 389 | { 390 | var orderRepo = _unitOfWork.GetRepository(); 391 | var productRepo = _unitOfWork.GetRepository(); 392 | 393 | // Create order 394 | var order = await orderRepo.InsertAsync(new Order 395 | { 396 | CustomerId = request.CustomerId, 397 | TotalAmount = request.Items.Sum(i => i.Price * i.Quantity) 398 | }); 399 | 400 | // Add order items and update inventory 401 | foreach (var item in request.Items) 402 | { 403 | await orderRepo.InsertAsync(new OrderItem 404 | { 405 | OrderId = order.Id, 406 | ProductId = item.ProductId, 407 | Quantity = item.Quantity, 408 | Price = item.Price 409 | }); 410 | 411 | // Update product inventory (example) 412 | await productRepo.UpdateAsync( 413 | setters => setters.SetProperty(p => p.Stock, p => p.Stock - item.Quantity), 414 | queryBuilder => queryBuilder.WithPredict(p => p.Id == item.ProductId) 415 | ); 416 | } 417 | 418 | // Commit all changes in a single transaction 419 | await _unitOfWork.CommitAsync(); 420 | } 421 | ``` 422 | 423 | This gets you started with the core features. The library handles the complexity while giving you clean, testable code! -------------------------------------------------------------------------------- /docs/ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | More changes: 4 | 5 | - Releases: https://github.com/WeihanLi/WeihanLi.EntityFramework/releases 6 | - PRs: https://github.com/WeihanLi/WeihanLi.EntityFramework/pulls?q=is%3Apr+is%3Aclosed+is%3Amerged+base%3Amaster 7 | 8 | ## WeihanLi.EntityFramework 9 | 10 | ### [WeihanLi.EntityFramework 1.8.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.8.0) 11 | 12 | - update IAuditUserIdProvider => IUserIdProvider 13 | - update OperationType => DataOperationType 14 | 15 | ### [WeihanLi.EntityFramework 1.7.1](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.7.1) 16 | 17 | - add `AuditDbContextInterceptor` extensions for `FluentAspectOptions` 18 | - upgrade `WeihanLi.Common` to make it possible to use `AspectCore` or `Castle` 19 | 20 | ### [WeihanLi.EntityFramework 1.7.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.7.0) 21 | 22 | - update `AuditEntry` 23 | - auto audit with aop 24 | 25 | ### [WeihanLi.EntityFramework 1.6.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.6.0) 26 | 27 | - add auto audit support 28 | - add `dbContext.GetTableName()` extensions 29 | - add `QueryWithNoLockInterceptor` 30 | 31 | ### [WeihanLi.EntityFramework 1.5.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.5.0) 32 | 33 | - update EF core to 3.1(drop support EF core 2.x) 34 | - update `EFRepository.FindAsync` to return `ValueTask` 35 | - update `IsRelationalDatabase` extension 36 | 37 | ### [WeihanLi.EntityFramework 1.4.2](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.4.2) 38 | 39 | - update `IEFUnitOfWork`/`IEFRepository` 40 | - add `Update`/`UpdateWithout` extension methods for dbContext 41 | - add `GetTableName`/`GetColumnName` extension method 42 | - add `dbContext.Database.IsRelational` 43 | - add auto audit support 44 | 45 | ### [WeihanLi.EntityFramework 1.3.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.3.0) 46 | 47 | - add `IEFUnitOfWork` 48 | - optimize `EFRepositoryQueryBuilder` set default predict to null 49 | 50 | ### [WeihanLi.EntityFramework 1.2.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.2.0) 51 | 52 | - add `IEFRepositoryFactory` 53 | 54 | ### [WeihanLi.EntityFramework 1.1.0](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.1.0) 55 | 56 | - add `Query` for `IEFRepository` return raw `IQueryable` 57 | 58 | ### [WeihanLi.EntityFramework 1.0.9](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.0.9) 59 | 60 | - add `Delete`/`Any` for `IEFRepository` 61 | - update pagedList extension 62 | - add `DbContextBase` 63 | 64 | ### [WeihanLi.EntityFramework 1.0.8](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.0.8) 65 | 66 | - rename Get with selector method name => `GetResult`/`FirstOrDefaultResult`/`GetPagedListResult` 67 | 68 | ### [WeihanLi.EntityFramework 1.0.7](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.0.7) 69 | 70 | - update `Update` for `EFRepository` 71 | - add `UpdateWithout` 72 | - remove none `QueryBuilder` extensions 73 | 74 | ### [WeihanLi.EntityFramework 1.0.6](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.0.6) 75 | 76 | - add `FirstOrDefault` for `EFRepository` 77 | 78 | ### [WeihanLi.EntityFramework 1.0.5](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.0.5) 79 | 80 | - add `CancellationToken` support for async operations 81 | - add `IEFRepositoryGenerator` 82 | - add `EFRepositoryQueryBuilder` for `EFRepository` 83 | - add [`sourceLink`](https://github.com/dotnet/sourcelink) support 84 | 85 | ### [WeihanLi.EntityFramework 1.0.3](https://www.nuget.org/packages/WeihanLi.EntityFramework/1.0.3) 86 | 87 | - add `EFRepositoryGenerator` 88 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/WeihanLi.EntityFramework.Sample/AutoAuditContext1.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using WeihanLi.EntityFramework.Audit; 3 | 4 | namespace WeihanLi.EntityFramework.Sample; 5 | 6 | public class AutoAuditContext1(DbContextOptions dbContextOptions, IServiceProvider serviceProvider) 7 | : AuditDbContext(dbContextOptions, serviceProvider) 8 | { 9 | public DbSet Jobs { get; set; } = null!; 10 | } 11 | 12 | 13 | public class AutoAuditContext2(DbContextOptions options) : DbContext(options) 14 | { 15 | public DbSet Jobs { get; set; } = null!; 16 | } 17 | 18 | public class TestJobEntity 19 | { 20 | public int Id { get; set; } 21 | public required string Name { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /samples/WeihanLi.EntityFramework.Sample/DbContextInterceptorSamples.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using WeihanLi.Extensions; 5 | 6 | namespace WeihanLi.EntityFramework.Sample; 7 | 8 | public static class DbContextInterceptorSamples 9 | { 10 | public static async Task RunAsync() 11 | { 12 | await InterceptorTest2(); 13 | } 14 | 15 | private static async Task InterceptorTest1() 16 | { 17 | var services = new ServiceCollection(); 18 | services.AddScoped(); 19 | services.AddDbContext((provider, options) => 20 | { 21 | options.AddInterceptors(provider.GetRequiredService()); 22 | options.UseInMemoryDatabase("test"); 23 | }); 24 | await using var provider = services.BuildServiceProvider(); 25 | using var scope = provider.CreateScope(); 26 | var dbContext = scope.ServiceProvider.GetRequiredService(); 27 | await dbContext.Database.EnsureCreatedAsync(); 28 | dbContext.Entities.Add(new TestEntity { Id = 1, Name = "1" }); 29 | await dbContext.SaveChangesAsync(); 30 | } 31 | 32 | private static async Task InterceptorTest2() 33 | { 34 | var services = new ServiceCollection(); 35 | services.AddDbContext(options => 36 | { 37 | options.UseInMemoryDatabase("test"); 38 | }); 39 | services.AddDbContextInterceptor(); 40 | await using var provider = services.BuildServiceProvider(); 41 | using var scope = provider.CreateScope(); 42 | var dbContext = scope.ServiceProvider.GetRequiredService(); 43 | await dbContext.Database.EnsureCreatedAsync(); 44 | dbContext.Entities.Add(new TestEntity { Id = 2, Name = "1" }); 45 | await dbContext.SaveChangesAsync(); 46 | } 47 | } 48 | 49 | file sealed class FileTestDbContext(DbContextOptions options) : DbContext(options) 50 | { 51 | public DbSet Entities { get; set; } = null!; 52 | } 53 | 54 | file sealed class TestEntity 55 | { 56 | public int Id { get; set; } 57 | public string? Name { get; set; } 58 | } 59 | 60 | file sealed class SavingInterceptor : SaveChangesInterceptor 61 | { 62 | public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, 63 | CancellationToken cancellationToken = default) 64 | { 65 | Console.WriteLine("SavingChangesAsync"); 66 | return base.SavingChangesAsync(eventData, result, cancellationToken); 67 | } 68 | 69 | public override ValueTask SavedChangesAsync(SaveChangesCompletedEventData eventData, int result, 70 | CancellationToken cancellationToken = default) 71 | { 72 | Console.WriteLine("SavedChangesAsync"); 73 | return base.SavedChangesAsync(eventData, result, cancellationToken); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /samples/WeihanLi.EntityFramework.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Infrastructure.Internal; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using System.Linq.Expressions; 8 | using System.Text.Json; 9 | using WeihanLi.Common; 10 | using WeihanLi.Common.Data; 11 | using WeihanLi.Common.Helpers; 12 | using WeihanLi.Common.Services; 13 | using WeihanLi.EntityFramework.Audit; 14 | using WeihanLi.EntityFramework.Interceptors; 15 | using WeihanLi.Extensions; 16 | 17 | namespace WeihanLi.EntityFramework.Sample; 18 | 19 | public static class Program 20 | { 21 | public static async Task Main(string[] args) 22 | { 23 | // SoftDeleteTest(); 24 | RepositoryTest(); 25 | // AutoAuditTest(); 26 | 27 | // await DbContextInterceptorSamples.RunAsync(); 28 | 29 | Console.WriteLine("completed"); 30 | Console.ReadLine(); 31 | } 32 | 33 | private static void AutoAuditTest() 34 | { 35 | // { 36 | // var services = new ServiceCollection(); 37 | // services.AddLogging(builder => builder.AddDefaultDelegateLogger()); 38 | // services.AddDbContext(options => 39 | // { 40 | // options.UseSqlite("Data Source=AutoAuditTest1.db"); 41 | // }); 42 | // services.AddEFAutoAudit(builder => 43 | // { 44 | // builder 45 | // .WithUserIdProvider(new DelegateUserIdProvider(() => "AutoAuditTest1")) 46 | // .EnrichWithProperty("MachineName", Environment.MachineName) 47 | // .EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName) 48 | // // 保存到自定义的存储 49 | // .WithStore("logs0.log") 50 | // // 忽略指定实体 51 | // .IgnoreEntity(); 52 | // }); 53 | // using var serviceProvider = services.BuildServiceProvider(); 54 | // using var scope = serviceProvider.CreateScope(); 55 | // var context = scope.ServiceProvider.GetRequiredService(); 56 | // context.Database.EnsureDeleted(); 57 | // context.Database.EnsureCreated(); 58 | // context.Jobs.Add(new TestJobEntity() { Name = "test1" }); 59 | // context.SaveChanges(); 60 | // var job = context.Jobs.Find(1); 61 | // if (job is not null) 62 | // { 63 | // context.Jobs.Remove(job); 64 | // context.SaveChanges(); 65 | // } 66 | // 67 | // var auditRecords = context.AuditRecords.AsNoTracking().ToArray(); 68 | // Console.WriteLine(auditRecords.ToJson()); 69 | // } 70 | // ConsoleHelper.ReadLineWithPrompt(); 71 | // { 72 | // var services = new ServiceCollection(); 73 | // services.AddLogging(builder => builder.AddDefaultDelegateLogger()); 74 | // services.AddDbContext((provider, options) => 75 | // { 76 | // options.UseSqlite("Data Source=AutoAuditTest2.db"); 77 | // options.AddInterceptors(provider.GetRequiredService()); 78 | // }); 79 | // services.AddEFAutoAudit(builder => 80 | // { 81 | // builder.EnrichWithProperty("AutoAudit", "EntityFramework") 82 | // .EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName) 83 | // .WithStore() 84 | // .WithAuditRecordsDbContextStore(options => 85 | // { 86 | // options.UseSqlite("Data Source=AutoAuditAuditRecords.db"); 87 | // }); 88 | // }); 89 | // using var serviceProvider = services.BuildServiceProvider(); 90 | // using var scope = serviceProvider.CreateScope(); 91 | // var context = scope.ServiceProvider.GetRequiredService(); 92 | // context.Database.EnsureDeleted(); 93 | // context.Database.EnsureCreated(); 94 | // context.Jobs.Add(new TestJobEntity() { Name = "test1" }); 95 | // context.SaveChanges(); 96 | // var job = context.Jobs.Find(1); 97 | // if (job is not null) 98 | // { 99 | // context.Jobs.Remove(job); 100 | // context.SaveChanges(); 101 | // } 102 | // 103 | // var auditRecordsContext = scope.ServiceProvider.GetRequiredService(); 104 | // var auditRecords = auditRecordsContext.AuditRecords.AsNoTracking().ToArray(); 105 | // Console.WriteLine(auditRecords.ToJson()); 106 | // } 107 | 108 | { 109 | var services = new ServiceCollection(); 110 | services.AddLogging(loggingBuilder => 111 | { 112 | loggingBuilder.AddDefaultDelegateLogger(); 113 | }); 114 | services.AddDbContext((provider, options) => 115 | { 116 | options 117 | .UseSqlite("Data Source=Test.db") 118 | // .AddInterceptors(ActivatorUtilities.GetServiceOrCreateInstance(provider)) 119 | ; 120 | }); 121 | services.AddDbContextInterceptor1(); 122 | 123 | services.AddEFAutoAudit(builder => 124 | { 125 | builder 126 | // 配置操作用户获取方式 127 | .WithUserIdProvider(new DelegateUserIdProvider(() => "EFTest")) 128 | //.WithUnModifiedProperty() // 保存未修改的属性,默认只保存发生修改的属性 129 | // 保存更多属性 130 | .EnrichWithProperty("MachineName", Environment.MachineName) 131 | .EnrichWithProperty(nameof(ApplicationHelper.ApplicationName), ApplicationHelper.ApplicationName) 132 | // 保存到自定义的存储 133 | .WithStore() 134 | // 忽略指定实体 135 | .IgnoreEntity() 136 | // 忽略指定实体的某个属性 137 | .IgnoreProperty(t => t.CreatedAt) 138 | // 忽略所有属性名称为 CreatedAt 的属性 139 | .IgnoreProperty("CreatedAt") 140 | ; 141 | }); 142 | DependencyResolver.SetDependencyResolver(services); 143 | DependencyResolver.TryInvoke(dbContext => 144 | { 145 | dbContext.Database.EnsureDeleted(); 146 | dbContext.Database.EnsureCreated(); 147 | var testEntity = new TestEntity() 148 | { 149 | Extra = new { Name = "Tom" }.ToJson(), 150 | CreatedAt = DateTimeOffset.Now, 151 | }; 152 | dbContext.TestEntities.Add(testEntity); 153 | dbContext.SaveChanges(); 154 | 155 | testEntity.CreatedAt = DateTimeOffset.Now; 156 | testEntity.Extra = new { Name = "Jerry" }.ToJson(); 157 | dbContext.SaveChanges(); 158 | 159 | dbContext.Remove(testEntity); 160 | dbContext.SaveChanges(); 161 | 162 | var testEntity1 = new TestEntity() 163 | { 164 | Extra = new { Name = "Tom1" }.ToJson(), 165 | CreatedAt = DateTimeOffset.Now, 166 | }; 167 | dbContext.TestEntities.Add(testEntity1); 168 | var testEntity2 = new TestEntity() 169 | { 170 | Extra = new { Name = "Tom2" }.ToJson(), 171 | CreatedAt = DateTimeOffset.Now, 172 | }; 173 | dbContext.TestEntities.Add(testEntity2); 174 | dbContext.SaveChanges(); 175 | }); 176 | DependencyResolver.TryInvokeAsync(async dbContext => 177 | { 178 | dbContext.Remove(new TestEntity() { Id = 2 }); 179 | await dbContext.SaveChangesAsync(); 180 | }).Wait(); 181 | } 182 | } 183 | 184 | private static void RepositoryTest() 185 | { 186 | var services = new ServiceCollection(); 187 | services.AddLogging(loggingBuilder => 188 | { 189 | loggingBuilder.AddDefaultDelegateLogger(); 190 | }); 191 | services.AddDbContext((provider, options) => 192 | { 193 | options 194 | //.EnableDetailedErrors() 195 | //.EnableSensitiveDataLogging() 196 | //.UseInMemoryDatabase("Tests") 197 | .UseSqlite("Data Source=Test.db") 198 | .AddInterceptors(ActivatorUtilities.GetServiceOrCreateInstance(provider)) 199 | //.UseSqlServer(@"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TestDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;") 200 | ; 201 | }); 202 | services.AddEFRepository(); 203 | DependencyResolver.SetDependencyResolver(services); 204 | 205 | DependencyResolver.Current.TryInvokeService(db => 206 | { 207 | db.Database.EnsureCreated(); 208 | var tableName = db.GetTableName(); 209 | 210 | if (db.Database.IsRelational()) 211 | { 212 | var conn = db.Database.GetDbConnection(); 213 | try 214 | { 215 | conn.Execute($"TRUNCATE TABLE {tableName}"); 216 | } 217 | catch 218 | { 219 | db.Set().ExecuteDelete(); 220 | } 221 | } 222 | 223 | var repo = db.GetRepository(); 224 | repo.Insert(new TestEntity() 225 | { 226 | CreatedAt = DateTimeOffset.Now, 227 | Extra = "{\"Name\": \"Tom\"}" 228 | }); 229 | 230 | repo.Update(x => x.Extra != null, x => x.Extra, new { Date = DateTimeOffset.Now }.ToJson()); 231 | System.Console.WriteLine("Extra updated"); 232 | 233 | // TODO: this is not working for now 234 | repo.Update(x => x.Extra != null, new Dictionary() 235 | { 236 | { "Extra", "12345"} 237 | }); 238 | 239 | repo.Update(x => x.SetProperty(_ => _.Extra, _ => "{}"), q => q.IgnoreQueryFilters()); 240 | 241 | var abc = db.TestEntities.AsNoTracking().ToArray(); 242 | Console.WriteLine($"{string.Join(Environment.NewLine, abc.Select(_ => _.ToJson()))}"); 243 | 244 | var entities = repo.Query(q => q.IgnoreQueryFilters(["not-null"])) 245 | .ToArray(); 246 | Console.WriteLine(entities.Length); 247 | 248 | entities = repo.Query(q => q.IgnoreQueryFilters()) 249 | .ToArray(); 250 | Console.WriteLine(entities.Length); 251 | 252 | var data = repo.Query(q => q.WithPredictIf(f => f.Id > 0, false)).ToArray(); 253 | Console.WriteLine(JsonSerializer.Serialize(data)); 254 | 255 | repo.Delete(x => x.Id > 0); 256 | }); 257 | 258 | DependencyResolver.Current.TryInvokeService>(repoFactory => 259 | { 260 | var repo = repoFactory.GetRepository(); 261 | var count = repo.Count(); 262 | Console.WriteLine(count); 263 | }); 264 | 265 | DependencyResolver.Current.TryInvokeService>(repo => 266 | { 267 | var ids0 = repo.GetResult(_ => _.Id).ToArray(); 268 | Console.WriteLine($"Ids: {ids0.StringJoin(",")}"); 269 | 270 | var list0 = repo.GetResult(_ => _.Id, queryBuilder => queryBuilder.WithPredict(t => t.Id > 0)).ToArray(); 271 | Console.WriteLine($"Ids: {list0.StringJoin(",")}"); 272 | 273 | repo.Insert(new TestEntity() { Extra = "{}", CreatedAt = DateTime.UtcNow, }); 274 | repo.Insert(new TestEntity() { Extra = "{}", CreatedAt = DateTime.UtcNow, }); 275 | 276 | var foundEntity = repo.Find(1); 277 | 278 | var whereExpression = ExpressionHelper.True(); 279 | Expression> idExp = t => t.Id > 0; 280 | var whereExpression1 = whereExpression 281 | .And(t => t.Id > 0) 282 | .And(ExpressionHelper.True()) 283 | .And(t => t.Id > -1); 284 | 285 | var abcExp = Expression.Lambda> 286 | (Expression.AndAlso(idExp.Body, whereExpression.Body), idExp.Parameters); 287 | 288 | var list00 = repo.GetResult(_ => _.Id, queryBuilder => 289 | queryBuilder.WithPredict(whereExpression1)).ToArray(); 290 | var list01 = repo.GetResult(_ => _.Id, queryBuilder => 291 | queryBuilder.WithPredict(abcExp)).ToArray(); 292 | Console.WriteLine($"Ids: {list00.StringJoin(",")}"); 293 | 294 | repo.Update(new TestEntity 295 | { 296 | Extra = new { Name = "Abcde", Count = 4 }.ToJson(), 297 | CreatedAt = DateTime.UtcNow, 298 | Id = list00[0] 299 | }, t => t.CreatedAt, t => t.Extra); 300 | 301 | repo.UpdateWithout(new TestEntity() { Id = list00[1], Extra = new { Name = "ADDDDD" }.ToJson() }, x => x.CreatedAt); 302 | 303 | repo.Insert(new[] 304 | { 305 | new TestEntity 306 | { 307 | Extra = new {Name = "Abcdes"}.ToJson(), 308 | CreatedAt = DateTime.Now 309 | }, 310 | new TestEntity 311 | { 312 | Extra = new {Name = "Abcdes"}.ToJson(), 313 | CreatedAt = DateTime.Now 314 | } 315 | }); 316 | var list = repo.GetResult(_ => _.Id).ToArray(); 317 | Console.WriteLine($"Ids: {list.StringJoin(",")}"); 318 | 319 | repo.Get(queryBuilder => queryBuilder 320 | .WithOrderBy(q => q.OrderByDescending(_ => _.Id))); 321 | 322 | var lastItem = repo.FirstOrDefault(queryBuilder => queryBuilder 323 | .WithOrderBy(q => q.OrderByDescending(_ => _.Id))); 324 | 325 | var list1 = repo.GetPagedListResult(x => x.Id, queryBuilder => queryBuilder 326 | .WithOrderBy(query => query.OrderByDescending(q => q.Id)), 2, 2 327 | ); 328 | 329 | var pagedList = repo.GetPagedListResult(x => x.Id, queryBuilder => queryBuilder 330 | .WithOrderBy(query => query.OrderByDescending(q => q.Id)) 331 | , 1, 2); 332 | Console.WriteLine(pagedList.ToJson()); 333 | 334 | Console.WriteLine($"Count: {repo.Count()}"); 335 | }); 336 | 337 | DependencyResolver.Current.TryInvokeService>(uow => 338 | { 339 | var originColor = Console.ForegroundColor; 340 | Console.ForegroundColor = ConsoleColor.Green; 341 | 342 | Console.WriteLine("********** UnitOfWork ************"); 343 | Console.WriteLine($"uow count0: {uow.DbSet().Count()}"); 344 | 345 | uow.DbSet().Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Extra = "1212", }); 346 | 347 | Console.WriteLine($"uow count1: {uow.DbSet().Count()}"); 348 | 349 | uow.DbSet().Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Extra = "1212", }); 350 | 351 | uow.GetRepository().Delete(uow.DbContext.TestEntities.First()); 352 | 353 | Console.ForegroundColor = originColor; 354 | 355 | uow.Commit(); 356 | 357 | Console.ForegroundColor = ConsoleColor.Green; 358 | 359 | Console.WriteLine($"uow count2: {uow.DbSet().Count()}"); 360 | Console.WriteLine("********** UnitOfWork ************"); 361 | 362 | Console.ForegroundColor = originColor; 363 | }); 364 | 365 | DependencyResolver.Current.TryInvokeService(db => 366 | { 367 | var tableName = db.GetTableName(); 368 | if (db.Database.IsRelational()) 369 | { 370 | var conn = db.Database.GetDbConnection(); 371 | try 372 | { 373 | conn.Execute($@"TRUNCATE TABLE {tableName}"); 374 | } 375 | catch 376 | { 377 | db.Set().ExecuteDelete(); 378 | } 379 | } 380 | }); 381 | } 382 | 383 | private static void SoftDeleteTest() 384 | { 385 | var services = new ServiceCollection(); 386 | services.AddLogging(loggingBuilder => 387 | { 388 | loggingBuilder.AddDefaultDelegateLogger(); 389 | }); 390 | 391 | services.AddSingleton(); 392 | services.AddEFAutoUpdateInterceptor(); 393 | 394 | services.AddDbContext((provider, options) => 395 | { 396 | options 397 | .UseSqlite("Data Source=SoftDeleteTest.db") 398 | .AddInterceptors(provider.GetRequiredService()); 399 | }); 400 | using var serviceProvider = services.BuildServiceProvider(); 401 | var scope = serviceProvider.CreateScope(); 402 | var context = scope.ServiceProvider.GetRequiredService(); 403 | context.Database.EnsureDeleted(); 404 | // initialize 405 | context.Database.EnsureCreated(); 406 | // delete all in case of before db not got clean up 407 | context.TestEntities.IgnoreQueryFilters().ExecuteDelete(); 408 | context.SaveChanges(); 409 | 410 | // add test data 411 | context.TestEntities.Add(new SoftDeleteEntity() 412 | { 413 | Id = 1, 414 | Name = "test" 415 | }); 416 | context.SaveChanges(); 417 | 418 | // remove data test 419 | var testEntity = context.TestEntities.Find(1); 420 | ArgumentNullException.ThrowIfNull(testEntity); 421 | context.TestEntities.Remove(testEntity); 422 | context.SaveChanges(); 423 | 424 | 425 | context.TestEntities2.Add(new SoftDeleteEntity2() 426 | { 427 | Id = 1, 428 | Name = "test" 429 | }); 430 | context.SaveChanges(); 431 | var testEntities = context.TestEntities2.AsNoTracking().ToArray(); 432 | var testEntity2 = context.TestEntities2.Find(1); 433 | ArgumentNullException.ThrowIfNull(testEntity2); 434 | context.TestEntities2.Remove(testEntity2); 435 | context.SaveChanges(); 436 | 437 | // get all data 438 | var entities = context.TestEntities.AsNoTracking().ToArray(); 439 | Console.WriteLine(entities.ToJson()); 440 | 441 | // get all data without global query filter 442 | entities = context.TestEntities.AsNoTracking().IgnoreQueryFilters().ToArray(); 443 | Console.WriteLine(entities.ToJson()); 444 | 445 | context.Database.EnsureDeleted(); 446 | } 447 | 448 | private static IServiceCollection AddDbContextInterceptor1( 449 | this IServiceCollection services, 450 | ServiceLifetime optionsLifetime = ServiceLifetime.Scoped 451 | ) 452 | where TContext : DbContext 453 | where TInterceptor : IInterceptor 454 | { 455 | Action optionsAction = (sp, builder) => 456 | { 457 | builder.AddInterceptors(sp.GetRequiredService()); 458 | }; 459 | services.Add(ServiceDescriptor.Describe(typeof(TInterceptor), typeof(TInterceptor), optionsLifetime)); 460 | services.Add(ServiceDescriptor.Describe(typeof(IDbContextOptionsConfiguration), _ => 461 | new DbContextOptionsConfiguration(optionsAction), optionsLifetime)); 462 | return services; 463 | } 464 | } 465 | 466 | 467 | public sealed class AuditConsoleStore : IAuditStore 468 | { 469 | private readonly string _fileName; 470 | 471 | public AuditConsoleStore() : this("audit-logs.log") 472 | { 473 | } 474 | public AuditConsoleStore(string fileName) 475 | { 476 | _fileName = fileName; 477 | } 478 | 479 | public Task Save(ICollection auditEntries) 480 | { 481 | foreach (var auditEntry in auditEntries) 482 | { 483 | Console.WriteLine(auditEntry.ToJson()); 484 | } 485 | 486 | return Task.CompletedTask; 487 | } 488 | } 489 | 490 | file sealed class AuditFileBatchStore : PeriodBatchingAuditStore 491 | { 492 | private readonly string _fileName; 493 | 494 | public AuditFileBatchStore() : this(null) 495 | { 496 | } 497 | 498 | public AuditFileBatchStore(string? fileName) : base(100, TimeSpan.FromSeconds(10)) 499 | { 500 | _fileName = fileName.GetValueOrDefault("audits.log"); 501 | } 502 | 503 | protected override async Task EmitBatchAsync(IEnumerable events) 504 | { 505 | var path = Path.Combine(Directory.GetCurrentDirectory(), _fileName); 506 | 507 | await using var fileStream = File.Exists(path) 508 | ? new FileStream(path, FileMode.Append) 509 | : File.Create(path); 510 | await fileStream.WriteAsync(events.ToJson().GetBytes()); 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /samples/WeihanLi.EntityFramework.Sample/SoftDeleteSampleContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using WeihanLi.Common.Models; 3 | 4 | namespace WeihanLi.EntityFramework.Sample; 5 | 6 | public class SoftDeleteSampleContext : DbContext 7 | { 8 | public SoftDeleteSampleContext(DbContextOptions options) : base(options) 9 | { 10 | } 11 | 12 | public virtual DbSet TestEntities { get; set; } = null!; 13 | 14 | public virtual DbSet TestEntities2 { get; set; } = null!; 15 | 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.Entity().HasQueryFilter(x => x.IsDeleted == false); 19 | 20 | modelBuilder.Entity().Property("IsDeleted"); 21 | modelBuilder.Entity().HasQueryFilter(x => EF.Property(x, "IsDeleted") == false); 22 | 23 | base.OnModelCreating(modelBuilder); 24 | } 25 | } 26 | 27 | public class SoftDeleteEntity : ISoftDeleteEntityWithDeleted 28 | { 29 | public int Id { get; set; } 30 | public string Name { get; set; } = "test"; 31 | public bool IsDeleted { get; set; } 32 | } 33 | 34 | public class SoftDeleteEntity2 : ISoftDeleteEntity, IEntityWithCreatedUpdatedAt 35 | { 36 | public int Id { get; set; } 37 | public string Name { get; set; } = "test"; 38 | public DateTimeOffset CreatedAt { get; set; } 39 | public DateTimeOffset UpdatedAt { get; set; } 40 | } 41 | -------------------------------------------------------------------------------- /samples/WeihanLi.EntityFramework.Sample/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace WeihanLi.EntityFramework; 7 | 8 | public class TestDbContext : DbContext 9 | { 10 | public TestDbContext(DbContextOptions options) : base(options) 11 | { 12 | } 13 | 14 | protected override void OnModelCreating(ModelBuilder modelBuilder) 15 | { 16 | base.OnModelCreating(modelBuilder); 17 | modelBuilder.Entity() 18 | // .HasQueryFilter("one-month-ago", t => t.CreatedAt > DateTime.Now.AddMonths(-1)) 19 | .HasQueryFilter("valid-id", t => t.Id > 0) 20 | .HasQueryFilter("not-null", t => t.Extra != null) 21 | ; 22 | } 23 | 24 | public DbSet TestEntities { get; set; } = null!; 25 | } 26 | 27 | [Table("tabTestEntities")] 28 | public class TestEntity 29 | { 30 | [Key] 31 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 32 | [Column("PKID")] 33 | public int Id { get; set; } 34 | 35 | [Column("ExtraSettings")] 36 | public string? Extra { get; set; } 37 | 38 | public DateTimeOffset CreatedAt { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /samples/WeihanLi.EntityFramework.Sample/WeihanLi.EntityFramework.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework.SourceGenerator/EFExtensionsGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using Microsoft.CodeAnalysis.Text; 4 | using System.Collections.Immutable; 5 | using System.Text; 6 | 7 | namespace WeihanLi.EntityFramework.SourceGenerator; 8 | 9 | [Generator] 10 | public sealed class EFExtensionsGenerator : IIncrementalGenerator 11 | { 12 | private const string EFGenCode = """ 13 | namespace WeihanLi.EntityFramework 14 | { 15 | [AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 16 | public sealed class EFExtensionsAttribute; 17 | } 18 | """; 19 | 20 | public void Initialize(IncrementalGeneratorInitializationContext context) 21 | { 22 | var additionalFiles = context.AdditionalTextsProvider 23 | .Where(file => file.Path.EndsWith(".ef.template")) 24 | .Select((file, cancellationToken) => file.GetText(cancellationToken)?.ToString()) 25 | .Where(content => !string.IsNullOrEmpty(content)); 26 | 27 | var dbContextDeclarations = context.SyntaxProvider 28 | .CreateSyntaxProvider( 29 | predicate: static (s, _) => IsDbContextDeclaration(s), 30 | transform: static (ctx, _) => GetDbContextDeclaration(ctx)) 31 | .Where(FilterDbContextDeclarations) 32 | ; 33 | 34 | var combined = dbContextDeclarations.Combine(additionalFiles.Collect()); 35 | 36 | context.RegisterSourceOutput(combined, (spc, source) => 37 | { 38 | var (dbContextDeclaration, templates) = source; 39 | var dbContextName = dbContextDeclaration!.Identifier.Text; 40 | var repositoryNamespace = dbContextDeclaration.Parent!.GetNamespace(); 41 | var generatedCode = GenerateRepositoryCode(dbContextName, repositoryNamespace, templates); 42 | // spc.AddSource($"{dbContextName}Repository.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); 43 | }); 44 | } 45 | 46 | private static bool IsDbContextDeclaration(SyntaxNode node) 47 | { 48 | if (node is not ClassDeclarationSyntax classDeclaration 49 | || classDeclaration.BaseList?.Types is null) 50 | return false; 51 | 52 | return classDeclaration.BaseList.Types.Any(t => 53 | t.ToString().Contains("DbContext")); 54 | } 55 | 56 | private static bool FilterDbContextDeclarations(ClassDeclarationSyntax classDeclaration) 57 | { 58 | return true; 59 | } 60 | 61 | private static ClassDeclarationSyntax GetDbContextDeclaration(GeneratorSyntaxContext context) 62 | { 63 | var classDeclaration = (ClassDeclarationSyntax)context.Node; 64 | return classDeclaration; 65 | } 66 | 67 | private static string GenerateRepositoryCode(string dbContextName, string repositoryNamespace, 68 | ImmutableArray templates) 69 | { 70 | var builder = new StringBuilder(); 71 | builder.AppendLine("using WeihanLi.EntityFramework;"); 72 | builder.AppendLine($"namespace {repositoryNamespace}"); 73 | builder.AppendLine("{"); 74 | 75 | foreach (var template in templates) 76 | { 77 | var code = template.Replace("{{DbContextName}}", dbContextName); 78 | builder.AppendLine(code); 79 | } 80 | 81 | builder.AppendLine("}"); 82 | return builder.ToString(); 83 | } 84 | } 85 | 86 | internal static class SyntaxNodeExtensions 87 | { 88 | public static string GetNamespace(this SyntaxNode node) 89 | { 90 | while (node != null) 91 | { 92 | if (node is NamespaceDeclarationSyntax namespaceDeclaration) 93 | { 94 | return namespaceDeclaration.Name.ToString(); 95 | } 96 | 97 | node = node.Parent; 98 | } 99 | 100 | return string.Empty; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework.SourceGenerator/WeihanLi.EntityFramework.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | latest 7 | true 8 | true 9 | true 10 | false 11 | WeihanLi.EntityFramework.SourceGenerator 12 | Source generator for WeihanLi.EntityFramework 13 | WeihanLi 14 | WeihanLi 15 | WeihanLi.EntityFramework 16 | Apache-2.0 17 | https://github.com/WeihanLi/WeihanLi.EntityFramework 18 | git 19 | Copyright 2019-2024 (c) WeihanLi 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/AuditDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using WeihanLi.Common.Models; 4 | 5 | namespace WeihanLi.EntityFramework.Audit; 6 | 7 | public abstract class AuditDbContextBase(DbContextOptions dbContextOptions, IServiceProvider serviceProvider) 8 | : DbContextBase(dbContextOptions) 9 | { 10 | private readonly IAuditStore[] _auditStores = serviceProvider.GetServices().ToArray(); 11 | private readonly IAuditPropertyEnricher[] _auditPropertyEnrichers = 12 | serviceProvider.GetServices().ToArray(); 13 | private readonly string? _auditUser = 14 | AuditConfig.Options.UserIdProviderFactory?.Invoke(serviceProvider)?.GetUserId(); 15 | 16 | protected List? AuditEntries { get; set; } 17 | 18 | protected override Task BeforeSaveChanges() 19 | { 20 | if (!AuditConfig.Options.AuditEnabled || _auditStores.Length <= 0) return Task.CompletedTask; 21 | 22 | AuditEntries = new List(); 23 | foreach (var entityEntry in ChangeTracker.Entries()) 24 | { 25 | if (entityEntry.State is EntityState.Detached or EntityState.Unchanged) 26 | { 27 | continue; 28 | } 29 | // 30 | if (AuditConfig.Options.EntityFilters.Any(entityFilter => 31 | entityFilter.Invoke(entityEntry) == false)) 32 | { 33 | continue; 34 | } 35 | 36 | AuditEntries.Add(new InternalAuditEntry(entityEntry)); 37 | } 38 | 39 | return Task.CompletedTask; 40 | } 41 | 42 | protected override async Task AfterSaveChanges() 43 | { 44 | if (AuditEntries is { Count: > 0 }) 45 | { 46 | var now = DateTimeOffset.Now; 47 | 48 | foreach (var entry in AuditEntries) 49 | { 50 | if (entry is InternalAuditEntry { TemporaryProperties.Count: > 0 } auditEntry) 51 | // update TemporaryProperties 52 | { 53 | foreach (var temporaryProperty in auditEntry.TemporaryProperties) 54 | { 55 | var colName = temporaryProperty.GetColumnName(); 56 | if (temporaryProperty.Metadata.IsPrimaryKey()) 57 | { 58 | auditEntry.KeyValues[colName] = temporaryProperty.CurrentValue; 59 | } 60 | 61 | switch (auditEntry.OperationType) 62 | { 63 | case DataOperationType.Add: 64 | auditEntry.NewValues![colName] = temporaryProperty.CurrentValue; 65 | break; 66 | 67 | case DataOperationType.Delete: 68 | auditEntry.OriginalValues![colName] = temporaryProperty.OriginalValue; 69 | break; 70 | 71 | case DataOperationType.Update: 72 | auditEntry.OriginalValues![colName] = temporaryProperty.OriginalValue; 73 | auditEntry.NewValues![colName] = temporaryProperty.CurrentValue; 74 | break; 75 | } 76 | } 77 | // set to null 78 | auditEntry.TemporaryProperties = null; 79 | } 80 | 81 | // apply enricher 82 | foreach (var enricher in _auditPropertyEnrichers) 83 | { 84 | enricher.Enrich(entry); 85 | } 86 | 87 | entry.UpdatedAt = now; 88 | entry.UpdatedBy = _auditUser; 89 | } 90 | 91 | await Task.WhenAll(_auditStores.Select(store => store.Save(AuditEntries))); 92 | } 93 | } 94 | } 95 | 96 | public abstract class AuditDbContext(DbContextOptions dbContextOptions, IServiceProvider serviceProvider) 97 | : AuditDbContextBase(dbContextOptions, serviceProvider) 98 | { 99 | public virtual DbSet AuditRecords { get; set; } = null!; 100 | 101 | protected override Task BeforeSaveChanges() 102 | { 103 | if (!AuditConfig.Options.AuditEnabled) return Task.CompletedTask; 104 | 105 | AuditEntries = new List(); 106 | foreach (var entityEntry in ChangeTracker.Entries()) 107 | { 108 | if (entityEntry.State is EntityState.Detached or EntityState.Unchanged) 109 | { 110 | continue; 111 | } 112 | 113 | if (entityEntry.Entity is AuditRecord) 114 | { 115 | continue; 116 | } 117 | 118 | //entityFilters 119 | if (AuditConfig.Options.EntityFilters.Any(entityFilter => 120 | entityFilter.Invoke(entityEntry) == false)) 121 | { 122 | continue; 123 | } 124 | 125 | AuditEntries.Add(new InternalAuditEntry(entityEntry)); 126 | } 127 | 128 | return Task.CompletedTask; 129 | } 130 | 131 | protected override async Task AfterSaveChanges() 132 | { 133 | if (AuditEntries is { Count: > 0 }) 134 | { 135 | await base.AfterSaveChanges(); 136 | AuditRecords.AddRange(AuditEntries.Select(a => a.ToAuditRecord())); 137 | await base.SaveChangesAsync(); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/AuditEntry.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using WeihanLi.Common.Models; 4 | using WeihanLi.Extensions; 5 | 6 | namespace WeihanLi.EntityFramework.Audit; 7 | 8 | public class AuditEntry 9 | { 10 | public string TableName { get; set; } = null!; 11 | 12 | public Dictionary? OriginalValues { get; set; } 13 | 14 | public Dictionary? NewValues { get; set; } 15 | 16 | public Dictionary KeyValues { get; } = new(); 17 | 18 | public DataOperationType OperationType { get; set; } 19 | 20 | public Dictionary Properties { get; } = new(); 21 | 22 | public DateTimeOffset UpdatedAt { get; set; } 23 | 24 | public string? UpdatedBy { get; set; } 25 | 26 | internal AuditRecord ToAuditRecord() 27 | { 28 | return new AuditRecord() 29 | { 30 | TableName = TableName, 31 | OperationType = OperationType, 32 | Extra = Properties.Count == 0 ? null : Properties.ToJson(), 33 | OriginValue = OriginalValues?.ToJson(), 34 | NewValue = NewValues?.ToJson(), 35 | ObjectId = KeyValues.ToJson(), 36 | UpdatedAt = UpdatedAt, 37 | UpdatedBy = UpdatedBy 38 | }; 39 | } 40 | } 41 | 42 | internal sealed class InternalAuditEntry : AuditEntry 43 | { 44 | public List? TemporaryProperties { get; set; } 45 | 46 | public InternalAuditEntry(EntityEntry entityEntry) 47 | { 48 | TableName = entityEntry.Metadata.GetTableName() ?? entityEntry.Metadata.Name; 49 | 50 | if (entityEntry.Properties.Any(x => x.IsTemporary)) 51 | { 52 | TemporaryProperties = new List(4); 53 | } 54 | 55 | if (entityEntry.State == EntityState.Added) 56 | { 57 | OperationType = DataOperationType.Add; 58 | NewValues = new Dictionary(); 59 | } 60 | else if (entityEntry.State == EntityState.Deleted) 61 | { 62 | OperationType = DataOperationType.Delete; 63 | OriginalValues = new Dictionary(); 64 | } 65 | else if (entityEntry.State == EntityState.Modified) 66 | { 67 | OperationType = DataOperationType.Update; 68 | OriginalValues = new Dictionary(); 69 | NewValues = new Dictionary(); 70 | } 71 | foreach (var propertyEntry in entityEntry.Properties) 72 | { 73 | if (AuditConfig.Options.PropertyFilters.Any(f => f.Invoke(entityEntry, propertyEntry) == false)) 74 | { 75 | continue; 76 | } 77 | 78 | if (propertyEntry.IsTemporary) 79 | { 80 | TemporaryProperties!.Add(propertyEntry); 81 | continue; 82 | } 83 | 84 | var columnName = propertyEntry.GetColumnName(); 85 | if (propertyEntry.Metadata.IsPrimaryKey()) 86 | { 87 | KeyValues[columnName] = propertyEntry.CurrentValue; 88 | } 89 | switch (entityEntry.State) 90 | { 91 | case EntityState.Added: 92 | NewValues![columnName] = propertyEntry.CurrentValue; 93 | break; 94 | 95 | case EntityState.Deleted: 96 | OriginalValues![columnName] = propertyEntry.OriginalValue; 97 | break; 98 | 99 | case EntityState.Modified: 100 | if (propertyEntry.IsModified || AuditConfig.Options.SaveUnModifiedProperties) 101 | { 102 | OriginalValues![columnName] = propertyEntry.OriginalValue; 103 | NewValues![columnName] = propertyEntry.CurrentValue; 104 | } 105 | break; 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/AuditExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Linq.Expressions; 5 | using WeihanLi.Common.Aspect; 6 | using WeihanLi.Common.Helpers; 7 | using WeihanLi.Common.Services; 8 | using WeihanLi.Extensions; 9 | 10 | namespace WeihanLi.EntityFramework.Audit; 11 | 12 | public static class AuditExtensions 13 | { 14 | #region AuditEntry 15 | 16 | public static bool WithProperty(this AuditEntry auditEntry, string propertyName, 17 | object propertyValue, bool overwrite = false) 18 | { 19 | ArgumentNullException.ThrowIfNull(auditEntry); 20 | 21 | if (auditEntry.Properties.ContainsKey(propertyName) && overwrite == false) 22 | { 23 | return false; 24 | } 25 | 26 | auditEntry.Properties[propertyName] = propertyValue; 27 | return true; 28 | } 29 | 30 | public static bool WithProperty(this AuditEntry auditEntry, string propertyName, 31 | Func propertyValueFactory, bool overwrite = false) 32 | { 33 | ArgumentNullException.ThrowIfNull(auditEntry); 34 | 35 | if (auditEntry.Properties.ContainsKey(propertyName) && overwrite == false) 36 | { 37 | return false; 38 | } 39 | 40 | auditEntry.Properties[propertyName] = propertyValueFactory?.Invoke(auditEntry); 41 | return true; 42 | } 43 | 44 | #endregion AuditEntry 45 | 46 | #region IAuditConfigBuilder 47 | 48 | public static IAuditConfigBuilder IgnoreEntity(this IAuditConfigBuilder configBuilder, Type entityType) 49 | { 50 | configBuilder.WithEntityFilter(entityEntry => entityEntry.Entity.GetType() != entityType); 51 | return configBuilder; 52 | } 53 | 54 | public static IAuditConfigBuilder IgnoreEntity(this IAuditConfigBuilder configBuilder) where TEntity : class 55 | { 56 | configBuilder.WithEntityFilter(entityEntry => entityEntry.Entity.GetType() != typeof(TEntity)); 57 | return configBuilder; 58 | } 59 | 60 | public static IAuditConfigBuilder IgnoreTable(this IAuditConfigBuilder configBuilder, string tableName) 61 | { 62 | configBuilder.WithEntityFilter(entityEntry => entityEntry.Metadata.GetTableName() != tableName); 63 | return configBuilder; 64 | } 65 | 66 | public static IAuditConfigBuilder WithEntityFilter(this IAuditConfigBuilder configBuilder, Func filterFunc) 67 | { 68 | configBuilder.WithEntityFilter(filterFunc); 69 | return configBuilder; 70 | } 71 | 72 | public static IAuditConfigBuilder IgnoreProperty(this IAuditConfigBuilder configBuilder, Expression> propertyExpression) where TEntity : class 73 | { 74 | var propertyName = propertyExpression.GetMemberName(); 75 | configBuilder.WithPropertyFilter(propertyEntry => propertyEntry.Metadata.Name != propertyName); 76 | return configBuilder; 77 | } 78 | 79 | public static IAuditConfigBuilder IgnoreProperty(this IAuditConfigBuilder configBuilder, string propertyName) 80 | { 81 | configBuilder.WithPropertyFilter(propertyEntry => propertyEntry.Metadata.Name != propertyName); 82 | return configBuilder; 83 | } 84 | 85 | public static IAuditConfigBuilder IgnoreColumn(this IAuditConfigBuilder configBuilder, string columnName) 86 | { 87 | configBuilder.WithPropertyFilter(propertyEntry => propertyEntry.GetColumnName() != columnName); 88 | return configBuilder; 89 | } 90 | 91 | public static IAuditConfigBuilder IgnoreColumn(this IAuditConfigBuilder configBuilder, string tableName, string columnName) 92 | { 93 | configBuilder.WithPropertyFilter((entityEntry, propertyEntry) => entityEntry.Metadata.GetTableName() != tableName 94 | && propertyEntry.GetColumnName() != columnName); 95 | return configBuilder; 96 | } 97 | 98 | public static IAuditConfigBuilder WithPropertyFilter(this IAuditConfigBuilder configBuilder, Func filterFunc) 99 | { 100 | configBuilder.WithPropertyFilter((entity, prop) => filterFunc.Invoke(prop)); 101 | return configBuilder; 102 | } 103 | 104 | public static IAuditConfigBuilder WithUserIdProvider(this IAuditConfigBuilder configBuilder) where TUserIdProvider : IUserIdProvider, new() 105 | { 106 | configBuilder.WithUserIdProvider(new TUserIdProvider()); 107 | return configBuilder; 108 | } 109 | 110 | public static IAuditConfigBuilder WithEnricher(this IAuditConfigBuilder configBuilder) where TEnricher : IAuditPropertyEnricher, new() 111 | { 112 | configBuilder.WithEnricher(new TEnricher()); 113 | return configBuilder; 114 | } 115 | 116 | public static IAuditConfigBuilder WithEnricher(this IAuditConfigBuilder configBuilder, params object[] parameters) where TEnricher : IAuditPropertyEnricher 117 | { 118 | configBuilder.WithEnricher(ActivatorHelper.CreateInstance(parameters)); 119 | return configBuilder; 120 | } 121 | 122 | public static IAuditConfigBuilder WithStore(this IAuditConfigBuilder configBuilder, params object[] parameters) where TAuditStore : IAuditStore 123 | { 124 | configBuilder.WithStore(ActivatorHelper.CreateInstance(parameters)); 125 | return configBuilder; 126 | } 127 | 128 | public static IAuditConfigBuilder WithAuditRecordsDbContextStore(this IAuditConfigBuilder configBuilder, Action optionsConfigure) 129 | { 130 | configBuilder.Services.AddDbContext(optionsConfigure); 131 | configBuilder.WithStore(); 132 | return configBuilder; 133 | } 134 | 135 | public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, object value, bool overwrite = false) 136 | { 137 | configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, value, overwrite)); 138 | return configBuilder; 139 | } 140 | 141 | public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, Func valueFactory, bool overwrite = false) 142 | { 143 | configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, valueFactory, overwrite)); 144 | return configBuilder; 145 | } 146 | 147 | public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, object value, Func predict, bool overwrite = false) 148 | { 149 | configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, e => value, predict, overwrite)); 150 | return configBuilder; 151 | } 152 | 153 | public static IAuditConfigBuilder EnrichWithProperty(this IAuditConfigBuilder configBuilder, string propertyName, Func valueFactory, Func predict, bool overwrite = false) 154 | { 155 | configBuilder.WithEnricher(new AuditPropertyEnricher(propertyName, valueFactory, predict, overwrite)); 156 | return configBuilder; 157 | } 158 | 159 | #endregion IAuditConfigBuilder 160 | 161 | #region FluentAspectOptions 162 | 163 | public static IInterceptionConfiguration InterceptDbContextSave(this FluentAspectOptions options) 164 | { 165 | return options.InterceptMethod(m => 166 | m.Name == nameof(DbContext.SaveChanges) 167 | || m.Name == nameof(DbContext.SaveChangesAsync)); 168 | } 169 | 170 | public static IInterceptionConfiguration InterceptDbContextSave(this FluentAspectOptions options) where TDbContext : DbContext 171 | { 172 | return options.Intercept(c => c.Target?.GetType().IsAssignableTo() == true 173 | && 174 | (c.ProxyMethod.Name == nameof(DbContext.SaveChanges) 175 | || c.ProxyMethod.Name == nameof(DbContext.SaveChangesAsync) 176 | ) 177 | ); 178 | } 179 | 180 | #endregion FluentAspectOptions 181 | } 182 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/AuditInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using WeihanLi.Common.Models; 5 | 6 | namespace WeihanLi.EntityFramework.Audit; 7 | 8 | public sealed class AuditInterceptor(IServiceProvider serviceProvider) : SaveChangesInterceptor 9 | { 10 | private List? AuditEntries { get; set; } 11 | 12 | public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) 13 | { 14 | PreSaveChanges(eventData.Context!); 15 | return base.SavingChanges(eventData, result); 16 | } 17 | 18 | public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, 19 | CancellationToken cancellationToken = default) 20 | { 21 | PreSaveChanges(eventData.Context!); 22 | return base.SavingChangesAsync(eventData, result, cancellationToken); 23 | } 24 | 25 | public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) 26 | { 27 | PostSaveChanges().GetAwaiter().GetResult(); 28 | var savedChanges = base.SavedChanges(eventData, result); 29 | return savedChanges; 30 | } 31 | 32 | public override async ValueTask SavedChangesAsync(SaveChangesCompletedEventData eventData, int result, 33 | CancellationToken cancellationToken = default) 34 | { 35 | await PostSaveChanges(); 36 | var savedChanges = await base.SavedChangesAsync(eventData, result, cancellationToken); 37 | return savedChanges; 38 | } 39 | 40 | private void PreSaveChanges(DbContext dbContext) 41 | { 42 | if (!AuditConfig.Options.AuditEnabled) 43 | return; 44 | 45 | if (!serviceProvider.GetServices().Any()) 46 | return; 47 | 48 | if (AuditEntries is null) 49 | { 50 | AuditEntries = new List(); 51 | } 52 | else 53 | { 54 | AuditEntries.Clear(); 55 | } 56 | 57 | foreach (var entityEntry in dbContext.ChangeTracker.Entries()) 58 | { 59 | if (entityEntry.State is EntityState.Detached or EntityState.Unchanged) 60 | { 61 | continue; 62 | } 63 | 64 | if (AuditConfig.Options.EntityFilters.Any(entityFilter => 65 | entityFilter.Invoke(entityEntry) == false)) 66 | { 67 | continue; 68 | } 69 | 70 | AuditEntries.Add(new InternalAuditEntry(entityEntry)); 71 | } 72 | } 73 | 74 | private async Task PostSaveChanges() 75 | { 76 | if (AuditEntries is { Count: > 0 }) 77 | { 78 | var now = DateTimeOffset.Now; 79 | 80 | var auditUserIdProvider = AuditConfig.Options.UserIdProviderFactory?.Invoke(serviceProvider); 81 | var auditUser = auditUserIdProvider?.GetUserId(); 82 | var enrichers = serviceProvider.GetServices().ToArray(); 83 | 84 | foreach (var entry in AuditEntries) 85 | { 86 | // update TemporaryProperties 87 | if (entry is InternalAuditEntry { TemporaryProperties.Count: > 0 } auditEntry) 88 | { 89 | foreach (var temporaryProperty in auditEntry.TemporaryProperties) 90 | { 91 | var colName = temporaryProperty.GetColumnName(); 92 | if (temporaryProperty.Metadata.IsPrimaryKey()) 93 | { 94 | auditEntry.KeyValues[colName] = temporaryProperty.CurrentValue; 95 | } 96 | 97 | switch (auditEntry.OperationType) 98 | { 99 | case DataOperationType.Add: 100 | auditEntry.NewValues![colName] = temporaryProperty.CurrentValue; 101 | break; 102 | 103 | case DataOperationType.Delete: 104 | auditEntry.OriginalValues![colName] = temporaryProperty.OriginalValue; 105 | break; 106 | 107 | case DataOperationType.Update: 108 | auditEntry.OriginalValues![colName] = temporaryProperty.OriginalValue; 109 | auditEntry.NewValues![colName] = temporaryProperty.CurrentValue; 110 | break; 111 | } 112 | } 113 | 114 | // set TemporaryProperties to null 115 | auditEntry.TemporaryProperties = null; 116 | } 117 | 118 | // apply enricher 119 | foreach (var enricher in enrichers) 120 | { 121 | enricher.Enrich(entry); 122 | } 123 | 124 | entry.UpdatedBy = auditUser; 125 | entry.UpdatedAt = now; 126 | } 127 | 128 | await Task.WhenAll( 129 | serviceProvider.GetServices() 130 | .Select(store => store.Save(AuditEntries)) 131 | ); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/AuditRecord.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using WeihanLi.Common.Models; 3 | 4 | namespace WeihanLi.EntityFramework.Audit; 5 | 6 | public class AuditRecord 7 | { 8 | public long Id { get; set; } 9 | 10 | [Required] 11 | [StringLength(128)] 12 | public string TableName { get; set; } = null!; 13 | 14 | public DataOperationType OperationType { get; set; } 15 | 16 | [StringLength(256)] 17 | public string? ObjectId { get; set; } 18 | 19 | public string? OriginValue { get; set; } 20 | 21 | public string? NewValue { get; set; } 22 | 23 | public string? Extra { get; set; } 24 | 25 | [StringLength(128)] 26 | public string? UpdatedBy { get; set; } 27 | 28 | public DateTimeOffset UpdatedAt { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/AuditRecordsDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WeihanLi.EntityFramework.Audit; 4 | 5 | public sealed class AuditRecordsDbContext(DbContextOptions dbContextOptions) 6 | : DbContext(dbContextOptions) 7 | { 8 | public DbSet AuditRecords { get; set; } = null!; 9 | } 10 | 11 | internal sealed class AuditRecordsDbContextStore(AuditRecordsDbContext dbContext) : IAuditStore 12 | { 13 | public async Task Save(ICollection auditEntries) 14 | { 15 | if (auditEntries is not { Count: > 0 }) 16 | return; 17 | 18 | foreach (var entry in auditEntries) 19 | { 20 | var record = entry.ToAuditRecord(); 21 | dbContext.Add(record); 22 | } 23 | 24 | await dbContext.Database.EnsureCreatedAsync(); 25 | await dbContext.SaveChangesAsync(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/IAuditConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using WeihanLi.Common; 5 | using WeihanLi.Common.Services; 6 | 7 | namespace WeihanLi.EntityFramework.Audit; 8 | 9 | public interface IAuditConfigBuilder 10 | { 11 | IServiceCollection Services { get; } 12 | 13 | IAuditConfigBuilder WithUserIdProvider(IUserIdProvider auditUserProvider) => 14 | WithUserIdProvider(_ => auditUserProvider); 15 | 16 | IAuditConfigBuilder WithUserIdProvider(Func auditUserProviderFactory); 17 | 18 | IAuditConfigBuilder WithUnmodifiedProperty(bool saveUnModifiedProperty = true); 19 | 20 | IAuditConfigBuilder WithStore(IAuditStore auditStore); 21 | IAuditConfigBuilder WithStore() where TStore : class, IAuditStore; 22 | 23 | IAuditConfigBuilder WithEntityFilter(Func entityFilter); 24 | 25 | IAuditConfigBuilder WithPropertyFilter(Func propertyFilter); 26 | 27 | IAuditConfigBuilder WithEnricher(IAuditPropertyEnricher enricher); 28 | 29 | IAuditConfigBuilder WithEnricher(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) 30 | where TEnricher : IAuditPropertyEnricher; 31 | } 32 | 33 | internal sealed class AuditConfigBuilder(IServiceCollection services) : IAuditConfigBuilder 34 | { 35 | private Func? _auditUserProviderFactory = 36 | sp => 37 | { 38 | var userIdProvider = sp.GetService(); 39 | return userIdProvider ?? EnvironmentUserIdProvider.Instance; 40 | }; 41 | private readonly List> _entityFilters = new(); 42 | private readonly List> _propertyFilters = new(); 43 | private bool _saveUnModifiedProperty; 44 | 45 | public IServiceCollection Services => services; 46 | 47 | public IAuditConfigBuilder WithUserIdProvider(Func? auditUserProviderFactory) 48 | { 49 | _auditUserProviderFactory = auditUserProviderFactory; 50 | return this; 51 | } 52 | 53 | public IAuditConfigBuilder WithUnmodifiedProperty(bool saveUnModifiedProperty = true) 54 | { 55 | _saveUnModifiedProperty = saveUnModifiedProperty; 56 | return this; 57 | } 58 | 59 | public IAuditConfigBuilder WithStore(IAuditStore auditStore) 60 | { 61 | ArgumentNullException.ThrowIfNull(auditStore); 62 | 63 | services.AddSingleton(auditStore); 64 | return this; 65 | } 66 | 67 | public IAuditConfigBuilder WithStore() where TStore : class, IAuditStore 68 | { 69 | services.AddScoped(); 70 | return this; 71 | } 72 | 73 | public IAuditConfigBuilder WithEntityFilter(Func entityFilter) 74 | { 75 | ArgumentNullException.ThrowIfNull(entityFilter); 76 | _entityFilters.Add(entityFilter); 77 | return this; 78 | } 79 | 80 | public IAuditConfigBuilder WithPropertyFilter(Func propertyFilter) 81 | { 82 | ArgumentNullException.ThrowIfNull(propertyFilter); 83 | _propertyFilters.Add(propertyFilter); 84 | return this; 85 | } 86 | 87 | public IAuditConfigBuilder WithEnricher(IAuditPropertyEnricher enricher) 88 | { 89 | ArgumentNullException.ThrowIfNull(enricher); 90 | services.AddSingleton(enricher); 91 | return this; 92 | } 93 | 94 | public IAuditConfigBuilder WithEnricher(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) 95 | where TEnricher : IAuditPropertyEnricher 96 | { 97 | services.TryAddEnumerable(new ServiceDescriptor(typeof(IAuditPropertyEnricher), typeof(TEnricher), serviceLifetime)); 98 | return this; 99 | } 100 | 101 | public AuditConfigOptions Build() 102 | { 103 | return new() 104 | { 105 | EntityFilters = _entityFilters, 106 | PropertyFilters = _propertyFilters, 107 | UserIdProviderFactory = _auditUserProviderFactory, 108 | SaveUnModifiedProperties = _saveUnModifiedProperty, 109 | }; 110 | } 111 | } 112 | 113 | internal sealed class AuditConfigOptions 114 | { 115 | public bool AuditEnabled { get; set; } = true; 116 | 117 | public bool SaveUnModifiedProperties { get; set; } 118 | 119 | public Func? UserIdProviderFactory { get; set; } 120 | 121 | private IReadOnlyCollection> _entityFilters = Array.Empty>(); 122 | 123 | public IReadOnlyCollection> EntityFilters 124 | { 125 | get => _entityFilters; 126 | set => _entityFilters = Guard.NotNull(value); 127 | } 128 | 129 | private IReadOnlyCollection> _propertyFilters = Array.Empty>(); 130 | 131 | public IReadOnlyCollection> PropertyFilters 132 | { 133 | get => _propertyFilters; 134 | set => _propertyFilters = Guard.NotNull(value); 135 | } 136 | } 137 | 138 | public static class AuditConfig 139 | { 140 | internal static AuditConfigOptions Options = new(); 141 | 142 | public static void EnableAudit() 143 | { 144 | Options.AuditEnabled = true; 145 | } 146 | 147 | public static void DisableAudit() 148 | { 149 | Options.AuditEnabled = false; 150 | } 151 | 152 | #nullable disable 153 | 154 | public static void Configure(IServiceCollection services, Action configAction) 155 | { 156 | ArgumentNullException.ThrowIfNull(services); 157 | if (configAction is null) 158 | return; 159 | #nullable restore 160 | 161 | var builder = new AuditConfigBuilder(services); 162 | configAction.Invoke(builder); 163 | Options = builder.Build(); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/IAuditPropertyEnricher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WeihanLi.Common.Helpers; 3 | 4 | namespace WeihanLi.EntityFramework.Audit; 5 | 6 | public interface IAuditPropertyEnricher : IEnricher 7 | { 8 | } 9 | 10 | public class AuditPropertyEnricher : PropertyEnricher, IAuditPropertyEnricher 11 | { 12 | public AuditPropertyEnricher(string propertyName, object propertyValue, bool overwrite = false) : base(propertyName, propertyValue, overwrite) 13 | { 14 | } 15 | 16 | public AuditPropertyEnricher(string propertyName, Func propertyValueFactory, bool overwrite = false) : base(propertyName, propertyValueFactory, overwrite) 17 | { 18 | } 19 | 20 | public AuditPropertyEnricher(string propertyName, Func propertyValueFactory, Func propertyPredict, bool overwrite = false) : base(propertyName, propertyValueFactory, propertyPredict, overwrite) 21 | { 22 | } 23 | 24 | protected override Action, bool> EnrichAction => 25 | (auditEntry, propertyName, valueFactory, overwrite) => auditEntry.WithProperty(propertyName, valueFactory, overwrite); 26 | } 27 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Audit/IAuditStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using WeihanLi.Common.Helpers.PeriodBatching; 5 | 6 | namespace WeihanLi.EntityFramework.Audit; 7 | 8 | public interface IAuditStore 9 | { 10 | Task Save(ICollection auditEntries); 11 | } 12 | 13 | public class PeriodBatchingAuditStore : PeriodicBatching, IAuditStore 14 | { 15 | public PeriodBatchingAuditStore(int batchSizeLimit, TimeSpan period) : base(batchSizeLimit, period) 16 | { 17 | } 18 | 19 | public PeriodBatchingAuditStore(int batchSizeLimit, TimeSpan period, int queueLimit) : base(batchSizeLimit, period, queueLimit) 20 | { 21 | } 22 | 23 | public Task Save(ICollection auditEntries) 24 | { 25 | foreach (var entry in auditEntries) 26 | { 27 | Emit(entry); 28 | } 29 | return Task.CompletedTask; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/DbContextBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | /// 8 | /// DbContextBase 9 | /// Custom DbContext template 10 | /// 11 | public abstract class DbContextBase : DbContext 12 | { 13 | protected DbContextBase() 14 | { 15 | } 16 | 17 | protected DbContextBase(DbContextOptions dbContextOptions) : base(dbContextOptions) 18 | { 19 | } 20 | 21 | protected virtual Task BeforeSaveChanges() => Task.CompletedTask; 22 | 23 | protected virtual Task AfterSaveChanges() => Task.CompletedTask; 24 | 25 | public override int SaveChanges() 26 | { 27 | BeforeSaveChanges().ConfigureAwait(false).GetAwaiter().GetResult(); 28 | var result = base.SaveChanges(); 29 | AfterSaveChanges().ConfigureAwait(false).GetAwaiter().GetResult(); 30 | return result; 31 | } 32 | 33 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) 34 | { 35 | await BeforeSaveChanges(); 36 | var result = await base.SaveChangesAsync(cancellationToken); 37 | await AfterSaveChanges(); 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/DbContextOptionsConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | 4 | namespace WeihanLi.EntityFramework; 5 | 6 | internal sealed class DbContextOptionsConfiguration( 7 | Action optionsAction 8 | ) 9 | : IDbContextOptionsConfiguration 10 | where TContext : DbContext 11 | { 12 | private readonly Action _optionsAction = optionsAction ?? throw new ArgumentNullException(nameof(optionsAction)); 13 | 14 | public void Configure(IServiceProvider serviceProvider, DbContextOptionsBuilder optionsBuilder) 15 | { 16 | _optionsAction.Invoke(serviceProvider, optionsBuilder); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/DbFunctions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Query; 3 | using System; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | public static class DbFunctions 8 | { 9 | [DbFunction("JSON_VALUE", "")] 10 | public static string? JsonValue(string column, [NotParameterized] string path) 11 | { 12 | throw new NotSupportedException(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using Microsoft.EntityFrameworkCore.Diagnostics; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | using System.Data; 9 | using System.Linq.Expressions; 10 | using WeihanLi.Common.Models; 11 | using WeihanLi.Common.Services; 12 | using WeihanLi.EntityFramework.Audit; 13 | using WeihanLi.EntityFramework.Interceptors; 14 | using WeihanLi.EntityFramework.Services; 15 | using WeihanLi.Extensions; 16 | 17 | namespace WeihanLi.EntityFramework; 18 | 19 | public static class EFExtensions 20 | { 21 | public static IEFRepository GetRepository(this TDbContext dbContext) 22 | where TEntity : class 23 | where TDbContext : DbContext 24 | { 25 | return new EFRepository(dbContext); 26 | } 27 | 28 | public static IEFUnitOfWork GetUnitOfWork(this TDbContext dbContext) 29 | where TDbContext : DbContext 30 | { 31 | return new EFUnitOfWork(dbContext); 32 | } 33 | 34 | public static IEFUnitOfWork GetUnitOfWork(TDbContext dbContext, IsolationLevel isolationLevel) 35 | where TDbContext : DbContext 36 | { 37 | return new EFUnitOfWork(dbContext, isolationLevel); 38 | } 39 | 40 | public static EntityEntry? Remove(this DbContext dbContext, params object[] keyValues) where TEntity : class 41 | { 42 | var entity = dbContext.Find(keyValues); 43 | if (entity == null) 44 | { 45 | return null; 46 | } 47 | 48 | return dbContext.Remove(entity); 49 | } 50 | 51 | public static EntityEntry Update(this DbContext dbContext, TEntity entity, params string[] propNames) where TEntity : class 52 | { 53 | if (propNames.IsNullOrEmpty()) 54 | { 55 | return dbContext.Update(entity); 56 | } 57 | var entry = dbContext.GetEntityEntry(entity, out var existBefore); 58 | if (existBefore) 59 | { 60 | foreach (var propEntry in entry.Properties) 61 | { 62 | if (!propNames.Contains(propEntry.Metadata.Name)) 63 | { 64 | propEntry.IsModified = false; 65 | } 66 | } 67 | } 68 | else 69 | { 70 | entry.State = EntityState.Unchanged; 71 | foreach (var propName in propNames) 72 | { 73 | entry.Property(propName).IsModified = true; 74 | } 75 | } 76 | 77 | return entry; 78 | } 79 | 80 | public static EntityEntry UpdateWithout(this DbContext dbContext, TEntity entity, params string[] propNames) where TEntity : class 81 | { 82 | if (propNames.IsNullOrEmpty()) 83 | { 84 | return dbContext.Update(entity); 85 | } 86 | var entry = dbContext.GetEntityEntry(entity, out _); 87 | entry.State = EntityState.Modified; 88 | foreach (var expression in propNames) 89 | { 90 | entry.Property(expression).IsModified = false; 91 | } 92 | 93 | return entry; 94 | } 95 | 96 | public static EntityEntry Update(this DbContext dbContext, TEntity entity, params Expression>[] propertyExpressions) where TEntity : class 97 | { 98 | if (propertyExpressions.IsNullOrEmpty()) 99 | { 100 | return dbContext.Update(entity); 101 | } 102 | 103 | var entry = dbContext.GetEntityEntry(entity, out var existBefore); 104 | 105 | if (existBefore) 106 | { 107 | var propNames = propertyExpressions.Select(x => x.GetMemberName()).ToArray(); 108 | 109 | foreach (var propEntry in entry.Properties) 110 | { 111 | if (!propNames.Contains(propEntry.Metadata.Name)) 112 | { 113 | propEntry.IsModified = false; 114 | } 115 | } 116 | } 117 | else 118 | { 119 | entry.State = EntityState.Unchanged; 120 | foreach (var expression in propertyExpressions) 121 | { 122 | entry.Property(expression).IsModified = true; 123 | } 124 | } 125 | 126 | return entry; 127 | } 128 | 129 | public static EntityEntry UpdateWithout(this DbContext dbContext, TEntity entity, params Expression>[] propertyExpressions) where TEntity : class 130 | { 131 | if (propertyExpressions.IsNullOrEmpty()) 132 | { 133 | return dbContext.Update(entity); 134 | } 135 | 136 | var entry = dbContext.GetEntityEntry(entity, out _); 137 | 138 | entry.State = EntityState.Modified; 139 | foreach (var expression in propertyExpressions) 140 | { 141 | entry.Property(expression).IsModified = false; 142 | } 143 | 144 | return entry; 145 | } 146 | 147 | public static string GetTableName(this DbContext dbContext) 148 | { 149 | var entityType = dbContext.Model.FindEntityType(typeof(TEntity)); 150 | ArgumentNullException.ThrowIfNull(entityType); 151 | return entityType.GetTableName()!; 152 | } 153 | 154 | public static KeyEntry[] GetKeyValues(this EntityEntry entityEntry) 155 | { 156 | if (!entityEntry.IsKeySet) 157 | return []; 158 | 159 | var keyProps = entityEntry.Properties 160 | .Where(p => p.Metadata.IsPrimaryKey()) 161 | .ToArray(); 162 | if (keyProps.Length == 0) 163 | return []; 164 | 165 | var keyEntries = new KeyEntry[keyProps.Length]; 166 | for (var i = 0; i < keyProps.Length; i++) 167 | { 168 | keyEntries[i] = new KeyEntry() 169 | { 170 | PropertyName = keyProps[i].Metadata.Name, 171 | ColumnName = keyProps[i].GetColumnName(), 172 | Value = keyProps[i].CurrentValue, 173 | }; 174 | } 175 | 176 | return keyEntries; 177 | } 178 | 179 | public static IServiceCollection AddEFAutoUpdateInterceptor(this IServiceCollection services) 180 | => services.AddEFAutoUpdateInterceptor(ServiceLifetime.Scoped); 181 | 182 | public static IServiceCollection AddEFAutoUpdateInterceptor(this IServiceCollection services, 183 | ServiceLifetime userProviderLifetime) 184 | { 185 | ArgumentNullException.ThrowIfNull(services); 186 | 187 | services.TryAddScoped(); 188 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 189 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 190 | services.TryAddEnumerable(ServiceDescriptor.Describe(typeof(IEntitySavingHandler), typeof(UpdatedBySavingHandler), userProviderLifetime)); 191 | 192 | return services; 193 | } 194 | 195 | public static IServiceCollection AddEFAutoUpdateInterceptor(this IServiceCollection services, 196 | ServiceLifetime userProviderLifetime = ServiceLifetime.Scoped) 197 | where TUserProvider : class, IUserIdProvider 198 | { 199 | services.AddEFAutoUpdateInterceptor(userProviderLifetime) 200 | .TryAdd(ServiceDescriptor.Describe(typeof(IUserIdProvider), typeof(TUserProvider), userProviderLifetime)); 201 | return services; 202 | } 203 | 204 | public static IServiceCollection AddEFAutoAudit(this IServiceCollection services, 205 | Action configAction) 206 | { 207 | ArgumentNullException.ThrowIfNull(configAction); 208 | 209 | services.TryAddScoped(); 210 | AuditConfig.Configure(services, configAction); 211 | return services; 212 | } 213 | 214 | public static EntityTypeBuilder WithSoftDeleteFilter(this EntityTypeBuilder entityTypeBuilder) 215 | where TEntity : class, ISoftDeleteEntityWithDeleted 216 | { 217 | ArgumentNullException.ThrowIfNull(entityTypeBuilder); 218 | return entityTypeBuilder.HasQueryFilter(x => x.IsDeleted == false); 219 | } 220 | 221 | public static IServiceCollection AddDbContextInterceptor( 222 | this IServiceCollection services, 223 | ServiceLifetime optionsLifetime = ServiceLifetime.Scoped 224 | ) 225 | where TContext : DbContext 226 | where TInterceptor : IInterceptor 227 | { 228 | ArgumentNullException.ThrowIfNull(services); 229 | Action optionsAction = (sp, builder) => 230 | { 231 | builder.AddInterceptors(sp.GetRequiredService()); 232 | }; 233 | services.TryAdd(ServiceDescriptor.Describe(typeof(TInterceptor), typeof(TInterceptor), optionsLifetime)); 234 | services.Add(ServiceDescriptor.Describe(typeof(IDbContextOptionsConfiguration), 235 | _ => new DbContextOptionsConfiguration(optionsAction), optionsLifetime)); 236 | return services; 237 | } 238 | 239 | private static EntityEntry GetEntityEntry(this DbContext dbContext, TEntity entity, out bool existBefore) 240 | where TEntity : class 241 | { 242 | var type = typeof(TEntity); 243 | 244 | var entityType = dbContext.Model.FindEntityType(type); 245 | var key = entityType?.FindPrimaryKey(); 246 | if (key is null) 247 | { 248 | throw new InvalidOperationException($"Type {type.FullName} had no primary key"); 249 | } 250 | 251 | var keysGetter = key.Properties 252 | .Select(x => x.PropertyInfo!.GetValueGetter()) 253 | .ToArray(); 254 | var keyValues = keysGetter 255 | .Select(x => x?.Invoke(entity)) 256 | .ToArray(); 257 | 258 | var originalEntity = dbContext.Set().Local 259 | .FirstOrDefault(x => GetEntityKeyValues(keysGetter, x).SequenceEqual(keyValues)); 260 | 261 | EntityEntry entityEntry; 262 | if (originalEntity is null) 263 | { 264 | existBefore = false; 265 | entityEntry = dbContext.Attach(entity); 266 | } 267 | else 268 | { 269 | existBefore = true; 270 | entityEntry = dbContext.Entry(originalEntity); 271 | entityEntry.CurrentValues.SetValues(entity); 272 | } 273 | 274 | return entityEntry; 275 | } 276 | 277 | private static object?[] GetEntityKeyValues(Func?[] keyValueGetter, TEntity entity) 278 | { 279 | var keyValues = keyValueGetter.Select(x => x?.Invoke(entity)).ToArray(); 280 | return keyValues; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFInternalExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | public static class EFInternalExtensions 8 | { 9 | public static string GetColumnName(this PropertyEntry propertyEntry) 10 | { 11 | var storeObjectId = 12 | StoreObjectIdentifier.Create(propertyEntry.Metadata.DeclaringType, StoreObjectType.Table); 13 | return propertyEntry.Metadata.GetColumnName(storeObjectId.GetValueOrDefault()) ?? propertyEntry.Metadata.Name; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Data; 4 | using System.Linq.Expressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace WeihanLi.EntityFramework; 9 | 10 | public static class EFRepositoryExtensions 11 | { 12 | public static Task UpdateAsync(this IEFRepository repository, 13 | TEntity entity, CancellationToken cancellationToken) 14 | where TDbContext : DbContext 15 | where TEntity : class 16 | => repository.UpdateAsync(entity, Array.Empty(), cancellationToken); 17 | 18 | public static Task UpdateAsync(this IEFRepository repository, 19 | TEntity entity, 20 | params Expression>[] propertyExpressions) 21 | where TDbContext : DbContext 22 | where TEntity : class 23 | => repository.UpdateAsync(entity, propertyExpressions); 24 | 25 | public static Task UpdateWithoutAsync(this IEFRepository repository, 26 | TEntity entity, 27 | params Expression>[] propertyExpressions) 28 | where TDbContext : DbContext 29 | where TEntity : class 30 | => repository.UpdateWithoutAsync(entity, propertyExpressions); 31 | 32 | public static async ValueTask FindAsync(this IEFRepository repository, 33 | params object[] keyValues) 34 | where TDbContext : DbContext 35 | where TEntity : class 36 | { 37 | return await repository.FindAsync(keyValues, CancellationToken.None); 38 | } 39 | 40 | public static Task DeleteAsync(this IEFRepository repository, 41 | params object[] keyValues) 42 | where TDbContext : DbContext 43 | where TEntity : class 44 | { 45 | return repository.DeleteAsync(keyValues, CancellationToken.None); 46 | } 47 | 48 | public static IEFUnitOfWork GetUnitOfWork( 49 | this IEFRepository repository) 50 | where TDbContext : DbContext 51 | where TEntity : class 52 | { 53 | return new EFUnitOfWork(repository.DbContext); 54 | } 55 | 56 | public static IEFUnitOfWork GetUnitOfWork( 57 | this IEFRepository repository, 58 | IsolationLevel isolationLevel 59 | ) 60 | where TDbContext : DbContext 61 | where TEntity : class 62 | { 63 | return new EFUnitOfWork(repository.DbContext, isolationLevel); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | internal sealed class EFRepositoryFactory : IEFRepositoryFactory 8 | where TDbContext : DbContext 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public EFRepositoryFactory(IServiceProvider serviceProvider) 13 | { 14 | _serviceProvider = serviceProvider; 15 | } 16 | 17 | public IEFRepository GetRepository() where TEntity : class 18 | { 19 | return _serviceProvider.GetRequiredService>(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | public static class EFRepositoryFactoryExtensions 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Options; 3 | using System.Linq; 4 | using System.Text; 5 | using WeihanLi.Extensions; 6 | 7 | namespace WeihanLi.EntityFramework; 8 | 9 | internal sealed class EFRepositoryGenerator : IEFRepositoryGenerator 10 | { 11 | private readonly EFRepositoryGeneratorOptions _generatorOptions; 12 | 13 | public EFRepositoryGenerator(IOptions options) 14 | { 15 | _generatorOptions = options.Value; 16 | } 17 | 18 | public string GenerateRepositoryCodeTextFor(string repositoryNamespace) where TDbContext : DbContext 19 | { 20 | var dbContextType = typeof(TDbContext); 21 | var entities = InternalHelper.GetDbContextSets(dbContextType); 22 | 23 | var modelNamespaces = entities.Select(p => p.PropertyType.GetGenericArguments()[0].Namespace).Distinct().ToList(); 24 | modelNamespaces.AddIfNotContains(dbContextType.Namespace); 25 | var entityNames = entities.Select(p => p.PropertyType.GetGenericArguments()[0].Name).ToArray(); 26 | // 27 | var builder = new StringBuilder(); 28 | builder.AppendLine("using WeihanLi.EntityFramework;"); 29 | foreach (var @namespace in modelNamespaces) 30 | { 31 | builder.AppendLine($"using {@namespace};"); 32 | } 33 | builder.AppendLine(); 34 | builder.AppendLine($"namespace {repositoryNamespace}"); 35 | builder.AppendLine("{"); 36 | foreach (var name in entityNames) 37 | { 38 | builder.AppendLine(GenerateRepository(dbContextType.Name, name)); 39 | } 40 | builder.AppendLine("}"); 41 | 42 | return builder.ToString(); 43 | } 44 | 45 | private string GenerateRepository(string dbContextName, string entityName) 46 | { 47 | var repositoryName = _generatorOptions.RepositoryNameResolver(entityName); 48 | if (_generatorOptions.GenerateInterface) 49 | { 50 | return $@" 51 | public partial interface I{repositoryName} : IEFRepository<{dbContextName}, {entityName}> {{ }} 52 | public partial class {repositoryName} : EFRepository<{dbContextName}, {entityName}>, I{repositoryName} 53 | {{ 54 | public {repositoryName}({dbContextName} dbContext) : base(dbContext) {{ }} 55 | }}"; 56 | } 57 | else 58 | { 59 | return $@" 60 | public partial class {repositoryName} : EFRepository<{dbContextName}, {entityName}> 61 | {{ 62 | public {repositoryName}({dbContextName} dbContext) : base(dbContext) {{ }} 63 | }}"; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryGeneratorExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | public static class EFRepositoryGeneratorExtensions 8 | { 9 | public static Task GenerateRepositoryCodeFor(this IEFRepositoryGenerator repositoryGenerator, string repositoryNamespace, 10 | string? outputPath = null) where TDbContext : DbContext 11 | { 12 | var repositoryText = repositoryGenerator.GenerateRepositoryCodeTextFor(repositoryNamespace); 13 | if (string.IsNullOrEmpty(outputPath)) 14 | { 15 | outputPath = $"{typeof(TDbContext).Name.Replace("DbContext", "").Replace("Context", "")}Repository"; 16 | } 17 | if (!outputPath.EndsWith(".cs")) 18 | { 19 | outputPath += ".generated.cs"; 20 | } 21 | return File.WriteAllTextAsync(outputPath, repositoryText); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryGeneratorOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WeihanLi.EntityFramework; 4 | 5 | public class EFRepositoryGeneratorOptions 6 | { 7 | public bool GenerateInterface { get; set; } = true; 8 | 9 | /// 10 | /// RepositoryNameResolver 11 | /// 12 | public Func RepositoryNameResolver { get; set; } 13 | = entityName => $"{entityName}Repository"; 14 | } 15 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFRepositoryQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Query; 3 | using System.Linq.Expressions; 4 | using WeihanLi.Common; 5 | 6 | namespace WeihanLi.EntityFramework; 7 | 8 | public class EFRepositoryQueryBuilder where TEntity : class 9 | { 10 | private readonly DbSet _dbSet; 11 | 12 | public EFRepositoryQueryBuilder(DbSet dbSet) 13 | { 14 | _dbSet = dbSet; 15 | } 16 | 17 | private readonly List>> _whereExpression = new(); 18 | 19 | public EFRepositoryQueryBuilder WithPredict(Expression> predict) 20 | { 21 | _whereExpression.Add(Guard.NotNull(predict)); 22 | return this; 23 | } 24 | 25 | public EFRepositoryQueryBuilder WithPredictIf(Expression> predict, bool condition) 26 | { 27 | if (condition) 28 | _whereExpression.Add(Guard.NotNull(predict)); 29 | return this; 30 | } 31 | 32 | private Func, IOrderedQueryable>? _orderByExpression; 33 | 34 | public EFRepositoryQueryBuilder WithOrderBy(Func, IOrderedQueryable> orderByExpression) 35 | { 36 | _orderByExpression = orderByExpression; 37 | return this; 38 | } 39 | 40 | private bool _disableTracking = true; 41 | 42 | public EFRepositoryQueryBuilder WithNoTracking(bool noTracking = true) 43 | { 44 | _disableTracking = noTracking; 45 | return this; 46 | } 47 | 48 | private bool _ignoreQueryFilters; 49 | 50 | public EFRepositoryQueryBuilder IgnoreQueryFilters(bool ignoreQueryFilters = true) 51 | { 52 | _ignoreQueryFilters = ignoreQueryFilters; 53 | return this; 54 | } 55 | 56 | private readonly HashSet _queryFiltersToIgnore = new(); 57 | 58 | public EFRepositoryQueryBuilder IgnoreQueryFilters(IReadOnlyCollection queryFilters, bool ignoreQueryFilters = true) 59 | { 60 | ArgumentNullException.ThrowIfNull(queryFilters); 61 | if (ignoreQueryFilters) 62 | { 63 | foreach (var queryFilter in queryFilters) 64 | { 65 | _queryFiltersToIgnore.Add(queryFilter); 66 | } 67 | } 68 | else 69 | { 70 | foreach (var queryFilter in queryFilters) 71 | { 72 | _queryFiltersToIgnore.Remove(queryFilter); 73 | } 74 | } 75 | return this; 76 | } 77 | 78 | private int _count; 79 | 80 | public EFRepositoryQueryBuilder WithCount(int count) 81 | { 82 | _count = count; 83 | return this; 84 | } 85 | 86 | private readonly List, IIncludableQueryable>> _includeExpressions = new(); 87 | 88 | public EFRepositoryQueryBuilder WithInclude(Func, IIncludableQueryable> include) 89 | { 90 | _includeExpressions.Add(include); 91 | return this; 92 | } 93 | 94 | public IQueryable Build() 95 | { 96 | IQueryable query = _dbSet; 97 | if (_disableTracking) 98 | { 99 | query = _dbSet.AsNoTracking(); 100 | } 101 | if (_ignoreQueryFilters) 102 | { 103 | query = query.IgnoreQueryFilters(); 104 | } 105 | else if (_queryFiltersToIgnore.Count > 0) 106 | { 107 | query = query.IgnoreQueryFilters(_queryFiltersToIgnore); 108 | } 109 | if (_whereExpression.Count > 0) 110 | { 111 | foreach (var expression in _whereExpression) 112 | { 113 | query = query.Where(expression); 114 | } 115 | } 116 | if (_orderByExpression != null) 117 | { 118 | query = _orderByExpression(query); 119 | } 120 | if (_count > 0) 121 | { 122 | query = query.Take(_count); 123 | } 124 | foreach (var include in _includeExpressions) 125 | { 126 | query = include(query); 127 | } 128 | return query; 129 | } 130 | 131 | public IQueryable Build(Expression> selector) 132 | { 133 | ArgumentNullException.ThrowIfNull(selector); 134 | var query = Build(); 135 | return query.Select(selector); 136 | } 137 | 138 | public IQueryable Build(Expression> selector) 139 | { 140 | ArgumentNullException.ThrowIfNull(selector); 141 | var query = Build(); 142 | return query.Select(selector); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Storage; 3 | using System; 4 | using System.Data; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace WeihanLi.EntityFramework; 9 | 10 | public class EFUnitOfWork : 11 | IEFUnitOfWork where TDbContext : DbContext 12 | , IDisposable 13 | { 14 | // https://docs.microsoft.com/en-us/ef/core/saving/transactions 15 | private readonly IDbContextTransaction? _transaction; 16 | 17 | public EFUnitOfWork(TDbContext dbContext) 18 | { 19 | DbContext = dbContext; 20 | if (DbContext.Database.IsRelational()) 21 | { 22 | _transaction = DbContext.Database.BeginTransaction(); 23 | } 24 | } 25 | 26 | internal EFUnitOfWork(TDbContext dbContext, IsolationLevel isolationLevel) 27 | { 28 | DbContext = dbContext; 29 | if (DbContext.Database.IsRelational()) 30 | { 31 | _transaction = DbContext.Database.BeginTransaction(isolationLevel); 32 | } 33 | } 34 | 35 | public TDbContext DbContext { get; } 36 | 37 | public IEFRepository GetRepository() where TEntity : class 38 | { 39 | return new EFRepository(DbContext); 40 | } 41 | 42 | public virtual void Commit() 43 | { 44 | DbContext.SaveChanges(); 45 | _transaction?.Commit(); 46 | } 47 | 48 | public virtual async Task CommitAsync(CancellationToken cancellationToken) 49 | { 50 | await DbContext.SaveChangesAsync(cancellationToken); 51 | if (_transaction is not null) 52 | { 53 | await _transaction.CommitAsync(cancellationToken); 54 | } 55 | } 56 | 57 | public virtual void Rollback() 58 | { 59 | _transaction?.Rollback(); 60 | } 61 | 62 | public virtual Task RollbackAsync(CancellationToken cancellationToken) 63 | { 64 | _transaction?.Rollback(); 65 | return Task.CompletedTask; 66 | } 67 | 68 | public virtual void Dispose() 69 | { 70 | _transaction?.Dispose(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/EFUnitOfWorkExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | 4 | namespace WeihanLi.EntityFramework; 5 | 6 | public static class EFUnitOfWorkExtensions 7 | { 8 | public static DbSet DbSet(this IEFUnitOfWork unitOfWork) 9 | where TEntity : class 10 | { 11 | if (unitOfWork is null) 12 | { 13 | throw new ArgumentNullException(nameof(unitOfWork)); 14 | } 15 | return unitOfWork.DbContext.Set(); 16 | } 17 | 18 | public static IEFRepository GetRepository( 19 | this IEFUnitOfWork unitOfWork) 20 | where TDbContext : DbContext 21 | where TEntity : class 22 | { 23 | if (unitOfWork is null) 24 | { 25 | throw new ArgumentNullException(nameof(unitOfWork)); 26 | } 27 | return new EFRepository(unitOfWork.DbContext); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/IEFRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Query; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using WeihanLi.Common.Data; 10 | using WeihanLi.Common.Models; 11 | 12 | namespace WeihanLi.EntityFramework; 13 | 14 | public interface IEFRepository : IRepository 15 | where TDbContext : DbContext 16 | where TEntity : class 17 | { 18 | TDbContext DbContext { get; } 19 | 20 | /// 21 | /// Find an entity 22 | /// 23 | /// keyValues 24 | /// the entity founded, if not found, null returned 25 | TEntity? Find(params object[] keyValues); 26 | 27 | /// 28 | /// Find an entity 29 | /// 30 | /// keyValues 31 | /// cancellationToken 32 | /// the entity founded, if not found, null returned 33 | ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken); 34 | 35 | int Update(Action> setExpression, Action>? queryBuilderAction = null); 36 | 37 | Task UpdateAsync( 38 | Action> setExpression, 39 | Action>? queryBuilderAction = null, 40 | CancellationToken cancellationToken = default); 41 | 42 | /// 43 | /// Delete a entity 44 | /// 45 | /// keyValues 46 | /// affected rows 47 | int Delete(object[] keyValues); 48 | 49 | /// 50 | /// Delete a entity 51 | /// 52 | /// entity 53 | /// cancellationToken 54 | /// affected rows 55 | Task DeleteAsync(object[] keyValues, CancellationToken cancellationToken); 56 | 57 | /// 58 | /// Gets the based on a predicate 59 | /// 60 | /// queryBuilder 61 | /// This method default no-tracking query. 62 | IQueryable Query(Action>? queryBuilderAction = null); 63 | 64 | /// 65 | /// Gets the based on a predicate 66 | /// 67 | /// queryBuilderAction 68 | /// This method default no-tracking query. 69 | List Get(Action>? queryBuilderAction = null); 70 | 71 | /// 72 | /// Gets the based on a predicate 73 | /// 74 | /// 75 | /// queryBuilderAction 76 | /// This method default no-tracking query. 77 | List GetResult(Expression> selector, Action>? queryBuilderAction = null); 78 | 79 | /// 80 | /// Gets the based on a predicate 81 | /// 82 | /// queryBuilder 83 | /// cancellationToken 84 | /// This method default no-tracking query. 85 | Task> GetAsync(Action>? queryBuilderAction = null, CancellationToken cancellationToken = default); 86 | 87 | /// 88 | /// Gets the based on a predicate 89 | /// 90 | /// selector 91 | /// queryBuilder 92 | /// 93 | /// This method default no-tracking query. 94 | Task> GetResultAsync(Expression> selector, Action>? queryBuilderAction = null, CancellationToken cancellationToken = default); 95 | 96 | /// 97 | /// Gets the based on a predicate 98 | /// 99 | /// queryBuilderAction 100 | /// This method default no-tracking query. 101 | bool Any(Action>? queryBuilderAction = null); 102 | 103 | /// 104 | /// Gets the based on a predicate 105 | /// 106 | /// queryBuilder 107 | /// cancellationToken 108 | /// This method default no-tracking query. 109 | Task AnyAsync(Action>? queryBuilderAction = null, CancellationToken cancellationToken = default); 110 | 111 | /// 112 | /// Gets the based on a predicate 113 | /// 114 | /// queryBuilderAction 115 | /// This method default no-tracking query. 116 | TEntity? FirstOrDefault(Action>? queryBuilderAction = null); 117 | 118 | /// 119 | /// Gets the based on a predicate 120 | /// 121 | /// 122 | /// queryBuilderAction 123 | /// This method default no-tracking query. 124 | TResult? FirstOrDefaultResult(Expression> selector, Action>? queryBuilderAction = null); 125 | 126 | /// 127 | /// Gets the based on a predicate 128 | /// 129 | /// queryBuilder 130 | /// cancellationToken 131 | /// This method default no-tracking query. 132 | Task FirstOrDefaultAsync(Action>? queryBuilderAction = null, CancellationToken cancellationToken = default); 133 | 134 | /// 135 | /// Gets the based on a predicate 136 | /// 137 | /// selector 138 | /// queryBuilder 139 | /// 140 | /// This method default no-tracking query. 141 | Task FirstOrDefaultResultAsync(Expression> selector, Action>? queryBuilderAction = null, CancellationToken cancellationToken = default); 142 | 143 | /// 144 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. 145 | /// 146 | /// queryBuilderAction 147 | /// The pageNumber of page. 148 | /// The size of the page. 149 | /// An that contains elements that satisfy the condition specified by . 150 | /// This method default no-tracking query. 151 | IPagedListResult GetPagedList(Action>? queryBuilderAction = null, int pageNumber = 1, int pageSize = 20); 152 | 153 | /// 154 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. 155 | /// 156 | /// A function to test each element for a condition. 157 | /// The number of page. 158 | /// The size of the page. 159 | /// 160 | /// A to observe while waiting for the task to complete. 161 | /// 162 | /// An that contains elements that satisfy the condition specified by . 163 | /// This method default no-tracking query. 164 | Task> GetPagedListAsync(Action>? queryBuilderAction = null, int pageNumber = 1, int pageSize = 20, CancellationToken cancellationToken = default); 165 | 166 | /// 167 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. 168 | /// 169 | /// The selector for projection. 170 | /// A function to test each element for a condition. 171 | /// pageNumber 172 | /// pageSize 173 | /// An that contains elements that satisfy the condition specified by . 174 | /// This method default no-tracking query. 175 | IPagedListResult GetPagedListResult(Expression> selector, Action>? queryBuilderAction = null, 176 | int pageNumber = 1, int pageSize = 20); 177 | 178 | /// 179 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. 180 | /// 181 | /// The selector for projection. 182 | /// A function to test each element for a condition. 183 | /// 184 | /// 185 | /// A to observe while waiting for the task to complete. 186 | /// 187 | /// 188 | /// An that contains elements that satisfy the condition specified by . 189 | /// This method default no-tracking query. 190 | Task> GetPagedListResultAsync(Expression> selector, Action>? queryBuilderAction = null, 191 | int pageNumber = 1, int pageSize = 20, CancellationToken cancellationToken = default); 192 | } 193 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/IEFRepositoryBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | 4 | namespace WeihanLi.EntityFramework; 5 | 6 | public interface IEFRepositoryBuilder 7 | { 8 | IServiceCollection Services { get; } 9 | } 10 | 11 | internal class EFRepositoryBuilder : IEFRepositoryBuilder 12 | { 13 | public IServiceCollection Services { get; } 14 | 15 | public EFRepositoryBuilder(IServiceCollection services) => Services = services ?? throw new ArgumentNullException(nameof(services)); 16 | } 17 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/IEFRepositoryFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WeihanLi.EntityFramework; 4 | 5 | public interface IEFRepositoryFactory where TDbContext : DbContext 6 | { 7 | IEFRepository GetRepository() where TEntity : class; 8 | } 9 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/IEFRepositoryGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WeihanLi.EntityFramework; 4 | 5 | public interface IEFRepositoryGenerator 6 | { 7 | string GenerateRepositoryCodeTextFor(string repositoryNamespace) where TDbContext : DbContext; 8 | } 9 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/IEFUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using WeihanLi.Common.Data; 4 | 5 | namespace WeihanLi.EntityFramework; 6 | 7 | public interface IEFUnitOfWork : IUnitOfWork, IDisposable where TDbContext : DbContext 8 | { 9 | TDbContext DbContext { get; } 10 | 11 | IEFRepository GetRepository() where TEntity : class; 12 | } 13 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/IQueryablePageListExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using WeihanLi.Common.Models; 6 | 7 | namespace WeihanLi.EntityFramework; 8 | 9 | public static class QueryablePageListExtensions 10 | { 11 | /// 12 | /// Converts the specified source to by the specified and . 13 | /// 14 | /// The type of the source. 15 | /// The source to paging. 16 | /// The number of the page, index from 1. 17 | /// The size of the page. 18 | /// An instance of implements interface. 19 | public static IPagedListResult ToPagedList(this IQueryable source, int pageNumber, int pageSize) 20 | { 21 | if (pageNumber <= 0) 22 | { 23 | pageNumber = 1; 24 | } 25 | if (pageSize <= 0) 26 | { 27 | pageSize = 10; 28 | } 29 | var count = source.Count(); 30 | if (count == 0) 31 | { 32 | return PagedListResult.Empty; 33 | } 34 | 35 | if (pageNumber > 1) 36 | { 37 | source = source.Skip((pageNumber - 1) * pageSize); 38 | } 39 | var items = source 40 | .Take(pageSize) 41 | .ToArray(); 42 | var pagedList = new PagedListResult() 43 | { 44 | PageNumber = pageNumber, 45 | PageSize = pageSize, 46 | TotalCount = count, 47 | Data = items 48 | }; 49 | 50 | return pagedList; 51 | } 52 | 53 | /// 54 | /// Converts the specified source to by the specified and . 55 | /// 56 | /// The type of the source. 57 | /// The source to paging. 58 | /// The number of the page, index from 1. 59 | /// The size of the page. 60 | /// 61 | /// A to observe while waiting for the task to complete. 62 | /// 63 | /// An instance of implements interface. 64 | public static async Task> ToPagedListAsync(this IQueryable source, int pageNumber, int pageSize, CancellationToken cancellationToken = default) 65 | { 66 | if (pageNumber <= 0) 67 | { 68 | pageNumber = 1; 69 | } 70 | if (pageSize <= 0) 71 | { 72 | pageSize = 10; 73 | } 74 | 75 | var count = await source.CountAsync(cancellationToken).ConfigureAwait(false); 76 | if (count == 0) 77 | { 78 | return PagedListResult.Empty; 79 | } 80 | 81 | if (pageNumber > 1) 82 | { 83 | source = source.Skip((pageNumber - 1) * pageSize); 84 | } 85 | var items = await source 86 | .Take(pageSize) 87 | .ToArrayAsync(cancellationToken) 88 | .ConfigureAwait(false); 89 | var pagedList = new PagedListResult() 90 | { 91 | PageNumber = pageNumber, 92 | PageSize = pageSize, 93 | TotalCount = count, 94 | Data = items 95 | }; 96 | 97 | return pagedList; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Interceptors/AutoUpdateInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Diagnostics; 2 | using WeihanLi.EntityFramework.Services; 3 | 4 | namespace WeihanLi.EntityFramework.Interceptors; 5 | 6 | public sealed class AutoUpdateInterceptor(IEnumerable handlers) : SaveChangesInterceptor 7 | { 8 | private readonly IEntitySavingHandler[] _handlers = handlers.ToArray(); 9 | 10 | public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) 11 | { 12 | OnSavingChanges(eventData); 13 | return base.SavingChanges(eventData, result); 14 | } 15 | 16 | public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, 17 | CancellationToken cancellationToken = default) 18 | { 19 | OnSavingChanges(eventData); 20 | return base.SavingChangesAsync(eventData, result, cancellationToken); 21 | } 22 | 23 | private void OnSavingChanges(DbContextEventData eventData) 24 | { 25 | ArgumentNullException.ThrowIfNull(eventData.Context); 26 | foreach (var entityEntry in eventData.Context.ChangeTracker.Entries()) 27 | { 28 | foreach (var handler in _handlers) 29 | { 30 | handler.Handle(entityEntry); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Interceptors/SoftDeleteInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using WeihanLi.Common.Models; 7 | using WeihanLi.EntityFramework.Services; 8 | 9 | namespace WeihanLi.EntityFramework.Interceptors; 10 | 11 | public sealed class SoftDeleteInterceptor : SaveChangesInterceptor 12 | { 13 | public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) 14 | { 15 | OnSavingChanges(eventData); 16 | return base.SavingChanges(eventData, result); 17 | } 18 | 19 | public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, 20 | CancellationToken cancellationToken = new CancellationToken()) 21 | { 22 | OnSavingChanges(eventData); 23 | return base.SavingChangesAsync(eventData, result, cancellationToken); 24 | } 25 | 26 | private static void OnSavingChanges(DbContextEventData eventData) 27 | { 28 | ArgumentNullException.ThrowIfNull(eventData.Context); 29 | eventData.Context.ChangeTracker.DetectChanges(); 30 | foreach (var entityEntry in eventData.Context.ChangeTracker.Entries()) 31 | { 32 | if (entityEntry is { State: EntityState.Deleted, Entity: ISoftDeleteEntityWithDeleted softDeleteEntity }) 33 | { 34 | foreach (var property in entityEntry.Properties) 35 | { 36 | property.IsModified = false; 37 | } 38 | softDeleteEntity.IsDeleted = true; 39 | entityEntry.State = EntityState.Modified; 40 | foreach (var property in entityEntry.Properties) 41 | { 42 | property.IsModified = property.Metadata.Name == SoftDeleteEntitySavingHandler.DefaultIsDeletedPropertyName; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/InternalHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Linq; 4 | using System.Reflection; 5 | using WeihanLi.Common; 6 | 7 | namespace WeihanLi.EntityFramework; 8 | 9 | public static class InternalHelper 10 | { 11 | public static PropertyInfo[] GetDbContextSets(Type dbContextType) 12 | { 13 | return CacheUtil.GetTypeProperties(dbContextType) 14 | .Where(p => p.PropertyType.IsGenericType && typeof(DbSet<>) == p.PropertyType.GetGenericTypeDefinition()) 15 | .ToArray(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/ServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using System; 5 | using WeihanLi.Common.Aspect; 6 | 7 | namespace WeihanLi.EntityFramework; 8 | 9 | public static class ServiceCollectionExtension 10 | { 11 | /// 12 | /// AddEFRepository 13 | /// 14 | /// services 15 | /// ef dbContext and repository serviceLifetime 16 | /// 17 | public static IEFRepositoryBuilder AddEFRepository(this IServiceCollection services, ServiceLifetime efServiceLifetime = ServiceLifetime.Scoped) 18 | { 19 | if (services is null) 20 | { 21 | throw new ArgumentNullException(nameof(services)); 22 | } 23 | 24 | services.TryAdd(new ServiceDescriptor(typeof(IEFRepository<,>), typeof(EFRepository<,>), efServiceLifetime)); 25 | services.TryAdd(new ServiceDescriptor(typeof(IEFUnitOfWork<>), typeof(EFUnitOfWork<>), efServiceLifetime)); 26 | services.TryAdd(new ServiceDescriptor(typeof(IEFRepositoryFactory<>), typeof(EFRepositoryFactory<>), efServiceLifetime)); 27 | services.TryAddSingleton(); 28 | 29 | return new EFRepositoryBuilder(services); 30 | } 31 | 32 | /// 33 | /// AddProxyDbContext 34 | /// 35 | /// DbContext Type 36 | /// services 37 | /// optionsAction 38 | /// serviceLifetime 39 | /// 40 | public static IServiceCollection AddProxyDbContext(this IServiceCollection services, 41 | Action optionsAction, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) where TDbContext : DbContext 42 | { 43 | services.AddDbContext(optionsAction, serviceLifetime); 44 | services.AddProxyService(serviceLifetime); 45 | return services; 46 | } 47 | 48 | ///// 49 | ///// AddProxyDbContextPool 50 | ///// 51 | ///// DbContext Type 52 | ///// services 53 | ///// optionsAction 54 | ///// poolSize 55 | ///// serviceLifetime 56 | ///// 57 | //public static IServiceCollection AddProxyDbContextPool(this IServiceCollection services, 58 | // Action optionsAction, int poolSize = 100, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) where TDbContext : DbContext 59 | //{ 60 | // services.AddDbContextPool(optionsAction, poolSize); 61 | // services.Add(new ServiceDescriptor(typeof(TDbContext), sp => 62 | // { 63 | // var dbContextPool = sp.GetService>(); 64 | // var proxyFactory = sp.GetRequiredService(); 65 | // return proxyFactory.CreateProxyWithTarget(dbContext); 66 | // }, serviceLifetime)); 67 | 68 | // return services; 69 | //} 70 | } 71 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Services/IEntitySavingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | 3 | namespace WeihanLi.EntityFramework.Services; 4 | 5 | public interface IEntitySavingHandler 6 | { 7 | void Handle(EntityEntry entityEntry); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Services/SoftDeleteSavingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using WeihanLi.Common.Models; 4 | 5 | namespace WeihanLi.EntityFramework.Services; 6 | 7 | /// 8 | /// Handle soft delete 9 | /// 10 | internal sealed class SoftDeleteEntitySavingHandler : IEntitySavingHandler 11 | { 12 | public const string DefaultIsDeletedPropertyName = "IsDeleted"; 13 | public void Handle(EntityEntry entityEntry) 14 | { 15 | if (entityEntry is not { State: EntityState.Deleted, Entity: ISoftDeleteEntity }) 16 | return; 17 | 18 | if (entityEntry.Entity is ISoftDeleteEntityWithDeleted softDeleteEntityWithDeleted) 19 | { 20 | softDeleteEntityWithDeleted.IsDeleted = true; 21 | } 22 | else 23 | { 24 | var prop = entityEntry.Property(DefaultIsDeletedPropertyName); 25 | prop.CurrentValue = true; 26 | } 27 | entityEntry.State = EntityState.Modified; 28 | foreach (var property in entityEntry.Properties) 29 | { 30 | property.IsModified = property.Metadata.Name == DefaultIsDeletedPropertyName; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Services/UpdatedAtEntitySavingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using WeihanLi.Common.Models; 4 | 5 | namespace WeihanLi.EntityFramework.Services; 6 | 7 | /// 8 | /// Auto update CreateAt/UpdatedAt 9 | /// 10 | internal sealed class UpdatedAtEntitySavingHandler : IEntitySavingHandler 11 | { 12 | public void Handle(EntityEntry entityEntry) 13 | { 14 | if (entityEntry is not 15 | { 16 | State: EntityState.Added or EntityState.Modified 17 | }) 18 | { 19 | return; 20 | } 21 | 22 | if (entityEntry.Entity is not IEntityWithUpdatedAt updatedAtEntity) 23 | { 24 | return; 25 | } 26 | 27 | if (entityEntry.State is EntityState.Added && entityEntry.Entity is IEntityWithCreatedUpdatedAt createdUpdatedAtEntity) 28 | { 29 | createdUpdatedAtEntity.CreatedAt = DateTimeOffset.Now; 30 | } 31 | 32 | updatedAtEntity.UpdatedAt = DateTimeOffset.Now; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/Services/UpdatedByEntitySavingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using WeihanLi.Common.Models; 4 | using WeihanLi.Common.Services; 5 | 6 | namespace WeihanLi.EntityFramework.Services; 7 | 8 | /// 9 | /// Auto update CreateBy/UpdatedBy 10 | /// 11 | internal sealed class UpdatedBySavingHandler(IUserIdProvider userIdProvider) : IEntitySavingHandler 12 | { 13 | private static readonly string DefaultUserId = $"{Environment.UserName}@{Environment.MachineName}"; 14 | public void Handle(EntityEntry entityEntry) 15 | { 16 | if (entityEntry is not 17 | { 18 | State: EntityState.Added or EntityState.Modified 19 | }) 20 | { 21 | return; 22 | } 23 | 24 | if (entityEntry.Entity is not IEntityWithUpdatedBy updatedByEntity) 25 | { 26 | return; 27 | } 28 | 29 | var userId = userIdProvider.GetUserId() ?? DefaultUserId; 30 | 31 | if (entityEntry.State is EntityState.Added && entityEntry.Entity is IEntityWithCreatedUpdatedBy createdUpdatedBy) 32 | { 33 | createdUpdatedBy.CreatedBy = userId; 34 | } 35 | 36 | updatedByEntity.UpdatedBy = userId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/WeihanLi.EntityFramework/WeihanLi.EntityFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net10.0 4 | WeihanLi.EntityFramework 5 | WeihanLi.EntityFramework 6 | https://avatars3.githubusercontent.com/u/7604648 7 | https://github.com/WeihanLi/WeihanLi.EntityFramework 8 | WeihanLi;ef;efcore;entityframework;entityframeworkcore 9 | EntityFramework Extensions 10 | 11 | https://github.com/WeihanLi/WeihanLi.EntityFramework/tree/dev/docs/ReleaseNotes.md 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/EFExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WeihanLi.EntityFramework.Test; 4 | 5 | internal static class EFExtensions 6 | { 7 | public static bool CleanData(this TDbContext dbContext) where TDbContext : DbContext 8 | { 9 | if (dbContext.Database.IsInMemory()) 10 | { 11 | return dbContext.Database.EnsureDeleted(); 12 | } 13 | else 14 | { 15 | dbContext.Database.EnsureCreated(); 16 | dbContext.Database.ExecuteSqlRaw("TRUNCATE TABLE TestEntities"); 17 | } 18 | 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/EFExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Xunit; 3 | 4 | namespace WeihanLi.EntityFramework.Test; 5 | 6 | public class EFExtensionsTest : EFTestBase 7 | { 8 | [Fact] 9 | public void GetTableNameTest() 10 | { 11 | if (Repository.DbContext.Database.IsRelational()) 12 | { 13 | var tableName = Repository.DbContext.GetTableName(); 14 | Assert.Equal("TestEntities", tableName); 15 | } 16 | } 17 | 18 | public EFExtensionsTest(EFTestFixture fixture) : base(fixture) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/EFRepositoryTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using WeihanLi.Common; 4 | using WeihanLi.Common.Helpers; 5 | using Xunit; 6 | 7 | namespace WeihanLi.EntityFramework.Test; 8 | 9 | public class EFRepositoryTest : EFTestBase 10 | { 11 | private readonly ITestOutputHelper _outputHelper; 12 | private readonly AsyncLock _lock = new AsyncLock(); 13 | 14 | public EFRepositoryTest(EFTestFixture fixture, ITestOutputHelper outputHelper) : base(fixture) 15 | { 16 | _outputHelper = outputHelper; 17 | } 18 | 19 | [Fact] 20 | public void InsertTest() 21 | { 22 | using (_lock.Lock()) 23 | { 24 | DependencyResolver.TryInvoke>(repo => 25 | { 26 | var entity = new TestEntity() { Name = "abc1", CreatedAt = DateTime.UtcNow, Extra = "" }; 27 | repo.Insert(entity); 28 | 29 | var entities = new[] 30 | { 31 | new TestEntity() {Name = "abc2", CreatedAt = DateTime.UtcNow, Extra = ""}, 32 | new TestEntity() {Name = "abc3", CreatedAt = DateTime.UtcNow, Extra = ""} 33 | }; 34 | repo.Insert(entities); 35 | }); 36 | } 37 | } 38 | 39 | [Fact] 40 | public async Task InsertAsyncTest() 41 | { 42 | using (await _lock.LockAsync(cancellationToken: TestContext.Current.CancellationToken)) 43 | { 44 | await DependencyResolver.TryInvokeAsync>(async repo => 45 | { 46 | var entity = new TestEntity() { Name = "abc1", CreatedAt = DateTime.UtcNow, Extra = "" }; 47 | await repo.InsertAsync(entity); 48 | 49 | var entities = new[] 50 | { 51 | new TestEntity() {Name = "abc2", CreatedAt = DateTime.UtcNow, Extra = ""}, 52 | new TestEntity() {Name = "abc3", CreatedAt = DateTime.UtcNow, Extra = ""} 53 | }; 54 | await repo.InsertAsync(entities); 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/EFTestBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using Xunit; 4 | 5 | namespace WeihanLi.EntityFramework.Test; 6 | 7 | public class EFTestBase : IClassFixture 8 | { 9 | public IServiceProvider Services { get; } 10 | public IEFRepository Repository { get; } 11 | 12 | public EFTestBase(EFTestFixture fixture) 13 | { 14 | Services = fixture.Services; 15 | Repository = fixture.Services 16 | .GetRequiredService>(); 17 | Repository.DbContext.CleanData(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/EFTestFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using WeihanLi.Common; 6 | using Xunit; 7 | 8 | namespace WeihanLi.EntityFramework.Test; 9 | 10 | public class EFTestFixture : IDisposable 11 | { 12 | private readonly IServiceScope _serviceScope; 13 | public IServiceProvider Services { get; } 14 | 15 | private const string DbConnectionString = 16 | @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Test;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; 17 | 18 | public EFTestFixture() 19 | { 20 | IServiceCollection serviceCollection = new ServiceCollection(); 21 | serviceCollection.AddDbContext(options => 22 | { 23 | //options.UseSqlServer(DbConnectionString); 24 | options.UseInMemoryDatabase("Tests"); 25 | options.EnableDetailedErrors(); 26 | }); 27 | serviceCollection.AddEFRepository(); 28 | DependencyResolver.SetDependencyResolver(serviceCollection); 29 | 30 | var serviceProvider = serviceCollection.BuildServiceProvider(); 31 | _serviceScope = serviceProvider.CreateScope(); 32 | 33 | Services = _serviceScope.ServiceProvider; 34 | } 35 | 36 | public void Dispose() 37 | { 38 | var dbContext = Services.GetRequiredService(); 39 | if (dbContext.Database.IsInMemory()) 40 | { 41 | dbContext.Database.EnsureDeleted(); 42 | } 43 | 44 | _serviceScope.Dispose(); 45 | } 46 | } 47 | 48 | [CollectionDefinition("EFTest", DisableParallelization = true)] 49 | public class EFTestCollection : ICollectionFixture 50 | { 51 | } 52 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/EFUnitOfWorkTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using WeihanLi.Common.Data; 8 | using Xunit; 9 | 10 | namespace WeihanLi.EntityFramework.Test; 11 | 12 | public class EFUnitOfWorkTest : EFTestBase 13 | { 14 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); 15 | private readonly ITestOutputHelper _output; 16 | 17 | public EFUnitOfWorkTest(EFTestFixture fixture, ITestOutputHelper outputHelper) : base(fixture) 18 | { 19 | _output = outputHelper; 20 | } 21 | 22 | [Fact] 23 | public void TransactionTest() 24 | { 25 | IServiceScope scope1 = null; 26 | try 27 | { 28 | _semaphore.Wait(TestContext.Current.CancellationToken); 29 | scope1 = Services.CreateScope(); 30 | var repository = scope1.ServiceProvider.GetRequiredService>(); 31 | 32 | _output.WriteLine($"----- TransactionTest Begin {DateTime.UtcNow.Ticks} -----"); 33 | 34 | repository.DbContext.Database.EnsureCreated(); 35 | 36 | repository.Insert(new TestEntity() 37 | { 38 | CreatedAt = DateTime.UtcNow, 39 | Name = "xss", 40 | }); 41 | repository.Update(new TestEntity() 42 | { 43 | Id = 1, 44 | Name = new string('x', 6) 45 | }, "Name"); 46 | using (var scope = Services.CreateScope()) 47 | { 48 | var repo = scope.ServiceProvider.GetRequiredService>(); 49 | repo.Insert(new TestEntity() 50 | { 51 | CreatedAt = DateTime.UtcNow, 52 | Name = new string('y', 6) 53 | }); 54 | } 55 | 56 | var beforeCount = repository.Count(); 57 | 58 | using var uow = repository.GetUnitOfWork(); 59 | uow.DbContext.Update(new TestEntity() 60 | { 61 | Id = 1, 62 | Name = new string('1', 6) 63 | }, "Name"); 64 | uow.DbContext.UpdateWithout(new TestEntity() 65 | { 66 | Id = 2, 67 | Name = new string('2', 6) 68 | }, x => x.CreatedAt); 69 | var entity = new TestEntity() 70 | { 71 | CreatedAt = DateTime.UtcNow, 72 | Name = "xyy1", 73 | }; 74 | uow.DbSet().Add(entity); 75 | uow.DbSet().Remove(entity); 76 | uow.DbSet().Add(new TestEntity() 77 | { 78 | CreatedAt = DateTime.UtcNow, 79 | Name = "xyy1", 80 | }); 81 | 82 | var beforeCommitCount = repository.Count(); 83 | Assert.Equal(beforeCount, beforeCommitCount); 84 | 85 | uow.Commit(); 86 | 87 | var committedCount = repository.Count(); 88 | Assert.Equal(committedCount, beforeCount + 1); 89 | 90 | using (var scope = Services.CreateScope()) 91 | { 92 | var repo = scope.ServiceProvider.GetRequiredService>() 93 | .GetRepository(); 94 | entity = repo.Find(1); 95 | Assert.Equal(new string('1', 6), entity.Name); 96 | 97 | entity = repo.Find(2); 98 | Assert.Equal(new string('2', 6), entity.Name); 99 | 100 | Assert.Equal(1, repo.Delete(new object[] { 1 })); 101 | } 102 | } 103 | finally 104 | { 105 | Repository.DbContext.CleanData(); 106 | scope1?.Dispose(); 107 | _output.WriteLine($"----- TransactionTest End {DateTime.UtcNow.Ticks} -----"); 108 | _semaphore.Release(); 109 | } 110 | } 111 | 112 | [Fact] 113 | public async Task TransactionAsyncTest() 114 | { 115 | IServiceScope scope1 = null; 116 | try 117 | { 118 | await _semaphore.WaitAsync(TestContext.Current.CancellationToken); 119 | scope1 = Services.CreateScope(); 120 | var repository = scope1.ServiceProvider.GetRequiredService>() 121 | .GetRepository(); 122 | 123 | _output.WriteLine($"----- TransactionAsyncTest Begin {DateTime.UtcNow.Ticks}-----"); 124 | 125 | repository.DbContext.Database.EnsureCreated(); 126 | 127 | //for (var i = 0; i < 3; i++) 128 | //{ 129 | // await Repository.InsertAsync(new TestEntity() 130 | // { 131 | // CreatedAt = DateTime.UtcNow, 132 | // Name = $"xss-{i}", 133 | // }); 134 | //} 135 | 136 | await repository.InsertAsync(new[] 137 | { 138 | new TestEntity() 139 | { 140 | CreatedAt = DateTime.UtcNow, 141 | Name = "xss1", 142 | }, 143 | new TestEntity() 144 | { 145 | CreatedAt = DateTime.UtcNow, 146 | Name = "xss2", 147 | }, 148 | new TestEntity() 149 | { 150 | CreatedAt = DateTime.UtcNow, 151 | Name = "xss3", 152 | } 153 | }, TestContext.Current.CancellationToken); 154 | 155 | using (var scope = Services.CreateScope()) 156 | { 157 | var repo = scope.ServiceProvider.GetRequiredService>(); 158 | await repo.InsertAsync(new TestEntity() 159 | { 160 | CreatedAt = DateTime.UtcNow, 161 | Name = "xxxxxx" 162 | }, TestContext.Current.CancellationToken); 163 | } 164 | 165 | var beforeCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); 166 | using var uow = repository.GetUnitOfWork(); 167 | uow.DbContext.Update(new TestEntity() 168 | { 169 | Id = 3, 170 | Name = new string('3', 6) 171 | }, "Name"); 172 | uow.DbContext.UpdateWithout(new TestEntity() 173 | { 174 | Id = 4, 175 | Name = new string('4', 6) 176 | }, x => x.CreatedAt); 177 | var entity = new TestEntity() 178 | { 179 | CreatedAt = DateTime.UtcNow, 180 | Name = "xyy1", 181 | }; 182 | uow.DbSet().Add(entity); 183 | uow.DbSet().Remove(entity); 184 | uow.DbSet().Add(new TestEntity() 185 | { 186 | CreatedAt = DateTime.UtcNow, 187 | Name = "xyy1", 188 | }); 189 | 190 | var beforeCommitCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); 191 | Assert.Equal(beforeCount, beforeCommitCount); 192 | 193 | await uow.CommitAsync(TestContext.Current.CancellationToken); 194 | 195 | var committedCount = await repository.CountAsync(cancellationToken: TestContext.Current.CancellationToken); 196 | Assert.Equal(committedCount, beforeCount + 1); 197 | 198 | entity = await repository.DbContext.FindAsync(new object[] { 3 }, TestContext.Current.CancellationToken); 199 | Assert.Equal(new string('3', 6), entity.Name); 200 | 201 | entity = await repository.DbContext.FindAsync(new object[] { 4 }, cancellationToken: TestContext.Current.CancellationToken); 202 | Assert.Equal(new string('4', 6), entity.Name); 203 | 204 | Assert.Equal(1, await Repository.DeleteAsync(1)); 205 | } 206 | finally 207 | { 208 | Repository.DbContext.CleanData(); 209 | scope1?.Dispose(); 210 | _output.WriteLine($"----- TransactionAsyncTest End {DateTime.UtcNow.Ticks} -----"); 211 | 212 | _semaphore.Release(); 213 | } 214 | } 215 | 216 | [Fact] 217 | public void RollbackTest() 218 | { 219 | try 220 | { 221 | _semaphore.Wait(TestContext.Current.CancellationToken); 222 | 223 | using (var scope = Services.CreateScope()) 224 | { 225 | var unitOfWork = scope.ServiceProvider 226 | .GetRequiredService>(); 227 | unitOfWork.DbContext.TestEntities 228 | .Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "saa" }); 229 | unitOfWork.DbContext.TestEntities 230 | .Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "saa" }); 231 | unitOfWork.Commit(); 232 | } 233 | using (var scope = Services.CreateScope()) 234 | { 235 | var unitOfWork = scope.ServiceProvider 236 | .GetRequiredService>(); 237 | var count = unitOfWork.DbContext.TestEntities.Count(); 238 | 239 | unitOfWork.DbContext.TestEntities.Add(new TestEntity() { Name = "xxx", CreatedAt = DateTime.UtcNow }); 240 | unitOfWork.DbContext.TestEntities.Add(new TestEntity() { Name = "xxx", CreatedAt = DateTime.UtcNow }); 241 | 242 | unitOfWork.Rollback(); 243 | 244 | var count2 = unitOfWork.DbContext.TestEntities.Count(); 245 | Assert.Equal(count, count2); 246 | } 247 | } 248 | finally 249 | { 250 | Repository.DbContext.CleanData(); 251 | _semaphore.Release(); 252 | } 253 | } 254 | 255 | [Fact] 256 | public async Task RollbackAsyncTest() 257 | { 258 | try 259 | { 260 | await _semaphore.WaitAsync(TestContext.Current.CancellationToken); 261 | 262 | using (var scope = Services.CreateScope()) 263 | { 264 | var unitOfWork = scope.ServiceProvider 265 | .GetRequiredService>(); 266 | unitOfWork.DbContext.TestEntities 267 | .Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "saa" }); 268 | unitOfWork.DbContext.TestEntities 269 | .Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "saa" }); 270 | await unitOfWork.CommitAsync(TestContext.Current.CancellationToken); 271 | } 272 | using (var scope = Services.CreateScope()) 273 | { 274 | var unitOfWork = scope.ServiceProvider 275 | .GetRequiredService>(); 276 | 277 | var count = unitOfWork.DbContext.TestEntities.Count(); 278 | 279 | unitOfWork.DbContext.TestEntities.Add(new TestEntity() { Name = "xxx", CreatedAt = DateTime.UtcNow }); 280 | unitOfWork.DbContext.TestEntities.Add(new TestEntity() { Name = "xxx", CreatedAt = DateTime.UtcNow }); 281 | 282 | await unitOfWork.RollbackAsync(TestContext.Current.CancellationToken); 283 | 284 | var count2 = unitOfWork.DbContext.TestEntities.Count(); 285 | Assert.Equal(count, count2); 286 | } 287 | } 288 | finally 289 | { 290 | Repository.DbContext.CleanData(); 291 | _semaphore.Release(); 292 | } 293 | } 294 | 295 | [Fact] 296 | public void HybridTest() 297 | { 298 | if (!Repository.DbContext.Database.IsRelational()) 299 | { 300 | return; 301 | } 302 | try 303 | { 304 | _semaphore.Wait(TestContext.Current.CancellationToken); 305 | 306 | using (var scope = Services.CreateScope()) 307 | { 308 | Assert.Equal(0, Repository.Count()); 309 | 310 | Repository.Insert(new TestEntity() 311 | { 312 | Name = "_00" 313 | }); 314 | Assert.Equal(1, Repository.Count()); 315 | 316 | var repository = scope.ServiceProvider.GetRequiredService>(); 317 | repository.Insert(new TestEntity() { Name = "x111", CreatedAt = DateTime.UtcNow, }); 318 | 319 | // 2 320 | var count0 = repository.Count(); 321 | 322 | Assert.Equal(2, count0); 323 | 324 | using var uow = scope.ServiceProvider.GetRequiredService>(); 325 | uow.DbContext.Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "xx" }); 326 | uow.DbContext.Add(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "xx" }); 327 | 328 | // 3 329 | var result = repository.Insert(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "yyyy" }); 330 | Assert.Equal(3, result); 331 | 332 | // 1 333 | result = repository.Insert(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "yyyy" }); 334 | Assert.Equal(1, result); 335 | 336 | // 6 337 | var count1 = repository.Count(); 338 | Assert.Equal(6, count1); 339 | 340 | Repository.Insert(new TestEntity() { Name = "_111", CreatedAt = DateTime.UtcNow, }); 341 | 342 | // 7 343 | var count2 = repository.Count(); 344 | Assert.Equal(7, count2); 345 | 346 | uow.Rollback(); 347 | 348 | // 3 349 | var count3 = repository.Count(); 350 | Assert.Equal(3, count3); 351 | 352 | // 1 353 | result = repository.Insert(new TestEntity() { CreatedAt = DateTime.UtcNow, Name = "yyyy" }); 354 | Assert.Equal(1, result); 355 | 356 | // 4 357 | var count4 = repository.Count(); 358 | Assert.Equal(4, count4); 359 | 360 | //uow.Commit(); 361 | } 362 | 363 | Repository.DbContext.CleanData(); 364 | 365 | using (var scope = Services.CreateScope()) 366 | { 367 | using (var uow = scope.ServiceProvider 368 | .GetRequiredService>()) 369 | { 370 | var repository = uow.GetRepository(); 371 | 372 | var count = repository.Count(); 373 | Assert.Equal(0, count); 374 | 375 | repository.Insert(new TestEntity() 376 | { 377 | Name = "zz_000", 378 | CreatedAt = DateTime.UtcNow, 379 | }); 380 | uow.DbContext.Add(new TestEntity() 381 | { 382 | Name = "zzz_000", 383 | CreatedAt = DateTime.UtcNow, 384 | }); 385 | uow.DbContext.Add(new TestEntity() 386 | { 387 | Name = "zzz_000", 388 | CreatedAt = DateTime.UtcNow, 389 | }); 390 | uow.Commit(); 391 | 392 | count = repository.Count(); 393 | Assert.Equal(3, count); 394 | } 395 | } 396 | } 397 | finally 398 | { 399 | Repository.DbContext.CleanData(); 400 | _semaphore.Release(); 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/RelationalTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Xunit; 3 | 4 | namespace WeihanLi.EntityFramework.Test; 5 | 6 | public class RelationalTest : EFTestBase 7 | { 8 | public RelationalTest(EFTestFixture fixture) : base(fixture) 9 | { 10 | } 11 | 12 | [Fact] 13 | public void IsRelationTest() 14 | { 15 | Assert.Equal(!Repository.DbContext.Database.IsInMemory(), 16 | Repository.DbContext.Database.IsRelational()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace WeihanLi.EntityFramework.Test; 7 | 8 | public class TestDbContext : DbContext 9 | { 10 | public TestDbContext(DbContextOptions options) : base(options) 11 | { 12 | } 13 | 14 | public DbSet TestEntities { get; set; } 15 | 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | } 19 | } 20 | 21 | public class TestEntity 22 | { 23 | [Key] 24 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 25 | public int Id { get; set; } 26 | 27 | public string Name { get; set; } 28 | 29 | public string Extra { get; set; } 30 | 31 | public DateTime CreatedAt { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/WeihanLi.EntityFramework.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | false 6 | exe 7 | disable 8 | 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/WeihanLi.EntityFramework.Test/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "parallelizeTestCollections": false 3 | } --------------------------------------------------------------------------------