Repository: VahidN/EFSecondLevelCache.Core Branch: master Commit: 1de038417ba2 Files: 107 Total size: 396.0 KB Directory structure: gitextract_jukctmgq/ ├── .gitattributes ├── .github/ │ ├── issue_template.md │ ├── lock.yml │ └── workflows/ │ └── build.yml ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── EFSecondLevelCache.Core.sln ├── LICENSE.md ├── README.md ├── global.json ├── src/ │ ├── EFSecondLevelCache.Core/ │ │ ├── Contracts/ │ │ │ ├── EFCacheDebugInfo.cs │ │ │ ├── EFCacheKey.cs │ │ │ ├── EFQueryDebugView.cs │ │ │ ├── IEFCacheKeyHashProvider.cs │ │ │ ├── IEFCacheKeyProvider.cs │ │ │ └── IEFCacheServiceProvider.cs │ │ ├── EFAsyncEnumerable.cs │ │ ├── EFAsyncEnumerator.cs │ │ ├── EFAsyncTaskEnumerable.cs │ │ ├── EFAsyncTaskEnumerator.cs │ │ ├── EFCacheKeyHashProvider.cs │ │ ├── EFCacheKeyProvider3x.cs │ │ ├── EFCachePolicy.cs │ │ ├── EFCacheServiceProvider.cs │ │ ├── EFCachedDbSet.cs │ │ ├── EFCachedDbSetExtensions.cs │ │ ├── EFCachedQueryExtensions.cs │ │ ├── EFCachedQueryProvider.cs │ │ ├── EFCachedQueryable.cs │ │ ├── EFChangeTrackerExtensions.cs │ │ ├── EFMaterializer.cs │ │ ├── EFQueryExpressionVisitor.cs │ │ ├── EFSecondLevelCache.Core.csproj │ │ ├── EFServiceCollectionExtensions.cs │ │ ├── EFStaticServiceProvider.cs │ │ ├── ParallelExtensions.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ ├── XxHashUnsafe.cs │ │ ├── _0-restore.bat │ │ └── _1-dotnet_pack.bat │ └── Tests/ │ ├── EFSecondLevelCache.Core.AspNetCoreSample/ │ │ ├── App_Data/ │ │ │ └── .gitkeep.txt │ │ ├── Controllers/ │ │ │ └── HomeController.cs │ │ ├── DataLayer/ │ │ │ ├── Entities/ │ │ │ │ ├── Post.cs │ │ │ │ ├── Product.cs │ │ │ │ ├── Tag.cs │ │ │ │ ├── TagProduct.cs │ │ │ │ └── User.cs │ │ │ ├── SampleContext.cs │ │ │ └── Utils/ │ │ │ ├── ApplicationDbContextSeedData.cs │ │ │ └── DBInitialization.cs │ │ ├── EFSecondLevelCache.Core.AspNetCoreSample.csproj │ │ ├── Migrations/ │ │ │ ├── 20191022095356_V2019_10_22_1323.Designer.cs │ │ │ ├── 20191022095356_V2019_10_22_1323.cs │ │ │ └── SampleContextModelSnapshot.cs │ │ ├── Models/ │ │ │ └── PostDto.cs │ │ ├── Others/ │ │ │ └── TestUtils.cs │ │ ├── Profiles/ │ │ │ └── PostProfile.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── Startup.cs │ │ ├── _0-restore.bat │ │ ├── _1-dotnet_run.bat │ │ ├── _update_db.bat │ │ ├── appsettings.json │ │ ├── web.config │ │ └── wwwroot/ │ │ └── App_Data/ │ │ └── .gitkeep │ ├── EFSecondLevelCache.Core.NET46Sample/ │ │ └── EFSecondLevelCache.Core.NET46Sample/ │ │ ├── App.config │ │ ├── DataLayer/ │ │ │ ├── ConfigureServices.cs │ │ │ ├── Entities/ │ │ │ │ └── Post.cs │ │ │ └── SampleContext.cs │ │ ├── EFSecondLevelCache.Core.NET46Sample.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ └── packages.config │ ├── EFSecondLevelCache.Core.PerformanceTests/ │ │ ├── BenchmarkTests.cs │ │ ├── EFSecondLevelCache.Core.PerformanceTests.csproj │ │ ├── Program.cs │ │ ├── SampleContext.cs │ │ ├── TestsServiceProvider.cs │ │ ├── _0-restore.bat │ │ ├── _1-dotnet_run.bat │ │ └── app_data/ │ │ └── .gitkeep │ ├── EFSecondLevelCache.Core.Tests/ │ │ ├── EFCacheServiceProviderTests.cs │ │ ├── EFCachedQueryProviderAsyncTests.cs │ │ ├── EFCachedQueryProviderBasicTests.cs │ │ ├── EFCachedQueryProviderInvalidationTests.cs │ │ ├── EFSecondLevelCache.Core.Tests.csproj │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ ├── TestsBase.cs │ │ ├── XxHashTests.cs │ │ ├── _0-restore.bat │ │ └── _1-dotnet_test.bat │ └── Issues/ │ ├── Issue15/ │ │ ├── ConfigureServices.cs │ │ ├── Issue15.csproj │ │ ├── Payment.cs │ │ ├── Program.cs │ │ ├── SampleContext.cs │ │ └── app_data/ │ │ └── .git.keep │ └── Issue43/ │ ├── Issue43.csproj │ ├── Program.cs │ ├── _0-restore.bat │ ├── _1-dotnet_run.bat │ └── app_data/ │ └── .git.keep ├── tag-it.bat └── update-dependencies.bat ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .github/issue_template.md ================================================ # Summary of the issue ## Environment ``` .NET Core SDK version: Microsoft.EntityFrameworkCore version: EFSecondLevelCache.Core version: ``` ## Example code/Steps to reproduce: ``` paste your core code ``` ## Output: ``` Exception message: Full Stack trace: ``` ================================================ FILE: .github/lock.yml ================================================ daysUntilLock: 90 skipCreatedBefore: false exemptLabels: [] lockLabel: false lockComment: > This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related problems. setLockReason: true ================================================ FILE: .github/workflows/build.yml ================================================ name: .NET Core Build on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.101 - name: Build DNTCaptcha.Core lib run: dotnet build ./src/EFSecondLevelCache.Core/EFSecondLevelCache.Core.csproj --configuration Release ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ [Xx]64/ [Xx]86/ [Bb]uild/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Un-comment the next line if you do not want to checkin # your web deploy settings because they may include unencrypted # passwords #*.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Microsoft Azure ApplicationInsights config file ApplicationInsights.config # Windows Store app package directory AppPackages/ BundleArtifacts/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # LightSwitch generated files GeneratedArtifacts/ ModelManifest.xml # Paket dependency manager .paket/paket.exe # FAKE - F# Make .fake/ /.idea /src/Tests/EFSecondLevelCache.Core.PerformanceTests/BenchmarkDotNet.Artifacts ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)-Issue43", "type": "coreclr", "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/Tests/Issues/Issue43/bin/Debug/netcoreapp3.1/Issue43.dll", "args": [], "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "internalConsole" }, { "name": ".NET Core Launch (web)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceRoot}/src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/bin/Debug/netcoreapp3.1/EFSecondLevelCache.Core.AspNetCoreSample.dll", "args": [], "cwd": "${workspaceRoot}/src/Tests/EFSecondLevelCache.Core.AspNetCoreSample", "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart", "launchBrowser": { "enabled": true, "args": "${auto-detect-url}", "windows": { "command": "cmd.exe", "args": "/C start ${auto-detect-url}" }, "osx": { "command": "open" }, "linux": { "command": "xdg-open" } }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" }, "sourceFileMap": { "/Views": "${workspaceRoot}/Views" } }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "workbench.colorCustomizations": { "activityBar.background": "#7295b1", "activityBar.activeBorder": "#e8d6e0", "activityBar.foreground": "#15202b", "activityBar.inactiveForeground": "#15202b99", "activityBarBadge.background": "#e8d6e0", "activityBarBadge.foreground": "#15202b", "titleBar.activeBackground": "#557c9b", "titleBar.inactiveBackground": "#557c9b99", "titleBar.activeForeground": "#e7e7e7", "titleBar.inactiveForeground": "#e7e7e799", "statusBar.background": "#557c9b", "statusBarItem.hoverBackground": "#7295b1", "statusBar.foreground": "#e7e7e7" }, "peacock.color": "#557c9b" } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "0.1.0", "command": "dotnet", "isShellCommand": true, "args": [], "tasks": [ { "taskName": "build", "args": [ "${workspaceRoot}/src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/EFSecondLevelCache.Core.AspNetCoreSample.csproj" ], "isBuildCommand": true, "problemMatcher": "$msCompile" } ] } ================================================ FILE: EFSecondLevelCache.Core.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0C0EA45E-C765-4D41-A1DA-EDDC8F239A5B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{312DDEFA-6800-4297-9B38-0C458BB28FED}" ProjectSection(SolutionItems) = preProject LICENSE.md = LICENSE.md README.md = README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFSecondLevelCache.Core", "src\EFSecondLevelCache.Core\EFSecondLevelCache.Core.csproj", "{755D6A18-E67A-4E14-8289-BD33A901C52B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFSecondLevelCache.Core.Tests", "src\Tests\EFSecondLevelCache.Core.Tests\EFSecondLevelCache.Core.Tests.csproj", "{5475D985-85D6-4DD2-899E-8E8B333B0070}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFSecondLevelCache.Core.AspNetCoreSample", "src\Tests\EFSecondLevelCache.Core.AspNetCoreSample\EFSecondLevelCache.Core.AspNetCoreSample.csproj", "{EB8B44EC-D677-47B3-8A72-3E340BFDF0DE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFSecondLevelCache.Core.NET46Sample", "src\Tests\EFSecondLevelCache.Core.NET46Sample\EFSecondLevelCache.Core.NET46Sample\EFSecondLevelCache.Core.NET46Sample.csproj", "{CEE7F26B-7A7E-4021-A37F-F9625EAF85CE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4D31E9D7-B686-44FF-8BCD-C17BA26A0333}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Issues", "Issues", "{EDE35C41-BFB7-492F-A8FD-C13FB276486C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Issue15", "src\Tests\Issues\Issue15\Issue15.csproj", "{767215E4-AF19-4AD0-968C-532BE14AF75A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFSecondLevelCache.Core.PerformanceTests", "src\Tests\EFSecondLevelCache.Core.PerformanceTests\EFSecondLevelCache.Core.PerformanceTests.csproj", "{01DE3CF0-AA97-43B3-B95C-45184DF223CB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Issue43", "src\Tests\Issues\Issue43\Issue43.csproj", "{65D28CEA-604F-4283-BDEE-9D6B669A37E1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {755D6A18-E67A-4E14-8289-BD33A901C52B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {755D6A18-E67A-4E14-8289-BD33A901C52B}.Debug|Any CPU.Build.0 = Debug|Any CPU {755D6A18-E67A-4E14-8289-BD33A901C52B}.Release|Any CPU.ActiveCfg = Release|Any CPU {755D6A18-E67A-4E14-8289-BD33A901C52B}.Release|Any CPU.Build.0 = Release|Any CPU {5475D985-85D6-4DD2-899E-8E8B333B0070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5475D985-85D6-4DD2-899E-8E8B333B0070}.Debug|Any CPU.Build.0 = Debug|Any CPU {5475D985-85D6-4DD2-899E-8E8B333B0070}.Release|Any CPU.ActiveCfg = Release|Any CPU {5475D985-85D6-4DD2-899E-8E8B333B0070}.Release|Any CPU.Build.0 = Release|Any CPU {EB8B44EC-D677-47B3-8A72-3E340BFDF0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EB8B44EC-D677-47B3-8A72-3E340BFDF0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB8B44EC-D677-47B3-8A72-3E340BFDF0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB8B44EC-D677-47B3-8A72-3E340BFDF0DE}.Release|Any CPU.Build.0 = Release|Any CPU {CEE7F26B-7A7E-4021-A37F-F9625EAF85CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CEE7F26B-7A7E-4021-A37F-F9625EAF85CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {CEE7F26B-7A7E-4021-A37F-F9625EAF85CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {CEE7F26B-7A7E-4021-A37F-F9625EAF85CE}.Release|Any CPU.Build.0 = Release|Any CPU {767215E4-AF19-4AD0-968C-532BE14AF75A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {767215E4-AF19-4AD0-968C-532BE14AF75A}.Debug|Any CPU.Build.0 = Debug|Any CPU {767215E4-AF19-4AD0-968C-532BE14AF75A}.Debug|x64.ActiveCfg = Debug|x64 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Debug|x64.Build.0 = Debug|x64 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Debug|x86.ActiveCfg = Debug|x86 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Debug|x86.Build.0 = Debug|x86 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Release|Any CPU.ActiveCfg = Release|Any CPU {767215E4-AF19-4AD0-968C-532BE14AF75A}.Release|Any CPU.Build.0 = Release|Any CPU {767215E4-AF19-4AD0-968C-532BE14AF75A}.Release|x64.ActiveCfg = Release|x64 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Release|x64.Build.0 = Release|x64 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Release|x86.ActiveCfg = Release|x86 {767215E4-AF19-4AD0-968C-532BE14AF75A}.Release|x86.Build.0 = Release|x86 {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Release|Any CPU.Build.0 = Release|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Debug|x64.ActiveCfg = Debug|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Debug|x64.Build.0 = Debug|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Debug|x86.ActiveCfg = Debug|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Debug|x86.Build.0 = Debug|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Release|x64.ActiveCfg = Release|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Release|x64.Build.0 = Release|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Release|x86.ActiveCfg = Release|Any CPU {01DE3CF0-AA97-43B3-B95C-45184DF223CB}.Release|x86.Build.0 = Release|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Release|Any CPU.Build.0 = Release|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Debug|x64.ActiveCfg = Debug|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Debug|x64.Build.0 = Debug|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Debug|x86.ActiveCfg = Debug|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Debug|x86.Build.0 = Debug|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Release|x64.ActiveCfg = Release|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Release|x64.Build.0 = Release|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Release|x86.ActiveCfg = Release|Any CPU {65D28CEA-604F-4283-BDEE-9D6B669A37E1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {755D6A18-E67A-4E14-8289-BD33A901C52B} = {0C0EA45E-C765-4D41-A1DA-EDDC8F239A5B} {5475D985-85D6-4DD2-899E-8E8B333B0070} = {0C0EA45E-C765-4D41-A1DA-EDDC8F239A5B} {EB8B44EC-D677-47B3-8A72-3E340BFDF0DE} = {0C0EA45E-C765-4D41-A1DA-EDDC8F239A5B} {CEE7F26B-7A7E-4021-A37F-F9625EAF85CE} = {0C0EA45E-C765-4D41-A1DA-EDDC8F239A5B} {4D31E9D7-B686-44FF-8BCD-C17BA26A0333} = {0C0EA45E-C765-4D41-A1DA-EDDC8F239A5B} {EDE35C41-BFB7-492F-A8FD-C13FB276486C} = {4D31E9D7-B686-44FF-8BCD-C17BA26A0333} {767215E4-AF19-4AD0-968C-532BE14AF75A} = {EDE35C41-BFB7-492F-A8FD-C13FB276486C} {01DE3CF0-AA97-43B3-B95C-45184DF223CB} = {4D31E9D7-B686-44FF-8BCD-C17BA26A0333} {65D28CEA-604F-4283-BDEE-9D6B669A37E1} = {EDE35C41-BFB7-492F-A8FD-C13FB276486C} EndGlobalSection EndGlobal ================================================ FILE: LICENSE.md ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # [Announcing a better Second Level Caching Library!](https://github.com/VahidN/EFSecondLevelCache.Core/issues/67) # EFSecondLevelCache.Core

GitHub Actions status

Entity Framework Core Second Level Caching Library. Second level caching is a query cache. The results of EF commands will be stored in the cache, so that the same EF commands will retrieve their data from the cache rather than executing them against the database again. ## Install via NuGet To install EFSecondLevelCache.Core, run the following command in the Package Manager Console: [![Nuget](https://img.shields.io/nuget/v/EFSecondLevelCache.Core)](https://github.com/VahidN/EFSecondLevelCache.Core) ``` PM> Install-Package EFSecondLevelCache.Core ``` You can also view the [package page](http://www.nuget.org/packages/EFSecondLevelCache.Core/) on NuGet. This library also uses the [CacheManager.Core](https://github.com/MichaCo/CacheManager), as a highly configurable cache manager. To use its in-memory caching mechanism, add these entries to the `.csproj` file: ```xml ``` And to get the latest versions of these libraries you can run the following command in the Package Manager Console: ``` PM> Update-Package ``` ## Usage 1- [Register the required services](/src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Startup.cs) of `EFSecondLevelCache.Core` and also `CacheManager.Core` ```csharp namespace EFSecondLevelCache.Core.AspNetCoreSample { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddEFSecondLevelCache(); // Add an in-memory cache service provider services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer() .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .Build()); } } } ``` If you want to use the Redis as the preferred cache provider, first install the `CacheManager.StackExchange.Redis` package and then register its required services: ```csharp // Add Redis cache service provider var jss = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; const string redisConfigurationKey = "redis"; services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer(serializationSettings: jss, deserializationSettings: jss) .WithUpdateMode(CacheUpdateMode.Up) .WithRedisConfiguration(redisConfigurationKey, config => { config.WithAllowAdmin() .WithDatabase(0) .WithEndpoint("localhost", 6379) // Enables keyspace notifications to react on eviction/expiration of items. // Make sure that all servers are configured correctly and 'notify-keyspace-events' is at least set to 'Exe', otherwise CacheManager will not retrieve any events. // See https://redis.io/topics/notifications#configuration for configuration details. .EnableKeyspaceEvents(); }) .WithMaxRetries(100) .WithRetryTimeout(50) .WithRedisCacheHandle(redisConfigurationKey) .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .Build()); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); ``` 2- [Setting up the cache invalidation](/src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/SampleContext.cs) by overriding the SaveChanges method to prevent stale reads: ```csharp namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer { public class SampleContext : DbContext { public SampleContext(DbContextOptions options) : base(options) { } public virtual DbSet Posts { get; set; } public override int SaveChanges() { var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; this.GetService().InvalidateCacheDependencies(changedEntityNames); return result; } } } ``` 3- Then to cache the results of the normal queries like: ```csharp var products = context.Products.Include(x => x.Tags).FirstOrDefault(); ``` We can use the new `Cacheable()` extension method: ```csharp // If you don't specify the `EFCachePolicy`, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. var products = context.Products.Include(x => x.Tags).Cacheable().FirstOrDefault(); // Async methods are supported too. // Or you can specify the `EFCachePolicy` explicitly to override the global settings. var post1 = context.Posts .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(5)) .FirstOrDefault(); // NOTE: It's better to add the `Cacheable()` method before the materialization methods such as `ToList()` or `FirstOrDefault()` to cover the whole expression tree. ``` Also AutoMapper's `ProjectTo()` method is supported: ```csharp var posts = context.Posts .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable() .ProjectTo(configuration: _mapper.ConfigurationProvider) .ToList(); ``` ## Guidance ### When to use Good candidates for query caching are global site settings and public data, such as infrequently changing articles or comments. It can also be beneficial to cache data specific to a user so long as the cache expires frequently enough relative to the size of the user base that memory consumption remains acceptable. Small, per-user data that frequently exceeds the cache's lifetime, such as a user's photo path, is better held in user claims, which are stored in cookies, than in this cache. ### Scope This cache is scoped to the application, not the current user. It does not use session variables. Accordingly, when retriveing cached per-user data, be sure queries in include code such as `.Where(x => .... && x.UserId == id)`. ### Invalidation This cache is updated when an entity is changed (insert, update, or delete) via a DbContext that uses this library. If the database is updated through some other means, such as a stored procedure or trigger, the cache becomes stale. ================================================ FILE: global.json ================================================ { "sdk": { "version": "3.1.101" } } ================================================ FILE: src/EFSecondLevelCache.Core/Contracts/EFCacheDebugInfo.cs ================================================ namespace EFSecondLevelCache.Core.Contracts { /// /// Stores the debug information of the caching process. /// public class EFCacheDebugInfo { /// /// Stores information of the computed key of the input LINQ query. /// public EFCacheKey EFCacheKey { set; get; } /// /// Determines this query is using the 2nd level cache or not. /// public bool IsCacheHit { set; get; } } } ================================================ FILE: src/EFSecondLevelCache.Core/Contracts/EFCacheKey.cs ================================================ using System.Collections.Generic; namespace EFSecondLevelCache.Core.Contracts { /// /// Stores information of the computed key of the input LINQ query. /// public class EFCacheKey { /// /// The computed key of the input LINQ query. /// public string Key { set; get; } /// /// Hash of the input LINQ query's computed key. /// public string KeyHash { set; get; } /// /// Determines which entities are used in this LINQ query. /// This array will be used to invalidate the related cache of all related queries automatically. /// public ISet CacheDependencies { set; get; } /// /// Stores information of the computed key of the input LINQ query. /// public EFCacheKey() { CacheDependencies = new HashSet(); } /// /// Equals /// /// /// public override bool Equals(object obj) { var efCacheKey = obj as EFCacheKey; if (efCacheKey == null) return false; return this.KeyHash == efCacheKey.KeyHash; } /// /// GetHashCode /// /// public override int GetHashCode() { unchecked { var hash = 17; hash = hash * 23 + KeyHash.GetHashCode(); return hash; } } } } ================================================ FILE: src/EFSecondLevelCache.Core/Contracts/EFQueryDebugView.cs ================================================ using System.Collections.Generic; namespace EFSecondLevelCache.Core.Contracts { /// /// Expression and its Dependencies /// public class EFQueryDebugView { /// /// Dependency items. /// public ISet Types { set; get; } /// /// Expression to a readable string. /// public string DebugView { set; get; } } } ================================================ FILE: src/EFSecondLevelCache.Core/Contracts/IEFCacheKeyHashProvider.cs ================================================ namespace EFSecondLevelCache.Core.Contracts { /// /// The CacheKey Hash Provider Contract. /// public interface IEFCacheKeyHashProvider { /// /// Computes the unique hash of the input. /// /// the input data to hash /// Hashed data string ComputeHash(string data); } } ================================================ FILE: src/EFSecondLevelCache.Core/Contracts/IEFCacheKeyProvider.cs ================================================ using System.Linq; using System.Linq.Expressions; namespace EFSecondLevelCache.Core.Contracts { /// /// CacheKeyProvider Contract. /// public interface IEFCacheKeyProvider { /// /// Gets an EF query and returns its hash to store in the cache. /// /// The EF query. /// An expression tree that represents a LINQ query. /// If you think the computed hash of the query is not enough, set this value. /// Information of the computed key of the input LINQ query. EFCacheKey GetEFCacheKey(IQueryable query, Expression expression, string saltKey = ""); } } ================================================ FILE: src/EFSecondLevelCache.Core/Contracts/IEFCacheServiceProvider.cs ================================================ using System.Collections.Generic; namespace EFSecondLevelCache.Core.Contracts { /// /// Cache Service Provider Contract. /// public interface IEFCacheServiceProvider { /// /// Removes the cached entries added by this library. /// void ClearAllCachedEntries(); /// /// Gets a cached entry by key. /// /// key to find /// cached value object GetValue(string cacheKey); /// /// Adds a new item to the cache. /// /// key /// value /// cache dependencies /// Defines the expiration mode of the cache item. If you set it to null, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. void InsertValue(string cacheKey, object value, ISet rootCacheKeys, EFCachePolicy cachePolicy); /// /// Invalidates all of the cache entries which are dependent on any of the specified root keys. /// /// cache dependencies void InvalidateCacheDependencies(string[] rootCacheKeys); /// /// Some cache providers won't accept null values. /// So we need a custom Null object here. It should be defined `static readonly` in your code. /// object NullObject { get; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFAsyncEnumerable.cs ================================================ using System.Collections.Generic; using System.Threading; namespace EFSecondLevelCache.Core { /// /// Asynchronous version of the IEnumerable interface, allowing elements of the enumerable sequence to be retrieved asynchronously. /// public class EFAsyncEnumerable : IAsyncEnumerable { private readonly IEnumerator _inner; /// /// Asynchronous version of the IEnumerable interface /// public EFAsyncEnumerable(IEnumerator inner) { _inner = inner; } /// /// Gets an asynchronous enumerator over the sequence. /// public IAsyncEnumerator GetEnumerator() { return new EFAsyncEnumerator(_inner); } /// /// Gets an asynchronous enumerator over the sequence. /// public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) { return new EFAsyncEnumerator(_inner); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFAsyncEnumerator.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace EFSecondLevelCache.Core { /// /// Asynchronous version of the IEnumerator of T interface that allows elements to be retrieved asynchronously. /// /// public class EFAsyncEnumerator : IAsyncEnumerator { private readonly IEnumerator _inner; /// /// Asynchronous version of the IEnumerator of T interface that allows elements to be retrieved asynchronously. /// /// The inner IEnumerator public EFAsyncEnumerator(IEnumerator inner) { _inner = inner; } /// /// Gets the current element in the iteration. /// public T Current => _inner.Current; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { _inner.Dispose(); } /// /// Advances the enumerator to the next element in the sequence, returning the result asynchronously. /// /// A System.Threading.CancellationToken to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the sequence. public Task MoveNext(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(_inner.MoveNext()); } /// /// Advances the enumerator to the next element in the sequence, returning the result asynchronously. /// /// A task that represents the asynchronous operation. The task result contains true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the sequence. public ValueTask MoveNextAsync() { return new ValueTask(_inner.MoveNext()); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public ValueTask DisposeAsync() { _inner.Dispose(); return default; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFAsyncTaskEnumerable.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace EFSecondLevelCache.Core { /// /// Asynchronous version of the IEnumerable interface, allowing elements of the enumerable sequence to be retrieved asynchronously. /// public class EFAsyncTaskEnumerable : IAsyncEnumerable { private readonly Task _task; /// /// Asynchronous version of the IEnumerable interface. /// public EFAsyncTaskEnumerable(Task task) { _task = task; } /// /// Gets an asynchronous enumerator over the sequence. /// public IAsyncEnumerator GetEnumerator() => new EFAsyncTaskEnumerator(_task); /// /// Gets an asynchronous enumerator over the sequence. /// public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) { return new EFAsyncTaskEnumerator(_task); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFAsyncTaskEnumerator.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace EFSecondLevelCache.Core { /// /// Asynchronous version of the IEnumerator interface, allowing elements to be retrieved asynchronously. /// public sealed class EFAsyncTaskEnumerator : IAsyncEnumerator { private readonly Task _task; private bool _moved; /// /// Asynchronous version of the IEnumerator interface /// public EFAsyncTaskEnumerator(Task task) { _task = task; } /// /// Gets the current element in the iteration. /// public T Current => !_moved ? default(T) : _task.Result; /// /// Advances the enumerator to the next element in the sequence, returning the result asynchronously. /// public async Task MoveNext(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (!_moved) { await _task.ConfigureAwait(false); _moved = true; return _moved; } return false; } /// /// Advances the enumerator to the next element in the sequence, returning the result asynchronously. /// public ValueTask MoveNextAsync() { return new ValueTask(MoveNext(new CancellationToken())); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public ValueTask DisposeAsync() { return default; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCacheKeyHashProvider.cs ================================================ using System; using EFSecondLevelCache.Core.Contracts; namespace EFSecondLevelCache.Core { /// /// Computes the unique hash of the input, using the xxHash algorithm. /// public class EFCacheKeyHashProvider : IEFCacheKeyHashProvider { /// /// Computes the unique hash of the input. /// /// the input data to hash /// Hashed data using the xxHash algorithm public string ComputeHash(string data) { if(string.IsNullOrWhiteSpace(data)) throw new ArgumentNullException(nameof(data)); return $"{XxHashUnsafe.ComputeHash(data):X}"; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCacheKeyProvider3x.cs ================================================ using System.Linq; using System.Linq.Expressions; using EFSecondLevelCache.Core.Contracts; using System; using CacheManager.Core; using Microsoft.Extensions.DependencyInjection; using System.Reflection; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore; namespace EFSecondLevelCache.Core { /// /// A custom cache key provider for EF queries. /// public class EFCacheKeyProvider : IEFCacheKeyProvider { private static readonly TypeInfo _queryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo(); private static readonly FieldInfo _queryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.First(x => x.Name == "_queryCompiler"); private static readonly FieldInfo _queryContextFactoryField = _queryCompilerTypeInfo.DeclaredFields.First(x => x.Name == "_queryContextFactory"); private static readonly FieldInfo _loggerField = _queryCompilerTypeInfo.DeclaredFields.First(x => x.Name == "_logger"); private static readonly TimeSpan _slidingExpirationTimeSpan = TimeSpan.FromMinutes(7); private static readonly ICacheManager _keysCacheManager = EFStaticServiceProvider.Instance.GetRequiredService>(); private readonly IEFCacheKeyHashProvider _cacheKeyHashProvider; /// /// A custom cache key provider for EF queries. /// /// Provides the custom hashing algorithm. public EFCacheKeyProvider(IEFCacheKeyHashProvider cacheKeyHashProvider) { _cacheKeyHashProvider = cacheKeyHashProvider; } /// /// Gets an EF query and returns its hashed key to store in the cache. /// /// The EF query. /// An expression tree that represents a LINQ query. /// If you think the computed hash of the query is not enough, set this value. /// Information of the computed key of the input LINQ query. public EFCacheKey GetEFCacheKey(IQueryable query, Expression expression, string saltKey = "") { var queryCompiler = (QueryCompiler)_queryCompilerField.GetValue(query.Provider); var (expressionKeyHash, modifiedExpression) = getExpressionKeyHash(queryCompiler, _cacheKeyHashProvider, expression); var cachedKey = _keysCacheManager.Get(expressionKeyHash); if (cachedKey != null) { return cachedKey; } var expressionPrinter = new ExpressionPrinter(); var sql = expressionPrinter.PrintDebug(modifiedExpression); var expressionVisitorResult = EFQueryExpressionVisitor.GetDebugView(expression); var key = $"{sql};{expressionVisitorResult.DebugView};{saltKey}"; var keyHash = _cacheKeyHashProvider.ComputeHash(key); var cacheKey = new EFCacheKey { Key = key, KeyHash = keyHash, CacheDependencies = expressionVisitorResult.Types }; setCache(expressionKeyHash, cacheKey); return cacheKey; } private static void setCache(string expressionKeyHash, EFCacheKey value) { _keysCacheManager.Add( new CacheItem(expressionKeyHash, value, ExpirationMode.Sliding, _slidingExpirationTimeSpan)); } private static (string ExpressionKeyHash, Expression ModifiedExpression) getExpressionKeyHash( QueryCompiler queryCompiler, IEFCacheKeyHashProvider cacheKeyHashProvider, Expression expression) { var queryContextFactory = (IQueryContextFactory)_queryContextFactoryField.GetValue(queryCompiler); var queryContext = queryContextFactory.Create(); var logger = (IDiagnosticsLogger)_loggerField.GetValue(queryCompiler); expression = queryCompiler.ExtractParameters(expression, queryContext, logger, parameterize: false); var expressionKey = $"{ExpressionEqualityComparer.Instance.GetHashCode(expression)};"; var parameterValues = queryContext.ParameterValues; if (parameterValues.Any()) { expressionKey = parameterValues.Aggregate(expressionKey, (current, item) => current + $"{item.Key}={item.Value?.GetHashCode()};"); } return (cacheKeyHashProvider.ComputeHash(expressionKey), expression); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCachePolicy.cs ================================================ using System; namespace EFSecondLevelCache.Core { /// /// Defines the supported expiration modes for cache items. /// public enum CacheExpirationMode { /// /// Defines absolute expiration. The item will expire after the expiration timeout. /// Absolute, /// /// Defines sliding expiration. The expiration timeout will be refreshed on every access. /// Sliding } /// /// EFCachePolicy determines the Expiration time of the cache. /// If you don't define it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// public class EFCachePolicy { /// /// Defines the expiration mode of the cache item. /// Its default value is Absolute. /// public CacheExpirationMode ExpirationMode { set; get; } /// /// The expiration timeout. /// Its default value is 20 minutes later. /// /// public TimeSpan Timeout { set; get; } = TimeSpan.FromMinutes(20); /// /// If you think the computed hash of the query to calculate the cache-key is not enough, set this value. /// Its default value is string.Empty. /// public string SaltKey { set; get; } = string.Empty; /// /// EFCachePolicy determines the Expiration time of the cache. /// public EFCachePolicy() { } /// /// EFCachePolicy determines the Expiration time of the cache. /// /// Defines the expiration mode of the cache item. /// The expiration timeout. public EFCachePolicy(CacheExpirationMode expirationMode, TimeSpan timeout) { ExpirationMode = expirationMode; Timeout = timeout; } /// /// EFCachePolicy determines the Expiration time of the cache. /// /// Defines the expiration mode of the cache item. /// The expiration timeout. /// If you think the computed hash of the query to calculate the cache-key is not enough, set this value. public EFCachePolicy(CacheExpirationMode expirationMode, TimeSpan timeout, string saltKey) { ExpirationMode = expirationMode; Timeout = timeout; SaltKey = saltKey; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCacheServiceProvider.cs ================================================ using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.DependencyInjection; using CacheManager.Core; using EFSecondLevelCache.Core.Contracts; namespace EFSecondLevelCache.Core { /// /// Using ICacheManager as a cache service. /// public class EFCacheServiceProvider : IEFCacheServiceProvider { private static readonly EFCacheKey _nullObject = new EFCacheKey(); private static readonly ICacheManager> _dependenciesCacheManager = EFStaticServiceProvider.Instance.GetRequiredService>>(); private static readonly ICacheManager _valuesCacheManager = EFStaticServiceProvider.Instance.GetRequiredService>(); private static readonly ReaderWriterLockSlim _vcmReaderWriterLock = new ReaderWriterLockSlim(); private static readonly ReaderWriterLockSlim _dcReaderWriterLock = new ReaderWriterLockSlim(); /// /// Some cache providers won't accept null values. /// So we need a custom Null object here. It should be defined `static readonly` in your code. /// public object NullObject => _nullObject; /// /// Using ICacheManager as a cache service. /// public EFCacheServiceProvider() { // Occurs when an item was removed by the cache handle due to expiration or e.g. memory pressure eviction. // Without _dependenciesCacheManager items, we can't invalidate cached items on Insert/Update/Delete. // So to prevent stale reads, we have to clear all cached data in this case. _dependenciesCacheManager.OnRemoveByHandle += (sender, args) => ClearAllCachedEntries(); } /// /// Removes the cached entries added by this library. /// public void ClearAllCachedEntries() { _vcmReaderWriterLock.TryWriteLocked(() => _valuesCacheManager.Clear()); _dcReaderWriterLock.TryWriteLocked(() => _dependenciesCacheManager.Clear()); } /// /// Gets a cached entry by key. /// /// key to find /// cached value public object GetValue(string cacheKey) { return _valuesCacheManager.Get(cacheKey); } /// /// Adds a new item to the cache. /// /// key /// value /// cache dependencies /// Defines the expiration mode of the cache item. public void InsertValue(string cacheKey, object value, ISet rootCacheKeys, EFCachePolicy cachePolicy) { if (value == null) { value = NullObject; // `HttpRuntime.Cache.Insert` won't accept null values. } foreach (var rootCacheKey in rootCacheKeys) { _dcReaderWriterLock.TryWriteLocked(() => { _dependenciesCacheManager.AddOrUpdate(rootCacheKey, new HashSet { cacheKey }, updateValue: set => { set.Add(cacheKey); return set; }); }); } _vcmReaderWriterLock.TryWriteLocked(() => { if (cachePolicy == null) { _valuesCacheManager.Add(cacheKey, value); } else { _valuesCacheManager.Add(new CacheItem( cacheKey, value, cachePolicy.ExpirationMode == CacheExpirationMode.Absolute ? ExpirationMode.Absolute : ExpirationMode.Sliding, cachePolicy.Timeout)); } }); } /// /// Invalidates all of the cache entries which are dependent on any of the specified root keys. /// /// cache dependencies public void InvalidateCacheDependencies(string[] rootCacheKeys) { foreach (var rootCacheKey in rootCacheKeys) { if (string.IsNullOrWhiteSpace(rootCacheKey)) { continue; } clearDependencyValues(rootCacheKey); _dcReaderWriterLock.TryWriteLocked(() => _dependenciesCacheManager.Remove(rootCacheKey)); } } private void clearDependencyValues(string rootCacheKey) { _dcReaderWriterLock.TryReadLocked(() => { var dependencyKeys = _dependenciesCacheManager.Get(rootCacheKey); if (dependencyKeys == null) { return; } foreach (var dependencyKey in dependencyKeys) { _vcmReaderWriterLock.TryWriteLocked(() => _valuesCacheManager.Remove(dependencyKey)); } }); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCachedDbSet.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query.Internal; namespace EFSecondLevelCache.Core { /// /// Provides functionality to evaluate queries against a specific data source. /// /// public class EFCachedDbSet : IOrderedQueryable, IAsyncEnumerable where TType : class { private readonly EFCachedQueryProvider _provider; /// /// Provides functionality to evaluate queries against a specific data source. /// /// The input EF query. /// Defines the expiration mode of the cache item. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// Cache Service Provider. public EFCachedDbSet( DbSet query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) { CachePolicy = cachePolicy; DebugInfo = debugInfo; CacheKeyProvider = cacheKeyProvider; CacheServiceProvider = cacheServiceProvider; Query = query; var queryable = Query.AsNoTracking().AsQueryable(); ElementType = queryable.ElementType; Expression = queryable.Expression; _provider = new EFCachedQueryProvider(queryable, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Asynchronous version of the IEnumerable interface /// public IAsyncEnumerable AsyncEnumerable => new EFAsyncEnumerable(this.AsEnumerable().GetEnumerator()); /// /// Gets an EF query and returns its hash to store in the cache. /// public IEFCacheKeyProvider CacheKeyProvider { get; } /// /// Cache Service Provider. /// public IEFCacheServiceProvider CacheServiceProvider { get; } /// /// Stores the debug information of the caching process. /// public EFCacheDebugInfo DebugInfo { get; } /// /// Gets the type of the element(s) that are returned when the expression tree associated with this instance of System.Linq.IQueryable is executed. /// public Type ElementType { get; } /// /// Gets the expression tree that is associated with the instance of System.Linq.IQueryable. /// public Expression Expression { get; } /// /// Gets the query provider that is associated with this data source. /// public IQueryProvider Provider => _provider; /// /// The input EF query. /// public DbSet Query { get; } /// /// Defines the expiration mode of the cache item. /// public EFCachePolicy CachePolicy { get; } /// /// Returns an enumerator that iterates through a collection. /// /// A collections that can be used to iterate through the collection. public IEnumerator GetEnumerator() { return _provider.Materializer.Materialize(Expression, () => Query.ToArray()).GetEnumerator(); } /// /// Returns an enumerator that iterates through a collection. /// /// A collections that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_provider.Materializer.Materialize(Expression, () => Query.ToArray())).GetEnumerator(); } /// /// Returns an enumerator that iterates through a collection. /// /// A collections that can be used to iterate through the collection. public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) { return new EFAsyncEnumerator( ((IEnumerable)_provider.Materializer.Materialize(Expression, () => Query.ToArray())).GetEnumerator()); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCachedDbSetExtensions.cs ================================================ using System; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore.Diagnostics; namespace EFSecondLevelCache.Core { /// /// DbSet Extensions /// public static class EFCachedDbSetExtensions { /// /// Finds an entity with the given primary key values. /// public static TEntity Find( this EFCachedDbSet cachedQueryable, params object[] keyValues) where TEntity : class { var query = buildFindQueryable(cachedQueryable, keyValues); return new EFCachedQueryable( query, cachedQueryable.CachePolicy, cachedQueryable.DebugInfo, cachedQueryable.CacheKeyProvider, cachedQueryable.CacheServiceProvider).FirstOrDefault(); } /// /// Finds an entity with the given primary key values. /// public static Task FindAsync( this EFCachedDbSet cachedQueryable, object[] keyValues, CancellationToken cancellationToken) where TEntity : class { var query = buildFindQueryable(cachedQueryable, keyValues); return new EFCachedQueryable( query, cachedQueryable.CachePolicy, cachedQueryable.DebugInfo, cachedQueryable.CacheKeyProvider, cachedQueryable.CacheServiceProvider).FirstOrDefaultAsync(cancellationToken); } /// /// Finds an entity with the given primary key values. /// public static Task FindAsync( this EFCachedDbSet cachedQueryable, params object[] keyValues) where TEntity : class { return cachedQueryable.FindAsync(keyValues, default(CancellationToken)); } private static IQueryable buildFindQueryable( EFCachedDbSet cachedQueryable, object[] keyValues) where TEntity : class { var set = cachedQueryable.Query; var context = set.GetInfrastructure().GetRequiredService().CurrentContext.Context; var keyProperties = context.Model.FindEntityType(typeof(TEntity)).FindPrimaryKey().Properties; if (keyProperties.Count != keyValues.Length) { if (keyProperties.Count == 1) { throw new ArgumentException( CoreStrings.FindNotCompositeKey(typeof(TEntity).ShortDisplayName(), keyValues.Length)); } throw new ArgumentException( CoreStrings.FindValueCountMismatch(typeof(TEntity).ShortDisplayName(), keyProperties.Count, keyValues.Length)); } for (var i = 0; i < keyValues.Length; i++) { if (keyValues[i] == null) { throw new ArgumentNullException(nameof(keyValues)); } var valueType = keyValues[i].GetType(); var propertyType = keyProperties[i].ClrType; if (valueType != propertyType) { throw new ArgumentException( CoreStrings.FindValueTypeMismatch( i, typeof(TEntity).ShortDisplayName(), valueType.ShortDisplayName(), propertyType.ShortDisplayName())); } } IQueryable query = context.Set().AsNoTracking(); var parameter = Expression.Parameter(typeof(TEntity), "x"); for (var i = 0; i < keyProperties.Count; i++) { var property = keyProperties[i]; var keyValue = keyValues[i]; var expression = Expression.Lambda( Expression.Equal( Expression.Property(parameter, property.Name), Expression.Constant(keyValue)), parameter) as Expression>; query = query.Where(expression); } return query; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCachedQueryExtensions.cs ================================================ using System; using System.Linq; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace EFSecondLevelCache.Core { /// /// Returns a new cached query. /// public static class EFCachedQueryExtensions { private static readonly IEFCacheKeyProvider _defaultCacheKeyProvider; private static readonly IEFCacheServiceProvider _defaultCacheServiceProvider; static EFCachedQueryExtensions() { var serviceProvider = EFStaticServiceProvider.Instance; _defaultCacheServiceProvider = serviceProvider.GetRequiredService(); _defaultCacheKeyProvider = serviceProvider.GetRequiredService(); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// Cache Service Provider. /// public static EFCachedQueryable Cacheable( this IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) { return new EFCachedQueryable(query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// Cache Service Provider. /// Provides functionality to evaluate queries against a specific data source. public static IQueryable Cacheable( this IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) { var type = typeof(EFCachedQueryable<>).MakeGenericType(query.ElementType); var cachedQueryable = Activator.CreateInstance(type, query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); return cachedQueryable as IQueryable; } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// Cache Service Provider. /// public static EFCachedDbSet Cacheable( this DbSet query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) where TType : class { return new EFCachedDbSet(query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Defines a mechanism for retrieving a service object. /// public static EFCachedQueryable Cacheable( this IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IServiceProvider serviceProvider) { var cacheServiceProvider = serviceProvider.GetRequiredService(); var cacheKeyProvider = serviceProvider.GetRequiredService(); return new EFCachedQueryable(query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Defines a mechanism for retrieving a service object. /// public static IQueryable Cacheable( this IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IServiceProvider serviceProvider) { var cacheServiceProvider = serviceProvider.GetRequiredService(); var cacheKeyProvider = serviceProvider.GetRequiredService(); return Cacheable(query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Defines a mechanism for retrieving a service object. /// public static EFCachedDbSet Cacheable( this DbSet query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IServiceProvider serviceProvider) where TType : class { var cacheServiceProvider = serviceProvider.GetRequiredService(); var cacheKeyProvider = serviceProvider.GetRequiredService(); return new EFCachedDbSet(query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Stores the debug information of the caching process. /// Defines a mechanism for retrieving a service object. /// public static EFCachedQueryable Cacheable( this IQueryable query, EFCacheDebugInfo debugInfo, IServiceProvider serviceProvider) { return Cacheable(query, null, debugInfo, serviceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// The input EF query. /// Stores the debug information of the caching process. /// Defines a mechanism for retrieving a service object. /// public static IQueryable Cacheable( this IQueryable query, EFCacheDebugInfo debugInfo, IServiceProvider serviceProvider) { return Cacheable(query, null, debugInfo, serviceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Stores the debug information of the caching process. /// Defines a mechanism for retrieving a service object. /// public static EFCachedDbSet Cacheable( this DbSet query, EFCacheDebugInfo debugInfo, IServiceProvider serviceProvider) where TType : class { return Cacheable(query, null, debugInfo, serviceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines a mechanism for retrieving a service object. /// public static EFCachedQueryable Cacheable( this IQueryable query, IServiceProvider serviceProvider) { return Cacheable(query, null, new EFCacheDebugInfo(), serviceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines a mechanism for retrieving a service object. /// public static EFCachedDbSet Cacheable( this DbSet query, IServiceProvider serviceProvider) where TType : class { return Cacheable(query, null, new EFCacheDebugInfo(), serviceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedQueryable Cacheable( this IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo) { return Cacheable(query, cachePolicy, debugInfo, _defaultCacheKeyProvider, _defaultCacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedDbSet Cacheable( this DbSet query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo) where TType : class { return Cacheable(query, cachePolicy, debugInfo, _defaultCacheKeyProvider, _defaultCacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static IQueryable Cacheable(this IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo) { return Cacheable(query, cachePolicy, debugInfo, _defaultCacheKeyProvider, _defaultCacheServiceProvider); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedQueryable Cacheable(this IQueryable query) { return Cacheable(query, null, new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// The input EF query. /// Provides functionality to evaluate queries against a specific data source. public static IQueryable Cacheable(this IQueryable query) { return Cacheable(query, null, new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedDbSet Cacheable(this DbSet query) where TType : class { return Cacheable(query, null, new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedQueryable Cacheable(this IQueryable query, EFCacheDebugInfo debugInfo) { return Cacheable(query, null, debugInfo); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedDbSet Cacheable(this DbSet query, EFCacheDebugInfo debugInfo) where TType : class { return Cacheable(query, null, debugInfo); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedQueryable Cacheable(this IQueryable query, EFCachePolicy cachePolicy) { return Cacheable(query, cachePolicy, new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. /// The expiration timeout. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedQueryable Cacheable( this IQueryable query, CacheExpirationMode expirationMode, TimeSpan timeout) { return Cacheable(query, new EFCachePolicy(expirationMode, timeout), new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. /// The expiration timeout. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedQueryable Cacheable( this IQueryable query, CacheExpirationMode expirationMode, TimeSpan timeout, EFCacheDebugInfo debugInfo) { return Cacheable(query, new EFCachePolicy(expirationMode, timeout), debugInfo); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Provides functionality to evaluate queries against a specific data source. public static IQueryable Cacheable(this IQueryable query, EFCachePolicy cachePolicy) { return Cacheable(query, cachePolicy, new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. If you set it to null or don't specify it, the global `new CacheManager.Core.ConfigurationBuilder().WithExpiration()` setting will be used automatically. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedDbSet Cacheable( this DbSet query, EFCachePolicy cachePolicy) where TType : class { return Cacheable(query, cachePolicy, new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. /// The expiration timeout. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedDbSet Cacheable( this DbSet query, CacheExpirationMode expirationMode, TimeSpan timeout) where TType : class { return Cacheable(query, new EFCachePolicy(expirationMode, timeout), new EFCacheDebugInfo()); } /// /// Returns a new query where the entities returned will be cached in the IEFCacheServiceProvider. /// /// Entity type. /// The input EF query. /// Defines the expiration mode of the cache item. /// The expiration timeout. /// Stores the debug information of the caching process. /// Provides functionality to evaluate queries against a specific data source. public static EFCachedDbSet Cacheable( this DbSet query, CacheExpirationMode expirationMode, TimeSpan timeout, EFCacheDebugInfo debugInfo) where TType : class { return Cacheable(query, new EFCachePolicy(expirationMode, timeout), debugInfo); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCachedQueryProvider.cs ================================================ using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore.Query.Internal; namespace EFSecondLevelCache.Core { /// /// Defines methods to create and execute queries that are described by an System.Linq.IQueryable object. /// public class EFCachedQueryProvider : IAsyncQueryProvider { private readonly IEFCacheKeyProvider _cacheKeyProvider; private readonly IEFCacheServiceProvider _cacheServiceProvider; private readonly EFCacheDebugInfo _debugInfo; private readonly EFCachePolicy _cachePolicy; private readonly IQueryable _query; private static readonly MethodInfo _fromResultMethodInfo = typeof(Task).GetMethod("FromResult"); private static readonly MethodInfo _materializeAsyncMethodInfo = typeof(EFMaterializer).GetMethod(nameof(EFMaterializer.MaterializeAsync)); /// /// Defines methods to create and execute queries that are described by an System.Linq.IQueryable object. /// /// The input EF query. /// Defines the expiration mode of the cache item. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// The Cache Service Provider. public EFCachedQueryProvider( IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) { _query = query; _cachePolicy = cachePolicy; _debugInfo = debugInfo; _cacheKeyProvider = cacheKeyProvider; _cacheServiceProvider = cacheServiceProvider; Materializer = new EFMaterializer(_query, _cachePolicy, _debugInfo, _cacheKeyProvider, _cacheServiceProvider); } /// /// Defines methods to create and execute queries that are described by an System.Linq.IQueryable object. /// public EFMaterializer Materializer { get; } /// /// Constructs an System.Linq.IQueryable of T object that can evaluate the query represented by a specified expression tree. /// /// The type of the elements that is returned. /// An expression tree that represents a LINQ query. /// An System.Linq.IQueryable of T that can evaluate the query represented by the specified expression tree. public IQueryable CreateQuery(Expression expression) { return (IQueryable)CreateQuery(expression); } /// /// Constructs an System.Linq.IQueryable object that can evaluate the query represented by a specified expression tree. /// /// An expression tree that represents a LINQ query. /// An System.Linq.IQueryable that can evaluate the query represented by the specified expression tree. public IQueryable CreateQuery(Expression expression) { var argumentType = expression.Type.GenericTypeArguments[0]; var cachedQueryable = typeof(EFCachedQueryable<>).MakeGenericType(argumentType); var constructorArgs = new object[] { _query.Provider.CreateQuery(expression), _cachePolicy, _debugInfo, _cacheKeyProvider, _cacheServiceProvider }; return (IQueryable)Activator.CreateInstance(cachedQueryable, constructorArgs); } /// /// Executes the query represented by a specified expression tree. /// /// An expression tree that represents a LINQ query. /// The value that results from executing the specified query. public object Execute(Expression expression) { return Materializer.Materialize(expression, () => _query.Provider.Execute(expression)); } /// /// Executes the strongly-typed query represented by a specified expression tree. /// /// The type of the value that results from executing the query. /// An expression tree that represents a LINQ query. /// The value that results from executing the specified query. public TResult Execute(Expression expression) { return Materializer.Materialize(expression, () => _query.Provider.Execute(expression)); } /// /// Asynchronously executes the strongly-typed query represented by a specified expression tree. /// /// The type of the value that results from executing the query. /// An expression tree that represents a LINQ query. /// A CancellationToken to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the value that results from executing the specified query. public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken) { var type = typeof(TResult); if (_query.Provider is EntityQueryProvider eqProvider) { if (isTaskOfT(type)) { var materializeAsyncMethod = _materializeAsyncMethodInfo.MakeGenericMethod(expression.Type); return (TResult)materializeAsyncMethod.Invoke(Materializer, new object[] { expression, new Func(() => eqProvider.ExecuteAsync(expression, cancellationToken)) }); } return Materializer.Materialize( expression, () => eqProvider.ExecuteAsync(expression, cancellationToken)); } if (isTaskOfT(type)) { var result = Execute(expression); var taskFromResultMethod = _fromResultMethodInfo.MakeGenericMethod(expression.Type); return (TResult)taskFromResultMethod.Invoke(null, new[] { result }); } return Execute(expression); } private static bool isTaskOfT(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFCachedQueryable.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore.Query.Internal; namespace EFSecondLevelCache.Core { /// /// Provides functionality to evaluate queries against a specific data source. /// /// Type of the entity. public class EFCachedQueryable : IOrderedQueryable, IAsyncEnumerable { private readonly EFCachedQueryProvider _provider; /// /// Provides functionality to evaluate queries against a specific data source. /// /// The input EF query. /// Defines the expiration mode of the cache item. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// Cache Service Provider. public EFCachedQueryable( IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) { CachePolicy = cachePolicy; DebugInfo = debugInfo; CacheKeyProvider = cacheKeyProvider; CacheServiceProvider = cacheServiceProvider; Query = query.MarkAsNoTracking(); _provider = new EFCachedQueryProvider(Query, cachePolicy, debugInfo, cacheKeyProvider, cacheServiceProvider); } /// /// Asynchronous version of the IEnumerable interface /// public IAsyncEnumerable AsyncEnumerable => new EFAsyncEnumerable(this.AsEnumerable().GetEnumerator()); /// /// Gets an EF query and returns its hash to store in the cache. /// public IEFCacheKeyProvider CacheKeyProvider { get; } /// /// Cache Service Provider. /// public IEFCacheServiceProvider CacheServiceProvider { get; } /// /// Stores the debug information of the caching process. /// public EFCacheDebugInfo DebugInfo { get; } /// /// Gets the type of the element(s) that are returned when the expression tree associated with this instance of System.Linq.IQueryable is executed. /// public Type ElementType => Query.ElementType; /// /// Gets the expression tree that is associated with the instance of System.Linq.IQueryable. /// public Expression Expression => Query.Expression; /// /// Gets the query provider that is associated with this data source. /// public IQueryProvider Provider => _provider; /// /// The input EF query. /// public IQueryable Query { get; } /// /// Defines the expiration mode of the cache item. /// public EFCachePolicy CachePolicy { get; } /// /// Returns an enumerator that iterates through a collection. /// /// A collections that can be used to iterate through the collection. public IEnumerator GetEnumerator() { return ((IEnumerable)_provider.Materializer.Materialize( Query.Expression, () => Query.ToArray())).GetEnumerator(); } /// /// Returns an enumerator that iterates through a collection. /// /// A collections that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_provider.Materializer.Materialize( Query.Expression, () => Query.ToArray())).GetEnumerator(); } /// /// Returns an enumerator that iterates through a collection. /// /// A collections that can be used to iterate through the collection. public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) { return new EFAsyncEnumerator( ((IEnumerable)_provider.Materializer.Materialize( Query.Expression, () => Query.ToArray())).GetEnumerator()); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFChangeTrackerExtensions.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace EFSecondLevelCache.Core { /// /// Change Tracker Extensions /// public static class EFChangeTrackerExtensions { private static readonly MethodInfo _asNoTrackingMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(EntityFrameworkQueryableExtensions.AsNoTracking)); /// /// Find the base types of the given type, recursively. /// public static IEnumerable GetBaseTypes(this Type type) { if (type.GetTypeInfo().BaseType == null) { return type.GetInterfaces(); } return Enumerable.Repeat(type.GetTypeInfo().BaseType, 1) .Concat(type.GetInterfaces()) .Concat(type.GetInterfaces().SelectMany(GetBaseTypes)) .Concat(type.GetTypeInfo().BaseType.GetBaseTypes()); } /// /// Using the ChangeTracker to find names of the changed entities. /// It calls ChangeTracker.DetectChanges() explicitly. /// public static string[] GetChangedEntityNames(this DbContext dbContext) { var typesList = new List(); foreach (var type in dbContext.GetChangedEntityTypes()) { typesList.Add(type); typesList.AddRange(type.GetBaseTypes().Where(t => t != typeof(object)).ToList()); } var changedEntityNames = typesList .Select(type => type.FullName) .Distinct() .ToArray(); return changedEntityNames; } /// /// Checks for changes to the entity and all owns entities. /// private static bool IsEntityChanged(EntityEntry entry) { return entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted || entry.References.Any(r => r.TargetEntry != null && r.TargetEntry.Metadata.IsOwned() && IsEntityChanged(r.TargetEntry)); } /// /// Using the ChangeTracker to find types of the changed entities. /// It calls ChangeTracker.DetectChanges() explicitly. /// public static IEnumerable GetChangedEntityTypes(this DbContext dbContext) { if (!dbContext.ChangeTracker.AutoDetectChangesEnabled) { // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene. dbContext.ChangeTracker.DetectChanges(); } return dbContext.ChangeTracker.Entries() .Where(IsEntityChanged) .Select(dbEntityEntry => dbEntityEntry.Entity.GetType()); } /// /// Applies the AsNoTracking method dynamically /// public static IQueryable MarkAsNoTracking(this IQueryable query) { if (typeof(TType).GetTypeInfo().IsClass) { return query.Provider.CreateQuery( Expression.Call(null, _asNoTrackingMethodInfo.MakeGenericMethod(typeof(TType)), query.Expression)); } return query; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFMaterializer.cs ================================================ using System; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core.Contracts; namespace EFSecondLevelCache.Core { /// /// Cache Result Container /// /// public class CacheResult { /// /// Could read from the cache? /// public bool CanRead { set; get; } /// /// EFCacheKey value /// public EFCacheKey CacheKey { set; get; } /// /// Retrieved result from the cache /// public T Result { set; get; } /// /// Cache Result Container /// public CacheResult() { } /// /// Cache Result Container /// /// Could read from the cache? /// EFCacheKey value /// Retrieved result from the cache public CacheResult(bool canRead, EFCacheKey cacheKey, T result) { CanRead = canRead; CacheKey = cacheKey; Result = result; } } /// /// Defines methods to create and execute queries that are described by an System.Linq.IQueryable object. /// public class EFMaterializer { private readonly IEFCacheKeyProvider _cacheKeyProvider; private readonly IEFCacheServiceProvider _cacheServiceProvider; private readonly EFCacheDebugInfo _debugInfo; private readonly EFCachePolicy _cachePolicy; private readonly IQueryable _query; private static readonly object _syncLock = new object(); private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); /// /// Defines methods to create and execute queries that are described by an System.Linq.IQueryable object. /// /// The input EF query. /// Defines the expiration mode of the cache item. /// Stores the debug information of the caching process. /// Gets an EF query and returns its hash to store in the cache. /// The Cache Service Provider. public EFMaterializer( IQueryable query, EFCachePolicy cachePolicy, EFCacheDebugInfo debugInfo, IEFCacheKeyProvider cacheKeyProvider, IEFCacheServiceProvider cacheServiceProvider) { _query = query; _cachePolicy = cachePolicy; _debugInfo = debugInfo; _cacheKeyProvider = cacheKeyProvider; _cacheServiceProvider = cacheServiceProvider; } /// /// Executes the query represented by a specified expression tree to cache its results. /// /// An expression tree that represents a LINQ query. /// How to run the query. /// The value that results from executing the specified query. public async Task MaterializeAsync(Expression expression, Func> materializer) { await _semaphoreSlim.WaitAsync(); try { var cacheResult = readFromCache(expression); if (cacheResult.CanRead) { return cacheResult.Result; } var result = await materializer(); _cacheServiceProvider.InsertValue(cacheResult.CacheKey.KeyHash, result, cacheResult.CacheKey.CacheDependencies, _cachePolicy); return result; } finally { _semaphoreSlim.Release(); } } /// /// Executes the query represented by a specified expression tree to cache its results. /// /// An expression tree that represents a LINQ query. /// How to run the query. /// The value that results from executing the specified query. public T Materialize(Expression expression, Func materializer) { lock (_syncLock) { var cacheResult = readFromCache(expression); if (cacheResult.CanRead) { return cacheResult.Result; } var result = materializer(); _cacheServiceProvider.InsertValue(cacheResult.CacheKey.KeyHash, result, cacheResult.CacheKey.CacheDependencies, _cachePolicy); return result; } } private CacheResult readFromCache(Expression expression) { var cacheKey = _cacheKeyProvider.GetEFCacheKey(_query, expression, _cachePolicy?.SaltKey); _debugInfo.EFCacheKey = cacheKey; var queryCacheKey = cacheKey.KeyHash; var result = _cacheServiceProvider.GetValue(queryCacheKey); if (Equals(result, _cacheServiceProvider.NullObject)) { _debugInfo.IsCacheHit = true; return new CacheResult(true, cacheKey, default); } if (result != null) { _debugInfo.IsCacheHit = true; return new CacheResult(true, cacheKey, (T)result); } return new CacheResult(false, cacheKey, default); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFQueryExpressionVisitor.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using EFSecondLevelCache.Core.Contracts; namespace EFSecondLevelCache.Core { /// /// Ref. https://github.com/dotnet/corefx/ -> src/System.Linq.Expressions/src/System/Linq/Expressions/DebugViewWriter.cs /// public class EFQueryExpressionVisitor : ExpressionVisitor { private const int MaxColumn = 120; private const int Tab = 4; private readonly TextWriter _out; private readonly Stack _stack = new Stack(); private readonly HashSet _types = new HashSet(); private int _column; private Flow _flow; // Associate every unique anonymous LabelTarget in the tree with an integer. // The id is used to create a name for the anonymous LabelTarget. // private Dictionary _labelIds; // Associate every unique anonymous LambdaExpression in the tree with an integer. // The id is used to create a name for the anonymous lambda. // private Dictionary _lambdaIds; // All the unique lambda expressions in the ET, will be used for displaying all // the lambda definitions. private Queue _lambdas; // Associate every unique anonymous parameter or variable in the tree with an integer. // The id is used to create a name for the anonymous parameter or variable. // private Dictionary _paramIds; private EFQueryExpressionVisitor(TextWriter file) { _out = file; } [Flags] private enum Flow { None, Space, NewLine, Break = 0x8000 // newline if column > MaxColumn } private int Base => _stack.Count > 0 ? _stack.Peek() : 0; private int Delta { get; set; } private int Depth => Base + Delta; /// /// Write out the given AST /// public static EFQueryDebugView GetDebugView(Expression node) { using (var writer = new StringWriter(CultureInfo.CurrentCulture)) { var efQueryExpressionVisitor = new EFQueryExpressionVisitor(writer); efQueryExpressionVisitor.writeTo(node); var types = efQueryExpressionVisitor._types; return new EFQueryDebugView { DebugView = writer.ToString(), Types = types }; } } /// /// /// /// /// protected override Expression VisitBinary(BinaryExpression node) { if (node.NodeType == ExpressionType.ArrayIndex) { parenthesizedVisit(node, node.Left); Out("["); Visit(node.Right); Out("]"); } else { bool parenthesizeLeft = needsParentheses(node, node.Left); bool parenthesizeRight = needsParentheses(node, node.Right); string op; bool isChecked = false; Flow beforeOp = Flow.Space; switch (node.NodeType) { case ExpressionType.Assign: op = "="; break; case ExpressionType.Equal: op = "=="; break; case ExpressionType.NotEqual: op = "!="; break; case ExpressionType.AndAlso: op = "&&"; beforeOp = Flow.Break | Flow.Space; break; case ExpressionType.OrElse: op = "||"; beforeOp = Flow.Break | Flow.Space; break; case ExpressionType.GreaterThan: op = ">"; break; case ExpressionType.LessThan: op = "<"; break; case ExpressionType.GreaterThanOrEqual: op = ">="; break; case ExpressionType.LessThanOrEqual: op = "<="; break; case ExpressionType.Add: op = "+"; break; case ExpressionType.AddAssign: op = "+="; break; case ExpressionType.AddAssignChecked: op = "+="; isChecked = true; break; case ExpressionType.AddChecked: op = "+"; isChecked = true; break; case ExpressionType.Subtract: op = "-"; break; case ExpressionType.SubtractAssign: op = "-="; break; case ExpressionType.SubtractAssignChecked: op = "-="; isChecked = true; break; case ExpressionType.SubtractChecked: op = "-"; isChecked = true; break; case ExpressionType.Divide: op = "/"; break; case ExpressionType.DivideAssign: op = "/="; break; case ExpressionType.Modulo: op = "%"; break; case ExpressionType.ModuloAssign: op = "%="; break; case ExpressionType.Multiply: op = "*"; break; case ExpressionType.MultiplyAssign: op = "*="; break; case ExpressionType.MultiplyAssignChecked: op = "*="; isChecked = true; break; case ExpressionType.MultiplyChecked: op = "*"; isChecked = true; break; case ExpressionType.LeftShift: op = "<<"; break; case ExpressionType.LeftShiftAssign: op = "<<="; break; case ExpressionType.RightShift: op = ">>"; break; case ExpressionType.RightShiftAssign: op = ">>="; break; case ExpressionType.And: op = "&"; break; case ExpressionType.AndAssign: op = "&="; break; case ExpressionType.Or: op = "|"; break; case ExpressionType.OrAssign: op = "|="; break; case ExpressionType.ExclusiveOr: op = "^"; break; case ExpressionType.ExclusiveOrAssign: op = "^="; break; case ExpressionType.Power: op = "**"; break; case ExpressionType.PowerAssign: op = "**="; break; case ExpressionType.Coalesce: op = "??"; break; default: throw new InvalidOperationException(); } if (parenthesizeLeft) { Out("(", Flow.None); } Visit(node.Left); if (parenthesizeLeft) { Out(Flow.None, ")", Flow.Break); } // prepend # to the operator to represent checked op if (isChecked) { op = string.Format( CultureInfo.CurrentCulture, "#{0}", op ); } Out(beforeOp, op, Flow.Space | Flow.Break); if (parenthesizeRight) { Out("(", Flow.None); } Visit(node.Right); if (parenthesizeRight) { Out(Flow.None, ")", Flow.Break); } } return node; } /// /// /// /// /// protected override Expression VisitBlock(BlockExpression node) { Out(".Block"); Out(string.Format(CultureInfo.CurrentCulture, "«{0}»", node.Type)); addType(node.Type); visitDeclarations(node.Variables); Out(" "); // Use ; to separate expressions in the block visitExpressions('{', ';', node.Expressions); return node; } /// /// /// /// /// protected override CatchBlock VisitCatchBlock(CatchBlock node) { Out(Flow.NewLine, $"}} .Catch ({node.Test}"); if (node.Variable != null) { Out(Flow.Space, ""); VisitParameter(node.Variable); } if (node.Filter != null) { Out(") .If (", Flow.Break); Visit(node.Filter); } Out(") {", Flow.NewLine); indent(); Visit(node.Body); dedent(); return node; } /// /// /// /// /// protected override Expression VisitConditional(ConditionalExpression node) { if (isSimpleExpression(node.Test)) { Out(".If ("); Visit(node.Test); Out(") {", Flow.NewLine); } else { Out(".If (", Flow.NewLine); indent(); Visit(node.Test); dedent(); Out(Flow.NewLine, ") {", Flow.NewLine); } indent(); Visit(node.IfTrue); dedent(); Out(Flow.NewLine, "} .Else {", Flow.NewLine); indent(); Visit(node.IfFalse); dedent(); Out(Flow.NewLine, "}"); return node; } /// /// /// /// /// protected override Expression VisitConstant(ConstantExpression node) { printConstant(node, node.Value); return node; } private void printConstant(ConstantExpression node, object value) { if (value == null) { Out("null"); } else if ((value is string) && node.Type == typeof(string)) { Out(string.Format( CultureInfo.CurrentCulture, "\"{0}\"", value)); } else if ((value is char) && node.Type == typeof(char)) { Out(string.Format( CultureInfo.CurrentCulture, "'{0}'", value)); } else if (((value is int) && node.Type == typeof(int)) || ((value is bool) && node.Type == typeof(bool))) { Out(value.ToString()); } else if (typeof(IEnumerable).IsAssignableFrom(value.GetType()) && !node.Type.ToString().Contains("Microsoft")) { Out($".Constant<{node.Type}>(new[]{{{string.Join(", ", ((IEnumerable)value).Cast().ToList())}}})"); } else { string suffix = getConstantValueSuffix(node.Type); if (suffix != null) { Out(value.ToString()); Out(suffix); } else { Out(string.Format( CultureInfo.CurrentCulture, ".Constant<{0}>({1})", node.Type, value)); addType(node.Type); } } } /// /// /// /// /// protected override Expression VisitDebugInfo(DebugInfoExpression node) { Out(string.Format( CultureInfo.CurrentCulture, ".DebugInfo({0}: {1}, {2} - {3}, {4})", node.Document.FileName, node.StartLine, node.StartColumn, node.EndLine, node.EndColumn) ); return node; } /// /// /// /// /// protected override Expression VisitDefault(DefaultExpression node) { Out($".Default({node.Type})"); addType(node.Type); return node; } /// /// /// /// /// protected override ElementInit VisitElementInit(ElementInit node) { if (node.Arguments.Count == 1) { Visit(node.Arguments[0]); } else { visitExpressions('{', node.Arguments); } return node; } /// /// /// /// /// protected override Expression VisitExtension(Expression node) { Out(string.Format(CultureInfo.CurrentCulture, ".Extension<{0}>", node.GetType())); if (node.CanReduce) { Out(Flow.Space, "{", Flow.NewLine); indent(); Visit(node.Reduce()); dedent(); Out(Flow.NewLine, "}"); } return node; } /// /// /// /// /// protected override Expression VisitGoto(GotoExpression node) { Out($".{node.Kind}", Flow.Space); Out(getLabelTargetName(node.Target), Flow.Space); Out("{", Flow.Space); Visit(node.Value); Out(Flow.Space, "}"); return node; } /// /// /// /// /// protected override Expression VisitIndex(IndexExpression node) { if (node.Indexer != null) { outMember(node, node.Object, node.Indexer); } else { parenthesizedVisit(node, node.Object); } visitExpressions('[', node.Arguments); return node; } /// /// /// /// /// protected override Expression VisitInvocation(InvocationExpression node) { Out(".Invoke "); parenthesizedVisit(node, node.Expression); visitExpressions('(', node.Arguments); return node; } /// /// /// /// /// protected override Expression VisitLabel(LabelExpression node) { Out(".Label", Flow.NewLine); indent(); Visit(node.DefaultValue); dedent(); newLine(); dumpLabel(node.Target); return node; } /// /// /// /// /// /// protected override Expression VisitLambda(Expression node) { Out( string.Format(CultureInfo.CurrentCulture, "{0} {1}<{2}>", ".Lambda", getLambdaName(node), node.Type ) ); addType(node.Type); if (_lambdas == null) { _lambdas = new Queue(); } // N^2 performance, for keeping the order of the lambdas. if (!_lambdas.Contains(node)) { _lambdas.Enqueue(node); } return node; } /// /// /// /// /// protected override Expression VisitListInit(ListInitExpression node) { Visit(node.NewExpression); visitExpressions('{', ',', node.Initializers, e => VisitElementInit(e)); return node; } /// /// /// /// /// protected override Expression VisitLoop(LoopExpression node) { Out(".Loop", Flow.Space); if (node.ContinueLabel != null) { dumpLabel(node.ContinueLabel); } Out(" {", Flow.NewLine); indent(); Visit(node.Body); dedent(); Out(Flow.NewLine, "}"); if (node.BreakLabel != null) { Out("", Flow.NewLine); dumpLabel(node.BreakLabel); } return node; } /// /// /// /// /// protected override Expression VisitMember(MemberExpression node) { outMember(node, node.Expression, node.Member); return node; } /// /// /// /// /// protected override MemberAssignment VisitMemberAssignment(MemberAssignment assignment) { Out(assignment.Member.Name); Out(Flow.Space, "=", Flow.Space); Visit(assignment.Expression); return assignment; } /// /// /// /// /// protected override Expression VisitMemberInit(MemberInitExpression node) { Visit(node.NewExpression); visitExpressions('{', ',', node.Bindings, e => VisitMemberBinding(e)); return node; } /// /// /// /// /// protected override MemberListBinding VisitMemberListBinding(MemberListBinding binding) { Out(binding.Member.Name); Out(Flow.Space, "=", Flow.Space); visitExpressions('{', ',', binding.Initializers, e => VisitElementInit(e)); return binding; } /// /// /// /// /// protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding binding) { Out(binding.Member.Name); Out(Flow.Space, "=", Flow.Space); visitExpressions('{', ',', binding.Bindings, e => VisitMemberBinding(e)); return binding; } /// /// /// /// /// protected override Expression VisitMethodCall(MethodCallExpression node) { Out(".Call "); if (node.Object != null) { parenthesizedVisit(node, node.Object); } else if (node.Method.DeclaringType != null) { Out(node.Method.DeclaringType.ToString()); } else { Out(""); } Out("."); Out(node.Method.Name); visitExpressions('(', node.Arguments); return node; } /// /// /// /// /// protected override Expression VisitNew(NewExpression node) { Out($".New {node.Type}"); addType(node.Type); visitExpressions('(', node.Arguments); return node; } /// /// /// /// /// protected override Expression VisitNewArray(NewArrayExpression node) { if (node.NodeType == ExpressionType.NewArrayBounds) { // .NewArray MyType[expr1, expr2] Out($".NewArray {node.Type.GetElementType()}"); addType(node.Type); visitExpressions('[', node.Expressions); } else { // .NewArray MyType {expr1, expr2} Out($".NewArray {node.Type}", Flow.Space); addType(node.Type); visitExpressions('{', node.Expressions); } return node; } /// /// /// /// /// protected override Expression VisitParameter(ParameterExpression node) { // Have '$' for the DebugView of ParameterExpressions Out("$"); if (string.IsNullOrEmpty(node.Name)) { // If no name if provided, generate a name as $var1, $var2. // No guarantee for not having name conflicts with user provided variable names. // int id = getParamId(node); Out($"var{id}"); } else { Out(getDisplayName(node.Name)); } return node; } /// /// /// /// /// protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node) { Out(".RuntimeVariables"); visitExpressions('(', node.Variables); return node; } /// /// /// /// /// protected override Expression VisitSwitch(SwitchExpression node) { Out(".Switch "); Out("("); Visit(node.SwitchValue); Out(") {", Flow.NewLine); Visit(node.Cases, VisitSwitchCase); if (node.DefaultBody != null) { Out(".Default:", Flow.NewLine); indent(); indent(); Visit(node.DefaultBody); dedent(); dedent(); newLine(); } Out("}"); return node; } /// /// /// /// /// protected override SwitchCase VisitSwitchCase(SwitchCase node) { foreach (var test in node.TestValues) { Out(".Case ("); Visit(test); Out("):", Flow.NewLine); } indent(); indent(); Visit(node.Body); dedent(); dedent(); newLine(); return node; } /// /// /// /// /// protected override Expression VisitTry(TryExpression node) { Out(".Try {", Flow.NewLine); indent(); Visit(node.Body); dedent(); Visit(node.Handlers, VisitCatchBlock); if (node.Finally != null) { Out(Flow.NewLine, "} .Finally {", Flow.NewLine); indent(); Visit(node.Finally); dedent(); } else if (node.Fault != null) { Out(Flow.NewLine, "} .Fault {", Flow.NewLine); indent(); Visit(node.Fault); dedent(); } Out(Flow.NewLine, "}"); return node; } /// /// /// /// /// protected override Expression VisitTypeBinary(TypeBinaryExpression node) { parenthesizedVisit(node, node.Expression); switch (node.NodeType) { case ExpressionType.TypeIs: Out(Flow.Space, ".Is", Flow.Space); break; case ExpressionType.TypeEqual: Out(Flow.Space, ".TypeEqual", Flow.Space); break; } Out(node.TypeOperand.ToString()); return node; } /// /// /// /// /// protected override Expression VisitUnary(UnaryExpression node) { switch (node.NodeType) { case ExpressionType.Convert: Out($"({node.Type})"); addType(node.Type); break; case ExpressionType.ConvertChecked: Out($"#({node.Type})"); addType(node.Type); break; case ExpressionType.TypeAs: break; case ExpressionType.Not: Out(node.Type == typeof(bool) ? "!" : "~"); break; case ExpressionType.OnesComplement: Out("~"); break; case ExpressionType.Negate: Out("-"); break; case ExpressionType.NegateChecked: Out("#-"); break; case ExpressionType.UnaryPlus: Out("+"); break; case ExpressionType.ArrayLength: break; case ExpressionType.Quote: Out("'"); break; case ExpressionType.Throw: if (node.Operand == null) { Out(".Rethrow"); } else { Out(".Throw", Flow.Space); } break; case ExpressionType.IsFalse: Out(".IsFalse"); break; case ExpressionType.IsTrue: Out(".IsTrue"); break; case ExpressionType.Decrement: Out(".Decrement"); break; case ExpressionType.Increment: Out(".Increment"); break; case ExpressionType.PreDecrementAssign: Out("--"); break; case ExpressionType.PreIncrementAssign: Out("++"); break; case ExpressionType.Unbox: Out(".Unbox"); break; } parenthesizedVisit(node, node.Operand); switch (node.NodeType) { case ExpressionType.TypeAs: Out(Flow.Space, ".As", Flow.Space | Flow.Break); Out(node.Type.ToString()); addType(node.Type); break; case ExpressionType.ArrayLength: Out(".Length"); break; case ExpressionType.PostDecrementAssign: Out("--"); break; case ExpressionType.PostIncrementAssign: Out("++"); break; } return node; } /// /// Return true if the input string contains any whitespace character. /// Otherwise false. /// private static bool containsWhiteSpace(string name) { foreach (char c in name) { if (char.IsWhiteSpace(c)) { return true; } } return false; } private static string getConstantValueSuffix(Type type) { if (type == typeof(uint)) { return "U"; } if (type == typeof(long)) { return "L"; } if (type == typeof(ulong)) { return "UL"; } if (type == typeof(double)) { return "D"; } if (type == typeof(float)) { return "F"; } if (type == typeof(decimal)) { return "M"; } return null; } private static string getDisplayName(string name) { if (containsWhiteSpace(name)) { // if name has whitespace in it, quote it return quoteName(name); } else { return name; } } private static int getId(T e, ref Dictionary ids) { if (ids == null) { ids = new Dictionary { { e, 1 } }; return 1; } else { if (!ids.TryGetValue(e, out int id)) { // e is met the first time id = ids.Count + 1; ids.Add(e, id); } return id; } } // the greater the higher private static int getOperatorPrecedence(Expression node) { // Roughly matches C# operator precedence, with some additional // operators. Also things which are not binary/unary expressions, // such as conditional and type testing, don't use this mechanism. switch (node.NodeType) { // Assignment case ExpressionType.Assign: case ExpressionType.ExclusiveOrAssign: case ExpressionType.AddAssign: case ExpressionType.AddAssignChecked: case ExpressionType.SubtractAssign: case ExpressionType.SubtractAssignChecked: case ExpressionType.DivideAssign: case ExpressionType.ModuloAssign: case ExpressionType.MultiplyAssign: case ExpressionType.MultiplyAssignChecked: case ExpressionType.LeftShiftAssign: case ExpressionType.RightShiftAssign: case ExpressionType.AndAssign: case ExpressionType.OrAssign: case ExpressionType.PowerAssign: case ExpressionType.Coalesce: return 1; // Conditional (?:) would go here // Conditional OR case ExpressionType.OrElse: return 2; // Conditional AND case ExpressionType.AndAlso: return 3; // Logical OR case ExpressionType.Or: return 4; // Logical XOR case ExpressionType.ExclusiveOr: return 5; // Logical AND case ExpressionType.And: return 6; // Equality case ExpressionType.Equal: case ExpressionType.NotEqual: return 7; // Relational, type testing case ExpressionType.GreaterThan: case ExpressionType.LessThan: case ExpressionType.GreaterThanOrEqual: case ExpressionType.LessThanOrEqual: case ExpressionType.TypeAs: case ExpressionType.TypeIs: case ExpressionType.TypeEqual: return 8; // Shift case ExpressionType.LeftShift: case ExpressionType.RightShift: return 9; // Additive case ExpressionType.Add: case ExpressionType.AddChecked: case ExpressionType.Subtract: case ExpressionType.SubtractChecked: return 10; // Multiplicative case ExpressionType.Divide: case ExpressionType.Modulo: case ExpressionType.Multiply: case ExpressionType.MultiplyChecked: return 11; // Unary case ExpressionType.Negate: case ExpressionType.NegateChecked: case ExpressionType.UnaryPlus: case ExpressionType.Not: case ExpressionType.Convert: case ExpressionType.ConvertChecked: case ExpressionType.PreIncrementAssign: case ExpressionType.PreDecrementAssign: case ExpressionType.OnesComplement: case ExpressionType.Increment: case ExpressionType.Decrement: case ExpressionType.IsTrue: case ExpressionType.IsFalse: case ExpressionType.Unbox: case ExpressionType.Throw: return 12; // Power, which is not in C# // But VB/Python/Ruby put it here, above unary. case ExpressionType.Power: return 13; // Primary, which includes all other node types: // member access, calls, indexing, new. case ExpressionType.PostIncrementAssign: case ExpressionType.PostDecrementAssign: default: return 14; // These aren't expressions, so never need parentheses: // constants, variables case ExpressionType.Constant: case ExpressionType.Parameter: return 15; } } private static bool isSimpleExpression(Expression node) { if (node is BinaryExpression binary) { return !(binary.Left is BinaryExpression || binary.Right is BinaryExpression); } return false; } private static bool needsParentheses(Expression parent, Expression child) { Debug.Assert(parent != null); if (child == null) { return false; } // Some nodes always have parentheses because of how they are // displayed, for example: ".Unbox(obj.Foo)" switch (parent.NodeType) { case ExpressionType.Increment: case ExpressionType.Decrement: case ExpressionType.IsTrue: case ExpressionType.IsFalse: case ExpressionType.Unbox: return true; } int childOpPrec = getOperatorPrecedence(child); int parentOpPrec = getOperatorPrecedence(parent); if (childOpPrec == parentOpPrec) { // When parent op and child op has the same precedence, // we want to be a little conservative to have more clarity. // Parentheses are not needed if // 1) Both ops are &&, ||, &, |, or ^, all of them are the only // op that has the precedence. // 2) Parent op is + or *, e.g. x + (y - z) can be simplified to // x + y - z. // 3) Parent op is -, / or %, and the child is the left operand. // In this case, if left and right operand are the same, we don't // remove parenthesis, e.g. (x + y) - (x + y) // switch (parent.NodeType) { case ExpressionType.AndAlso: case ExpressionType.OrElse: case ExpressionType.And: case ExpressionType.Or: case ExpressionType.ExclusiveOr: // Since these ops are the only ones on their precedence, // the child op must be the same. Debug.Assert(child.NodeType == parent.NodeType); // We remove the parenthesis, e.g. x && y && z return false; case ExpressionType.Add: case ExpressionType.AddChecked: case ExpressionType.Multiply: case ExpressionType.MultiplyChecked: return false; case ExpressionType.Subtract: case ExpressionType.SubtractChecked: case ExpressionType.Divide: case ExpressionType.Modulo: BinaryExpression binary = parent as BinaryExpression; Debug.Assert(binary != null); // Need to have parenthesis for the right operand. return child == binary.Right; } return true; } // Special case: negate of a constant needs parentheses, to // disambiguate it from a negative constant. if (child.NodeType == ExpressionType.Constant && (parent.NodeType == ExpressionType.Negate || parent.NodeType == ExpressionType.NegateChecked)) { return true; } // If the parent op has higher precedence, need parentheses for the child. return childOpPrec < parentOpPrec; } private static string quoteName(string name) { return string.Format(CultureInfo.CurrentCulture, "'{0}'", name); } private Flow checkBreak(Flow flow) { if ((flow & Flow.Break) != 0) { if (_column > (MaxColumn + Depth)) { flow = Flow.NewLine; } else { flow &= ~Flow.Break; } } return flow; } private void dedent() { Delta -= Tab; } private void dumpLabel(LabelTarget target) { Out(string.Format(CultureInfo.CurrentCulture, ".LabelTarget {0}:", getLabelTargetName(target))); } private Flow getFlow(Flow flow) { var last = checkBreak(_flow); flow = checkBreak(flow); // Get the biggest flow that is requested None < Space < NewLine return (Flow)Math.Max((int)last, (int)flow); } private int getLabelTargetId(LabelTarget target) { Debug.Assert(string.IsNullOrEmpty(target.Name)); return getId(target, ref _labelIds); } private string getLabelTargetName(LabelTarget target) { if (string.IsNullOrEmpty(target.Name)) { // Create the label target name as #Label1, #Label2, etc. return string.Format(CultureInfo.CurrentCulture, "#Label{0}", getLabelTargetId(target)); } else { return getDisplayName(target.Name); } } private int getLambdaId(LambdaExpression le) { Debug.Assert(string.IsNullOrEmpty(le.Name)); return getId(le, ref _lambdaIds); } private string getLambdaName(LambdaExpression lambda) { if (string.IsNullOrEmpty(lambda.Name)) { return $"#Lambda{getLambdaId(lambda)}"; } return getDisplayName(lambda.Name); } private int getParamId(ParameterExpression p) { Debug.Assert(string.IsNullOrEmpty(p.Name)); return getId(p, ref _paramIds); } private void indent() { Delta += Tab; } private void newLine() { _flow = Flow.NewLine; } private void Out(string s) { Out(Flow.None, s, Flow.None); } private void Out(Flow before, string s) { Out(before, s, Flow.None); } private void Out(string s, Flow after) { Out(Flow.None, s, after); } private void Out(Flow before, string s, Flow after) { switch (getFlow(before)) { case Flow.None: break; case Flow.Space: write(" "); break; case Flow.NewLine: writeLine(); write(new string(' ', Depth)); break; } write(s); _flow = after; } // Prints ".instanceField" or "declaringType.staticField" private void outMember(Expression node, Expression instance, MemberInfo member) { if (instance != null) { parenthesizedVisit(node, instance); Out($".{member.Name}"); if (instance is ConstantExpression instanceExp) { var memberInfoValue = getMemberInfoValue(member, instanceExp.Value); printConstant(instanceExp, memberInfoValue); } } else { // For static members, include the type name Out($"{member.DeclaringType}.{member.Name}"); } } private static object getMemberInfoValue(MemberInfo memberInfo, object forObject) { var field = memberInfo as FieldInfo; if (field != null) { return field.GetValue(forObject); } var property = memberInfo as PropertyInfo; if (property != null) { return property.GetValue(forObject, null); } return null; } private void parenthesizedVisit(Expression parent, Expression nodeToVisit) { if (needsParentheses(parent, nodeToVisit)) { Out("("); Visit(nodeToVisit); Out(")"); } else { Visit(nodeToVisit); } } private void visitDeclarations(IList expressions) { visitExpressions('(', ',', expressions, variable => { Out(variable.Type.ToString()); if (variable.IsByRef) { Out("&"); } Out(" "); VisitParameter(variable); }); } private void visitExpressions(char open, IList expressions) where T : Expression { visitExpressions(open, ',', expressions); } private void visitExpressions(char open, char separator, IList expressions) where T : Expression { visitExpressions(open, separator, expressions, e => Visit(e)); } private void visitExpressions(char open, char separator, IList expressions, Action visit) { Out(open.ToString()); if (expressions != null) { indent(); bool isFirst = true; foreach (T e in expressions) { if (isFirst) { if (open == '{' || expressions.Count > 1) { newLine(); } isFirst = false; } else { Out(separator.ToString(), Flow.NewLine); } visit(e); } dedent(); } char close; switch (open) { case '(': close = ')'; break; case '{': close = '}'; break; case '[': close = ']'; break; case '<': close = '>'; break; default: throw new InvalidOperationException(); } if (open == '{') { newLine(); } Out(close.ToString(), Flow.Break); } private void write(string s) { _out.Write(s); _column += s.Length; } private void writeLambda(LambdaExpression lambda) { Out( string.Format( CultureInfo.CurrentCulture, ".Lambda {0}<{1}>", getLambdaName(lambda), lambda.Type) ); visitDeclarations(lambda.Parameters); Out(Flow.Space, "{", Flow.NewLine); indent(); Visit(lambda.Body); dedent(); Out(Flow.NewLine, "}"); Debug.Assert(_stack.Count == 0); } private void writeLine() { _out.WriteLine(); _column = 0; } private void writeTo(Expression node) { if (node is LambdaExpression lambda) { writeLambda(lambda); } else { Visit(node); Debug.Assert(_stack.Count == 0); } // // Output all lambda expression definitions. // in the order of their appearances in the tree. // while (_lambdas?.Count > 0) { writeLine(); writeLine(); writeLambda(_lambdas.Dequeue()); } } private void addType(Type type) { var typeInfo = type.GetTypeInfo(); if (typeInfo.IsGenericType) { foreach (var genericType in type.GetGenericArguments()) { var genericTypeInfo = genericType.GetTypeInfo(); if (genericTypeInfo.IsGenericType) { addType(genericType); } else { if (genericTypeInfo.IsClass) { var item = genericType.ToString(); if (!isCompilerGenerated(item)) { _types.Add(item); } } } } } else { if (typeInfo.IsClass) { var item = type.ToString(); if (!isCompilerGenerated(item)) { _types.Add(item); } } } } private bool isCompilerGenerated(string name) { return name.Contains("c__DisplayClass"); } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFSecondLevelCache.Core.csproj ================================================  Entity Framework Core Second Level Caching Library. 2.9.1 Vahid Nasiri netstandard2.0;net461; netstandard2.0 true EFSecondLevelCache.Core EFSecondLevelCache.Core EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore https://github.com/VahidN/EFSecondLevelCache.Core Apache-2.0 false false false true NET4_6_1 NETSTANDARD2_0 true anycpu ================================================ FILE: src/EFSecondLevelCache.Core/EFServiceCollectionExtensions.cs ================================================ using System; using EFSecondLevelCache.Core.Contracts; using Microsoft.Extensions.DependencyInjection; namespace EFSecondLevelCache.Core { /// /// ServiceCollection Extensions /// public static class EFServiceCollectionExtensions { /// /// A collection of service descriptors. /// public static IServiceCollection ServiceCollection { get; set; } /// /// Registers the required services of the EFSecondLevelCache.Core. /// public static IServiceCollection AddEFSecondLevelCache(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); ServiceCollection = services; return services; } } } ================================================ FILE: src/EFSecondLevelCache.Core/EFStaticServiceProvider.cs ================================================ using System; using System.Threading; using Microsoft.Extensions.DependencyInjection; namespace EFSecondLevelCache.Core { /// /// A lazy loaded thread-safe singleton App ServiceProvider. /// It's required for static `Cacheable()` methods. /// public static class EFStaticServiceProvider { private static readonly Lazy _serviceProviderBuilder = new Lazy(getServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); /// /// Defines a mechanism for retrieving a service object. /// public static IServiceProvider Instance { get; } = _serviceProviderBuilder.Value; private static IServiceProvider getServiceProvider() { var serviceProvider = EFServiceCollectionExtensions.ServiceCollection?.BuildServiceProvider(); return serviceProvider ?? throw new InvalidOperationException("Please add `AddEFSecondLevelCache()` method to your `IServiceCollection`."); } } } ================================================ FILE: src/EFSecondLevelCache.Core/ParallelExtensions.cs ================================================ using System; using System.Threading; namespace EFSecondLevelCache.Core { /// /// Reader writer locking utils /// public static class ParallelExtensions { /// /// Tries to enter the lock in read mode, with an optional integer time-out. /// public static void TryReadLocked(this ReaderWriterLockSlim readerWriterLock, Action action, int timeout = Timeout.Infinite) { if (!readerWriterLock.TryEnterReadLock(timeout)) { throw new TimeoutException(); } try { action(); } finally { readerWriterLock.ExitReadLock(); } } /// /// Tries to enter the lock in write mode, with an optional time-out. /// public static void TryWriteLocked(this ReaderWriterLockSlim readerWriterLock, Action action, int timeout = Timeout.Infinite) { if (!readerWriterLock.TryEnterWriteLock(timeout)) { throw new TimeoutException(); } try { action(); } finally { readerWriterLock.ExitWriteLock(); } } } } ================================================ FILE: src/EFSecondLevelCache.Core/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("EFSecondLevelCache.Core")] [assembly: AssemblyTrademark("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("755d6a18-e67a-4e14-8289-bd33a901c52b")] ================================================ FILE: src/EFSecondLevelCache.Core/XxHashUnsafe.cs ================================================  using System; namespace EFSecondLevelCache.Core { /// /// xxHash is an extremely fast non-cryptographic Hash algorithm, working at speeds close to RAM limits. /// http://code.google.com/p/xxhash/ /// public static class XxHashUnsafe { private const uint Prime1 = 2654435761U; private const uint Prime2 = 2246822519U; private const uint Prime3 = 3266489917U; private const uint Prime4 = 668265263U; private const int Prime5 = 0x165667b1; private const uint Seed = 0xc58f1a7b; /// /// Computes the xxHash of the input string. xxHash is an extremely fast non-cryptographic Hash algorithm. /// /// the input string /// xxHash public static unsafe UInt32 ComputeHash(string data) { fixed (char* input = data) { return hash((byte*)input, (uint)data.Length * sizeof(char), Seed); } } /// /// Computes the xxHash of the input byte array. xxHash is an extremely fast non-cryptographic Hash algorithm. /// /// the input byte array /// xxHash public static unsafe uint ComputeHash(byte[] data) { fixed (byte* input = &data[0]) { return hash(input, (uint)data.Length, Seed); } } /// /// Computes the xxHash of the input byte array. xxHash is an extremely fast non-cryptographic Hash algorithm. /// /// the input byte array /// start offset /// length /// initial seed /// xxHash public static unsafe uint ComputeHash(byte[] data, int offset, uint len, uint seed) { fixed (byte* input = &data[offset]) { return hash(input, len, seed); } } private unsafe static uint hash(byte* data, uint len, uint seed) { if (len < 16) return hashSmall(data, len, seed); var v1 = seed + Prime1; var v2 = v1 * Prime2 + len; var v3 = v2 * Prime3; var v4 = v3 * Prime4; var p = (uint*)data; var limit = (uint*)(data + len - 16); while (p < limit) { v1 += rotl32(v1, 13); v1 *= Prime1; v1 += *p; p++; v2 += rotl32(v2, 11); v2 *= Prime1; v2 += *p; p++; v3 += rotl32(v3, 17); v3 *= Prime1; v3 += *p; p++; v4 += rotl32(v4, 19); v4 *= Prime1; v4 += *p; p++; } p = limit; v1 += rotl32(v1, 17); v2 += rotl32(v2, 19); v3 += rotl32(v3, 13); v4 += rotl32(v4, 11); v1 *= Prime1; v2 *= Prime1; v3 *= Prime1; v4 *= Prime1; v1 += *p; p++; v2 += *p; p++; v3 += *p; p++; v4 += *p; v1 *= Prime2; v2 *= Prime2; v3 *= Prime2; v4 *= Prime2; v1 += rotl32(v1, 11); v2 += rotl32(v2, 17); v3 += rotl32(v3, 19); v4 += rotl32(v4, 13); v1 *= Prime3; v2 *= Prime3; v3 *= Prime3; v4 *= Prime3; var crc = v1 + rotl32(v2, 3) + rotl32(v3, 6) + rotl32(v4, 9); crc ^= crc >> 11; crc += (Prime4 + len) * Prime1; crc ^= crc >> 15; crc *= Prime2; crc ^= crc >> 13; return crc; } private unsafe static uint hashSmall(byte* data, uint len, uint seed) { var p = data; var bEnd = data + len; var limit = bEnd - 4; var idx = seed + Prime1; uint crc = Prime5; while (p < limit) { crc += (*(uint*)p) + idx; idx++; crc += rotl32(crc, 17) * Prime4; crc *= Prime1; p += 4; } while (p < bEnd) { crc += (*p) + idx; idx++; crc *= Prime1; p++; } crc += len; crc ^= crc >> 15; crc *= Prime2; crc ^= crc >> 13; crc *= Prime3; crc ^= crc >> 16; return crc; } private static UInt32 rotl32(UInt32 x, int r) { return (x << r) | (x >> (32 - r)); } } } ================================================ FILE: src/EFSecondLevelCache.Core/_0-restore.bat ================================================ rmdir /S /Q bin rmdir /S /Q obj dotnet restore pause ================================================ FILE: src/EFSecondLevelCache.Core/_1-dotnet_pack.bat ================================================ dotnet pack -c release pause ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/App_Data/.gitkeep.txt ================================================ ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Controllers/HomeController.cs ================================================ using System.Linq; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using Microsoft.AspNetCore.Mvc; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities; using EFSecondLevelCache.Core.Contracts; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using AutoMapper; using EFSecondLevelCache.Core.AspNetCoreSample.Models; using AutoMapper.QueryableExtensions; using EFSecondLevelCache.Core.AspNetCoreSample.Others; namespace EFSecondLevelCache.Core.AspNetCoreSample.Controllers { public class HomeController : Controller { private readonly SampleContext _context; private readonly IMapper _mapper; private static List _inMemoryPosts; public HomeController(SampleContext context, IMapper mapper) { _context = context; _mapper = mapper; cacheInMemory(); } private void cacheInMemory() { if (_inMemoryPosts == null) { _inMemoryPosts = _context.Set().AsNoTracking().ToList(); } } /// /// Get https://localhost:5001/home/RunInMemory /// public IActionResult RunInMemory() { var debugInfo = new EFCacheDebugInfo(); var post1 = _inMemoryPosts.AsQueryable() .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(debugInfo) .FirstOrDefault(); return Json(new { post1?.Title, debugInfo }); } public async Task Index() { var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(debugInfo) .FirstOrDefaultAsync(); return Json(new { post1.Title, debugInfo }); } public async Task TaskWhenAll() { var debugInfo = new EFCacheDebugInfo(); var task1 = _context.Set().Where(x => x.Id > 0).Cacheable(debugInfo).ToListAsync(); var results = await Task.WhenAll(task1); return Json(new { results, debugInfo }); } public async Task MapToDtoBefore() { var debugInfo = new EFCacheDebugInfo(); var posts = await _context.Set() .Where(x => x.Id > 0) .OrderBy(x => x.Id) .ProjectTo(configuration: _mapper.ConfigurationProvider) .Cacheable(debugInfo) .ToListAsync(); return Json(new { posts, debugInfo }); } public async Task MapToDtoAfter() { var debugInfo = new EFCacheDebugInfo(); var posts = await _context.Set() .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(debugInfo) .ProjectTo(configuration: _mapper.ConfigurationProvider) .ToListAsync(); return Json(new { posts, debugInfo }); } /// /// Get https://localhost:5001/home/WithSlidingExpiration /// public async Task WithSlidingExpiration() { var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(new EFCachePolicy(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(5)), debugInfo) .FirstOrDefaultAsync(); return Json(new { post1.Title, debugInfo }); } /// /// Get https://localhost:5001/home/WithAbsoluteExpiration /// public async Task WithAbsoluteExpiration() { var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5), debugInfo) .FirstOrDefaultAsync(); return Json(new { post1.Title, debugInfo }); } public async Task AsyncTest() { var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Posts .Where(x => x.Id > 0) .Cacheable(debugInfo) .FirstOrDefaultAsync(); return Json(new { post1.Title, debugInfo }); } public async Task CountTest() { var debugInfo = new EFCacheDebugInfo(); var count = await _context.Posts .Where(x => x.Id > 0) .Cacheable(debugInfo) .CountAsync(); return Json(new { count, debugInfo }); } public async Task CountWithParamsTest() { var debugInfo = new EFCacheDebugInfo(); var count = await _context.Posts .Cacheable(debugInfo) .CountAsync(x => x.Id > 0); return Json(new { count, debugInfo }); } public async Task CollectionsTest() { var collection1 = new[] { 1, 2, 3 }; var debugInfo1 = new EFCacheDebugInfo(); var post1 = await _context.Posts .Where(x => collection1.Contains(x.Id)) .Cacheable(debugInfo1) .FirstOrDefaultAsync(); var collection2 = new[] { 1, 2, 3, 4 }; var debugInfo2 = new EFCacheDebugInfo(); var post2 = await _context.Posts .Where(x => collection2.Contains(x.Id)) .Cacheable(debugInfo2) .FirstOrDefaultAsync(); return Json(new { post1.Title, post2.Id, debugInfo1, debugInfo2 }); } /// /// Get https://localhost:5001/home/StringEqualsTest /// public async Task StringEqualsTest() { var debugInfo = new EFCacheDebugInfo(); var rnd = new Random(); var value = rnd.Next(1, 1000000).ToString(); var post1 = await _context.Posts .Where(x => x.Title.Equals(value)) .Cacheable(debugInfo) .FirstOrDefaultAsync(); return Json(new { post1?.Title, debugInfo }); } /// /// Get https://localhost:5001/home/Issue36 /// public IActionResult Issue36() { User user1; const string user1Name = "User1"; if (!_context.Users.Any(user => user.Name == user1Name)) { user1 = new User { Name = user1Name }; user1 = _context.Users.Add(user1).Entity; } else { user1 = _context.Users.First(user => user.Name == user1Name); } var product = new Product { ProductName = "P981122", IsActive = true, Notes = "Notes ...", ProductNumber = "098112", User = user1 }; product = _context.Products.Add(product).Entity; _context.SaveChanges(); // 1st query, reading from db var debugInfo1 = new EFCacheDebugInfo(); var firstQueryResult = _context.Products .Cacheable(debugInfo1) .FirstOrDefault(p => p.ProductId == product.ProductId); var debugInfoWithWhere1 = new EFCacheDebugInfo(); var firstQueryWithWhereClauseResult = _context.Products.Where(p => p.ProductId == product.ProductId) .Cacheable(debugInfoWithWhere1) .FirstOrDefault(); // Delete it from db, invalidates the cache on SaveChanges _context.Products.Remove(product); _context.SaveChanges(); // same query, reading from 2nd level cache? No. var debugInfo2 = new EFCacheDebugInfo(); var secondQueryResult = _context.Products .Cacheable(debugInfo2) .FirstOrDefault(p => p.ProductId == product.ProductId); // same query, reading from 2nd level cache? No. var debugInfo3 = new EFCacheDebugInfo(); var thirdQueryResult = _context.Products.Where(p => p.ProductId == product.ProductId) .Cacheable(debugInfo3) .FirstOrDefault(); // retrieving it directly from database var p98 = _context.Products.FirstOrDefault(p => p.ProductId == product.ProductId); return Json(new { firstQueryResult, isFirstQueryCached = debugInfo1, firstQueryWithWhereClauseResult, isFirstQueryWithWhereClauseCached = debugInfoWithWhere1, secondQueryResult, isSecondQueryCached = debugInfo2, thirdQueryResult, isThirdQueryCached = debugInfo3, directlyFromDatabase = p98 }); } public IActionResult DynamicGetWithCacheableAtFirst() { var debugInfo = new EFCacheDebugInfo(); var users = _context.Users.DynamicGetWithCacheableAtFirst(debugInfo, x => x.Id > 0, x => x.Posts); return Json(new { users, debugInfo }); } public IActionResult DynamicGetWithCacheableAtEnd() { var debugInfo = new EFCacheDebugInfo(); var users = _context.Users.DynamicGetWithCacheableAtEnd(debugInfo, x => x.Id > 0, x => x.Posts); return Json(new { users, debugInfo }); // https://github.com/aspnet/EntityFrameworkCore/issues/12098 } public async Task FirstOrDefaultInline() { var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Where(x => x.Id == 1) .Cacheable(debugInfo) .FirstOrDefaultAsync(); return Json(new { post1.Title, debugInfo }); } public async Task FirstOrDefaultInlineAtTheEnd() { var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Cacheable(debugInfo) .FirstOrDefaultAsync(x => x.Id == 1); return Json(new { post1.Title, debugInfo }); } public async Task FirstOrDefaultWithParam() { var param1 = 1; var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Where(x => x.Id == param1) .Cacheable(debugInfo) .FirstOrDefaultAsync(); return Json(new { post1.Title, debugInfo }); } public async Task FirstOrDefaultAtTheEndWithParam() { var param1 = 1; var debugInfo = new EFCacheDebugInfo(); var post1 = await _context.Set() .Cacheable(debugInfo) .FirstOrDefaultAsync(x => x.Id == param1); return Json(new { post1.Title, debugInfo }); } // https://localhost:5001/home/FirstOrDefaultWithParams public async Task FirstOrDefaultWithParams() { var param1 = 1; var debugInfo1 = new EFCacheDebugInfo(); var param2 = param1; var post1 = await _context.Set() .Where(x => x.Id == param2) .Cacheable(debugInfo1) .FirstOrDefaultAsync(); param1 = 2; var debugInfo2 = new EFCacheDebugInfo(); var post2 = await _context.Set() .Where(x => x.Id == param1) .Cacheable(debugInfo2) .FirstOrDefaultAsync(); return Json(new { post1Title = post1.Title, debugInfo1, post2Title = post2.Title, debugInfo2 }); } // https://github.com/VahidN/EFSecondLevelCache.Core/issues/53 // https://localhost:5001/home/FirstOrDefaultWithParams2 public async Task FirstOrDefaultWithParams2() { var param1 = 1; var debugInfo1 = new EFCacheDebugInfo(); var post1 = await _context.GetFirstOrDefaultAsync(debugInfo1, x => x.Id == param1); param1 = 2; var debugInfo2 = new EFCacheDebugInfo(); var post2 = await _context.GetFirstOrDefaultAsync(debugInfo2, x => x.Id == param1); return Json(new { post1Title = post1.Title, debugInfo1, post2Title = post2.Title, debugInfo2 }); } public async Task TestEnumsWithParams() { var param1 = UserStatus.Active; var debugInfo = new EFCacheDebugInfo(); var user1 = await _context.Set() .Cacheable(debugInfo) .FirstOrDefaultAsync(x => x.UserStatus == param1); return Json(new { user1, debugInfo }); } public async Task TestInlineEnums() { var debugInfo = new EFCacheDebugInfo(); var user1 = await _context.Set() .Cacheable(debugInfo) .FirstOrDefaultAsync(x => x.UserStatus == UserStatus.Active); return Json(new { user1, debugInfo }); } public async Task TestEFCachedDbSet() { var users1 = await _context.CachedPosts.OrderByDescending(x => x.Id).ToListAsync(); var debugInfo2 = new EFCacheDebugInfo(); var users2 = await _context.Set().Cacheable(debugInfo2).OrderByDescending(x => x.Id).ToListAsync(); var debugInfo3 = new EFCacheDebugInfo(); var users3 = await _context.Set().OrderByDescending(x => x.Id).Cacheable(debugInfo3).ToListAsync(); return Json(new { users1, debugInfo2, debugInfo3 }); } // https://github.com/VahidN/EFSecondLevelCache.Core/issues/65 // https://localhost:5001/home/TestIncludes public async Task TestIncludes() { var debugInfo1 = new EFCacheDebugInfo(); var firstProductIncludeTags1 = await _context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .Cacheable(debugInfo1) .FirstOrDefaultAsync(); var debugInfo2 = new EFCacheDebugInfo(); var firstProductIncludeTags2 = await _context.Products .Cacheable(debugInfo2) .Include(x => x.TagProducts).ThenInclude(x => x.Tag) .FirstOrDefaultAsync(); return Json(new { productName1 = firstProductIncludeTags1.ProductName, productName2 = debugInfo1, firstProductIncludeTags2.ProductName, debugInfo2 }); } public async Task TestFind() { var debugInfo = new EFCacheDebugInfo(); var product1 = await _context.Products .Cacheable(debugInfo) .FindAsync(1); return Json(new { product1.ProductName, debugInfo }); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Entities/Post.cs ================================================ namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities { public class Post { public int Id { get; set; } public string Title { get; set; } public virtual User User { get; set; } public int UserId { get; set; } } public class Page : Post { } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Entities/Product.cs ================================================ using System.Collections.Generic; namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities { public class Product { public Product() { TagProducts = new HashSet(); } public int ProductId { get; set; } public string ProductNumber { get; set; } public string ProductName { get; set; } public string Notes { get; set; } public bool IsActive { get; set; } public virtual ICollection TagProducts { get; set; } public virtual User User { get; set; } public int UserId { get; set; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Entities/Tag.cs ================================================ using System.Collections.Generic; namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities { public class Tag { public Tag() { TagProducts = new HashSet(); } public int Id { get; set; } public string Name { get; set; } public virtual ICollection TagProducts { get; set; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Entities/TagProduct.cs ================================================ namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities { public class TagProduct { public int TagId { get; set; } public int ProductProductId { get; set; } public virtual Product Product { get; set; } public virtual Tag Tag { get; set; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Entities/User.cs ================================================ using System.Collections.Generic; namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities { public class User { public User() { Posts = new HashSet(); Products = new HashSet(); } public int Id { get; set; } public string Name { get; set; } public UserStatus UserStatus { get; set; } public virtual ICollection Posts { get; set; } public virtual ICollection Products { get; set; } } public enum UserStatus { Active, Disabled } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/SampleContext.cs ================================================ using System.Linq; using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer { public class SampleContext : DbContext { public virtual DbSet Posts { get; set; } public virtual DbSet Products { get; set; } public virtual DbSet TagProducts { get; set; } public virtual DbSet Tags { get; set; } public virtual DbSet Users { get; set; } public EFCachedDbSet CachedPosts => this.Set().Cacheable(); public SampleContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.HasIndex(e => e.UserId); entity.HasOne(d => d.User) .WithMany(p => p.Posts) .HasForeignKey(d => d.UserId); entity.HasDiscriminator("post_type") .HasValue("post_base") .HasValue("post_page"); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.ProductId); entity.HasIndex(e => e.ProductName) .IsUnique(); entity.HasIndex(e => e.UserId); entity.Property(e => e.ProductName) .IsRequired() .HasMaxLength(50); entity.Property(e => e.ProductNumber) .IsRequired() .HasMaxLength(30); entity.HasOne(d => d.User) .WithMany(p => p.Products) .HasForeignKey(d => d.UserId); }); modelBuilder.Entity(entity => { entity.HasKey(e => new { e.TagId, e.ProductProductId }); entity.HasIndex(e => e.ProductProductId); entity.HasIndex(e => e.TagId); entity.Property(e => e.TagId); entity.Property(e => e.ProductProductId); entity.HasOne(d => d.Product) .WithMany(p => p.TagProducts) .HasForeignKey(d => d.ProductProductId); entity.HasOne(d => d.Tag) .WithMany(p => p.TagProducts) .HasForeignKey(d => d.TagId); }); modelBuilder.Entity(entity => { entity.Property(e => e.Name).IsRequired(); }); } public override int SaveChanges() { var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; this.GetService().InvalidateCacheDependencies(changedEntityNames); return result; } public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChangesAsync(cancellationToken); this.ChangeTracker.AutoDetectChangesEnabled = true; this.GetService().InvalidateCacheDependencies(changedEntityNames); return result; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Utils/ApplicationDbContextSeedData.cs ================================================ using System.Linq; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities; using Microsoft.Extensions.DependencyInjection; namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Utils { public static class ApplicationDbContextSeedData { public static void SeedData(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService(); User user1; const string user1Name = "User1"; if (!context.Users.Any(user => user.Name == user1Name)) { user1 = new User { Name = user1Name }; user1 = context.Users.Add(user1).Entity; } else { user1 = context.Users.First(user => user.Name == user1Name); } const string product4Name = "Product4"; if (!context.Products.Any(product => product.ProductName == product4Name)) { var product4 = new Product { ProductName = product4Name, IsActive = false, Notes = "Notes ...", ProductNumber = "004", User = user1 }; product4 = context.Products.Add(product4).Entity; var tag4 = new Tag { Name = "Tag4" }; context.Tags.Add(tag4); var productTag = new TagProduct { Tag = tag4, Product = product4 }; context.TagProducts.Add(productTag); } const string product1Name = "Product1"; if (!context.Products.Any(product => product.ProductName == product1Name)) { var product1 = new Product { ProductName = product1Name, IsActive = true, Notes = "Notes ...", ProductNumber = "001", User = user1 }; product1 = context.Products.Add(product1).Entity; var tag1 = new Tag { Name = "Tag1" }; context.Tags.Add(tag1); var productTag = new TagProduct { Tag = tag1, Product = product1 }; context.TagProducts.Add(productTag); } const string product2Name = "Product2"; if (!context.Products.Any(product => product.ProductName == product2Name)) { var product2 = new Product { ProductName = product2Name, IsActive = true, Notes = "Notes ...", ProductNumber = "002", User = user1 }; product2 = context.Products.Add(product2).Entity; var tag2 = new Tag { Name = "Tag2" }; context.Tags.Add(tag2); var productTag = new TagProduct { Tag = tag2, Product = product2 }; context.TagProducts.Add(productTag); } const string product3Name = "Product3"; if (!context.Products.Any(product => product.ProductName == product3Name)) { var product3 = new Product { ProductName = product3Name, IsActive = true, Notes = "Notes ...", ProductNumber = "003", User = user1 }; product3 = context.Products.Add(product3).Entity; var tag3 = new Tag { Name = "Tag3" }; context.Tags.Add(tag3); var productTag = new TagProduct { Tag = tag3, Product = product3 }; context.TagProducts.Add(productTag); } const string post1Title = "Post1"; if (!context.Posts.Any(post => post.Title == post1Title)) { var page1 = new Page { Title = post1Title, User = user1 }; context.Posts.Add(page1); } const string post2Title = "Post2"; if (!context.Posts.Any(post => post.Title == post2Title)) { var page2 = new Page { Title = post2Title, User = user1 }; context.Posts.Add(page2); } context.SaveChanges(); } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/DataLayer/Utils/DBInitialization.cs ================================================ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Utils { public static class DbInitialization { public static void Initialize(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService(); // Applies any pending migrations for the context to the database. // Will create the database if it does not already exist. context.Database.Migrate(); } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/EFSecondLevelCache.Core.AspNetCoreSample.csproj ================================================  netcoreapp3.1 true EFSecondLevelCache.Core.AspNetCoreSample Exe EFSecondLevelCache.Core.AspNetCoreSample PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all anycpu ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Migrations/20191022095356_V2019_10_22_1323.Designer.cs ================================================ // using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EFSecondLevelCache.Core.AspNetCoreSample.Migrations { [DbContext(typeof(SampleContext))] [Migration("20191022095356_V2019_10_22_1323")] partial class V2019_10_22_1323 { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "3.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Post", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("Title") .HasColumnType("nvarchar(max)"); b.Property("UserId") .HasColumnType("int"); b.Property("post_type") .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); b.HasIndex("UserId"); b.ToTable("Posts"); b.HasDiscriminator("post_type").HasValue("post_base"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product", b => { b.Property("ProductId") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("IsActive") .HasColumnType("bit"); b.Property("Notes") .HasColumnType("nvarchar(max)"); b.Property("ProductName") .IsRequired() .HasColumnType("nvarchar(50)") .HasMaxLength(50); b.Property("ProductNumber") .IsRequired() .HasColumnType("nvarchar(30)") .HasMaxLength(30); b.Property("UserId") .HasColumnType("int"); b.HasKey("ProductId"); b.HasIndex("ProductName") .IsUnique(); b.HasIndex("UserId"); b.ToTable("Products"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Tag", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("Name") .HasColumnType("nvarchar(max)"); b.HasKey("Id"); b.ToTable("Tags"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.TagProduct", b => { b.Property("TagId") .HasColumnType("int"); b.Property("ProductProductId") .HasColumnType("int"); b.HasKey("TagId", "ProductProductId"); b.HasIndex("ProductProductId"); b.HasIndex("TagId"); b.ToTable("TagProducts"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("UserStatus") .HasColumnType("int"); b.HasKey("Id"); b.ToTable("Users"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Page", b => { b.HasBaseType("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Post"); b.HasDiscriminator().HasValue("post_page"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Post", b => { b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.User", "User") .WithMany("Posts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product", b => { b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.User", "User") .WithMany("Products") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.TagProduct", b => { b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product", "Product") .WithMany("TagProducts") .HasForeignKey("ProductProductId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Tag", "Tag") .WithMany("TagProducts") .HasForeignKey("TagId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Migrations/20191022095356_V2019_10_22_1323.cs ================================================ using Microsoft.EntityFrameworkCore.Migrations; namespace EFSecondLevelCache.Core.AspNetCoreSample.Migrations { public partial class V2019_10_22_1323 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Tags", columns: table => new { Id = table.Column(nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Name = table.Column(nullable: true) }, constraints: table => { table.PrimaryKey("PK_Tags", x => x.Id); }); migrationBuilder.CreateTable( name: "Users", columns: table => new { Id = table.Column(nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Name = table.Column(nullable: false), UserStatus = table.Column(nullable: false) }, constraints: table => { table.PrimaryKey("PK_Users", x => x.Id); }); migrationBuilder.CreateTable( name: "Posts", columns: table => new { Id = table.Column(nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Title = table.Column(nullable: true), UserId = table.Column(nullable: false), post_type = table.Column(nullable: false) }, constraints: table => { table.PrimaryKey("PK_Posts", x => x.Id); table.ForeignKey( name: "FK_Posts_Users_UserId", column: x => x.UserId, principalTable: "Users", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "Products", columns: table => new { ProductId = table.Column(nullable: false) .Annotation("SqlServer:Identity", "1, 1"), ProductNumber = table.Column(maxLength: 30, nullable: false), ProductName = table.Column(maxLength: 50, nullable: false), Notes = table.Column(nullable: true), IsActive = table.Column(nullable: false), UserId = table.Column(nullable: false) }, constraints: table => { table.PrimaryKey("PK_Products", x => x.ProductId); table.ForeignKey( name: "FK_Products_Users_UserId", column: x => x.UserId, principalTable: "Users", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateTable( name: "TagProducts", columns: table => new { TagId = table.Column(nullable: false), ProductProductId = table.Column(nullable: false) }, constraints: table => { table.PrimaryKey("PK_TagProducts", x => new { x.TagId, x.ProductProductId }); table.ForeignKey( name: "FK_TagProducts_Products_ProductProductId", column: x => x.ProductProductId, principalTable: "Products", principalColumn: "ProductId", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_TagProducts_Tags_TagId", column: x => x.TagId, principalTable: "Tags", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "IX_Posts_UserId", table: "Posts", column: "UserId"); migrationBuilder.CreateIndex( name: "IX_Products_ProductName", table: "Products", column: "ProductName", unique: true); migrationBuilder.CreateIndex( name: "IX_Products_UserId", table: "Products", column: "UserId"); migrationBuilder.CreateIndex( name: "IX_TagProducts_ProductProductId", table: "TagProducts", column: "ProductProductId"); migrationBuilder.CreateIndex( name: "IX_TagProducts_TagId", table: "TagProducts", column: "TagId"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "Posts"); migrationBuilder.DropTable( name: "TagProducts"); migrationBuilder.DropTable( name: "Products"); migrationBuilder.DropTable( name: "Tags"); migrationBuilder.DropTable( name: "Users"); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Migrations/SampleContextModelSnapshot.cs ================================================ // using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EFSecondLevelCache.Core.AspNetCoreSample.Migrations { [DbContext(typeof(SampleContext))] partial class SampleContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "3.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Post", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("Title") .HasColumnType("nvarchar(max)"); b.Property("UserId") .HasColumnType("int"); b.Property("post_type") .IsRequired() .HasColumnType("nvarchar(max)"); b.HasKey("Id"); b.HasIndex("UserId"); b.ToTable("Posts"); b.HasDiscriminator("post_type").HasValue("post_base"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product", b => { b.Property("ProductId") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("IsActive") .HasColumnType("bit"); b.Property("Notes") .HasColumnType("nvarchar(max)"); b.Property("ProductName") .IsRequired() .HasColumnType("nvarchar(50)") .HasMaxLength(50); b.Property("ProductNumber") .IsRequired() .HasColumnType("nvarchar(30)") .HasMaxLength(30); b.Property("UserId") .HasColumnType("int"); b.HasKey("ProductId"); b.HasIndex("ProductName") .IsUnique(); b.HasIndex("UserId"); b.ToTable("Products"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Tag", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("Name") .HasColumnType("nvarchar(max)"); b.HasKey("Id"); b.ToTable("Tags"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.TagProduct", b => { b.Property("TagId") .HasColumnType("int"); b.Property("ProductProductId") .HasColumnType("int"); b.HasKey("TagId", "ProductProductId"); b.HasIndex("ProductProductId"); b.HasIndex("TagId"); b.ToTable("TagProducts"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("UserStatus") .HasColumnType("int"); b.HasKey("Id"); b.ToTable("Users"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Page", b => { b.HasBaseType("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Post"); b.HasDiscriminator().HasValue("post_page"); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Post", b => { b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.User", "User") .WithMany("Posts") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product", b => { b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.User", "User") .WithMany("Products") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); modelBuilder.Entity("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.TagProduct", b => { b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product", "Product") .WithMany("TagProducts") .HasForeignKey("ProductProductId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.HasOne("EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Tag", "Tag") .WithMany("TagProducts") .HasForeignKey("TagId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); #pragma warning restore 612, 618 } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Models/PostDto.cs ================================================ namespace EFSecondLevelCache.Core.AspNetCoreSample.Models { public class PostDto { public string Title { get; set; } public string Author { get; set; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Others/TestUtils.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; namespace EFSecondLevelCache.Core.AspNetCoreSample.Others { public static class TestUtils { public static IEnumerable DynamicGetWithCacheableAtFirst( this IQueryable query, EFCacheDebugInfo debugInfo, Expression> filter = null, params Expression>[] include) where T : class { query = query.Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5), debugInfo); if (filter != null) query = query.Where(filter); if (include != null) { foreach (var includeProperty in include.ToList()) query = query.Include(includeProperty); } return query.ToList(); } public static IEnumerable DynamicGetWithCacheableAtEnd( this IQueryable query, EFCacheDebugInfo debugInfo, Expression> filter = null, params Expression>[] include) where T : class { if (filter != null) query = query.Where(filter); if (include != null) { foreach (var includeProperty in include.ToList()) query = query.Include(includeProperty); } query = query.Cacheable(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5), debugInfo); return query.ToList(); } public static async Task GetFirstOrDefaultAsync( this TDbContext dbContext, EFCacheDebugInfo eFCacheDebugInfo, Expression> predicate = null, Func, IOrderedQueryable> orderBy = null, Func, IIncludableQueryable> include = null, bool disableTracking = true, CancellationToken cancellationToken = default(CancellationToken)) where TEntity : class where TDbContext : DbContext { IQueryable query = dbContext.Set(); if (disableTracking) { query = query.AsNoTracking(); } if (include != null) { query = include(query); } if (predicate != null) { query = query.Where(predicate); } if (orderBy != null) { return await orderBy(query) .Cacheable(eFCacheDebugInfo) .FirstOrDefaultAsync(cancellationToken); } else { return await query .Cacheable(eFCacheDebugInfo) .FirstOrDefaultAsync(cancellationToken); } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Profiles/PostProfile.cs ================================================ using AutoMapper; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities; using EFSecondLevelCache.Core.AspNetCoreSample.Models; namespace EFSecondLevelCache.Core.AspNetCoreSample.Profiles { public class PostProfile : Profile { public PostProfile() { CreateMap() .ForMember(dest => dest.Author, opt => opt.MapFrom(src => $"{src.User.Name}")) .ForMember(dest => dest.Title, opt => opt.MapFrom(src => $"{src.Title}")); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Program.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace EFSecondLevelCache.Core.AspNetCoreSample { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:19452", "sslPort": 44364 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "MVCFinal3": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/Startup.cs ================================================ using System; using AutoMapper; using CacheManager.Core; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using EFSecondLevelCache.Core.AspNetCoreSample.Profiles; using System.Reflection; using Microsoft.Extensions.Hosting; namespace EFSecondLevelCache.Core.AspNetCoreSample { public class Startup { private readonly string _contentRootPath; public Startup(IConfiguration configuration, IWebHostEnvironment env) { _contentRootPath = env.ContentRootPath; Configuration = configuration; } public IConfiguration Configuration { get; } public void Configure( IApplicationBuilder app, IWebHostEnvironment env, IServiceScopeFactory scopeFactory) { //app.UseBlockingDetection(); scopeFactory.Initialize(); scopeFactory.SeedData(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } public void ConfigureServices(IServiceCollection services) { services.AddEFSecondLevelCache(); // addInMemoryCacheServiceProvider(services); addRedisCacheServiceProvider(services); services.AddDbContext(optionsBuilder => { var useInMemoryDatabase = Configuration["UseInMemoryDatabase"].Equals("true", StringComparison.OrdinalIgnoreCase); if (useInMemoryDatabase) { optionsBuilder.UseInMemoryDatabase("TestDb"); } else { var connectionString = Configuration["ConnectionStrings:ApplicationDbContextConnection"]; if (connectionString.Contains("%CONTENTROOTPATH%")) { connectionString = connectionString.Replace("%CONTENTROOTPATH%", _contentRootPath); } optionsBuilder.UseSqlServer( connectionString , serverDbContextOptionsBuilder => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; serverDbContextOptionsBuilder.CommandTimeout(minutes); }); optionsBuilder.EnableSensitiveDataLogging(); optionsBuilder.ConfigureWarnings(w => { }); } }); services.AddAutoMapper(typeof(PostProfile).GetTypeInfo().Assembly); services.AddControllersWithViews(); } private static void addInMemoryCacheServiceProvider(IServiceCollection services) { var jss = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer(serializationSettings: jss, deserializationSettings: jss) .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); } private static void addRedisCacheServiceProvider(IServiceCollection services) { var jss = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; const string redisConfigurationKey = "redis"; services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer(serializationSettings: jss, deserializationSettings: jss) .WithUpdateMode(CacheUpdateMode.Up) .WithRedisConfiguration(redisConfigurationKey, config => { config.WithAllowAdmin() .WithDatabase(0) .WithEndpoint("localhost", 6379) // Enables keyspace notifications to react on eviction/expiration of items. // Make sure that all servers are configured correctly and 'notify-keyspace-events' is at least set to 'Exe', otherwise CacheManager will not retrieve any events. // See https://redis.io/topics/notifications#configuration for configuration details. .EnableKeyspaceEvents(); }) .WithMaxRetries(100) .WithRetryTimeout(50) .WithRedisCacheHandle(redisConfigurationKey) .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .Build()); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/_0-restore.bat ================================================ rmdir /S /Q bin rmdir /S /Q obj dotnet restore pause ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/_1-dotnet_run.bat ================================================ dotnet watch run ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/_update_db.bat ================================================ For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b) For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b) echo %mydate%_%mytime% dotnet ef --configuration Release migrations add V%mydate%_%mytime% dotnet ef --configuration Release database update pause ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/appsettings.json ================================================ { "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "ConnectionStrings": { "ApplicationDbContextConnection": "Server=(localdb)\\mssqllocaldb;Initial Catalog=EFSecondLevelCacheCore2019;AttachDBFilename=%CONTENTROOTPATH%\\App_Data\\EFSecondLevelCacheCore2019.mdf;Trusted_Connection=True;" }, "UseInMemoryDatabase": false } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/web.config ================================================  ================================================ FILE: src/Tests/EFSecondLevelCache.Core.AspNetCoreSample/wwwroot/App_Data/.gitkeep ================================================ ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/App.config ================================================  ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/DataLayer/ConfigureServices.cs ================================================ using System; using CacheManager.Core; using Microsoft.Extensions.DependencyInjection; using System.Threading; using EFSecondLevelCache.Core.Contracts; namespace EFSecondLevelCache.Core.NET46Sample.DataLayer { public static class ConfigureServices { private static readonly Lazy _serviceProviderBuilder = new Lazy(getServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); /// /// A lazy loaded thread-safe singleton /// public static IServiceProvider Instance { get; } = _serviceProviderBuilder.Value; public static IEFCacheServiceProvider GetEFCacheServiceProvider() { return Instance.GetRequiredService(); } private static IServiceProvider getServiceProvider() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext(ServiceLifetime.Scoped); services.AddEFSecondLevelCache(); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer() .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); var serviceProvider = services.BuildServiceProvider(); return serviceProvider; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/DataLayer/Entities/Post.cs ================================================ namespace EFSecondLevelCache.Core.NET46Sample.DataLayer.Entities { public class Post { public int Id { get; set; } public string Title { get; set; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/DataLayer/SampleContext.cs ================================================ using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core.Contracts; using EFSecondLevelCache.Core.NET46Sample.DataLayer.Entities; using Microsoft.EntityFrameworkCore; namespace EFSecondLevelCache.Core.NET46Sample.DataLayer { public class SampleContext : DbContext { private static readonly IEFCacheServiceProvider _efCacheServiceProvider = ConfigureServices.GetEFCacheServiceProvider(); public virtual DbSet Posts { get; set; } public override int SaveChanges() { this.ChangeTracker.DetectChanges(); var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; _efCacheServiceProvider.InvalidateCacheDependencies(changedEntityNames); return result; } public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { this.ChangeTracker.DetectChanges(); var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChangesAsync(cancellationToken); this.ChangeTracker.AutoDetectChangesEnabled = true; _efCacheServiceProvider.InvalidateCacheDependencies(changedEntityNames); return result; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseInMemoryDatabase("TestDb"); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample.csproj ================================================  Debug AnyCPU {CEE7F26B-7A7E-4021-A37F-F9625EAF85CE} Exe Properties EFSecondLevelCache.Core.NET46Sample EFSecondLevelCache.Core.NET46Sample v4.6 512 true AnyCPU true full false bin\Debug\ DEBUG;TRACE prompt 4 AnyCPU pdbonly true bin\Release\ TRACE prompt 4 ..\..\..\..\packages\CacheManager.Core.1.0.0\lib\net45\CacheManager.Core.dll ..\..\..\..\packages\CacheManager.Microsoft.Extensions.Caching.Memory.1.0.0\lib\net451\CacheManager.Microsoft.Extensions.Caching.Memory.dll ..\..\..\..\packages\CacheManager.Serialization.Json.1.0.0\lib\net45\CacheManager.Serialization.Json.dll ..\..\..\..\packages\CacheManager.StackExchange.Redis.1.0.0\lib\net45\CacheManager.StackExchange.Redis.dll ..\..\..\..\packages\EFSecondLevelCache.Core.1.0.4\lib\netstandard1.3\EFSecondLevelCache.Core.dll ..\..\..\..\packages\Microsoft.AspNetCore.Http.Abstractions.1.1.1\lib\net451\Microsoft.AspNetCore.Http.Abstractions.dll ..\..\..\..\packages\Microsoft.AspNetCore.Http.Features.1.1.1\lib\net451\Microsoft.AspNetCore.Http.Features.dll ..\..\..\..\packages\Microsoft.EntityFrameworkCore.1.1.1\lib\net451\Microsoft.EntityFrameworkCore.dll ..\..\..\..\packages\Microsoft.EntityFrameworkCore.InMemory.1.1.1\lib\net451\Microsoft.EntityFrameworkCore.InMemory.dll ..\..\..\..\packages\Microsoft.EntityFrameworkCore.Relational.1.1.1\lib\net451\Microsoft.EntityFrameworkCore.Relational.dll ..\..\..\..\packages\Microsoft.Extensions.Caching.Abstractions.1.1.1\lib\netstandard1.0\Microsoft.Extensions.Caching.Abstractions.dll ..\..\..\..\packages\Microsoft.Extensions.Caching.Memory.1.1.1\lib\net451\Microsoft.Extensions.Caching.Memory.dll ..\..\..\..\packages\Microsoft.Extensions.DependencyInjection.1.1.0\lib\netstandard1.1\Microsoft.Extensions.DependencyInjection.dll True ..\..\..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.1.1.0\lib\netstandard1.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll True ..\..\..\..\packages\Microsoft.Extensions.Logging.1.1.1\lib\netstandard1.1\Microsoft.Extensions.Logging.dll ..\..\..\..\packages\Microsoft.Extensions.Logging.Abstractions.1.1.1\lib\netstandard1.1\Microsoft.Extensions.Logging.Abstractions.dll ..\..\..\..\packages\Microsoft.Extensions.Options.1.1.1\lib\netstandard1.0\Microsoft.Extensions.Options.dll ..\..\..\..\packages\Microsoft.Extensions.Primitives.1.1.0\lib\netstandard1.0\Microsoft.Extensions.Primitives.dll True ..\..\..\..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll True ..\..\..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll ..\..\..\..\packages\Remotion.Linq.2.1.1\lib\net45\Remotion.Linq.dll True ..\..\..\..\packages\StackExchange.Redis.StrongName.1.2.1\lib\net46\StackExchange.Redis.StrongName.dll ..\..\..\..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll True ..\..\..\..\packages\System.Collections.Immutable.1.3.1\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll True ..\..\..\..\packages\System.Console.4.3.0\lib\net46\System.Console.dll True ..\..\..\..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll True ..\..\..\..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll True ..\..\..\..\packages\System.Interactive.Async.3.1.1\lib\net46\System.Interactive.Async.dll ..\..\..\..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll True ..\..\..\..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll True ..\..\..\..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll True ..\..\..\..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll True ..\..\..\..\packages\System.Net.Http.4.3.1\lib\net46\System.Net.Http.dll ..\..\..\..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll True ..\..\..\..\packages\System.Reflection.TypeExtensions.4.3.0\lib\net46\System.Reflection.TypeExtensions.dll True ..\..\..\..\packages\System.Runtime.CompilerServices.Unsafe.4.3.0\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll True ..\..\..\..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll True ..\..\..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net46\System.Security.Cryptography.Algorithms.dll True ..\..\..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll True ..\..\..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll True ..\..\..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net46\System.Security.Cryptography.X509Certificates.dll True ..\..\..\..\packages\System.Text.Encodings.Web.4.3.0\lib\netstandard1.0\System.Text.Encodings.Web.dll True ..\..\..\..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll True ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/Program.cs ================================================ using System; using System.Linq; using EFSecondLevelCache.Core.NET46Sample.DataLayer; using EFSecondLevelCache.Core.NET46Sample.DataLayer.Entities; namespace EFSecondLevelCache.Core.NET46Sample { class Program { static void Main(string[] args) { using (var context = new SampleContext()) { context.Posts.Add(new Post { Title = "Title 1" }); context.SaveChanges(); var posts = context.Posts.Cacheable().ToList(); Console.WriteLine(posts.First().Title); } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("EFSecondLevelCache.Core.NET46Sample")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("EFSecondLevelCache.Core.NET46Sample")] [assembly: AssemblyCopyright("Copyright © 2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("cee7f26b-7a7e-4021-a37f-f9625eaf85ce")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: src/Tests/EFSecondLevelCache.Core.NET46Sample/EFSecondLevelCache.Core.NET46Sample/packages.config ================================================  ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/BenchmarkTests.cs ================================================ using System; using System.Linq; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; using Microsoft.Extensions.DependencyInjection; namespace EFSecondLevelCache.Core.PerformanceTests { public class BenchmarkTests { private int _count; [GlobalSetup] public void Setup() { Console.WriteLine("SetupDatabase"); SetupDatabase(); } private static void SetupDatabase() { using (var serviceScope = TestsServiceProvider.WithJsonSerializerInstance.GetRequiredService().CreateScope()) { using (var db = serviceScope.ServiceProvider.GetRequiredService()) { if (db.Database.EnsureCreated()) { var student1 = new Student { Name = "user 1" }; db.Students.Add(student1); var student2 = new Student { Name = "user 2" }; db.Students.Add(student2); var student3 = new Student { Name = "user 3" }; db.Students.Add(student3); db.SaveChanges(); } } } } [Benchmark(Baseline = true)] public void RunQueryDirectly() { using (var serviceScope = TestsServiceProvider.WithJsonSerializerInstance.GetRequiredService().CreateScope()) { using (var db = serviceScope.ServiceProvider.GetRequiredService()) { var students = db.Students.Where(x => x.Id > 0).ToList(); _count = students.Count; } } } [Benchmark] public void RunCacheableQueryWithJsonSerializer() { using (var serviceScope = TestsServiceProvider.WithJsonSerializerInstance.GetRequiredService().CreateScope()) { using (var db = serviceScope.ServiceProvider.GetRequiredService()) { var students = db.Students.Where(x => x.Id > 0).Cacheable().ToList(); _count = students.Count; } } } [Benchmark] public void RunCacheableQueryWithGzJsonSerializer() { using (var serviceScope = TestsServiceProvider.WithGzJsonSerializerInstance.GetRequiredService().CreateScope()) { using (var db = serviceScope.ServiceProvider.GetRequiredService()) { var students = db.Students.Where(x => x.Id > 0).Cacheable().ToList(); _count = students.Count; } } } [Benchmark] public void RunCacheableQueryWithDictionaryHandle() { using (var serviceScope = TestsServiceProvider.WithDictionaryHandleInstance.GetRequiredService().CreateScope()) { using (var db = serviceScope.ServiceProvider.GetRequiredService()) { var students = db.Students.Where(x => x.Id > 0).Cacheable().ToList(); _count = students.Count; } } } [Benchmark] public void RunCacheableQueryWithMicrosoftMemoryCache() { using (var serviceScope = TestsServiceProvider.WithMicrosoftMemoryCacheInstance.GetRequiredService().CreateScope()) { using (var db = serviceScope.ServiceProvider.GetRequiredService()) { var students = db.Students.Where(x => x.Id > 0).Cacheable().ToList(); _count = students.Count; } } } [GlobalCleanup] public void GlobalCleanup() { Console.WriteLine($"_count: {_count}"); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/EFSecondLevelCache.Core.PerformanceTests.csproj ================================================ Exe netcoreapp3.1 ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/Program.cs ================================================ using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Horology; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; namespace EFSecondLevelCache.Core.PerformanceTests { class Program { static void Main(string[] args) { var config = ManualConfig.Create(DefaultConfig.Instance) .With(BenchmarkDotNet.Analysers.EnvironmentAnalyser.Default) .With(BenchmarkDotNet.Exporters.MarkdownExporter.GitHub) .With(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default) .With(StatisticColumn.Mean) .With(StatisticColumn.Median) .With(StatisticColumn.StdDev) .With(StatisticColumn.OperationsPerSecond) .With(BaselineRatioColumn.RatioMean) .With(RankColumn.Arabic) .With(Job.Core .WithIterationCount(10) .WithInvocationCount(16) .WithIterationTime(TimeInterval.FromSeconds(10)) .WithWarmupCount(4) .WithLaunchCount(1)); BenchmarkRunner.Run(config); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/SampleContext.cs ================================================ using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore.Infrastructure; namespace EFSecondLevelCache.Core.PerformanceTests { public class Student { public int Id { get; set; } public string Name { get; set; } } public class SampleContext : DbContext { public virtual DbSet Students { get; set; } public SampleContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Name) .IsRequired() .HasMaxLength(450); }); } public override int SaveChanges() { var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; this.GetService().InvalidateCacheDependencies(changedEntityNames); return result; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/TestsServiceProvider.cs ================================================ using System; using System.IO; using CacheManager.Core; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System.Threading; using Newtonsoft.Json; namespace EFSecondLevelCache.Core.PerformanceTests { /// /// A lazy loaded thread-safe singleton /// public static class TestsServiceProvider { private static readonly Lazy _jsonSerializerProviderBuilder = new Lazy(getWithJsonSerializerServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy _gzJsonSerializerProviderBuilder = new Lazy(getWithGzJsonSerializerServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy _dictionaryHandleProviderBuilder = new Lazy(getWithDictionaryHandleServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); private static readonly Lazy _microsoftMemoryCacheProviderBuilder = new Lazy(getWithMicrosoftMemoryCacheHandleServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); public static IServiceProvider WithJsonSerializerInstance { get; } = _jsonSerializerProviderBuilder.Value; public static IServiceProvider WithGzJsonSerializerInstance { get; } = _gzJsonSerializerProviderBuilder.Value; public static IServiceProvider WithDictionaryHandleInstance { get; } = _dictionaryHandleProviderBuilder.Value; public static IServiceProvider WithMicrosoftMemoryCacheInstance { get; } = _microsoftMemoryCacheProviderBuilder.Value; private static IServiceProvider getWithJsonSerializerServiceProvider() { return createServiceProvider(sc => addJsonSerializer(sc)); } private static IServiceProvider getWithGzJsonSerializerServiceProvider() { return createServiceProvider(sc => addGzJsonSerializer(sc)); } private static IServiceProvider getWithDictionaryHandleServiceProvider() { return createServiceProvider(sc => addDictionaryHandle(sc)); } private static IServiceProvider getWithMicrosoftMemoryCacheHandleServiceProvider() { return createServiceProvider(sc => addMicrosoftMemoryCacheHandle(sc)); } private static void addJsonSerializer(ServiceCollection services) { var jss = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer(serializationSettings: jss, deserializationSettings: jss) .WithUpdateMode(CacheUpdateMode.Up) .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); } private static void addGzJsonSerializer(ServiceCollection services) { var jss = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithGzJsonSerializer(serializationSettings: jss, deserializationSettings: jss) .WithUpdateMode(CacheUpdateMode.Up) .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache2") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); } private static void addDictionaryHandle(ServiceCollection services) { services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithDictionaryHandle() .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); } private static void addMicrosoftMemoryCacheHandle(ServiceCollection services) { services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithMicrosoftMemoryCacheHandle() .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); } private static IServiceProvider createServiceProvider(Action config) { var services = new ServiceCollection(); services.AddEFSecondLevelCache(); config(services); var connectionString = getConnectionString(); services.AddDbContext(optionsBuilder => optionsBuilder.UseSqlServer(connectionString)); return services.BuildServiceProvider(); } private static string getConnectionString() { var appPath = Environment.CurrentDirectory.Split(new[] { "bin" }, StringSplitOptions.None); var appDataDir = Path.Combine(appPath[0], "app_data"); if (!Directory.Exists(appDataDir)) { Directory.CreateDirectory(appDataDir); } var connectionString = "Server=(localdb)\\mssqllocaldb;Initial Catalog=EFSecondLevelCacheCore.Perf.Test;AttachDBFilename=|DataDirectory|\\EFSecondLevelCacheCore.Perf.Test.mdf;Trusted_Connection=True;" .Replace("|DataDirectory|", appDataDir); Console.WriteLine($"Using {connectionString}"); return connectionString; } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/_0-restore.bat ================================================ rmdir /S /Q bin rmdir /S /Q obj dotnet restore pause ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/_1-dotnet_run.bat ================================================ dotnet watch run -c Release ================================================ FILE: src/Tests/EFSecondLevelCache.Core.PerformanceTests/app_data/.gitkeep ================================================ ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/EFCacheServiceProviderTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities; using EFSecondLevelCache.Core.Contracts; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFSecondLevelCache.Core.Tests { [TestClass] public class EFCacheServiceProviderTests { private readonly IEFCacheServiceProvider _cacheService; public EFCacheServiceProviderTests() { _cacheService = TestsBase.GetInMemoryCacheServiceProvider(); //_cacheService = TestsBase.GetRedisCacheServiceProvider(); } [TestInitialize] public void ClearEFGlobalCacheBeforeEachTest() { _cacheService.ClearAllCachedEntries(); } [TestMethod] public void TestCacheInvalidationWithTwoRoots() { _cacheService.InsertValue("EF_key1", "value1", new HashSet { "entity1.model", "entity2.model" }, null); _cacheService.InsertValue("EF_key2", "value2", new HashSet { "entity1.model", "entity2.model" }, null); var value1 = _cacheService.GetValue("EF_key1"); Assert.IsNotNull(value1); var value2 = _cacheService.GetValue("EF_key2"); Assert.IsNotNull(value2); _cacheService.InvalidateCacheDependencies(new[] { "entity2.model" }); value1 = _cacheService.GetValue("EF_key1"); Assert.IsNull(value1); value2 = _cacheService.GetValue("EF_key2"); Assert.IsNull(value2); } [TestMethod] public void TestCacheInvalidationWithOneRoot() { _cacheService.InsertValue("EF_key1", "value1", new HashSet { "entity1" }, null); _cacheService.InsertValue("EF_key2", "value2", new HashSet { "entity1" }, null); var value1 = _cacheService.GetValue("EF_key1"); Assert.IsNotNull(value1); var value2 = _cacheService.GetValue("EF_key2"); Assert.IsNotNull(value2); _cacheService.InvalidateCacheDependencies(new[] { "entity1" }); value1 = _cacheService.GetValue("EF_key1"); Assert.IsNull(value1); value2 = _cacheService.GetValue("EF_key2"); Assert.IsNull(value2); } [TestMethod] public void TestObjectCacheInvalidationWithOneRoot() { const string rootCacheKey = "EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities.Product"; _cacheService.InvalidateCacheDependencies(new string[] { rootCacheKey }); var val11888622 = _cacheService.GetValue("11888622"); Assert.IsNull(val11888622); _cacheService.InsertValue("11888622", new Product { ProductId = 5041 }, new HashSet { rootCacheKey }, null); var val44513A63 = _cacheService.GetValue("44513A63"); Assert.IsNull(val44513A63); _cacheService.InsertValue("44513A63", new Product { ProductId = 5041 }, new HashSet { rootCacheKey }, null); _cacheService.InvalidateCacheDependencies(new string[] { rootCacheKey }); val11888622 = _cacheService.GetValue("11888622"); Assert.IsNull(val11888622); val44513A63 = _cacheService.GetValue("44513A63"); Assert.IsNull(val44513A63); } [TestMethod] public void TestCacheInvalidationWithSimilarRoots() { _cacheService.InsertValue("EF_key1", "value1", new HashSet { "entity1", "entity2" }, null); _cacheService.InsertValue("EF_key2", "value2", new HashSet { "entity2" }, null); var value1 = _cacheService.GetValue("EF_key1"); Assert.IsNotNull(value1); var value2 = _cacheService.GetValue("EF_key2"); Assert.IsNotNull(value2); _cacheService.InvalidateCacheDependencies(new[] { "entity2" }); value1 = _cacheService.GetValue("EF_key1"); Assert.IsNull(value1); value2 = _cacheService.GetValue("EF_key2"); Assert.IsNull(value2); } [TestMethod] public void TestInsertingNullValues() { _cacheService.InsertValue("EF_key1", null, new HashSet { "entity1", "entity2" }, null); var value1 = _cacheService.GetValue("EF_key1"); Assert.IsTrue(Equals(value1, _cacheService.NullObject), $"value1 is `{value1}`"); } [TestMethod] public void TestParallelInsertsAndRemoves() { var tests = new List(); for (var i = 0; i < 4000; i++) { var i1 = i; tests.Add(() => _cacheService.InsertValue($"EF_key{i1}", i1, new HashSet { "entity1", "entity2" }, null)); } for (var i = 0; i < 400; i++) { if (i % 2 == 0) { tests.Add(() => _cacheService.InvalidateCacheDependencies(new[] { "entity1" })); } else { tests.Add(() => _cacheService.InvalidateCacheDependencies(new[] { "entity2" })); } } var rnd = new Random(); Parallel.Invoke(tests.OrderBy(a => rnd.Next()).ToArray()); var value1 = _cacheService.GetValue("EF_key1"); Assert.IsNull(value1); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/EFCachedQueryProviderAsyncTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFSecondLevelCache.Core.Tests { [TestClass] public class EFCachedQueryProviderAsyncTests { private readonly IServiceProvider _serviceProvider; public EFCachedQueryProviderAsyncTests() { _serviceProvider = TestsBase.GetServiceProvider(); } [TestInitialize] public void ClearEFGlobalCacheBeforeEachTest() { _serviceProvider.GetRequiredService().ClearAllCachedEntries(); } [TestMethod] public async Task TestSecondLevelCacheUsingAsyncMethodsDoesNotHitTheDatabase() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("1st async query, reading from db"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToListAsync(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); var hash1 = debugInfo1.EFCacheKey.KeyHash; Console.WriteLine("same async query, reading from 2nd level cache"); var debugInfo2 = new EFCacheDebugInfo(); var list2 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToListAsync(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); var hash2 = debugInfo2.EFCacheKey.KeyHash; Console.WriteLine("same async query, reading from 2nd level cache."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToListAsync(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); var hash3 = debugInfo3.EFCacheKey.KeyHash; Assert.AreEqual(hash1, hash2); Assert.AreEqual(hash2, hash3); Console.WriteLine("different async query, reading from db."); var debugInfo4 = new EFCacheDebugInfo(); var list4 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo4, _serviceProvider) .ToListAsync(); Assert.AreEqual(false, debugInfo4.IsCacheHit); Assert.IsTrue(list4.Any()); var hash4 = debugInfo4.EFCacheKey.KeyHash; Assert.AreNotSame(hash3, hash4); Console.WriteLine("different async query, reading from db."); var debugInfo5 = new EFCacheDebugInfo(); var product1 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo5, _serviceProvider) .FirstOrDefaultAsync(); Assert.AreEqual(false, debugInfo5.IsCacheHit); Assert.IsNotNull(product1); var hash5 = debugInfo5.EFCacheKey.KeyHash; Assert.AreNotSame(hash4, hash5); } } } [TestMethod] public async Task TestSecondLevelCacheUsingDifferentAsyncMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product3"; Console.WriteLine("ToListAsync"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToListAsync(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("CountAsync"); var debugInfo2 = new EFCacheDebugInfo(); var count = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .CountAsync(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(count > 0); Console.WriteLine("FirstOrDefaultAsync"); var debugInfo3 = new EFCacheDebugInfo(); var product1 = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .FirstOrDefaultAsync(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsTrue(product1 != null); Console.WriteLine("AnyAsync"); var debugInfo4 = new EFCacheDebugInfo(); var any = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo4, _serviceProvider) .AnyAsync(); Assert.AreEqual(false, debugInfo4.IsCacheHit); Assert.IsTrue(any); Console.WriteLine("SumAsync"); var debugInfo5 = new EFCacheDebugInfo(); var sum = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo5, _serviceProvider) .SumAsync(x => x.ProductId); Assert.AreEqual(false, debugInfo5.IsCacheHit); Assert.IsTrue(sum > 0); } } } [TestMethod] public async Task TestSecondLevelCacheUsingTwoCountAsyncMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product2"; Console.WriteLine("Count 1, From DB"); var debugInfo2 = new EFCacheDebugInfo(); var count = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .CountAsync(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(count > 0); Console.WriteLine("Count 2, Reading from cache"); var debugInfo3 = new EFCacheDebugInfo(); count = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .CountAsync(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(count > 0); } } } [TestMethod] public async Task TestSecondLevelCacheUsingFindAsyncMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var debugInfo = new EFCacheDebugInfo(); var product1 = await context.Products .Cacheable(debugInfo, _serviceProvider) .FindAsync(1); Assert.AreEqual(false, debugInfo.IsCacheHit); Assert.IsTrue(product1 != null); var debugInfo2 = new EFCacheDebugInfo(); product1 = await context.Products .Cacheable(debugInfo2, _serviceProvider) .FindAsync(1); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(product1 != null); } } } [TestMethod] [Ignore] public void TestParallelAsyncCalls() { var serviceScope = _serviceProvider.GetRequiredService().CreateScope(); var context = serviceScope.ServiceProvider.GetRequiredService(); var tests = new List(); for (var i = 0; i < 4000; i++) { var i1 = i.ToString(); tests.Add(async () => { var count = await context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive && product.ProductName == i1) .Cacheable(_serviceProvider) .CountAsync(); }); } var rnd = new Random(); Parallel.Invoke(tests.OrderBy(a => rnd.Next()).ToArray()); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/EFCachedQueryProviderBasicTests.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; using AutoMapper.QueryableExtensions; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using EFSecondLevelCache.Core.AspNetCoreSample.Models; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFSecondLevelCache.Core.Tests { [TestClass] public class EFCachedQueryProviderBasicTests { private readonly IServiceProvider _serviceProvider; public EFCachedQueryProviderBasicTests() { _serviceProvider = TestsBase.GetServiceProvider(); } [TestInitialize] public void ClearEFGlobalCacheBeforeEachTest() { _serviceProvider.GetRequiredService().ClearAllCachedEntries(); } [TestMethod] public void TestIncludeMethodAffectsKeyCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("a normal query"); var product1IncludeTags = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag).FirstOrDefault(); Assert.IsNotNull(product1IncludeTags); Console.WriteLine("1st query using Include method."); var debugInfo1 = new EFCacheDebugInfo(); var firstProductIncludeTags = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .Cacheable(debugInfo1, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProductIncludeTags); Assert.AreEqual(false, debugInfo1.IsCacheHit); var hash1 = debugInfo1.EFCacheKey.KeyHash; var cacheDependencies1 = debugInfo1.EFCacheKey.CacheDependencies; Console.WriteLine( @"2nd query looks the same, but it doesn't have the Include method, so it shouldn't produce the same queryKeyHash. This was the problem with just parsing the LINQ expression, without considering the produced SQL."); var debugInfo2 = new EFCacheDebugInfo(); var firstProduct = context.Products.Cacheable(debugInfo2, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProduct); Assert.AreEqual(false, debugInfo2.IsCacheHit); var hash2 = debugInfo2.EFCacheKey.KeyHash; var cacheDependencies2 = debugInfo2.EFCacheKey.CacheDependencies; Assert.AreNotEqual(hash1, hash2); Assert.AreNotEqual(cacheDependencies1, cacheDependencies2); } } } [TestMethod] public void TestQueriesUsingDifferentParameterValuesWillNotUseTheCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st query, reading from db."); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive && product.ProductName == "Product1") .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNotNull(list1); Console.WriteLine("2nd query, reading from db."); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == false && product.ProductName == "Product1") .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsNotNull(list2); Console.WriteLine("third query, reading from db."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == false && product.ProductName == "Product2") .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsNotNull(list3); Console.WriteLine("4th query, same as third one, reading from cache."); var debugInfo4 = new EFCacheDebugInfo(); var list4 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == false && product.ProductName == "Product2") .Cacheable(debugInfo4, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo4.IsCacheHit); Assert.IsNotNull(list4); } } } [TestMethod] public void TestSecondLevelCacheCreatesTheCommandTreeAfterCallingTheSameNormalQuery() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product3"; Console.WriteLine("1st normal query, reading from db."); var list1 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .ToList(); Assert.IsTrue(list1.Any()); Console.WriteLine("same query as Cacheable, reading from db."); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); var hash2 = debugInfo2.EFCacheKey.KeyHash; Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); var hash3 = debugInfo3.EFCacheKey.KeyHash; Assert.AreEqual(hash2, hash3); } } } [TestMethod] public void TestSecondLevelCacheDoesNotHitTheDatabase() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("1st query, reading from db."); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); var hash1 = debugInfo1.EFCacheKey.KeyHash; Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); var hash2 = debugInfo2.EFCacheKey.KeyHash; Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); var hash3 = debugInfo3.EFCacheKey.KeyHash; Assert.AreEqual(hash1, hash2); Assert.AreEqual(hash2, hash3); Console.WriteLine("different query, reading from db."); var debugInfo4 = new EFCacheDebugInfo(); var list4 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo4, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo4.IsCacheHit); Assert.IsTrue(list4.Any()); var hash4 = debugInfo4.EFCacheKey.KeyHash; Assert.AreNotSame(hash3, hash4); } } } [TestMethod] public void TestSecondLevelCacheInTwoDifferentContextsDoesNotHitTheDatabase() { var isActive = true; var name = "Product2"; string hash2; string hash3; Console.WriteLine("context 1."); using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st query as Cacheable, reading from db."); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); hash2 = debugInfo2.EFCacheKey.KeyHash; } } Console.WriteLine("context 2"); using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); hash3 = debugInfo3.EFCacheKey.KeyHash; } } Assert.AreEqual(hash2, hash3); } [TestMethod] public void TestSecondLevelCacheInTwoDifferentParallelContexts() { var isActive = true; var name = "Product1"; var debugInfo2 = new EFCacheDebugInfo(); var debugInfo3 = new EFCacheDebugInfo(); var task1 = Task.Factory.StartNew(() => { Console.WriteLine("context 1."); using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st query as Cacheable."); var list2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.IsTrue(list2.Any()); } } }); var task2 = Task.Factory.StartNew(() => { Console.WriteLine("context 2"); using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("same query"); var list3 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.IsTrue(list3.Any()); } } }); Task.WaitAll(task1, task2); Assert.AreEqual(debugInfo2.EFCacheKey.KeyHash, debugInfo3.EFCacheKey.KeyHash); } [TestMethod] public void TestSecondLevelCacheUsingDifferentSyncMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("Count"); var debugInfo2 = new EFCacheDebugInfo(); var count = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .Count(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(count > 0); Console.WriteLine("ToList"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("FirstOrDefault"); var debugInfo3 = new EFCacheDebugInfo(); var product1 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .FirstOrDefault(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsTrue(product1 != null); Console.WriteLine("Any"); var debugInfo4 = new EFCacheDebugInfo(); var any = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo4, _serviceProvider) .Any(); Assert.AreEqual(false, debugInfo4.IsCacheHit); Assert.IsTrue(any); Console.WriteLine("Sum"); var debugInfo5 = new EFCacheDebugInfo(); var sum = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == "Product2") .Cacheable(debugInfo5, _serviceProvider) .Sum(x => x.ProductId); Assert.AreEqual(false, debugInfo5.IsCacheHit); Assert.IsTrue(sum > 0); } } } [TestMethod] public void TestSecondLevelCacheUsingTwoCountMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product3"; Console.WriteLine("Count 1"); var debugInfo2 = new EFCacheDebugInfo(); var count = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .Count(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(count > 0); Console.WriteLine("Count 2"); var debugInfo3 = new EFCacheDebugInfo(); count = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .Count(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(count > 0); } } } [TestMethod] public void TestSecondLevelCacheUsingProjections() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("Projection 1"); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Select(x => x.ProductId) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); Console.WriteLine("Projection 2"); var debugInfo3 = new EFCacheDebugInfo(); list2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Select(x => x.ProductId) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list2.Any()); } } } [TestMethod] public void TestSecondLevelCacheUsingFiltersAfterCacheableMethod() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("Filters After Cacheable Method 1."); var debugInfo2 = new EFCacheDebugInfo(); var product1 = context.Products .Cacheable(debugInfo2, _serviceProvider) .FirstOrDefault(product => product.IsActive); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsNotNull(product1); Console.WriteLine("Filters After Cacheable Method 2, Same query."); var debugInfo3 = new EFCacheDebugInfo(); product1 = context.Products .Cacheable(debugInfo3, _serviceProvider) .FirstOrDefault(product => product.IsActive); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsNotNull(product1); Console.WriteLine("Filters After Cacheable Method 3, Different query."); var debugInfo4 = new EFCacheDebugInfo(); product1 = context.Products .Cacheable(debugInfo4, _serviceProvider) .FirstOrDefault(product => !product.IsActive); Assert.AreEqual(false, debugInfo4.IsCacheHit); Assert.IsNotNull(product1); Console.WriteLine("Filters After Cacheable Method 4, Different query."); var debugInfo5 = new EFCacheDebugInfo(); product1 = context.Products .Cacheable(debugInfo5, _serviceProvider) .FirstOrDefault(product => product.ProductName == "Product2"); Assert.AreEqual(false, debugInfo5.IsCacheHit); Assert.IsNotNull(product1); Console.WriteLine("Filters After Cacheable Method 5, Different query."); var debugInfo6 = new EFCacheDebugInfo(); product1 = context.Products .Cacheable(debugInfo6, _serviceProvider) .FirstOrDefault(product => product.TagProducts.Any(tag => tag.TagId == 1)); Assert.AreEqual(false, debugInfo6.IsCacheHit); Assert.IsNotNull(product1); } } } [TestMethod] public void TestEagerlyLoadingMultipleLevels() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("a normal query"); var product1IncludeTags = context.Users .Include(x => x.Products) .ThenInclude(x => x.TagProducts) .ThenInclude(x => x.Tag) .FirstOrDefault(); Assert.IsNotNull(product1IncludeTags); Console.WriteLine("1st query using Include method."); var debugInfo1 = new EFCacheDebugInfo(); var firstProductIncludeTags = context.Users .Include(x => x.Products) .ThenInclude(x => x.TagProducts) .ThenInclude(x => x.Tag) .Cacheable(debugInfo1, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProductIncludeTags); Assert.AreEqual(false, debugInfo1.IsCacheHit); var hash1 = debugInfo1.EFCacheKey.KeyHash; var cacheDependencies1 = debugInfo1.EFCacheKey.CacheDependencies; Console.WriteLine("same cached query using Include method."); var debugInfo11 = new EFCacheDebugInfo(); var firstProductIncludeTags11 = context.Users .Include(x => x.Products) .ThenInclude(x => x.TagProducts) .ThenInclude(x => x.Tag) .Cacheable(debugInfo11, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProductIncludeTags11); Assert.AreEqual(true, debugInfo11.IsCacheHit); Console.WriteLine( @"2nd query looks the same, but it doesn't have the Include method, so it shouldn't produce the same queryKeyHash. This was the problem with just parsing the LINQ expression, without considering the produced SQL."); var debugInfo2 = new EFCacheDebugInfo(); var firstProduct = context.Users.Cacheable(debugInfo2, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProduct); Assert.AreEqual(false, debugInfo2.IsCacheHit); var hash2 = debugInfo2.EFCacheKey.KeyHash; var cacheDependencies2 = debugInfo2.EFCacheKey.CacheDependencies; Assert.AreNotEqual(hash1, hash2); Assert.AreNotEqual(cacheDependencies1, cacheDependencies2); } } } [TestMethod] public void TestIncludeMethodAndProjectionAffectsKeyCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("a normal query"); var product1IncludeTags = context.Products .Include(x => x.TagProducts).ThenInclude(x => x.Tag) .Select(x => new { Name = x.ProductName, Tag = x.TagProducts.Select(y => y.Tag) }) .OrderBy(x => x.Name) .FirstOrDefault(); Assert.IsNotNull(product1IncludeTags); } } string hash1; ISet cacheDependencies1; using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st Cacheable query using Include method, reading from db"); var debugInfo1 = new EFCacheDebugInfo(); var firstProductIncludeTags = context.Products .Include(x => x.TagProducts).ThenInclude(x => x.Tag) .Select(x => new { Name = x.ProductName, Tag = x.TagProducts.Select(y => y.Tag) }) .OrderBy(x => x.Name) .Cacheable(debugInfo1, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProductIncludeTags); Assert.AreEqual(false, debugInfo1.IsCacheHit); hash1 = debugInfo1.EFCacheKey.KeyHash; cacheDependencies1 = debugInfo1.EFCacheKey.CacheDependencies; } } using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("same Cacheable query, reading from 2nd level cache"); var debugInfo2 = new EFCacheDebugInfo(); var firstProductIncludeTags2 = context.Products .Include(x => x.TagProducts).ThenInclude(x => x.Tag) .Select(x => new { Name = x.ProductName, Tag = x.TagProducts.Select(y => y.Tag) }) .OrderBy(x => x.Name) .Cacheable(debugInfo2, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProductIncludeTags2); Assert.AreEqual(true, debugInfo2.IsCacheHit); } } using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine( @"3rd query looks the same, but it doesn't have the Include method, so it shouldn't produce the same queryKeyHash. This was the problem with just parsing the LINQ expression, without considering the produced SQL."); var debugInfo3 = new EFCacheDebugInfo(); var firstProduct = context.Products .Select(x => new { Name = x.ProductName, Tag = x.TagProducts.Select(y => y.Tag) }) .OrderBy(x => x.Name) .Cacheable(debugInfo3, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProduct); Assert.AreEqual(false, debugInfo3.IsCacheHit); var hash3 = debugInfo3.EFCacheKey.KeyHash; var cacheDependencies3 = debugInfo3.EFCacheKey.CacheDependencies; Assert.AreNotEqual(hash1, hash3); Assert.AreNotEqual(cacheDependencies1, cacheDependencies3); } } } [TestMethod] public void TestParallelQueriesShouldCacheData() { var debugInfo1 = new EFCacheDebugInfo(); TestsBase.ExecuteInParallel(() => { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var firstProductIncludeTags = context.Products .Include(x => x.TagProducts).ThenInclude(x => x.Tag) .Select(x => new { Name = x.ProductName, Tag = x.TagProducts.Select(y => y.Tag) }) .OrderBy(x => x.Name) .Cacheable(debugInfo1, _serviceProvider) .FirstOrDefault(); Assert.IsNotNull(firstProductIncludeTags); } } }); Assert.AreEqual(true, debugInfo1.IsCacheHit); } [TestMethod] public void TestSecondLevelCacheUsingFindMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var debugInfo = new EFCacheDebugInfo(); var product1 = context.Products .Cacheable(debugInfo, _serviceProvider) .Find(1); Assert.AreEqual(false, debugInfo.IsCacheHit); Assert.IsTrue(product1 != null); var debugInfo2 = new EFCacheDebugInfo(); product1 = context.Products .Cacheable(debugInfo2, _serviceProvider) .Find(1); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(product1 != null); } } } [TestMethod] public void TestNullValuesWillUseTheCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st query, reading from db."); var debugInfo1 = new EFCacheDebugInfo(); var item1 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive && product.ProductName == "Product1xx") .Cacheable(debugInfo1) .FirstOrDefault(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNull(item1); Console.WriteLine("2nd query, reading from cache."); var debugInfo2 = new EFCacheDebugInfo(); var item2 = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive && product.ProductName == "Product1xx") .Cacheable(debugInfo2) .FirstOrDefault(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsNull(item2); } } } [TestMethod] public void TestEqualsMethodWillUseTheCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st query, reading from db."); var debugInfo1 = new EFCacheDebugInfo(); var item1 = context.Products .Where(product => product.ProductId == 2 && product.ProductName.Equals("Product1")) .Cacheable(debugInfo1) .FirstOrDefault(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNotNull(item1); Console.WriteLine("2nd query, reading from cache."); var debugInfo2 = new EFCacheDebugInfo(); var item2 = context.Products .Where(product => product.ProductId == 2 && product.ProductName.Equals("Product1")) .Cacheable(debugInfo2) .FirstOrDefault(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsNotNull(item2); Console.WriteLine("3rd query, reading from db."); var debugInfo3 = new EFCacheDebugInfo(); var item3 = context.Products .Where(product => product.ProductId == 1 && product.ProductName.Equals("Product1")) .Cacheable(debugInfo3) .FirstOrDefault(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsNull(item3); } } } [TestMethod] public void TestSecondLevelCacheDoesNotHitTheDatabaseForIQueryableCacheables() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("1st query, reading from db."); var debugInfo1 = new EFCacheDebugInfo(); var list1IQueryable = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) as IQueryable; var list1 = (list1IQueryable.Cacheable(debugInfo1, _serviceProvider) as IEnumerable).Cast().ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo2 = new EFCacheDebugInfo(); var list2IQueryable = context.Products .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) as IQueryable; var list2 = (list2IQueryable.Cacheable(debugInfo2, _serviceProvider) as IEnumerable).Cast().ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); } } } [TestMethod] public void Test2DifferentCollectionsWillNotUseTheCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var collection1 = new[] { 1, 2, 3 }; var debugInfo1 = new EFCacheDebugInfo(); var item1 = context.Products .Where(product => collection1.Contains(product.ProductId)) .Cacheable(debugInfo1) .FirstOrDefault(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNotNull(item1); var collection2 = new[] { 1, 2, 3, 4 }; var debugInfo2 = new EFCacheDebugInfo(); var item2 = context.Products .Where(product => collection2.Contains(product.ProductId)) .Cacheable(debugInfo2) .FirstOrDefault(); Assert.AreEqual(false, debugInfo2.IsCacheHit); // Works with `RelationalQueryModelVisitor` Assert.IsNotNull(item2); } } } [TestMethod] [ExpectedException(typeof(InvalidOperationException))] // This is a bug or a limitation in EF Core public void TestSubqueriesWillUseTheCache() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var debugInfo1 = new EFCacheDebugInfo(); var item1 = context.Products.Select(product => new { prop1 = product.UserId, prop2 = context.TagProducts.Where(tag => tag.ProductProductId == product.ProductId) .Cacheable().Select(tag => new { tag.TagId }) }).FirstOrDefault(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNotNull(item1); var debugInfo2 = new EFCacheDebugInfo(); var item2 = context.Products.Select(product => new { prop1 = product.UserId, prop2 = context.TagProducts.Where(tag => tag.ProductProductId == product.ProductId) .Cacheable().Select(tag => new { tag.TagId }) }).FirstOrDefault(); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsNotNull(item2); } } } [TestMethod] public void TestSecondLevelCacheUsingProjectToMethods() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var mapper = serviceScope.ServiceProvider.GetRequiredService(); var debugInfo = new EFCacheDebugInfo(); var posts = context.Posts .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(debugInfo, _serviceProvider) .ProjectTo(configuration: mapper.ConfigurationProvider) .ToList(); Assert.AreEqual(false, debugInfo.IsCacheHit); Assert.IsTrue(posts != null); var debugInfo2 = new EFCacheDebugInfo(); posts = context.Posts .Where(x => x.Id > 0) .OrderBy(x => x.Id) .Cacheable(debugInfo2, _serviceProvider) .ProjectTo(configuration: mapper.ConfigurationProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(posts != null); } } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/EFCachedQueryProviderInvalidationTests.cs ================================================ using System; using System.Linq; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Entities; using EFSecondLevelCache.Core.Contracts; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.EntityFrameworkCore; namespace EFSecondLevelCache.Core.Tests { [TestClass] public class EFCachedQueryProviderInvalidationTests { private readonly IServiceProvider _serviceProvider; public EFCachedQueryProviderInvalidationTests() { _serviceProvider = TestsBase.GetServiceProvider(); } [TestInitialize] public void ClearEFGlobalCacheBeforeEachTest() { _serviceProvider.GetRequiredService().ClearAllCachedEntries(); } [TestMethod] public void TestInsertingDataIntoTheSameTableShouldInvalidateTheCacheAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product2"; Console.WriteLine("1st query, reading from db"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("same query, reading from 2nd level cache"); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); Console.WriteLine("inserting data, invalidates the cache on SaveChanges"); var rnd = new Random(); var newProduct = new Product { IsActive = false, ProductName = $"Product{rnd.Next()}", ProductNumber = rnd.Next().ToString(), Notes = "Notes ...", UserId = 1 }; context.Products.Add(newProduct); context.ChangeTracker.DetectChanges(); var changedEntityNames = context.GetChangedEntityNames(); Assert.IsTrue(debugInfo2.EFCacheKey.CacheDependencies.Any(item => changedEntityNames.Contains(item))); context.SaveChanges(); Console.WriteLine("same query after insert, reading from database."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); } } } [TestMethod] public void TestInsertingDataToOtherTablesShouldNotInvalidateTheCacheDependencyAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product3"; Console.WriteLine("1st query, reading from db (it dependes on/includes the Tags table)"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); Console.WriteLine( "inserting data into a *non-related* table, shouldn't invalidate the cache on SaveChanges."); var rnd = new Random(); var user = new User { Name = $"User {rnd.Next()}" }; context.Users.Add(user); context.ChangeTracker.DetectChanges(); var changedEntityNames = context.GetChangedEntityNames(); Assert.IsFalse(debugInfo2.EFCacheKey.CacheDependencies.Any(item => changedEntityNames.Contains(item))); context.SaveChanges(); Console.WriteLine("same query after insert, reading from 2nd level cache."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); } } } [TestMethod] public void TestInsertingDataToRelatedTablesShouldInvalidateTheCacheDependencyAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("1st query, reading from db (it dependes on/includes the Tags table)."); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("same query, reading from 2nd level cache"); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); Console.WriteLine("inserting data into a *related* table, invalidates the cache on SaveChanges."); var rnd = new Random(); var tag = new Tag { Name = $"Tag {rnd.Next()}" }; context.Tags.Add(tag); context.ChangeTracker.DetectChanges(); var changedEntityNames = context.GetChangedEntityNames(); Assert.IsTrue(debugInfo2.EFCacheKey.CacheDependencies.Any(item => changedEntityNames.Contains(item))); context.SaveChanges(); Console.WriteLine( "same query after insert, reading from database (it dependes on/includes the Tags table)"); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); } } } [TestMethod] [Ignore("This doesn't work with `EntityFrameworkInMemoryDatabase`. Because it doesn't support constraints.")] public void TestTransactionRollbackShouldNotInvalidateTheCacheDependencyAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = true; var name = "Product1"; Console.WriteLine("1st query, reading from db."); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsTrue(list1.Any()); Console.WriteLine("same query, reading from 2nd level cache."); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); Console.WriteLine( "inserting data with transaction.Rollback, shouldn't invalidate the cache on SaveChanges."); try { var rnd = new Random(); var newProduct = new Product { IsActive = false, ProductName = "Product1", // It has an `IsUnique` constraint. ProductNumber = rnd.Next().ToString(), Notes = "Notes ...", UserId = 1 }; context.Products.Add(newProduct); context.SaveChanges(); // it uses a transaction behind the scene. } catch (Exception ex) { // NOTE: This doesn't work with `EntityFrameworkInMemoryDatabase`. Because it doesn't support constraints. // ProductName is duplicate here and should throw an exception on save changes // and rollback the transaction automatically. Console.WriteLine(ex.ToString()); } Console.WriteLine("same query after insert, reading from 2nd level cache."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo3.IsCacheHit); Assert.IsTrue(list3.Any()); } } } [TestMethod] public void TestRemoveDataShouldInvalidateTheCacheAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { var isActive = false; var name = "Product4"; Console.WriteLine("1st query, reading from db"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo1, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNotNull(list1); Console.WriteLine("same query, reading from 2nd level cache"); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo2, _serviceProvider) .ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.IsTrue(list2.Any()); Console.WriteLine("removing data, invalidates the cache on SaveChanges"); var product1 = context.Products.First(product => product.ProductName == name); context.Products.Remove(product1); context.ChangeTracker.DetectChanges(); var changedEntityNames = context.GetChangedEntityNames(); Assert.IsTrue(debugInfo2.EFCacheKey.CacheDependencies.Any(item => changedEntityNames.Contains(item))); context.SaveChanges(); Console.WriteLine("same query after remove, reading from database."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Products.Include(x => x.TagProducts).ThenInclude(x => x.Tag) .OrderBy(product => product.ProductNumber) .Where(product => product.IsActive == isActive && product.ProductName == name) .Cacheable(debugInfo3, _serviceProvider) .ToList(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.IsNotNull(list3); } } } [TestMethod] public void TestRemoveTptDataShouldInvalidateTheCacheAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { Console.WriteLine("1st query, reading from db"); var debugInfo1 = new EFCacheDebugInfo(); var list1 = context.Posts.OfType().Cacheable(debugInfo1, _serviceProvider).ToList(); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.AreEqual(2, list1.Count); Console.WriteLine("same query, reading from 2nd level cache"); var debugInfo2 = new EFCacheDebugInfo(); var list2 = context.Posts.OfType().Cacheable(debugInfo2, _serviceProvider).ToList(); Assert.AreEqual(true, debugInfo2.IsCacheHit); Assert.AreEqual(2, list2.Count); Console.WriteLine("removing data, invalidates the cache on SaveChanges"); var post1 = context.Posts.First(post => post.Title == "Post1"); context.Posts.Remove(post1); context.ChangeTracker.DetectChanges(); var changedEntityNames = context.GetChangedEntityNames(); Assert.IsTrue(debugInfo2.EFCacheKey.CacheDependencies.Any(item => changedEntityNames.Contains(item))); context.SaveChanges(); Console.WriteLine("same query after remove, reading from database."); var debugInfo3 = new EFCacheDebugInfo(); var list3 = context.Posts.OfType().Cacheable(debugInfo3, _serviceProvider).ToList(); Assert.AreEqual(false, debugInfo3.IsCacheHit); Assert.AreEqual(1, list3.Count); } } } [TestMethod] public void TestAddThenRemoveDataShouldInvalidateTheCacheAutomatically() { using (var serviceScope = _serviceProvider.GetRequiredService().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService()) { User user1; const string user1Name = "User1"; if (!context.Users.Any(user => user.Name == user1Name)) { user1 = new User { Name = user1Name }; user1 = context.Users.Add(user1).Entity; } else { user1 = context.Users.First(user => user.Name == user1Name); } var product = new Product { ProductName = "P98", IsActive = true, Notes = "Notes ...", ProductNumber = "098", User = user1 }; context.Products.Add(product); context.SaveChanges(); Console.WriteLine("1st query, reading from db"); var debugInfo1 = new EFCacheDebugInfo(); var p98 = context.Products .Cacheable(debugInfo1, _serviceProvider) .FirstOrDefault(p => p.ProductId == product.ProductId); Assert.AreEqual(false, debugInfo1.IsCacheHit); Assert.IsNotNull(p98); var debugInfoWithWhere1 = new EFCacheDebugInfo(); var firstQueryWithWhereClauseResult = context.Products.Where(p => p.ProductId == product.ProductId) .Cacheable(debugInfoWithWhere1) .FirstOrDefault(); Assert.AreEqual(false, debugInfoWithWhere1.IsCacheHit); Assert.IsNotNull(firstQueryWithWhereClauseResult); Console.WriteLine("Delete it from db, invalidates the cache on SaveChanges"); context.Products.Remove(product); context.SaveChanges(); Console.WriteLine("same query, reading from 2nd level cache?"); var debugInfo2 = new EFCacheDebugInfo(); p98 = context.Products .Cacheable(debugInfo2, _serviceProvider) .FirstOrDefault(p => p.ProductId == product.ProductId); Assert.AreEqual(false, debugInfo2.IsCacheHit); Assert.IsNull(p98); var debugInfoWithWhere2 = new EFCacheDebugInfo(); var firstQueryWithWhereClauseResult2 = context.Products.Where(p => p.ProductId == product.ProductId) .Cacheable(debugInfoWithWhere2) .FirstOrDefault(); Assert.AreEqual(false, debugInfoWithWhere2.IsCacheHit); Assert.IsNull(firstQueryWithWhereClauseResult2); Console.WriteLine("retrieving it directly from database"); p98 = context.Products .FirstOrDefault(p => p.ProductId == product.ProductId); Assert.IsNull(p98); } } } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/EFSecondLevelCache.Core.Tests.csproj ================================================  netcoreapp3.1 EFSecondLevelCache.Core.Tests EFSecondLevelCache.Core.Tests true false false false false ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("EFSecondLevelCache.Core.Tests")] [assembly: AssemblyTrademark("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("5475d985-85d6-4dd2-899e-8e8b333b0070")] ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/TestsBase.cs ================================================ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using AutoMapper; using CacheManager.Core; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer; using EFSecondLevelCache.Core.AspNetCoreSample.DataLayer.Utils; using EFSecondLevelCache.Core.AspNetCoreSample.Profiles; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; //[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] // Workers: The number of threads to run the tests. Set it to 0 to use the number of core of your computer. namespace EFSecondLevelCache.Core.Tests { public static class TestsBase { public static IEFCacheServiceProvider GetInMemoryCacheServiceProvider() { var services = new ServiceCollection(); services.AddEFSecondLevelCache(); addInMemoryCacheServiceProvider(services); var serviceProvider = services.BuildServiceProvider(); return serviceProvider.GetRequiredService(); } public static IEFCacheServiceProvider GetRedisCacheServiceProvider() { var services = new ServiceCollection(); services.AddEFSecondLevelCache(); addRedisCacheServiceProvider(services); var serviceProvider = services.BuildServiceProvider(); return serviceProvider.GetRequiredService(); } public static IServiceProvider GetServiceProvider() { var services = new ServiceCollection(); services.AddSingleton(provider => { return new Microsoft.Extensions.Configuration.ConfigurationBuilder() .AddInMemoryCollection(new[] { new KeyValuePair("UseInMemoryDatabase", "true"), }) .Build(); }); services.AddEntityFrameworkInMemoryDatabase().AddDbContext(optionsBuilder => { optionsBuilder.UseInMemoryDatabase("TestDb"); }); services.AddAutoMapper(typeof(PostProfile).GetTypeInfo().Assembly); services.AddEFSecondLevelCache(); addInMemoryCacheServiceProvider(services); //addRedisCacheServiceProvider(services); var serviceProvider = services.BuildServiceProvider(); var serviceScope = serviceProvider.GetRequiredService(); serviceScope.SeedData(); return serviceProvider; } public static void ExecuteInParallel(Action test, int count = 40) { var tests = new Action[count]; for (var i = 0; i < count; i++) { tests[i] = test; } Parallel.Invoke(tests); } private static void addInMemoryCacheServiceProvider(IServiceCollection services) { services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer() .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); } private static void addRedisCacheServiceProvider(IServiceCollection services) { var jss = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; const string redisConfigurationKey = "redis"; services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer(serializationSettings: jss, deserializationSettings: jss) .WithUpdateMode(CacheUpdateMode.Up) .WithRedisConfiguration(redisConfigurationKey, config => { config.WithAllowAdmin() .WithDatabase(0) .WithEndpoint("localhost", 6379) // Enables keyspace notifications to react on eviction/expiration of items. // Make sure that all servers are configured correctly and 'notify-keyspace-events' is at least set to 'Exe', otherwise CacheManager will not retrieve any events. // See https://redis.io/topics/notifications#configuration for configuration details. .EnableKeyspaceEvents(); }) .WithMaxRetries(100) .WithRetryTimeout(50) .WithRedisCacheHandle(redisConfigurationKey) .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/XxHashTests.cs ================================================ using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFSecondLevelCache.Core.Tests { internal sealed class TestConstants { public static readonly string Empty = ""; public static readonly string FooBar = "foobar"; public static readonly string LoremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut ornare aliquam mauris, at volutpat massa. Phasellus pulvinar purus eu venenatis commodo."; } [TestClass] public class XxHashTests { [TestMethod] public void TestEmptyXxHashReturnsCorrectValue() { var hash = XxHashUnsafe.ComputeHash(TestConstants.Empty); Assert.AreEqual((uint)0x02cc5d05, hash); } [TestMethod] public void TestFooBarXxHashReturnsCorrectValue() { var hash = XxHashUnsafe.ComputeHash(TestConstants.FooBar); Assert.AreEqual((uint)2348340516, hash); } [TestMethod] public void TestLoremIpsumXxHashReturnsCorrectValue() { var hash = XxHashUnsafe.ComputeHash(TestConstants.LoremIpsum); Assert.AreEqual((uint)4046722717, hash); } } } ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/_0-restore.bat ================================================ rmdir /S /Q bin rmdir /S /Q obj dotnet restore pause ================================================ FILE: src/Tests/EFSecondLevelCache.Core.Tests/_1-dotnet_test.bat ================================================ dotnet test pause ================================================ FILE: src/Tests/Issues/Issue15/ConfigureServices.cs ================================================ using System; using System.Threading; using CacheManager.Core; using EFSecondLevelCache.Core; using EFSecondLevelCache.Core.Contracts; using Microsoft.Extensions.DependencyInjection; namespace Issue15 { public static class ConfigureServices { private static readonly Lazy _serviceProviderBuilder = new Lazy(getServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); /// /// A lazy loaded thread-safe singleton /// public static IServiceProvider Instance { get; } = _serviceProviderBuilder.Value; public static IEFCacheServiceProvider GetEFCacheServiceProvider() { return Instance.GetRequiredService(); } private static IServiceProvider getServiceProvider() { var services = new ServiceCollection(); services.AddEFSecondLevelCache(); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer() .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); var serviceProvider = services.BuildServiceProvider(); return serviceProvider; } } } ================================================ FILE: src/Tests/Issues/Issue15/Issue15.csproj ================================================ Exe netcoreapp3.1 latest ================================================ FILE: src/Tests/Issues/Issue15/Payment.cs ================================================ namespace Issue15 { public class Payment { public int Id { set; get; } public int IdPaymentType { set; get; } public string Language { set; get; } } } ================================================ FILE: src/Tests/Issues/Issue15/Program.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using EFSecondLevelCache.Core; using Microsoft.EntityFrameworkCore; namespace Issue15 { class Program { static async Task Main(string[] args) { SetupDatabase(); var serviceProvider = ConfigureServices.Instance; using (var context = new SampleContext()) { Console.WriteLine($"1,en"); /* SELECT TOP(2) [u].[Id], [u].[IdPaymentType], [u].[Language] FROM [Payments] AS [u] WHERE ([u].[IdPaymentType] = 1) AND ([u].[Language] = N'en') */ var item = await context.Payments .Where(u => u.IdPaymentType == 1 && u.Language.Equals("en")) .Cacheable(serviceProvider) .SingleOrDefaultAsync(); Console.WriteLine($"1,en -> item.Id= {item.Id}"); // --> 1 Console.WriteLine($"{Environment.NewLine}2,en"); /* SELECT TOP(2) [u].[Id], [u].[IdPaymentType], [u].[Language] FROM [Payments] AS [u] WHERE ([u].[IdPaymentType] = 2) AND ([u].[Language] = N'en') */ item = await context.Payments .Where(u => u.IdPaymentType == 2 && u.Language.Equals("en")) .Cacheable(serviceProvider) .SingleOrDefaultAsync(); Console.WriteLine($"2,en -> item.Id= {item.Id}"); // --> 3 Console.WriteLine($"{Environment.NewLine}3,en"); /* SELECT TOP(2) [u].[Id], [u].[IdPaymentType], [u].[Language] FROM [Payments] AS [u] WHERE ([u].[IdPaymentType] = 3) AND ([u].[Language] = N'en') */ item = await context.Payments .Where(u => u.IdPaymentType == 3 && u.Language.Equals("en")) .Cacheable(serviceProvider) .SingleOrDefaultAsync(); Console.WriteLine($"3,en -> item.Id= {item.Id}"); // --> 5 Console.WriteLine($"{Environment.NewLine}3,pl"); /* SELECT [u].[Id], [u].[IdPaymentType], [u].[Language] FROM [Payments] AS [u] WHERE ([u].[IdPaymentType] = 3) AND ([u].[Language] = N'pl') */ item = await context.Payments .Where(u => u.IdPaymentType == 3 && u.Language.Equals("pl")) .Cacheable(serviceProvider) .SingleOrDefaultAsync(); Console.WriteLine($"3,pl -> item.Id= {item.Id}"); // --> 6 Console.WriteLine($"{Environment.NewLine}2,pl"); /* SELECT TOP(2) [u].[Id], [u].[IdPaymentType], [u].[Language] FROM [Payments] AS [u] WHERE ([u].[IdPaymentType] = 2) AND ([u].[Language] = N'pl') */ item = await context.Payments .Where(u => u.IdPaymentType == 2 && u.Language.Equals("pl")) .Cacheable(serviceProvider) .SingleOrDefaultAsync(); Console.WriteLine($"2,pl -> item.Id= {item.Id}"); // --> 4 Console.WriteLine($"{Environment.NewLine}1,pl"); /* SELECT [u].[Id], [u].[IdPaymentType], [u].[Language] FROM [Payments] AS [u] WHERE ([u].[IdPaymentType] = 1) AND ([u].[Language] = N'pl') */ item = await context.Payments .Where(u => u.IdPaymentType == 1 && u.Language.Equals("pl")) .Cacheable(serviceProvider) .SingleOrDefaultAsync(); Console.WriteLine($"1,pl -> item.Id= {item.Id}"); // --> 2 } Console.WriteLine("Press a key ..."); Console.ReadKey(); } private static void SetupDatabase() { using (var db = new SampleContext()) { if (db.Database.EnsureCreated()) { var item1 = new Payment { IdPaymentType = 1, Language = "en" }; db.Payments.Add(item1); var item2 = new Payment { IdPaymentType = 1, Language = "pl" }; db.Payments.Add(item2); var item3 = new Payment { IdPaymentType = 2, Language = "en" }; db.Payments.Add(item3); var item4 = new Payment { IdPaymentType = 2, Language = "pl" }; db.Payments.Add(item4); var item5 = new Payment { IdPaymentType = 3, Language = "en" }; db.Payments.Add(item5); var item6 = new Payment { IdPaymentType = 3, Language = "pl" }; db.Payments.Add(item6); db.SaveChanges(); } } } } } ================================================ FILE: src/Tests/Issues/Issue15/SampleContext.cs ================================================ using System.IO; using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; namespace Issue15 { public class SampleContext : DbContext { private static readonly IEFCacheServiceProvider _efCacheServiceProvider = ConfigureServices.GetEFCacheServiceProvider(); public DbSet Payments { get; set; } public SampleContext() { } public SampleContext(DbContextOptions options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( @"Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=EFSecondLevelCache.Issue15;AttachDbFilename=|DataDirectory|\EFSecondLevelCache.Issue15.mdf;Integrated Security=True;MultipleActiveResultSets=True;" .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "app_data"))); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.HasIndex(e => new { e.IdPaymentType, e.Language }).IsUnique(); }); } public override int SaveChanges() { this.ChangeTracker.DetectChanges(); var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; _efCacheServiceProvider.InvalidateCacheDependencies(changedEntityNames); return result; } public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { this.ChangeTracker.DetectChanges(); var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChangesAsync(cancellationToken); this.ChangeTracker.AutoDetectChangesEnabled = true; _efCacheServiceProvider.InvalidateCacheDependencies(changedEntityNames); return result; } } } ================================================ FILE: src/Tests/Issues/Issue15/app_data/.git.keep ================================================ ================================================ FILE: src/Tests/Issues/Issue43/Issue43.csproj ================================================ Exe netcoreapp3.1 ================================================ FILE: src/Tests/Issues/Issue43/Program.cs ================================================ using System.IO; using System.Threading; using System.Threading.Tasks; using EFSecondLevelCache.Core; using EFSecondLevelCache.Core.Contracts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Linq; using CacheManager.Core; using Microsoft.Extensions.DependencyInjection; namespace Issue43 { public static class ConfigureServices { private static readonly Lazy _serviceProviderBuilder = new Lazy(getServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication); /// /// A lazy loaded thread-safe singleton /// public static IServiceProvider Instance { get; } = _serviceProviderBuilder.Value; public static IEFCacheServiceProvider GetEFCacheServiceProvider() { return Instance.GetRequiredService(); } private static IServiceProvider getServiceProvider() { var services = new ServiceCollection(); services.AddEFSecondLevelCache(); services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() .WithJsonSerializer() .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") .WithExpiration(ExpirationMode.Absolute, TimeSpan.FromMinutes(10)) .DisablePerformanceCounters() .DisableStatistics() .Build()); return services.BuildServiceProvider(); } } public class User { public int Id { set; get; } public string Name { set; get; } } public class SampleContext : DbContext { private static readonly IEFCacheServiceProvider _efCacheServiceProvider = ConfigureServices.GetEFCacheServiceProvider(); public DbSet Users { get; set; } public SampleContext() { } public SampleContext(DbContextOptions options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connectionString = @"Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=EFSecondLevelCache.Issue43;AttachDbFilename=|DataDirectory|\EFSecondLevelCache.Issue43.mdf;Integrated Security=True;MultipleActiveResultSets=True;" .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "app_data")); optionsBuilder.UseSqlServer(connectionString); } public override int SaveChanges() { this.ChangeTracker.DetectChanges(); var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; _efCacheServiceProvider.InvalidateCacheDependencies(changedEntityNames); return result; } public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { this.ChangeTracker.DetectChanges(); var changedEntityNames = this.GetChangedEntityNames(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChangesAsync(cancellationToken); this.ChangeTracker.AutoDetectChangesEnabled = true; _efCacheServiceProvider.InvalidateCacheDependencies(changedEntityNames); return result; } } class Program { static async Task Main(string[] args) { SetupDatabase(); var serviceProvider = ConfigureServices.Instance; using (var context = new SampleContext()) { var debugInfo = new EFCacheDebugInfo(); var item = await context.Users.Where(x => x.Name == "user-1").Cacheable(debugInfo, serviceProvider).FirstOrDefaultAsync(); Console.WriteLine($"FirstOrDefaultAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item.Name}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Where(x => x.Name == "user-1").Cacheable(debugInfo, serviceProvider).FirstOrDefaultAsync(); Console.WriteLine($"FirstOrDefaultAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item.Name}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Where(x => x.Name == "user-11").Cacheable(debugInfo, serviceProvider).FirstOrDefaultAsync(); Console.WriteLine($"FirstOrDefaultAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item?.Name}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Where(x => x.Name == "user-11").Cacheable(debugInfo, serviceProvider).FirstOrDefaultAsync(); Console.WriteLine($"FirstOrDefaultAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item?.Name}"); debugInfo = new EFCacheDebugInfo(); item = context.Users.Cacheable(debugInfo, serviceProvider).Find(1); Console.WriteLine($"Find->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item?.Name}"); debugInfo = new EFCacheDebugInfo(); item = context.Users.Cacheable(debugInfo, serviceProvider).Find(1); Console.WriteLine($"Find->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item?.Name}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Cacheable(debugInfo, serviceProvider).FindAsync(1); Console.WriteLine($"FindAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item?.Name}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Cacheable(debugInfo, serviceProvider).FindAsync(1); Console.WriteLine($"FindAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item?.Name}"); debugInfo = new EFCacheDebugInfo(); var items = await context.Users.Where(x => x.Name == "user-1").Cacheable(debugInfo, serviceProvider).ToListAsync(); Console.WriteLine($"ToListAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{items[0].Name}"); debugInfo = new EFCacheDebugInfo(); items = await context.Users.Where(x => x.Name == "user-1").Cacheable(debugInfo, serviceProvider).ToListAsync(); Console.WriteLine($"ToListAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{items[0].Name}"); debugInfo = new EFCacheDebugInfo(); items = context.Users.Where(x => x.Name == "user-2").Cacheable(debugInfo, serviceProvider).ToList(); Console.WriteLine($"ToList->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{items[0].Name}"); debugInfo = new EFCacheDebugInfo(); items = context.Users.Where(x => x.Name == "user-2").Cacheable(debugInfo, serviceProvider).ToList(); Console.WriteLine($"ToList->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{items[0].Name}"); debugInfo = new EFCacheDebugInfo(); item = context.Users.Where(x => x.Name == "user-3").Cacheable(debugInfo, serviceProvider).FirstOrDefault(); Console.WriteLine($"FirstOrDefault->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item.Name}"); debugInfo = new EFCacheDebugInfo(); item = context.Users.Where(x => x.Name == "user-3").Cacheable(debugInfo, serviceProvider).FirstOrDefault(); Console.WriteLine($"FirstOrDefault->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item.Name}"); debugInfo = new EFCacheDebugInfo(); var count = context.Users.Where(x => x.Name == "user-3").Cacheable(debugInfo, serviceProvider).Count(); Console.WriteLine($"Count->IsCacheHit: {debugInfo.IsCacheHit}, count:{count}"); debugInfo = new EFCacheDebugInfo(); count = context.Users.Where(x => x.Name == "user-3").Cacheable(debugInfo, serviceProvider).Count(); Console.WriteLine($"Count->IsCacheHit: {debugInfo.IsCacheHit}, count:{count}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Where(x => x.Name == "user-1").Cacheable(debugInfo, serviceProvider).SingleOrDefaultAsync(); Console.WriteLine($"SingleOrDefaultAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item.Name}"); debugInfo = new EFCacheDebugInfo(); item = await context.Users.Where(x => x.Name == "user-1").Cacheable(debugInfo, serviceProvider).SingleOrDefaultAsync(); Console.WriteLine($"SingleOrDefaultAsync->IsCacheHit: {debugInfo.IsCacheHit}, items[0]:{item.Name}"); } } private static void SetupDatabase() { using (var db = new SampleContext()) { if (db.Database.EnsureCreated()) { var item1 = new User { Name = "user-1" }; db.Users.Add(item1); var item2 = new User { Name = "user-2" }; db.Users.Add(item2); var item3 = new User { Name = "user-3" }; db.Users.Add(item3); db.SaveChanges(); } } } } } ================================================ FILE: src/Tests/Issues/Issue43/_0-restore.bat ================================================ rmdir /S /Q bin rmdir /S /Q obj dotnet restore pause ================================================ FILE: src/Tests/Issues/Issue43/_1-dotnet_run.bat ================================================ dotnet watch run ================================================ FILE: src/Tests/Issues/Issue43/app_data/.git.keep ================================================ ================================================ FILE: tag-it.bat ================================================ git tag -a 2.9.0 -m "Published 2.9.0 to nuget.org" git push --follow-tags pause ================================================ FILE: update-dependencies.bat ================================================ dotnet tool update -g dotnet-outdated dotnet outdated -u dotnet restore pause