├── _config.yml ├── .vs ├── ProjectSettings.json ├── slnx.sqlite ├── VSWorkspaceState.json └── EntityFrameworkCore.Cacheable │ └── v15 │ └── .suo ├── nuget_icon.png ├── nuget_icon_light.ico ├── nuget_icon_light.png ├── EntityFrameworkCore.Cacheable ├── nuget_icon_light.ico ├── CacheableOptions.cs ├── Check.cs ├── CacheableOptionsExtension.cs ├── app.config ├── ICacheProvider.cs ├── EntityFrameworkCore.Cacheable.csproj ├── Diagnostics │ └── CoreEventId.cs ├── ExpressionVisitors │ └── CachableExpressionVisitor.cs ├── Extensions │ ├── EntityFrameworkQueryableExtensions.cs │ ├── DbContextOptionsBuilderExtensions.cs │ └── SharedTypeExtensions.cs ├── MemoryCacheProvider.cs └── CustomQueryCompiler.cs ├── EntityFrameworkCore.CacheableTests ├── BusinessTestLogic │ ├── Tag.cs │ ├── PersonPhoto.cs │ ├── PostTag.cs │ ├── Person.cs │ ├── Blog.cs │ ├── Post.cs │ ├── AgnosticBloggingContext.cs │ └── BloggingContext.cs ├── Logging │ ├── LogMessageEntry.cs │ ├── DebugLoggerProvider.cs │ └── DebugLogger.cs ├── app.config ├── Properties │ └── AssemblyInfo.cs ├── packages.config ├── CacheableAsyncExpressionTests.cs ├── EntityFrameworkCore.CacheableTests.csproj ├── CacheableExpressionTests.cs └── CacheableAgnosticContextTests.cs ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── EntityFrameworkCore.Cacheable.sln ├── .gitattributes ├── .gitignore ├── README.md └── LICENSE /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.vs/ProjectSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentProjectSetting": null 3 | } -------------------------------------------------------------------------------- /nuget_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/HEAD/nuget_icon.png -------------------------------------------------------------------------------- /.vs/slnx.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/HEAD/.vs/slnx.sqlite -------------------------------------------------------------------------------- /.vs/VSWorkspaceState.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExpandedNodes": [ 3 | "" 4 | ], 5 | "PreviewInSolutionExplorer": false 6 | } -------------------------------------------------------------------------------- /nuget_icon_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/HEAD/nuget_icon_light.ico -------------------------------------------------------------------------------- /nuget_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/HEAD/nuget_icon_light.png -------------------------------------------------------------------------------- /.vs/EntityFrameworkCore.Cacheable/v15/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/HEAD/.vs/EntityFrameworkCore.Cacheable/v15/.suo -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/nuget_icon_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/HEAD/EntityFrameworkCore.Cacheable/nuget_icon_light.ico -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/Tag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 8 | { 9 | public class Tag 10 | { 11 | public string TagId { get; set; } 12 | 13 | public List Posts { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/PersonPhoto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 8 | { 9 | public class PersonPhoto 10 | { 11 | public int PersonPhotoId { get; set; } 12 | public string Caption { get; set; } 13 | public byte[] Photo { get; set; } 14 | 15 | public Person Person { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/PostTag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 8 | { 9 | public class PostTag 10 | { 11 | public int PostTagId { get; set; } 12 | 13 | public int PostId { get; set; } 14 | public Post Post { get; set; } 15 | 16 | public string TagId { get; set; } 17 | public Tag Tag { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 8 | { 9 | public class Person 10 | { 11 | public int PersonId { get; set; } 12 | public string Name { get; set; } 13 | 14 | public List AuthoredPosts { get; set; } 15 | public List OwnedBlogs { get; set; } 16 | 17 | public int? PhotoId { get; set; } 18 | public PersonPhoto Photo { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/Blog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 9 | { 10 | public class Blog 11 | { 12 | [Key] 13 | public int BlogId { get; set; } 14 | public string Url { get; set; } 15 | public int? Rating { get; set; } 16 | 17 | public List Posts { get; set; } 18 | 19 | public int OwnerId { get; set; } 20 | public Person Owner { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/Post.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 8 | { 9 | public class Post 10 | { 11 | public int PostId { get; set; } 12 | public string Title { get; set; } 13 | public string Content { get; set; } 14 | public int Rating { get; set; } 15 | 16 | public int BlogId { get; set; } 17 | public Blog Blog { get; set; } 18 | 19 | public int AuthorId { get; set; } 20 | public Person Author { get; set; } 21 | 22 | public List Tags { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/Logging/LogMessageEntry.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace EntityFrameworkCore.CacheableTests.Logging 9 | { 10 | public struct LogMessageEntry 11 | { 12 | public LogMessageEntry(LogLevel logLevel 13 | , EventId eventId 14 | , string message 15 | , Exception exception) 16 | { 17 | TimeStamp = DateTime.Now; 18 | LogLevel = logLevel; 19 | EventId = eventId; 20 | Message = message; 21 | Exception = exception; 22 | } 23 | 24 | public DateTime TimeStamp; 25 | public LogLevel LogLevel; 26 | public EventId EventId; 27 | public String Message; 28 | public Exception Exception; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Describe what is not working as expected. 11 | 12 | If you are seeing an exception, include the full exceptions details (message and stack trace). 13 | 14 | ``` 15 | Exception message: 16 | Stack trace: 17 | ``` 18 | 19 | ### Steps to reproduce 20 | Include a complete code listing (or project/solution) that we can run to reproduce the issue. 21 | 22 | Partial code listings, or multiple fragments of code, will slow down our response or cause us to push the issue back to you to provide code to reproduce the issue. 23 | 24 | ```c# 25 | Console.WriteLine("Hello World!"); 26 | ``` 27 | 28 | ### Further technical details 29 | EntityFrameworkCore.Cacheable version: (found in project.csproj or packages.config) 30 | EF Core version: (found in project.csproj or packages.config) 31 | IDE: (e.g. Visual Studio 2017 15.4) 32 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/Logging/DebugLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace EntityFrameworkCore.CacheableTests.Logging 10 | { 11 | public sealed class DebugLoggerProvider : ILoggerProvider 12 | { 13 | private readonly ConcurrentDictionary _loggers; 14 | private readonly ConcurrentBag _messageQueue = new ConcurrentBag(); 15 | 16 | public IReadOnlyCollection Entries => _messageQueue; 17 | 18 | public DebugLoggerProvider() 19 | { 20 | _loggers = new ConcurrentDictionary(); 21 | } 22 | 23 | public ILogger CreateLogger(string categoryName) 24 | { 25 | return _loggers.GetOrAdd(categoryName, loggerName => new DebugLogger(_messageQueue)); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/AgnosticBloggingContext.cs: -------------------------------------------------------------------------------- 1 | using EntityFrameworkCore.Cacheable; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 5 | { 6 | public class AgnosticBloggingContext : DbContext 7 | { 8 | private readonly int? _minBlogId; 9 | 10 | public AgnosticBloggingContext() 11 | { } 12 | 13 | public AgnosticBloggingContext(DbContextOptions options, int? minBlogId = null) 14 | : base(options) 15 | { 16 | _minBlogId = minBlogId; 17 | } 18 | 19 | protected override void OnModelCreating(ModelBuilder modelBuilder) 20 | { 21 | base.OnModelCreating(modelBuilder); 22 | 23 | // Add global query filters to all entities 24 | if (_minBlogId.HasValue) 25 | { 26 | modelBuilder.Entity() 27 | .HasQueryFilter(e => e.BlogId >= _minBlogId); 28 | } 29 | } 30 | 31 | public DbSet Blogs { get; set; } 32 | public DbSet Posts { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/CacheableOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.Cacheable 8 | { 9 | /// 10 | /// Options how to handle result caching. 11 | /// 12 | public class CacheableOptions 13 | { 14 | /// 15 | /// Limits the lifetime of cached query results. Default value is 5 minutes. 16 | /// 17 | public TimeSpan TimeToLive { get; set; } = TimeSpan.FromMinutes(5); 18 | 19 | /// 20 | /// Set true if null result should be cached, otherwise false. Default value is true. 21 | /// 22 | public Boolean CacheNullResult { get; set; } = true; 23 | 24 | /// 25 | /// Set true if empty result should be cached, otherwise false. Default value is true. 26 | /// 27 | /// 28 | /// This options does not work in Async queries. 29 | /// 30 | public Boolean CacheEmptyResult { get; set; } = true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/Check.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace EntityFrameworkCore.Cacheable 8 | { 9 | /// 10 | /// Helper class to check parameter. 11 | /// 12 | public static class Check 13 | { 14 | /// 15 | /// Check if parameter is . 16 | /// 17 | /// Parameter value 18 | /// Parameter nam 19 | public static void NotNull(object obj, string name) 20 | { 21 | if (obj == null) 22 | throw new ArgumentNullException("name"); 23 | } 24 | 25 | /// 26 | /// Check if parameter is empty. 27 | /// 28 | /// Parameter value 29 | /// Parameter nam 30 | public static void NotEmpty(string obj, string name) 31 | { 32 | if (String.IsNullOrEmpty(obj)) 33 | throw new ArgumentNullException("name"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/CacheableOptionsExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Infrastructure; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace EntityFrameworkCore.Cacheable 8 | { 9 | public class CacheableOptionsExtension : IDbContextOptionsExtension 10 | { 11 | ICacheProvider _cacheProvider; 12 | 13 | internal CacheableOptionsExtension(ICacheProvider cacheProvider) 14 | { 15 | _cacheProvider = cacheProvider; 16 | } 17 | 18 | public string LogFragment => $"Using {_cacheProvider.GetType().Name}"; 19 | 20 | public bool ApplyServices(IServiceCollection services) 21 | { 22 | services.AddSingleton(_cacheProvider); 23 | 24 | return false; 25 | } 26 | 27 | public long GetServiceProviderHashCode() => 0L; 28 | 29 | public void Validate(IDbContextOptions options) 30 | { 31 | } 32 | 33 | /// 34 | /// The option set from the method. 35 | /// 36 | public virtual ICacheProvider CacheProvider => _cacheProvider; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/BusinessTestLogic/BloggingContext.cs: -------------------------------------------------------------------------------- 1 | using EntityFrameworkCore.Cacheable; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace EntityFrameworkCore.CacheableTests.BusinessTestLogic 5 | { 6 | public class BloggingContext : DbContext 7 | { 8 | private readonly int? _minBlogId; 9 | 10 | public BloggingContext() 11 | { } 12 | 13 | public BloggingContext(DbContextOptions options, int? minBlogId = null) 14 | : base(options) 15 | { 16 | _minBlogId = minBlogId; 17 | } 18 | 19 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 20 | { 21 | optionsBuilder.UseSecondLevelCache(); 22 | } 23 | 24 | protected override void OnModelCreating(ModelBuilder modelBuilder) 25 | { 26 | base.OnModelCreating(modelBuilder); 27 | 28 | // Add global query filters to all entities 29 | if (_minBlogId.HasValue) 30 | { 31 | modelBuilder.Entity() 32 | .HasQueryFilter(e => e.BlogId >= _minBlogId); 33 | } 34 | } 35 | 36 | public DbSet Blogs { get; set; } 37 | public DbSet Posts { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über folgende 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die einer Assembly zugeordnet sind. 8 | [assembly: AssemblyTitle("EntityFrameworkCore.CacheableTests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("EntityFrameworkCore.CacheableTests")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Wenn ComVisible auf "false" festgelegt wird, sind die Typen innerhalb dieser Assembly 18 | // für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von 19 | // COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. 20 | [assembly: ComVisible(false)] 21 | 22 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 23 | [assembly: Guid("f8950eb7-eaa3-42b3-b853-d8319a0845bd")] 24 | 25 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: 26 | // 27 | // Hauptversion 28 | // Nebenversion 29 | // Buildnummer 30 | // Revision 31 | // 32 | // Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern 33 | // übernehmen, indem Sie "*" eingeben: 34 | // [Assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/Logging/DebugLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace EntityFrameworkCore.CacheableTests.Logging 13 | { 14 | public class DebugLogger : ILogger 15 | { 16 | private const int _maxQueuedMessages = 1024; 17 | 18 | private readonly ConcurrentBag _messageQueue; 19 | 20 | public DebugLogger(ConcurrentBag messageQueue) 21 | { 22 | _messageQueue = messageQueue; 23 | } 24 | 25 | public IDisposable BeginScope(TState state) 26 | { 27 | return new Scope(); 28 | } 29 | 30 | public bool IsEnabled(LogLevel logLevel) 31 | { 32 | return true; 33 | } 34 | 35 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 36 | { 37 | var formatedValue = formatter(state, exception); 38 | var entry =new LogMessageEntry(logLevel, eventId, formatedValue, exception); 39 | 40 | //var result = JsonConvert.SerializeObject(entry, Formatting.Indented); 41 | //Debug.WriteLine(result); 42 | 43 | _messageQueue.Add(entry); 44 | } 45 | 46 | class Scope : IDisposable 47 | { 48 | public void Dispose() 49 | { 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/ICacheProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace EntityFrameworkCore.Cacheable 6 | { 7 | /// 8 | /// A cache provider to store and recieve query results./> 9 | /// 10 | public interface ICacheProvider 11 | { 12 | /// 13 | /// Creates a unique key to identify a query expression. 14 | /// 15 | /// Query expression 16 | /// Query parameter values 17 | /// Unique key object 18 | object CreateQueryKey(Expression expression, IReadOnlyDictionary parameterValues); 19 | 20 | /// 21 | /// Add a query result to cache storage, by given key. 22 | /// 23 | /// 24 | /// Key to identify query result 25 | /// Query result 26 | /// Options 27 | void SetCachedResult(object key, TResult value, TimeSpan timeToLive); 28 | 29 | /// 30 | /// Try to get a cached query result from storage. 31 | /// 32 | /// 33 | /// Key to identify query result 34 | /// Stored query result if key was found 35 | /// Returns when the given key was found otherwise. 36 | bool TryGetCachedResult(object key, out TResult cacheResult); 37 | } 38 | } -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/EntityFrameworkCore.Cacheable.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 2.0.1.0 6 | Steffen Mangold 7 | 8 | true 9 | true 10 | https://raw.githubusercontent.com/SteffenMangold/EntityFrameworkCore.Cacheable/master/nuget_icon_light.png 11 | https://github.com/SteffenMangold/EntityFrameworkCore.Cacheable/blob/master/LICENSE 12 | Copyright Steffen Mangold 2018 13 | entity-framework database c-sharp dotnet-core dotnet-standard dotnet-framework orm cache 14 | https://github.com/SteffenMangold/EntityFrameworkCore.Cacheable 15 | https://github.com/SteffenMangold/EntityFrameworkCore.Cacheable 16 | Entity Framework (EF) Core Cacheable is a high performance second level query cache extention library, for the popular Entity Framework data access technology. 17 | It provides caching functionality for all types of query results. Based on expression tree and parameters, the context decide rather to execute query against database or returning result from memory. 18 | nuget_icon_light.ico 19 | github 20 | 2.0.1.0 21 | 2.0.1.0 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.168 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C98FF34D-52C3-44DA-9740-263E47AB5490}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | LICENSE = LICENSE 11 | nuget_icon.png = nuget_icon.png 12 | nuget_icon_light.ico = nuget_icon_light.ico 13 | nuget_icon_light.png = nuget_icon_light.png 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.CacheableTests", "EntityFrameworkCore.CacheableTests\EntityFrameworkCore.CacheableTests.csproj", "{F8950EB7-EAA3-42B3-B853-D8319A0845BD}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Cacheable", "EntityFrameworkCore.Cacheable\EntityFrameworkCore.Cacheable.csproj", "{2E5890B2-9409-45CA-977A-5AC2F5B907B2}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {F8950EB7-EAA3-42B3-B853-D8319A0845BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {F8950EB7-EAA3-42B3-B853-D8319A0845BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {F8950EB7-EAA3-42B3-B853-D8319A0845BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {F8950EB7-EAA3-42B3-B853-D8319A0845BD}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {2E5890B2-9409-45CA-977A-5AC2F5B907B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {2E5890B2-9409-45CA-977A-5AC2F5B907B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {2E5890B2-9409-45CA-977A-5AC2F5B907B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {2E5890B2-9409-45CA-977A-5AC2F5B907B2}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {B94B9A84-34AD-4181-8409-76784A0DB0D1} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/Diagnostics/CoreEventId.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace EntityFrameworkCore.Cacheable.Diagnostics 10 | { 11 | /// 12 | /// 13 | /// Event IDs for events that correspond to messages logged to an 14 | /// and events sent to a . 15 | /// 16 | /// 17 | /// These IDs are also used with to configure the 18 | /// behavior of warnings. 19 | /// 20 | /// 21 | public static class CacheableEventId 22 | { 23 | /// 24 | /// The lower-bound for event IDs used by any Entity Framework or provider code. 25 | /// 26 | public const int CacheableBaseId = 50000; 27 | 28 | // Warning: These values must not change between releases. 29 | // Only add new values to the end of sections, never in the middle. 30 | // Try to use naming and be consistent with existing names. 31 | private enum Id 32 | { 33 | // Query events 34 | CacheHit = CacheableBaseId, 35 | QueryResultCached, 36 | //FirstWithoutOrderByAndFilterWarning, 37 | 38 | //// Infrastructure events 39 | //SensitiveDataLoggingEnabledWarning = CacheableBaseId + 100, 40 | //ServiceProviderCreated, 41 | } 42 | 43 | private static readonly string _queryPrefix = DbLoggerCategory.Query.Name + "."; 44 | private static EventId MakeQueryId(Id id) => new EventId((int)id, _queryPrefix + id); 45 | 46 | /// 47 | /// 48 | /// A query result is returned from cache. 49 | /// 50 | /// 51 | /// This event is in the category. 52 | /// 53 | /// 54 | public static readonly EventId CacheHit = MakeQueryId(Id.CacheHit); 55 | 56 | /// 57 | /// 58 | /// A query result is stored by the cache. 59 | /// 60 | /// 61 | /// This event is in the category. 62 | /// 63 | /// 64 | public static readonly EventId QueryResultCached = MakeQueryId(Id.QueryResultCached); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/ExpressionVisitors/CachableExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; 4 | using Microsoft.EntityFrameworkCore.Query.Internal; 5 | using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using System.Linq; 10 | using System.Linq.Expressions; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace EntityFrameworkCore.Cacheable.ExpressionVisitors 15 | { 16 | public class CachableExpressionVisitor : ExpressionVisitor 17 | { 18 | private Boolean _isCacheable = false; 19 | private CacheableOptions _options = null; 20 | 21 | public CachableExpressionVisitor() 22 | { 23 | } 24 | 25 | protected override Expression VisitMethodCall(MethodCallExpression node) 26 | { 27 | if (node.Method.IsGenericMethod) 28 | { 29 | var genericMethodDefinition = node.Method.GetGenericMethodDefinition(); 30 | 31 | // find cachable query extention calls 32 | if (genericMethodDefinition == EntityFrameworkQueryableExtensions.CacheableMethodInfo) 33 | { 34 | // get parameter with "last one win" 35 | _options = node.Arguments 36 | .OfType() 37 | .Where(a => a.Value is CacheableOptions) 38 | .Select(a => (CacheableOptions)a.Value) 39 | .Last(); 40 | 41 | _isCacheable = true; 42 | 43 | // cut out extension expression 44 | return Visit(node.Arguments[0]); 45 | } 46 | } 47 | 48 | return base.VisitMethodCall(node); 49 | } 50 | 51 | /// 52 | /// Visit the query expression tree and find extract cachable parameter 53 | /// 54 | /// Query expression 55 | /// Is expression marked as cacheable 56 | /// Timespan befor expiration of cached query result 57 | /// 58 | public virtual Expression GetExtractCachableParameter(Expression expression, out Boolean isCacheable, out CacheableOptions options) 59 | { 60 | var visitedExpression = Visit(expression); 61 | 62 | isCacheable = _isCacheable; 63 | options = _options; 64 | 65 | return visitedExpression; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/Extensions/EntityFrameworkQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Query; 2 | using Microsoft.EntityFrameworkCore.Query.Internal; 3 | using System; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | 8 | namespace EntityFrameworkCore.Cacheable 9 | { 10 | public static class EntityFrameworkQueryableExtensions 11 | { 12 | internal static readonly MethodInfo CacheableMethodInfo 13 | = typeof(EntityFrameworkQueryableExtensions) 14 | .GetTypeInfo() 15 | .GetMethods() 16 | .Where(m => m.Name == nameof(Cacheable)) 17 | .Where(m => m.GetParameters().Any(p => p.ParameterType == typeof(CacheableOptions))) 18 | .Single(); 19 | 20 | /// 21 | /// Returns a new query where the result will be cached base on the parameter. 22 | /// 23 | /// The type of entity being queried. 24 | /// The source query. 25 | /// Limits the lifetime of cached query results. 26 | /// A new query where the result set will be cached. 27 | public static IQueryable Cacheable(this IQueryable source, [NotParameterized] TimeSpan timeToLive) 28 | { 29 | Check.NotNull(source, nameof(source)); 30 | Check.NotNull(timeToLive, nameof(timeToLive)); 31 | 32 | return source.Cacheable(new CacheableOptions 33 | { 34 | TimeToLive = timeToLive 35 | }); 36 | } 37 | 38 | /// 39 | /// Returns a new query where the result will be cached base on the parameter. 40 | /// 41 | /// The type of entity being queried. 42 | /// The source query. 43 | /// Options how to handle cached query results. 44 | /// A new query where the result set will be cached. 45 | public static IQueryable Cacheable(this IQueryable source, [NotParameterized] CacheableOptions options) 46 | { 47 | Check.NotNull(source, nameof(source)); 48 | Check.NotNull(options, nameof(options)); 49 | 50 | return 51 | source.Provider is EntityQueryProvider 52 | ? source.Provider.CreateQuery( 53 | Expression.Call( 54 | instance: null, 55 | method: CacheableMethodInfo.MakeGenericMethod(typeof(T)), 56 | arg0: source.Expression, 57 | arg1: Expression.Constant(options))) 58 | : source; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/Extensions/DbContextOptionsBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | using Microsoft.EntityFrameworkCore.Query.Internal; 4 | 5 | namespace EntityFrameworkCore.Cacheable 6 | { 7 | public static class DbContextOptionsBuilderExtensions 8 | { 9 | /// 10 | /// Configures the context to support second level query caching. 11 | /// 12 | /// The builder being used to configure the context. 13 | /// The options builder so that further configuration can be chained. 14 | public static DbContextOptionsBuilder UseSecondLevelCache(this DbContextOptionsBuilder optionsBuilder) 15 | { 16 | return optionsBuilder.UseSecondLevelCache(new MemoryCacheProvider()); 17 | } 18 | 19 | /// 20 | /// Configures the context to support second level query caching. 21 | /// 22 | /// The builder being used to configure the context. 23 | /// The cache provider to storage query results. 24 | /// The options builder so that further configuration can be chained. 25 | public static DbContextOptionsBuilder UseSecondLevelCache(this DbContextOptionsBuilder optionsBuilder, ICacheProvider cacheProvider) 26 | { 27 | optionsBuilder.ReplaceService(); 28 | 29 | ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(new CacheableOptionsExtension(cacheProvider)); 30 | 31 | return optionsBuilder; 32 | } 33 | 34 | /// 35 | /// Configures the context to support second level query caching. 36 | /// 37 | /// The builder being used to configure the context. 38 | /// The options builder so that further configuration can be chained. 39 | public static DbContextOptionsBuilder UseSecondLevelCache(this DbContextOptionsBuilder optionsBuilder) where TContext : DbContext 40 | { 41 | return optionsBuilder.UseSecondLevelCache(new MemoryCacheProvider()); 42 | } 43 | 44 | /// 45 | /// Configures the context to support second level query caching. 46 | /// 47 | /// The builder being used to configure the context. 48 | /// The options builder so that further configuration can be chained. 49 | public static DbContextOptionsBuilder UseSecondLevelCache(this DbContextOptionsBuilder optionsBuilder, ICacheProvider cacheProvider) where TContext : DbContext 50 | { 51 | optionsBuilder.ReplaceService(); 52 | 53 | ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(new CacheableOptionsExtension(cacheProvider)); 54 | 55 | return optionsBuilder; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/MemoryCacheProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Query.Expressions.Internal; 2 | using Microsoft.EntityFrameworkCore.Query.Internal; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Microsoft.Extensions.Internal; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Data.HashFunction; 8 | using System.Data.HashFunction.xxHash; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Linq.Expressions; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace EntityFrameworkCore.Cacheable 16 | { 17 | /// 18 | /// Default CacheProvider based on ./> 19 | /// 20 | public class MemoryCacheProvider : ICacheProvider 21 | { 22 | private static IHashFunction __hashFunction; 23 | private static readonly Object __syncLock = new Object(); 24 | 25 | private static IMemoryCache __cache = new MemoryCache(new MemoryCacheOptions() 26 | { 27 | Clock = new SystemClock() 28 | }); 29 | 30 | /// 31 | /// Creates a new instance. 32 | /// 33 | /// 34 | /// Set a hash seed. 35 | /// 36 | public MemoryCacheProvider(ulong seed) 37 | { 38 | __hashFunction = xxHashFactory.Instance.Create(new xxHashConfig 39 | { 40 | // 64 bit size is also used by MS-SQL server to identify queries 41 | HashSizeInBits = 64, 42 | Seed = seed 43 | }); 44 | } 45 | 46 | /// 47 | /// Creates a new instance. 48 | /// 49 | public MemoryCacheProvider() : this(0UL) 50 | { 51 | } 52 | 53 | /// 54 | /// Try to get a cached query result from storage. 55 | /// 56 | /// 57 | /// Key to identify query result 58 | /// Stored query result if key was found 59 | /// Returns when the given key was found otherwise. 60 | public Boolean TryGetCachedResult(object key, out TResult cacheResult) 61 | { 62 | return __cache.TryGetValue(key, out cacheResult); 63 | } 64 | 65 | /// 66 | /// Add a query result to cache storage, by given key. 67 | /// 68 | /// 69 | /// Key to identify query result 70 | /// Query result 71 | /// Options 72 | public void SetCachedResult(object key, TResult value, TimeSpan timeToLive) 73 | { 74 | __cache.Set(key, value, timeToLive); 75 | } 76 | 77 | /// 78 | /// Creates a unique key to identify a query expression. 79 | /// 80 | /// Query expression 81 | /// Query parameter values 82 | /// Unique key object 83 | public object CreateQueryKey(Expression expression, IReadOnlyDictionary parameterValues) 84 | { 85 | // use internal hash cide to identify base query 86 | var expressionHashCode = ExpressionEqualityComparer.Instance.GetHashCode(expression); 87 | 88 | // creating a Uniform Resource Identifier 89 | var expressionCacheKey = $"hash://{expressionHashCode}"; 90 | 91 | // if query has parameter add key values as uri-query string 92 | if (parameterValues.Count > 0) 93 | { 94 | var parameterStrings = parameterValues.Select(d => $"{d.Key}={d.Value?.GetHashCode()}"); 95 | expressionCacheKey += $"?{String.Join("&", parameterStrings)}"; 96 | } 97 | 98 | var hash = __hashFunction.ComputeHash(expressionCacheKey); 99 | 100 | return hash.AsBase64String(); 101 | } 102 | 103 | public static void ClearCache() 104 | { 105 | var oldCache = __cache; 106 | 107 | __cache = new MemoryCache(new MemoryCacheOptions() 108 | { 109 | Clock = new SystemClock() 110 | }); 111 | 112 | oldCache.Dispose(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 |

6 | 7 |

8 | EntityFrameworkCore.Cacheable 9 |

10 | 11 |

12 | A high-performance second-level query cache for Entity Framework Core. 13 |

14 | 15 |

16 | 17 | 18 | 19 | 20 |

21 | 22 |
23 |
24 | 25 | ## What is EF Core Cacheable? 26 | 27 | Entity Framework (EF) Core Cacheable is an extension library for the popular Entity Framework data access technology. 28 | 29 | It provides caching functionality for all types of query results. Based on the expression tree and parameters, the context decides whether to execute the query against the database or return the result from memory. 30 | 31 | ## How caching affects performance 32 | 33 | 34 | This is a sample result of 1,000 iterations of an uncached and cached query called against a well-performing MSSQL database. 35 | 36 | ``` 37 | Average database query duration [+00:00:00.1698972]. 38 | Average cache query duration [+00:00:00.0000650]. 39 | Cached queries are x2,611 times faster. 40 | ``` 41 | 42 | Even with an InMemory test database, the results are significantly faster. 43 | 44 | ``` 45 | Average database query duration [+00:00:00.0026076]. 46 | Average cache query duration [+00:00:00.0000411]. 47 | Cached queries are x63 times faster. 48 | ``` 49 | 50 | The performance gain can be even higher, depending on the database performance. 51 | 52 | 53 | ## Install via NuGet 54 | 55 | You can view the [package page on NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Cacheable/). 56 | 57 | To install `EntityFrameworkCore.Cacheable`, run the following command in the Package Manager Console: 58 | 59 | ``` 60 | PM> Install-Package EntityFrameworkCore.Cacheable 61 | ``` 62 | 63 | 64 | This library also uses the [Data.HashFunction](https://github.com/brandondahler/Data.HashFunction/) and [aspnet.Extensions](https://github.com/aspnet/Extensions) as InMemory cache. 65 | 66 | 67 | ## Configuring a DbContext 68 | 69 | There are three types of configurations for the DbContext to support `Cachable.` 70 | Each sample only uses' UseSqlite' to show the pattern. 71 | 72 | For more information about this, please read [configuring DbContextOptions](https://docs.microsoft.com/de-de/ef/core/miscellaneous/configuring-dbcontext#configuring-dbcontextoptions). 73 | 74 | ### Constructor argument 75 | 76 | Application code to initialize from constructor argument: 77 | 78 | ```csharp 79 | var optionsBuilder = new DbContextOptionsBuilder(); 80 | optionsBuilder 81 | .UseSqlite("Data Source=blog.db") 82 | .UseSecondLevelCache(); 83 | 84 | using (var context = new CacheableBloggingContext(optionsBuilder.Options)) 85 | { 86 | // do stuff 87 | } 88 | ``` 89 | 90 | ### OnConfiguring 91 | 92 | Context code with `OnConfiguring`: 93 | 94 | ```csharp 95 | public partial class CacheableBloggingContext : DbContext 96 | { 97 | public DbSet Blogs { get; set; } 98 | 99 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 100 | { 101 | if (!optionsBuilder.IsConfigured) 102 | { 103 | optionsBuilder.UseSqlite("Data Source=blog.db"); 104 | optionsBuilder.UseSecondLevelCache(); 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | ### Using DbContext with dependency injection 111 | 112 | Adding the Dbcontext to dependency injection: 113 | 114 | ```csharp 115 | public void ConfigureServices(IServiceCollection services) 116 | { 117 | services.AddDbContext(options => options 118 | .UseSqlite("Data Source=blog.db")) 119 | .UseSecondLevelCache(); 120 | } 121 | ``` 122 | 123 | 124 | This requires [adding a constructor argument](https://docs.microsoft.com/de-de/ef/core/miscellaneous/configuring-dbcontext#using-dbcontext-with-dependency-injection) to your DbContext type that accepts DbContextOptions. 125 | 126 | 127 | ## Usage 128 | 129 | To use result caching, you simply need to add `.Cacheable(...` to your query and define a TTL parameter. 130 | 131 | 132 | ```csharp 133 | var cacheableQuery = cacheableContext.Books 134 | .Include(d => d.Pages) 135 | .ThenInclude(d => d.Lines) 136 | .Where(d => d.ID == 200) 137 | .Cacheable(TimeSpan.FromSeconds(60)); 138 | ``` 139 | 140 | ### Custom Cache Provider 141 | 142 | 143 | Alternatively, you can provide a custom implementation of `ICacheProvider` (default is `MemoryCacheProvider`). 144 | This provides an easy option for supporting other caching systems like [![](https://redis.io/images/favicon.png) redis](https://redis.io/) or [Memcached](https://memcached.org/). 145 | 146 | ```csharp 147 | optionsBuilder.UseSecondLevelCache(new MyCachingProvider()); 148 | ``` 149 | 150 | 151 | ----------------- 152 | 153 | 154 | ## Contributors 155 | 156 | The following contributors have either created (that only me:stuck_out_tongue_winking_eye:) the project, have contributed 157 | code, are actively maintaining it (including documentation), or in other ways 158 | being helpful contributors to this project. 159 | 160 | 161 | | | Name | GitHub | 162 | | :--------------------------------------------------------------------------------: | --------------------- | ------------------------------------------------------- | 163 | | | Steffen Mangold | [@SteffenMangold](https://github.com/SteffenMangold) | 164 | | | Smit Patel | [@smitpatel](https://github.com/smitpatel) | 165 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/CacheableAsyncExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using EntityFrameworkCore.Cacheable.Diagnostics; 2 | using EntityFrameworkCore.CacheableTests; 3 | using EntityFrameworkCore.CacheableTests.BusinessTestLogic; 4 | using EntityFrameworkCore.CacheableTests.Logging; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Diagnostics; 11 | using System.Linq; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace EntityFrameworkCore.Cacheable.Tests 16 | { 17 | [TestClass] 18 | [TestCategory("EntityFrameworkCore.Cacheable.AsyncExpressions")] 19 | public class CacheableAsyncExpressionTests 20 | { 21 | /// 22 | /// Testing entity result cache functionality. 23 | /// 24 | [TestMethod] 25 | public async Task EntityAsyncExpressionTest() 26 | { 27 | MemoryCacheProvider.ClearCache(); 28 | 29 | var loggerProvider = new DebugLoggerProvider(); 30 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 31 | 32 | var options = new DbContextOptionsBuilder() 33 | .UseLoggerFactory(loggerFactory) 34 | .UseInMemoryDatabase(databaseName: "EntityExpressionTest") 35 | .Options; 36 | 37 | // create test entries 38 | using (var initContext = new BloggingContext(options)) 39 | { 40 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 41 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 42 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 43 | initContext.SaveChanges(); 44 | } 45 | 46 | using (var entityContext = new BloggingContext(options)) 47 | { 48 | // shoud not hit cache, because first execution 49 | var result = await entityContext.Blogs 50 | .Where(d => d.BlogId > 1) 51 | .Cacheable(TimeSpan.FromMinutes(5)) 52 | .ToListAsync(); 53 | 54 | // shoud hit cache, because second execution 55 | var cachedResult = await entityContext.Blogs 56 | .Where(d => d.BlogId > 1) 57 | .Cacheable(TimeSpan.FromMinutes(5)) 58 | .ToListAsync(); 59 | 60 | Assert.AreEqual(2, result.Count); 61 | Assert.AreEqual(result.Count, cachedResult.Count); 62 | } 63 | 64 | // find "cache hit" log entries 65 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 66 | 67 | // cache should hit one time 68 | Assert.IsTrue(logs.Count() == 1); 69 | } 70 | 71 | /// 72 | /// Testing projection result cache functionality. 73 | /// 74 | [TestMethod] 75 | public async Task ProjectionAsyncExpressionTest() 76 | { 77 | MemoryCacheProvider.ClearCache(); 78 | 79 | var loggerProvider = new DebugLoggerProvider(); 80 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 81 | 82 | var options = new DbContextOptionsBuilder() 83 | .UseLoggerFactory(loggerFactory) 84 | .UseInMemoryDatabase(databaseName: "ProjectionExpressionTest") 85 | .Options; 86 | 87 | // create test entries 88 | using (var initContext = new BloggingContext(options)) 89 | { 90 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 91 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 92 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 93 | initContext.SaveChanges(); 94 | } 95 | 96 | using (var projectionContext = new BloggingContext(options)) 97 | { 98 | // shoud not hit cache, because first execution 99 | var result = await projectionContext.Blogs 100 | .Where(d => d.BlogId > 1) 101 | .Select(d => new 102 | { 103 | d.BlogId, 104 | d.Rating 105 | }) 106 | .Cacheable(TimeSpan.FromMinutes(5)) 107 | .ToListAsync(); 108 | 109 | // shoud hit cache, because second execution 110 | var cachedResult = await projectionContext.Blogs 111 | .Where(d => d.BlogId > 1) 112 | .Select(d => new 113 | { 114 | d.BlogId, 115 | d.Rating 116 | }) 117 | .Cacheable(TimeSpan.FromMinutes(5)) 118 | .ToListAsync(); 119 | 120 | Assert.AreEqual(2, result.Count); 121 | Assert.AreEqual(result.Count, cachedResult.Count); 122 | } 123 | 124 | // find "cache hit" log entries 125 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 126 | 127 | // cache should hit one time 128 | Assert.IsTrue(logs.Count() == 1); 129 | } 130 | 131 | /// 132 | /// Testing projection result cache functionality. 133 | /// 134 | [TestMethod] 135 | public async Task SingleProjectionAsyncExpressionTest() 136 | { 137 | MemoryCacheProvider.ClearCache(); 138 | 139 | var loggerProvider = new DebugLoggerProvider(); 140 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 141 | 142 | var options = new DbContextOptionsBuilder() 143 | .UseLoggerFactory(loggerFactory) 144 | .UseInMemoryDatabase(databaseName: "ProjectionExpressionTest") 145 | .Options; 146 | 147 | // create test entries 148 | using (var initContext = new BloggingContext(options)) 149 | { 150 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 151 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 152 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 153 | initContext.SaveChanges(); 154 | } 155 | 156 | using (var projectionContext = new BloggingContext(options)) 157 | { 158 | // shoud not hit cache, because first execution 159 | var result = await projectionContext.Blogs 160 | .Where(d => d.BlogId == 1) 161 | .Select(d => new 162 | { 163 | d.BlogId, 164 | d.Rating 165 | }) 166 | .Cacheable(TimeSpan.FromMinutes(5)) 167 | .SingleOrDefaultAsync(); 168 | 169 | // shoud hit cache, because second execution 170 | var cachedResult = await projectionContext.Blogs 171 | .Where(d => d.BlogId == 1) 172 | .Select(d => new 173 | { 174 | d.BlogId, 175 | d.Rating 176 | }) 177 | .Cacheable(TimeSpan.FromMinutes(5)) 178 | .SingleOrDefaultAsync(); 179 | 180 | Assert.IsNotNull(result); 181 | Assert.AreSame(result, cachedResult); 182 | } 183 | 184 | // find "cache hit" log entries 185 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 186 | 187 | // cache should hit one time 188 | Assert.IsTrue(logs.Count() == 1); 189 | } 190 | 191 | /// 192 | /// Testing constant result cache functionality. 193 | /// 194 | [TestMethod] 195 | public async Task ConstantAsyncExpressionTest() 196 | { 197 | MemoryCacheProvider.ClearCache(); 198 | 199 | var loggerProvider = new DebugLoggerProvider(); 200 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 201 | 202 | var options = new DbContextOptionsBuilder() 203 | .UseLoggerFactory(loggerFactory) 204 | .UseInMemoryDatabase(databaseName: "ConstantExpressionTest") 205 | .Options; 206 | 207 | // create test entries 208 | using (var initContext = new BloggingContext(options)) 209 | { 210 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 211 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 212 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 213 | initContext.SaveChanges(); 214 | } 215 | 216 | using (var constantContext = new BloggingContext(options)) 217 | { 218 | // shoud not hit cache, because first execution 219 | var result = await constantContext.Blogs 220 | .Where(d => d.BlogId > 1) 221 | .Select(d => d.BlogId) 222 | .Cacheable(TimeSpan.FromMinutes(5)) 223 | .ToListAsync(); 224 | 225 | // shoud hit cache, because second execution 226 | var cachedResult = await constantContext.Blogs 227 | .Where(d => d.BlogId > 1) 228 | .Select(d => d.BlogId) 229 | .Cacheable(TimeSpan.FromMinutes(5)) 230 | .ToListAsync(); 231 | 232 | Assert.AreEqual(2, result.Count); 233 | Assert.AreEqual(result.Count, cachedResult.Count); 234 | } 235 | 236 | // find "cache hit" log entries 237 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 238 | 239 | // cache should hit one time 240 | Assert.IsTrue(logs.Count() == 1); 241 | } 242 | } 243 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/Extensions/SharedTypeExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Runtime.CompilerServices; 9 | 10 | // ReSharper disable once CheckNamespace 11 | namespace System 12 | { 13 | [DebuggerStepThrough] 14 | internal static class SharedTypeExtensions 15 | { 16 | public static Type UnwrapNullableType(this Type type) => Nullable.GetUnderlyingType(type) ?? type; 17 | 18 | public static bool IsNullableType(this Type type) 19 | { 20 | var typeInfo = type.GetTypeInfo(); 21 | 22 | return !typeInfo.IsValueType 23 | || typeInfo.IsGenericType 24 | && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>); 25 | } 26 | 27 | public static bool IsValidEntityType(this Type type) 28 | => type.GetTypeInfo().IsClass; 29 | 30 | public static Type MakeNullable(this Type type, bool nullable = true) 31 | => type.IsNullableType() == nullable 32 | ? type 33 | : nullable 34 | ? typeof(Nullable<>).MakeGenericType(type) 35 | : type.UnwrapNullableType(); 36 | 37 | public static bool IsNumeric(this Type type) 38 | { 39 | type = type.UnwrapNullableType(); 40 | 41 | return type.IsInteger() 42 | || type == typeof(decimal) 43 | || type == typeof(float) 44 | || type == typeof(double); 45 | } 46 | 47 | public static bool IsInteger(this Type type) 48 | { 49 | type = type.UnwrapNullableType(); 50 | 51 | return type == typeof(int) 52 | || type == typeof(long) 53 | || type == typeof(short) 54 | || type == typeof(byte) 55 | || type == typeof(uint) 56 | || type == typeof(ulong) 57 | || type == typeof(ushort) 58 | || type == typeof(sbyte) 59 | || type == typeof(char); 60 | } 61 | 62 | public static bool IsSignedInteger(this Type type) 63 | => type == typeof(int) 64 | || type == typeof(long) 65 | || type == typeof(short) 66 | || type == typeof(sbyte); 67 | 68 | public static bool IsAnonymousType(this Type type) 69 | => type.Name.StartsWith("<>") 70 | && type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), inherit: false).Length > 0 71 | && type.Name.Contains("AnonymousType"); 72 | 73 | public static bool IsTupleType(this Type type) 74 | { 75 | if (type == typeof(Tuple)) 76 | { 77 | return true; 78 | } 79 | 80 | if (type.IsGenericType) 81 | { 82 | var genericDefinition = type.GetGenericTypeDefinition(); 83 | if (genericDefinition == typeof(Tuple<>) 84 | || genericDefinition == typeof(Tuple<,>) 85 | || genericDefinition == typeof(Tuple<,,>) 86 | || genericDefinition == typeof(Tuple<,,,>) 87 | || genericDefinition == typeof(Tuple<,,,,>) 88 | || genericDefinition == typeof(Tuple<,,,,,>) 89 | || genericDefinition == typeof(Tuple<,,,,,,>) 90 | || genericDefinition == typeof(Tuple<,,,,,,,>) 91 | || genericDefinition == typeof(Tuple<,,,,,,,>)) 92 | { 93 | return true; 94 | } 95 | } 96 | 97 | return false; 98 | } 99 | 100 | public static PropertyInfo GetAnyProperty(this Type type, string name) 101 | { 102 | var props = type.GetRuntimeProperties().Where(p => p.Name == name).ToList(); 103 | if (props.Count > 1) 104 | { 105 | throw new AmbiguousMatchException(); 106 | } 107 | 108 | return props.SingleOrDefault(); 109 | } 110 | 111 | public static bool IsInstantiable(this Type type) => IsInstantiable(type.GetTypeInfo()); 112 | 113 | private static bool IsInstantiable(TypeInfo type) 114 | => !type.IsAbstract 115 | && !type.IsInterface 116 | && (!type.IsGenericType || !type.IsGenericTypeDefinition); 117 | 118 | public static bool IsGrouping(this Type type) => IsGrouping(type.GetTypeInfo()); 119 | 120 | private static bool IsGrouping(TypeInfo type) 121 | => type.IsGenericType 122 | && (type.GetGenericTypeDefinition() == typeof(IGrouping<,>) 123 | || type.GetGenericTypeDefinition() == typeof(IAsyncGrouping<,>)); 124 | 125 | public static Type UnwrapEnumType(this Type type) 126 | { 127 | var isNullable = type.IsNullableType(); 128 | var underlyingNonNullableType = isNullable ? type.UnwrapNullableType() : type; 129 | if (!underlyingNonNullableType.GetTypeInfo().IsEnum) 130 | { 131 | return type; 132 | } 133 | 134 | var underlyingEnumType = Enum.GetUnderlyingType(underlyingNonNullableType); 135 | return isNullable ? MakeNullable(underlyingEnumType) : underlyingEnumType; 136 | } 137 | 138 | public static Type GetSequenceType(this Type type) 139 | { 140 | var sequenceType = TryGetSequenceType(type); 141 | if (sequenceType == null) 142 | { 143 | // TODO: Add exception message 144 | throw new ArgumentException(); 145 | } 146 | 147 | return sequenceType; 148 | } 149 | 150 | public static Type TryGetSequenceType(this Type type) 151 | => type.TryGetElementType(typeof(IEnumerable<>)) 152 | ?? type.TryGetElementType(typeof(IAsyncEnumerable<>)); 153 | 154 | public static Type TryGetElementType(this Type type, Type interfaceOrBaseType) 155 | { 156 | if (type.GetTypeInfo().IsGenericTypeDefinition) 157 | { 158 | return null; 159 | } 160 | 161 | var types = GetGenericTypeImplementations(type, interfaceOrBaseType); 162 | 163 | Type singleImplementation = null; 164 | foreach (var impelementation in types) 165 | { 166 | if (singleImplementation == null) 167 | { 168 | singleImplementation = impelementation; 169 | } 170 | else 171 | { 172 | singleImplementation = null; 173 | break; 174 | } 175 | } 176 | 177 | return singleImplementation?.GetTypeInfo().GenericTypeArguments.FirstOrDefault(); 178 | } 179 | 180 | public static IEnumerable GetGenericTypeImplementations(this Type type, Type interfaceOrBaseType) 181 | { 182 | var typeInfo = type.GetTypeInfo(); 183 | if (!typeInfo.IsGenericTypeDefinition) 184 | { 185 | var baseTypes = interfaceOrBaseType.GetTypeInfo().IsInterface 186 | ? typeInfo.ImplementedInterfaces 187 | : type.GetBaseTypes(); 188 | foreach (var baseType in baseTypes) 189 | { 190 | if (baseType.GetTypeInfo().IsGenericType 191 | && baseType.GetGenericTypeDefinition() == interfaceOrBaseType) 192 | { 193 | yield return baseType; 194 | } 195 | } 196 | 197 | if (type.GetTypeInfo().IsGenericType 198 | && type.GetGenericTypeDefinition() == interfaceOrBaseType) 199 | { 200 | yield return type; 201 | } 202 | } 203 | } 204 | 205 | public static IEnumerable GetBaseTypes(this Type type) 206 | { 207 | type = type.GetTypeInfo().BaseType; 208 | 209 | while (type != null) 210 | { 211 | yield return type; 212 | 213 | type = type.GetTypeInfo().BaseType; 214 | } 215 | } 216 | 217 | public static IEnumerable GetTypesInHierarchy(this Type type) 218 | { 219 | while (type != null) 220 | { 221 | yield return type; 222 | 223 | type = type.GetTypeInfo().BaseType; 224 | } 225 | } 226 | 227 | public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[] types) 228 | { 229 | types = types ?? Array.Empty(); 230 | 231 | return type.GetTypeInfo().DeclaredConstructors 232 | .SingleOrDefault( 233 | c => !c.IsStatic 234 | && c.GetParameters().Select(p => p.ParameterType).SequenceEqual(types)); 235 | } 236 | 237 | public static IEnumerable GetPropertiesInHierarchy(this Type type, string name) 238 | { 239 | do 240 | { 241 | var typeInfo = type.GetTypeInfo(); 242 | foreach (var propertyInfo in typeInfo.DeclaredProperties) 243 | { 244 | if (propertyInfo.Name.Equals(name, StringComparison.Ordinal) 245 | && !(propertyInfo.GetMethod ?? propertyInfo.SetMethod).IsStatic) 246 | { 247 | yield return propertyInfo; 248 | } 249 | } 250 | 251 | type = typeInfo.BaseType; 252 | } 253 | while (type != null); 254 | } 255 | 256 | public static IEnumerable GetMembersInHierarchy(this Type type) 257 | { 258 | do 259 | { 260 | // Do the whole hierarchy for properties first since looking for fields is slower. 261 | foreach (var propertyInfo in type.GetRuntimeProperties().Where(pi => !(pi.GetMethod ?? pi.SetMethod).IsStatic)) 262 | { 263 | yield return propertyInfo; 264 | } 265 | 266 | foreach (var fieldInfo in type.GetRuntimeFields().Where(f => !f.IsStatic)) 267 | { 268 | yield return fieldInfo; 269 | } 270 | 271 | type = type.BaseType; 272 | } 273 | while (type != null); 274 | } 275 | 276 | public static IEnumerable GetMembersInHierarchy(this Type type, string name) 277 | => type.GetMembersInHierarchy().Where(m => m.Name == name); 278 | 279 | private static readonly Dictionary _commonTypeDictionary = new Dictionary 280 | { 281 | #pragma warning disable IDE0034 // Simplify 'default' expression - default causes default(object) 282 | { typeof(int), default(int) }, 283 | { typeof(Guid), default(Guid) }, 284 | { typeof(DateTime), default(DateTime) }, 285 | { typeof(DateTimeOffset), default(DateTimeOffset) }, 286 | { typeof(long), default(long) }, 287 | { typeof(bool), default(bool) }, 288 | { typeof(double), default(double) }, 289 | { typeof(short), default(short) }, 290 | { typeof(float), default(float) }, 291 | { typeof(byte), default(byte) }, 292 | { typeof(char), default(char) }, 293 | { typeof(uint), default(uint) }, 294 | { typeof(ushort), default(ushort) }, 295 | { typeof(ulong), default(ulong) }, 296 | { typeof(sbyte), default(sbyte) } 297 | #pragma warning restore IDE0034 // Simplify 'default' expression 298 | }; 299 | 300 | public static object GetDefaultValue(this Type type) 301 | { 302 | if (!type.GetTypeInfo().IsValueType) 303 | { 304 | return null; 305 | } 306 | 307 | // A bit of perf code to avoid calling Activator.CreateInstance for common types and 308 | // to avoid boxing on every call. This is about 50% faster than just calling CreateInstance 309 | // for all value types. 310 | return _commonTypeDictionary.TryGetValue(type, out var value) 311 | ? value 312 | : Activator.CreateInstance(type); 313 | } 314 | 315 | public static IEnumerable GetConstructibleTypes(this Assembly assembly) 316 | => assembly.GetLoadableDefinedTypes().Where( 317 | t => !t.IsAbstract 318 | && !t.IsGenericTypeDefinition); 319 | 320 | public static IEnumerable GetLoadableDefinedTypes(this Assembly assembly) 321 | { 322 | try 323 | { 324 | return assembly.DefinedTypes; 325 | } 326 | catch (ReflectionTypeLoadException ex) 327 | { 328 | return ex.Types.Where(t => t != null).Select(IntrospectionExtensions.GetTypeInfo); 329 | } 330 | } 331 | } 332 | } -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/EntityFrameworkCore.CacheableTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {F8950EB7-EAA3-42B3-B853-D8319A0845BD} 8 | Library 9 | Properties 10 | EntityFrameworkCore.CacheableTests 11 | EntityFrameworkCore.CacheableTests 12 | v4.6.2 13 | 512 14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 10.0 16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 18 | False 19 | UnitTest 20 | 21 | 22 | 23 | 24 | 25 | true 26 | full 27 | false 28 | bin\Debug\ 29 | DEBUG;TRACE 30 | prompt 31 | 4 32 | 7.2 33 | 34 | 35 | pdbonly 36 | true 37 | bin\Release\ 38 | TRACE 39 | prompt 40 | 4 41 | 7.2 42 | 43 | 44 | 45 | ..\packages\Microsoft.EntityFrameworkCore.2.2.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.dll 46 | 47 | 48 | ..\packages\Microsoft.EntityFrameworkCore.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.Abstractions.dll 49 | 50 | 51 | ..\packages\Microsoft.EntityFrameworkCore.InMemory.2.2.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.InMemory.dll 52 | 53 | 54 | ..\packages\Microsoft.Extensions.Caching.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll 55 | 56 | 57 | ..\packages\Microsoft.Extensions.Caching.Memory.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll 58 | 59 | 60 | ..\packages\Microsoft.Extensions.Configuration.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.dll 61 | 62 | 63 | ..\packages\Microsoft.Extensions.Configuration.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll 64 | 65 | 66 | ..\packages\Microsoft.Extensions.Configuration.Binder.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.Binder.dll 67 | 68 | 69 | ..\packages\Microsoft.Extensions.DependencyInjection.2.2.0\lib\net461\Microsoft.Extensions.DependencyInjection.dll 70 | 71 | 72 | ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll 73 | 74 | 75 | ..\packages\Microsoft.Extensions.Logging.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Logging.dll 76 | 77 | 78 | ..\packages\Microsoft.Extensions.Logging.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll 79 | 80 | 81 | ..\packages\Microsoft.Extensions.Options.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Options.dll 82 | 83 | 84 | ..\packages\Microsoft.Extensions.Primitives.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll 85 | 86 | 87 | ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll 88 | 89 | 90 | ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll 91 | 92 | 93 | ..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll 94 | 95 | 96 | ..\packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll 97 | 98 | 99 | 100 | ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll 101 | 102 | 103 | ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll 104 | 105 | 106 | ..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll 107 | 108 | 109 | 110 | ..\packages\System.Data.HashFunction.Core.2.0.0\lib\net45\System.Data.HashFunction.Core.dll 111 | 112 | 113 | ..\packages\System.Data.HashFunction.Interfaces.2.0.0\lib\net45\System.Data.HashFunction.Interfaces.dll 114 | 115 | 116 | ..\packages\System.Data.HashFunction.xxHash.2.0.0\lib\net45\System.Data.HashFunction.xxHash.dll 117 | 118 | 119 | ..\packages\System.Diagnostics.DiagnosticSource.4.5.1\lib\net46\System.Diagnostics.DiagnosticSource.dll 120 | 121 | 122 | ..\packages\System.Interactive.Async.3.2.0\lib\net46\System.Interactive.Async.dll 123 | 124 | 125 | ..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll 126 | 127 | 128 | 129 | ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 130 | 131 | 132 | ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | {2e5890b2-9409-45ca-977a-5ac2f5b907b2} 167 | EntityFrameworkCore.Cacheable 168 | 169 | 170 | 171 | 172 | 173 | 174 | False 175 | 176 | 177 | False 178 | 179 | 180 | False 181 | 182 | 183 | False 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}". 193 | 194 | 195 | 196 | 197 | 198 | 205 | -------------------------------------------------------------------------------- /EntityFrameworkCore.Cacheable/CustomQueryCompiler.cs: -------------------------------------------------------------------------------- 1 | using EntityFrameworkCore.Cacheable.Diagnostics; 2 | using EntityFrameworkCore.Cacheable.ExpressionVisitors; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Diagnostics; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Internal; 7 | using Microsoft.EntityFrameworkCore.Query; 8 | using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; 9 | using Microsoft.EntityFrameworkCore.Query.Internal; 10 | using Microsoft.EntityFrameworkCore.Storage; 11 | using Microsoft.Extensions.Logging; 12 | using Remotion.Linq.Clauses.StreamedData; 13 | using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; 14 | using System; 15 | using System.Collections; 16 | using System.Collections.Generic; 17 | using System.Diagnostics; 18 | using System.Linq; 19 | using System.Linq.Expressions; 20 | using System.Reflection; 21 | using System.Runtime.ExceptionServices; 22 | using System.Threading; 23 | using System.Threading.Tasks; 24 | 25 | namespace EntityFrameworkCore.Cacheable 26 | { 27 | /// 28 | /// Extended to handle query caching. 29 | /// 30 | public class CustomQueryCompiler : QueryCompiler 31 | { 32 | private readonly IQueryContextFactory _queryContextFactory; 33 | private readonly ICompiledQueryCache _compiledQueryCache; 34 | private readonly ICompiledQueryCacheKeyGenerator _compiledQueryCacheKeyGenerator; 35 | private readonly IDatabase _database; 36 | private readonly IDiagnosticsLogger _logger; 37 | private readonly IQueryModelGenerator _queryModelGenerator; 38 | 39 | private readonly Type _contextType; 40 | private readonly ICacheProvider _cacheProvider; 41 | 42 | private readonly CacheableOptionsExtension _cacheableOptions; 43 | 44 | private readonly Func _logFormatter; 45 | 46 | private static MethodInfo ToListMethod { get; } 47 | = typeof(Enumerable).GetTypeInfo() 48 | .GetDeclaredMethod(nameof(Enumerable.ToList)); 49 | 50 | private static MethodInfo CompileQueryMethod { get; } 51 | = typeof(IDatabase).GetTypeInfo() 52 | .GetDeclaredMethod(nameof(IDatabase.CompileQuery)); 53 | 54 | private static MethodInfo CompileAsyncQueryMethod { get; } 55 | = typeof(IDatabase).GetTypeInfo() 56 | .GetDeclaredMethod(nameof(IDatabase.CompileAsyncQuery)); 57 | 58 | public CustomQueryCompiler(IQueryContextFactory queryContextFactory, ICompiledQueryCache compiledQueryCache, ICompiledQueryCacheKeyGenerator compiledQueryCacheKeyGenerator 59 | , IDatabase database, IDiagnosticsLogger logger, ICurrentDbContext currentContext, IQueryModelGenerator queryModelGenerator, IEvaluatableExpressionFilter evaluatableExpressionFilter) 60 | : base(queryContextFactory, compiledQueryCache, compiledQueryCacheKeyGenerator, database, logger, currentContext, queryModelGenerator) 61 | { 62 | Check.NotNull(queryContextFactory, nameof(queryContextFactory)); 63 | Check.NotNull(compiledQueryCache, nameof(compiledQueryCache)); 64 | Check.NotNull(compiledQueryCacheKeyGenerator, nameof(compiledQueryCacheKeyGenerator)); 65 | Check.NotNull(database, nameof(database)); 66 | Check.NotNull(logger, nameof(logger)); 67 | Check.NotNull(currentContext, nameof(currentContext)); 68 | Check.NotNull(evaluatableExpressionFilter, nameof(evaluatableExpressionFilter)); 69 | 70 | _queryContextFactory = queryContextFactory; 71 | _compiledQueryCache = compiledQueryCache; 72 | _compiledQueryCacheKeyGenerator = compiledQueryCacheKeyGenerator; 73 | _database = database; 74 | _logger = logger; 75 | _contextType = currentContext.Context.GetType(); 76 | _queryModelGenerator = queryModelGenerator; 77 | 78 | _logFormatter = (queryKey, ex) => $"Cache hit for query [0x{queryKey}] with: {ex?.Message ?? "no error"}"; 79 | 80 | _cacheableOptions = currentContext.Context 81 | .GetService() 82 | .ContextOptions 83 | .FindExtension(); 84 | 85 | _cacheProvider = currentContext.Context.GetService(); 86 | } 87 | 88 | public override TResult Execute(Expression query) 89 | { 90 | Check.NotNull(query, nameof(query)); 91 | 92 | var queryContext = _queryContextFactory.Create(); 93 | 94 | // search for cacheable operator and extract parameter 95 | var cachableExpressionVisitor = new CachableExpressionVisitor(); 96 | query = cachableExpressionVisitor.GetExtractCachableParameter(query, out bool isCacheable, out CacheableOptions options); 97 | 98 | 99 | query = _queryModelGenerator.ExtractParameters(_logger, query, queryContext); 100 | 101 | // if cacheable operator is part of the query use cache logic 102 | if (isCacheable) 103 | { 104 | // generate key to identify query 105 | var queryKey = _cacheProvider.CreateQueryKey(query, queryContext.ParameterValues); 106 | 107 | if (_cacheProvider.TryGetCachedResult(queryKey, out TResult cacheResult)) 108 | { 109 | _logger.Logger.Log(LogLevel.Debug, CacheableEventId.CacheHit, queryKey, null, _logFormatter); 110 | 111 | //cache was hit, so return cached query result 112 | return cacheResult; 113 | } 114 | else // cache was not hit 115 | { 116 | var cacheKey = _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, false); 117 | var compiledQuery = _compiledQueryCache.GetOrAddQuery(cacheKey, () => CompileQueryCore(query, _queryModelGenerator, _database, _logger, _contextType)); 118 | 119 | // excecute query 120 | var queryResult = compiledQuery(queryContext); 121 | 122 | // add query result to cache 123 | if(ShouldResultBeCached(queryResult, options)) 124 | _cacheProvider.SetCachedResult(queryKey, queryResult, options.TimeToLive); 125 | 126 | _logger.Logger.Log(LogLevel.Debug, CacheableEventId.QueryResultCached, queryKey, null, _logFormatter); 127 | 128 | return queryResult; 129 | } 130 | } 131 | else 132 | { 133 | // return default query result 134 | var cacheKey = _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, false); 135 | var compiledQuery = _compiledQueryCache.GetOrAddQuery(cacheKey, () => CompileQueryCore(query, _queryModelGenerator, _database, _logger, _contextType)); 136 | 137 | return compiledQuery(queryContext); 138 | } 139 | } 140 | 141 | private static Func CompileQueryCore( 142 | Expression query, 143 | IQueryModelGenerator queryModelGenerator, 144 | IDatabase database, 145 | IDiagnosticsLogger logger, 146 | Type contextType, 147 | bool getRealResult = false) 148 | { 149 | var queryModel = queryModelGenerator.ParseQuery(query); 150 | 151 | var resultItemType 152 | = (queryModel.GetOutputDataInfo() 153 | as StreamedSequenceInfo)?.ResultItemType 154 | ?? typeof(TResult); 155 | 156 | if (resultItemType == typeof(TResult)) 157 | { 158 | var compiledQuery = database.CompileQuery(queryModel); 159 | 160 | return qc => 161 | { 162 | try 163 | { 164 | return compiledQuery(qc).First(); 165 | } 166 | catch (Exception exception) 167 | { 168 | logger.QueryIterationFailed(contextType, exception); 169 | 170 | throw; 171 | } 172 | }; 173 | } 174 | 175 | try 176 | { 177 | // differs from base implmentation to return DB query result 178 | var compileFunction = (Func)CompileQueryMethod 179 | .MakeGenericMethod(resultItemType) 180 | .Invoke(database, new object[] { queryModel }); 181 | 182 | return qc => 183 | { 184 | try 185 | { 186 | // calling ToList to materialize result 187 | var genericToListMethod = ToListMethod.MakeGenericMethod(new Type[] { resultItemType }); 188 | var result = genericToListMethod.Invoke(compileFunction(qc), new object[] { compileFunction(qc) }); 189 | 190 | return (TResult)result; 191 | } 192 | catch (Exception exception) 193 | { 194 | logger.QueryIterationFailed(contextType, exception); 195 | 196 | throw; 197 | } 198 | }; 199 | } 200 | catch (TargetInvocationException e) 201 | { 202 | ExceptionDispatchInfo.Capture(e.InnerException).Throw(); 203 | 204 | throw; 205 | } 206 | } 207 | 208 | public override IAsyncEnumerable ExecuteAsync(Expression query) 209 | { 210 | Check.NotNull(query, nameof(query)); 211 | 212 | var queryContext = _queryContextFactory.Create(); 213 | 214 | // search for cacheable operator and extract parameter 215 | var cachableExpressionVisitor = new CachableExpressionVisitor(); 216 | query = cachableExpressionVisitor.GetExtractCachableParameter(query, out bool isCacheable, out CacheableOptions options); 217 | 218 | query = _queryModelGenerator.ExtractParameters(_logger, query, queryContext); 219 | 220 | // if cacheable operator is part of the query use cache logic 221 | if (isCacheable) 222 | { 223 | // generate key to identify query 224 | var queryKey = _cacheProvider.CreateQueryKey(query, queryContext.ParameterValues); 225 | 226 | if (_cacheProvider.TryGetCachedResult>(queryKey, out IAsyncEnumerable cacheResult)) 227 | { 228 | _logger.Logger.Log(LogLevel.Debug, CacheableEventId.CacheHit, queryKey, null, _logFormatter); 229 | 230 | //cache was hit, so return cached query result 231 | return cacheResult; 232 | } 233 | else // cache was not hit 234 | { 235 | var cacheKey = _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, true); 236 | var compiledQuery = _compiledQueryCache.GetOrAddAsyncQuery(cacheKey, () => CompileAsyncQueryCore>(query, _queryModelGenerator, _database)); 237 | 238 | // excecute query 239 | var queryResult = compiledQuery(queryContext); 240 | 241 | // add query result to cache 242 | _cacheProvider.SetCachedResult>(queryKey, queryResult, options.TimeToLive); 243 | 244 | _logger.Logger.Log(LogLevel.Debug, CacheableEventId.QueryResultCached, queryKey, null, _logFormatter); 245 | 246 | return queryResult; 247 | } 248 | } 249 | else 250 | { 251 | // return default query result 252 | var cacheKey = _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, true); 253 | var compiledQuery = _compiledQueryCache.GetOrAddAsyncQuery(cacheKey, () => CompileAsyncQueryCore>(query, _queryModelGenerator, _database)); 254 | 255 | // parameter 'cacheProvider' is null, the result will not be cached 256 | return compiledQuery(queryContext); 257 | } 258 | } 259 | 260 | public override Task ExecuteAsync(Expression query, CancellationToken cancellationToken) 261 | { 262 | Check.NotNull(query, nameof(query)); 263 | 264 | var queryContext = _queryContextFactory.Create(); 265 | 266 | queryContext.CancellationToken = cancellationToken; 267 | 268 | // search for cacheable operator and extract parameter 269 | var cachableExpressionVisitor = new CachableExpressionVisitor(); 270 | query = cachableExpressionVisitor.GetExtractCachableParameter(query, out bool isCacheable, out CacheableOptions options); 271 | 272 | query = _queryModelGenerator.ExtractParameters(_logger, query, queryContext); 273 | 274 | // if cacheable operator is part of the query use cache logic 275 | if (isCacheable) 276 | { 277 | // generate key to identify query 278 | var queryKey = _cacheProvider.CreateQueryKey(query, queryContext.ParameterValues); 279 | 280 | if (_cacheProvider.TryGetCachedResult(queryKey, out TResult cacheResult)) 281 | { 282 | _logger.Logger.Log(LogLevel.Debug, CacheableEventId.CacheHit, queryKey, null, _logFormatter); 283 | 284 | //cache was hit, so return cached query result 285 | return Task.FromResult(cacheResult); 286 | } 287 | else // cache was not hit 288 | { 289 | var cacheKey = _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, true); 290 | var compiledQuery = _compiledQueryCache.GetOrAddAsyncQuery(cacheKey, () => CompileAsyncQueryCore>(query, _queryModelGenerator, _database)); 291 | 292 | // excecute query 293 | return ExecuteSingletonAsyncQuery(queryContext, compiledQuery, _logger, _contextType, _logFormatter, _cacheProvider, queryKey, options); 294 | } 295 | } 296 | else 297 | { 298 | // return default query result 299 | var cacheKey = _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, true); 300 | var compiledQuery = _compiledQueryCache.GetOrAddAsyncQuery(cacheKey, () => CompileAsyncQueryCore>(query, _queryModelGenerator, _database)); 301 | 302 | // parameter 'cacheProvider' is null, the result will not be cached 303 | return ExecuteSingletonAsyncQuery(queryContext, compiledQuery, _logger, _contextType, _logFormatter, null, null, null); 304 | } 305 | } 306 | 307 | private static async Task ExecuteSingletonAsyncQuery( 308 | QueryContext queryContext, 309 | Func> compiledQuery, 310 | IDiagnosticsLogger logger, 311 | Type contextType, 312 | Func logFormatter, 313 | ICacheProvider cacheProvider, 314 | object queryKey, 315 | CacheableOptions options) 316 | { 317 | try 318 | { 319 | var asyncEnumerable = compiledQuery(queryContext); 320 | 321 | using (var asyncEnumerator = asyncEnumerable.GetEnumerator()) 322 | { 323 | await asyncEnumerator.MoveNext(queryContext.CancellationToken); 324 | 325 | if (cacheProvider != null) 326 | { 327 | // add query result to cache 328 | if (ShouldResultBeCached(asyncEnumerator.Current, options)) 329 | cacheProvider.SetCachedResult(queryKey, asyncEnumerator.Current, options.TimeToLive); 330 | 331 | logger.Logger.Log(LogLevel.Debug, CacheableEventId.QueryResultCached, queryKey, null, logFormatter); 332 | } 333 | 334 | return asyncEnumerator.Current; 335 | } 336 | } 337 | catch (Exception exception) 338 | { 339 | logger.QueryIterationFailed(contextType, exception); 340 | 341 | throw; 342 | } 343 | } 344 | 345 | private static Func CompileAsyncQueryCore( 346 | Expression query, 347 | IQueryModelGenerator queryModelGenerator, 348 | IDatabase database) 349 | { 350 | var queryModel = queryModelGenerator.ParseQuery(query); 351 | 352 | var resultItemType 353 | = (queryModel.GetOutputDataInfo() 354 | as StreamedSequenceInfo)?.ResultItemType 355 | ?? typeof(TResult).TryGetSequenceType(); 356 | 357 | try 358 | { 359 | return (Func)CompileAsyncQueryMethod 360 | .MakeGenericMethod(resultItemType) 361 | .Invoke(database, new object[] { queryModel }); 362 | } 363 | catch (TargetInvocationException e) 364 | { 365 | ExceptionDispatchInfo.Capture(e.InnerException).Throw(); 366 | 367 | throw; 368 | } 369 | } 370 | 371 | private static Boolean ShouldResultBeCached(TResult result, CacheableOptions options) 372 | { 373 | if (!options.CacheNullResult && result == null) 374 | { 375 | return false; 376 | } 377 | 378 | if (result is IEnumerable) 379 | { 380 | var enumerable = result as IEnumerable; 381 | 382 | return enumerable.Any(); 383 | } 384 | 385 | return true; 386 | } 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/CacheableExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using EntityFrameworkCore.Cacheable.Diagnostics; 2 | using EntityFrameworkCore.CacheableTests; 3 | using EntityFrameworkCore.CacheableTests.BusinessTestLogic; 4 | using EntityFrameworkCore.CacheableTests.Logging; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Diagnostics; 11 | using System.Linq; 12 | using System.Threading; 13 | 14 | namespace EntityFrameworkCore.Cacheable.Tests 15 | { 16 | [TestClass] 17 | [TestCategory("EntityFrameworkCore.Cacheable.Expressions")] 18 | public class CacheableExpressionTests 19 | { 20 | /// 21 | /// Testing cache expiration functionality. 22 | /// 23 | //[TestMethod] 24 | public void ExpirationTest() 25 | { 26 | MemoryCacheProvider.ClearCache(); 27 | 28 | var loggerProvider = new DebugLoggerProvider(); 29 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 30 | 31 | var options = new DbContextOptionsBuilder() 32 | .UseLoggerFactory(loggerFactory) 33 | .UseInMemoryDatabase(databaseName: "ExpirationTest") 34 | .Options; 35 | 36 | // create test entries 37 | using (var initContext = new BloggingContext(options)) 38 | { 39 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 40 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 41 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 42 | initContext.SaveChanges(); 43 | } 44 | 45 | using (var expirationContext = new BloggingContext(options)) 46 | { 47 | // shoud not hit cache, because first execution 48 | var result = expirationContext.Blogs 49 | .Where(d => d.BlogId == 1) 50 | .Cacheable(TimeSpan.FromSeconds(5)) 51 | .ToList(); 52 | 53 | // shoud hit cache, because second execution 54 | result = expirationContext.Blogs 55 | .Where(d => d.BlogId == 1) 56 | .Cacheable(TimeSpan.FromSeconds(5)) 57 | .ToList(); 58 | 59 | // shoud not hit cache, because different parameter 60 | result = expirationContext.Blogs 61 | .Where(d => d.BlogId == 2) 62 | .Cacheable(TimeSpan.FromSeconds(5)) 63 | .ToList(); 64 | 65 | Thread.Sleep(TimeSpan.FromSeconds(10)); 66 | 67 | // shoud not hit cache, because expiration 68 | result = expirationContext.Blogs 69 | .Where(d => d.BlogId == 1) 70 | .Cacheable(TimeSpan.FromSeconds(5)) 71 | .ToList(); 72 | } 73 | 74 | // find "cache hit" log entries 75 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 76 | 77 | // cache should hit one time 78 | Assert.IsTrue(logs.Count() == 1); 79 | } 80 | 81 | /// 82 | /// Testing entity result cache functionality. 83 | /// 84 | [TestMethod] 85 | public void EntityExpressionTest() 86 | { 87 | MemoryCacheProvider.ClearCache(); 88 | 89 | var loggerProvider = new DebugLoggerProvider(); 90 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 91 | 92 | var options = new DbContextOptionsBuilder() 93 | .UseLoggerFactory(loggerFactory) 94 | .UseInMemoryDatabase(databaseName: "EntityExpressionTest") 95 | .Options; 96 | 97 | // create test entries 98 | using (var initContext = new BloggingContext(options)) 99 | { 100 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 101 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 102 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 103 | initContext.SaveChanges(); 104 | } 105 | 106 | using (var entityContext = new BloggingContext(options)) 107 | { 108 | // shoud not hit cache, because first execution 109 | var result = entityContext.Blogs 110 | .Where(d => d.BlogId > 1) 111 | .Cacheable(TimeSpan.FromMinutes(5)) 112 | .ToList(); 113 | 114 | // shoud hit cache, because second execution 115 | var cachedResult = entityContext.Blogs 116 | .Where(d => d.BlogId > 1) 117 | .Cacheable(TimeSpan.FromMinutes(5)) 118 | .ToList(); 119 | 120 | Assert.AreEqual(result.Count, cachedResult.Count); 121 | } 122 | 123 | // find "cache hit" log entries 124 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 125 | 126 | // cache should hit one time 127 | Assert.IsTrue(logs.Count() == 1); 128 | } 129 | 130 | /// 131 | /// Testing projection result cache functionality. 132 | /// 133 | [TestMethod] 134 | public void ProjectionExpressionTest() 135 | { 136 | MemoryCacheProvider.ClearCache(); 137 | 138 | var loggerProvider = new DebugLoggerProvider(); 139 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 140 | 141 | var options = new DbContextOptionsBuilder() 142 | .UseLoggerFactory(loggerFactory) 143 | .UseInMemoryDatabase(databaseName: "ProjectionExpressionTest") 144 | .Options; 145 | 146 | // create test entries 147 | using (var initContext = new BloggingContext(options)) 148 | { 149 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 150 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 151 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 152 | initContext.SaveChanges(); 153 | } 154 | 155 | using (var projectionContext = new BloggingContext(options)) 156 | { 157 | // shoud not hit cache, because first execution 158 | var result = projectionContext.Blogs 159 | .Where(d => d.BlogId > 1) 160 | .Select(d => new 161 | { 162 | d.BlogId, 163 | d.Rating 164 | }) 165 | .Cacheable(TimeSpan.FromMinutes(5)) 166 | .ToList(); 167 | 168 | // shoud hit cache, because second execution 169 | var cachedResult = projectionContext.Blogs 170 | .Where(d => d.BlogId > 1) 171 | .Select(d => new 172 | { 173 | d.BlogId, 174 | d.Rating 175 | }) 176 | .Cacheable(TimeSpan.FromMinutes(5)) 177 | .ToList(); 178 | 179 | Assert.AreEqual(result.Count, cachedResult.Count); 180 | } 181 | 182 | // find "cache hit" log entries 183 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 184 | 185 | // cache should hit one time 186 | Assert.IsTrue(logs.Count() == 1); 187 | } 188 | 189 | /// 190 | /// Testing projection result cache functionality. 191 | /// 192 | [TestMethod] 193 | public void SingleProjectionExpressionTest() 194 | { 195 | MemoryCacheProvider.ClearCache(); 196 | 197 | var loggerProvider = new DebugLoggerProvider(); 198 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 199 | 200 | var options = new DbContextOptionsBuilder() 201 | .UseLoggerFactory(loggerFactory) 202 | .UseInMemoryDatabase(databaseName: "ProjectionExpressionTest") 203 | .Options; 204 | 205 | // create test entries 206 | using (var initContext = new BloggingContext(options)) 207 | { 208 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 209 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 210 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 211 | initContext.SaveChanges(); 212 | } 213 | 214 | using (var projectionContext = new BloggingContext(options)) 215 | { 216 | // shoud not hit cache, because first execution 217 | var result = projectionContext.Blogs 218 | .Where(d => d.BlogId == 1) 219 | .Select(d => new 220 | { 221 | d.BlogId, 222 | d.Rating 223 | }) 224 | .Cacheable(TimeSpan.FromMinutes(5)) 225 | .SingleOrDefault(); 226 | 227 | Thread.Sleep(TimeSpan.FromSeconds(1)); 228 | 229 | // shoud hit cache, because second execution 230 | var cachedResult = projectionContext.Blogs 231 | .Where(d => d.BlogId == 1) 232 | .Select(d => new 233 | { 234 | d.BlogId, 235 | d.Rating 236 | }) 237 | .Cacheable(TimeSpan.FromMinutes(5)) 238 | .SingleOrDefault(); 239 | 240 | Assert.IsNotNull(result); 241 | Assert.AreSame(result, cachedResult); 242 | } 243 | 244 | // find "cache hit" log entries 245 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 246 | 247 | // cache should hit one time 248 | Assert.IsTrue(logs.Count() == 1); 249 | } 250 | 251 | /// 252 | /// Testing constant result cache functionality. 253 | /// 254 | [TestMethod] 255 | public void ConstantExpressionTest() 256 | { 257 | MemoryCacheProvider.ClearCache(); 258 | 259 | var loggerProvider = new DebugLoggerProvider(); 260 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 261 | 262 | var options = new DbContextOptionsBuilder() 263 | .UseLoggerFactory(loggerFactory) 264 | .UseInMemoryDatabase(databaseName: "ConstantExpressionTest") 265 | .Options; 266 | 267 | // create test entries 268 | using (var initContext = new BloggingContext(options)) 269 | { 270 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 271 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 272 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 273 | initContext.SaveChanges(); 274 | } 275 | 276 | using (var constantContext = new BloggingContext(options)) 277 | { 278 | // shoud not hit cache, because first execution 279 | var result = constantContext.Blogs 280 | .Where(d => d.BlogId > 1) 281 | .Select(d => d.BlogId) 282 | .Cacheable(TimeSpan.FromMinutes(5)) 283 | .ToList(); 284 | 285 | // shoud hit cache, because second execution 286 | var cachedResult = constantContext.Blogs 287 | .Where(d => d.BlogId > 1) 288 | .Select(d => d.BlogId) 289 | .Cacheable(TimeSpan.FromMinutes(5)) 290 | .ToList(); 291 | 292 | Assert.AreEqual(result.Count, cachedResult.Count); 293 | } 294 | 295 | // find "cache hit" log entries 296 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 297 | 298 | // cache should hit one time 299 | Assert.IsTrue(logs.Count() == 1); 300 | } 301 | 302 | /// 303 | /// Testing null parameter query . 304 | /// 305 | [TestMethod] 306 | public void NullValueExpressionTest() 307 | { 308 | MemoryCacheProvider.ClearCache(); 309 | 310 | var loggerProvider = new DebugLoggerProvider(); 311 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 312 | 313 | var options = new DbContextOptionsBuilder() 314 | .UseLoggerFactory(loggerFactory) 315 | .UseInMemoryDatabase(databaseName: "ConstantExpressionTest") 316 | .Options; 317 | 318 | // create test entries 319 | using (var initContext = new BloggingContext(options)) 320 | { 321 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 322 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 323 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 324 | initContext.SaveChanges(); 325 | } 326 | 327 | int? ratingValue = null; 328 | 329 | using (var constantContext = new BloggingContext(options)) 330 | { 331 | // shoud not hit cache, because first execution 332 | var result = constantContext.Blogs 333 | .Where(d => d.BlogId > 1) 334 | .Where(d => d.Rating == ratingValue) 335 | .Select(d => d.BlogId) 336 | .Cacheable(TimeSpan.FromMinutes(5)) 337 | .ToList(); 338 | 339 | // shoud hit cache, because second execution 340 | var cachedResult = constantContext.Blogs 341 | .Where(d => d.BlogId > 1) 342 | .Where(d => d.Rating == ratingValue) 343 | .Select(d => d.BlogId) 344 | .Cacheable(TimeSpan.FromMinutes(5)) 345 | .ToList(); 346 | 347 | Assert.AreEqual(result.Count, cachedResult.Count); 348 | } 349 | 350 | // find "cache hit" log entries 351 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 352 | 353 | // cache should hit one time 354 | Assert.IsTrue(logs.Count() == 1); 355 | } 356 | 357 | /// 358 | /// Testing null parameter query . 359 | /// 360 | [TestMethod] 361 | public void GlobalQueryFilterTest() 362 | { 363 | MemoryCacheProvider.ClearCache(); 364 | 365 | var loggerProvider = new DebugLoggerProvider(); 366 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 367 | 368 | var options = new DbContextOptionsBuilder() 369 | .UseLoggerFactory(loggerFactory) 370 | .UseInMemoryDatabase(databaseName: "GlobalQueryFilterTest") 371 | .Options; 372 | 373 | // create test entries 374 | using (var initContext = new BloggingContext(options)) 375 | { 376 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 377 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 378 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 379 | initContext.SaveChanges(); 380 | } 381 | 382 | using (var constantContext = new BloggingContext(options, minBlogId: 2)) 383 | { 384 | // shoud not hit cache, because no Cacheable call 385 | var rawResult = constantContext.Blogs 386 | .Count(); 387 | 388 | // shoud not hit cache, because first execution 389 | var result = constantContext.Blogs 390 | .Cacheable(TimeSpan.FromMinutes(5)) 391 | .Count(); 392 | 393 | // shoud hit cache, because second execution 394 | var cachedResult = constantContext.Blogs 395 | .Cacheable(TimeSpan.FromMinutes(5)) 396 | .Count(); 397 | 398 | Assert.AreEqual(result, cachedResult); 399 | } 400 | 401 | // find "cache hit" log entries 402 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 403 | 404 | // cache should hit one time 405 | Assert.IsTrue(logs.Count() == 1); 406 | } 407 | 408 | /// 409 | /// Testing performance of cache functionality. 410 | /// 411 | /// 412 | /// It only tests agains InMemory database, so the test is not expected to be much faster. 413 | /// 414 | [TestMethod] 415 | public void PerformanceTest() 416 | { 417 | MemoryCacheProvider.ClearCache(); 418 | 419 | decimal loopCount = 1000; 420 | 421 | var loggerProvider = new DebugLoggerProvider(); 422 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 423 | 424 | var options = new DbContextOptionsBuilder() 425 | .UseLoggerFactory(loggerFactory) 426 | .UseInMemoryDatabase(databaseName: "PerformanceTest") 427 | .Options; 428 | 429 | // create test entries 430 | using (var initContext = new BloggingContext(options)) 431 | { 432 | initContext.ChangeTracker.AutoDetectChangesEnabled = false; 433 | 434 | for (int i = 0; i < 100000; i++) 435 | { 436 | initContext.Blogs.Add(new Blog 437 | { 438 | Url = $"http://sample.com/cat{i}", 439 | 440 | Posts = new List 441 | { 442 | { new Post {Title = $"Post{1}"} } 443 | } 444 | }); 445 | } 446 | initContext.SaveChanges(); 447 | } 448 | 449 | var rawOptions = new DbContextOptionsBuilder() 450 | .UseLoggerFactory(loggerFactory) 451 | .UseInMemoryDatabase(databaseName: "PerformanceTest") 452 | .Options; 453 | 454 | using (var performanceContext = new BloggingContext(rawOptions)) 455 | { 456 | Stopwatch watch = new Stopwatch(); 457 | watch.Start(); 458 | 459 | // raw queries 460 | for (int i = 0; i < loopCount; i++) 461 | { 462 | var result = performanceContext.Blogs 463 | .Where(d => d.BlogId >= 0) 464 | .Take(100) 465 | .ToList(); 466 | } 467 | 468 | var rawTimeSpan = watch.Elapsed; 469 | 470 | Debug.WriteLine($"Average default context database query duration [+{TimeSpan.FromTicks((long)(rawTimeSpan.Ticks / loopCount))}]."); 471 | } 472 | 473 | using (var performanceContext = new BloggingContext(options)) 474 | { 475 | Stopwatch watch = new Stopwatch(); 476 | watch.Start(); 477 | 478 | // uncached queries 479 | for (int i = 0; i < loopCount; i++) 480 | { 481 | var result = performanceContext.Blogs 482 | .Where(d => d.BlogId >= 0) 483 | .Take(100) 484 | .ToList(); 485 | } 486 | 487 | var uncachedTimeSpan = watch.Elapsed; 488 | 489 | // caching query result 490 | performanceContext.Blogs 491 | .Where(d => d.BlogId >= 0) 492 | .Cacheable(TimeSpan.FromMinutes(10)) 493 | .Take(100) 494 | .ToList(); 495 | 496 | watch.Restart(); 497 | 498 | // cached queries 499 | for (int i = 0; i < loopCount; i++) 500 | { 501 | var result = performanceContext.Blogs 502 | .Where(d => d.BlogId >= 0) 503 | .Cacheable(TimeSpan.FromMinutes(10)) 504 | .Take(100) 505 | .ToList(); 506 | } 507 | 508 | var cachedTimeSpan = watch.Elapsed; 509 | 510 | 511 | // find log entries 512 | var queryResultsCachedCount = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.QueryResultCached).Count(); 513 | var cacheHitsCount = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit).Count(); 514 | 515 | // check cache event counts 516 | Assert.IsTrue(queryResultsCachedCount == 1); 517 | Assert.IsTrue(cacheHitsCount == loopCount); 518 | 519 | Debug.WriteLine($"Average database query duration [+{TimeSpan.FromTicks((long)(uncachedTimeSpan.Ticks / loopCount))}]."); 520 | Debug.WriteLine($"Average cache query duration [+{TimeSpan.FromTicks((long)(cachedTimeSpan.Ticks / loopCount))}]."); 521 | Debug.WriteLine($"Cached queries are x{((Decimal)uncachedTimeSpan.Ticks / (Decimal)cachedTimeSpan.Ticks)-1:N2} times faster."); 522 | 523 | Assert.IsTrue(cachedTimeSpan < uncachedTimeSpan); 524 | } 525 | } 526 | } 527 | } -------------------------------------------------------------------------------- /EntityFrameworkCore.CacheableTests/CacheableAgnosticContextTests.cs: -------------------------------------------------------------------------------- 1 | using EntityFrameworkCore.Cacheable.Diagnostics; 2 | using EntityFrameworkCore.CacheableTests.BusinessTestLogic; 3 | using EntityFrameworkCore.CacheableTests.Logging; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Threading; 12 | 13 | namespace EntityFrameworkCore.Cacheable.Tests 14 | { 15 | [TestClass] 16 | [TestCategory("EntityFrameworkCore.Cacheable.Expressions")] 17 | public class CacheableAgnosticContextTests 18 | { 19 | /// 20 | /// Testing cache expiration functionality. 21 | /// 22 | //[TestMethod] 23 | public void ExpirationTest() 24 | { 25 | MemoryCacheProvider.ClearCache(); 26 | 27 | var loggerProvider = new DebugLoggerProvider(); 28 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 29 | 30 | var options = new DbContextOptionsBuilder() 31 | .UseLoggerFactory(loggerFactory) 32 | .UseInMemoryDatabase(databaseName: "ExpirationTest") 33 | .UseSecondLevelCache() 34 | .Options; 35 | 36 | // create test entries 37 | using (var initContext = new AgnosticBloggingContext(options)) 38 | { 39 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 40 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 41 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 42 | initContext.SaveChanges(); 43 | } 44 | 45 | using (var expirationContext = new AgnosticBloggingContext(options)) 46 | { 47 | // shoud not hit cache, because first execution 48 | var result = expirationContext.Blogs 49 | .Where(d => d.BlogId == 1) 50 | .Cacheable(TimeSpan.FromSeconds(5)) 51 | .ToList(); 52 | 53 | // shoud hit cache, because second execution 54 | result = expirationContext.Blogs 55 | .Where(d => d.BlogId == 1) 56 | .Cacheable(TimeSpan.FromSeconds(5)) 57 | .ToList(); 58 | 59 | // shoud not hit cache, because different parameter 60 | result = expirationContext.Blogs 61 | .Where(d => d.BlogId == 2) 62 | .Cacheable(TimeSpan.FromSeconds(5)) 63 | .ToList(); 64 | 65 | Thread.Sleep(TimeSpan.FromSeconds(10)); 66 | 67 | // shoud not hit cache, because expiration 68 | result = expirationContext.Blogs 69 | .Where(d => d.BlogId == 1) 70 | .Cacheable(TimeSpan.FromSeconds(5)) 71 | .ToList(); 72 | } 73 | 74 | // find "cache hit" log entries 75 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 76 | 77 | // cache should hit one time 78 | Assert.IsTrue(logs.Count() == 1); 79 | } 80 | 81 | /// 82 | /// Testing entity result cache functionality. 83 | /// 84 | [TestMethod] 85 | public void EntityExpressionTest() 86 | { 87 | MemoryCacheProvider.ClearCache(); 88 | 89 | var loggerProvider = new DebugLoggerProvider(); 90 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 91 | 92 | var options = new DbContextOptionsBuilder() 93 | .UseLoggerFactory(loggerFactory) 94 | .UseInMemoryDatabase(databaseName: "EntityExpressionTest") 95 | .UseSecondLevelCache() 96 | .Options; 97 | 98 | // create test entries 99 | using (var initContext = new AgnosticBloggingContext(options)) 100 | { 101 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 102 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 103 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 104 | initContext.SaveChanges(); 105 | } 106 | 107 | using (var entityContext = new AgnosticBloggingContext(options)) 108 | { 109 | // shoud not hit cache, because first execution 110 | var result = entityContext.Blogs 111 | .Where(d => d.BlogId > 1) 112 | .Cacheable(TimeSpan.FromMinutes(5)) 113 | .ToList(); 114 | 115 | // shoud hit cache, because second execution 116 | var cachedResult = entityContext.Blogs 117 | .Where(d => d.BlogId > 1) 118 | .Cacheable(TimeSpan.FromMinutes(5)) 119 | .ToList(); 120 | 121 | Assert.AreEqual(result.Count, cachedResult.Count); 122 | } 123 | 124 | // find "cache hit" log entries 125 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 126 | 127 | // cache should hit one time 128 | Assert.IsTrue(logs.Count() == 1); 129 | } 130 | 131 | /// 132 | /// Testing projection result cache functionality. 133 | /// 134 | [TestMethod] 135 | public void ProjectionExpressionTest() 136 | { 137 | MemoryCacheProvider.ClearCache(); 138 | 139 | var loggerProvider = new DebugLoggerProvider(); 140 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 141 | 142 | var options = new DbContextOptionsBuilder() 143 | .UseLoggerFactory(loggerFactory) 144 | .UseInMemoryDatabase(databaseName: "ProjectionExpressionTest") 145 | .UseSecondLevelCache() 146 | .Options; 147 | 148 | // create test entries 149 | using (var initContext = new AgnosticBloggingContext(options)) 150 | { 151 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 152 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 153 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 154 | initContext.SaveChanges(); 155 | } 156 | 157 | using (var projectionContext = new AgnosticBloggingContext(options)) 158 | { 159 | // shoud not hit cache, because first execution 160 | var result = projectionContext.Blogs 161 | .Where(d => d.BlogId > 1) 162 | .Select(d => new 163 | { 164 | d.BlogId, 165 | d.Rating 166 | }) 167 | .Cacheable(TimeSpan.FromMinutes(5)) 168 | .ToList(); 169 | 170 | // shoud hit cache, because second execution 171 | var cachedResult = projectionContext.Blogs 172 | .Where(d => d.BlogId > 1) 173 | .Select(d => new 174 | { 175 | d.BlogId, 176 | d.Rating 177 | }) 178 | .Cacheable(TimeSpan.FromMinutes(5)) 179 | .ToList(); 180 | 181 | Assert.AreEqual(result.Count, cachedResult.Count); 182 | } 183 | 184 | // find "cache hit" log entries 185 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 186 | 187 | // cache should hit one time 188 | Assert.IsTrue(logs.Count() == 1); 189 | } 190 | 191 | /// 192 | /// Testing projection result cache functionality. 193 | /// 194 | [TestMethod] 195 | public void SingleProjectionExpressionTest() 196 | { 197 | MemoryCacheProvider.ClearCache(); 198 | 199 | var loggerProvider = new DebugLoggerProvider(); 200 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 201 | 202 | var options = new DbContextOptionsBuilder() 203 | .UseLoggerFactory(loggerFactory) 204 | .UseInMemoryDatabase(databaseName: "ProjectionExpressionTest") 205 | .UseSecondLevelCache() 206 | .Options; 207 | 208 | // create test entries 209 | using (var initContext = new AgnosticBloggingContext(options)) 210 | { 211 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 212 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 213 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 214 | initContext.SaveChanges(); 215 | } 216 | 217 | using (var projectionContext = new AgnosticBloggingContext(options)) 218 | { 219 | // shoud not hit cache, because first execution 220 | var result = projectionContext.Blogs 221 | .Where(d => d.BlogId == 1) 222 | .Select(d => new 223 | { 224 | d.BlogId, 225 | d.Rating 226 | }) 227 | .Cacheable(TimeSpan.FromMinutes(5)) 228 | .SingleOrDefault(); 229 | 230 | Thread.Sleep(TimeSpan.FromSeconds(1)); 231 | 232 | // shoud hit cache, because second execution 233 | var cachedResult = projectionContext.Blogs 234 | .Where(d => d.BlogId == 1) 235 | .Select(d => new 236 | { 237 | d.BlogId, 238 | d.Rating 239 | }) 240 | .Cacheable(TimeSpan.FromMinutes(5)) 241 | .SingleOrDefault(); 242 | 243 | Assert.IsNotNull(result); 244 | Assert.AreSame(result, cachedResult); 245 | } 246 | 247 | // find "cache hit" log entries 248 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 249 | 250 | // cache should hit one time 251 | Assert.IsTrue(logs.Count() == 1); 252 | } 253 | 254 | /// 255 | /// Testing constant result cache functionality. 256 | /// 257 | [TestMethod] 258 | public void ConstantExpressionTest() 259 | { 260 | MemoryCacheProvider.ClearCache(); 261 | 262 | var loggerProvider = new DebugLoggerProvider(); 263 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 264 | 265 | var options = new DbContextOptionsBuilder() 266 | .UseLoggerFactory(loggerFactory) 267 | .UseInMemoryDatabase(databaseName: "ConstantExpressionTest") 268 | .UseSecondLevelCache() 269 | .Options; 270 | 271 | // create test entries 272 | using (var initContext = new AgnosticBloggingContext(options)) 273 | { 274 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 275 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 276 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 277 | initContext.SaveChanges(); 278 | } 279 | 280 | using (var constantContext = new AgnosticBloggingContext(options)) 281 | { 282 | // shoud not hit cache, because first execution 283 | var result = constantContext.Blogs 284 | .Where(d => d.BlogId > 1) 285 | .Select(d => d.BlogId) 286 | .Cacheable(TimeSpan.FromMinutes(5)) 287 | .ToList(); 288 | 289 | // shoud hit cache, because second execution 290 | var cachedResult = constantContext.Blogs 291 | .Where(d => d.BlogId > 1) 292 | .Select(d => d.BlogId) 293 | .Cacheable(TimeSpan.FromMinutes(5)) 294 | .ToList(); 295 | 296 | Assert.AreEqual(result.Count, cachedResult.Count); 297 | } 298 | 299 | // find "cache hit" log entries 300 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 301 | 302 | // cache should hit one time 303 | Assert.IsTrue(logs.Count() == 1); 304 | } 305 | 306 | /// 307 | /// Testing null parameter query . 308 | /// 309 | [TestMethod] 310 | public void NullValueExpressionTest() 311 | { 312 | MemoryCacheProvider.ClearCache(); 313 | 314 | var loggerProvider = new DebugLoggerProvider(); 315 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 316 | 317 | var options = new DbContextOptionsBuilder() 318 | .UseLoggerFactory(loggerFactory) 319 | .UseInMemoryDatabase(databaseName: "ConstantExpressionTest") 320 | .UseSecondLevelCache() 321 | .Options; 322 | 323 | // create test entries 324 | using (var initContext = new AgnosticBloggingContext(options)) 325 | { 326 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 327 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 328 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 329 | initContext.SaveChanges(); 330 | } 331 | 332 | int? ratingValue = null; 333 | 334 | using (var constantContext = new AgnosticBloggingContext(options)) 335 | { 336 | // shoud not hit cache, because first execution 337 | var result = constantContext.Blogs 338 | .Where(d => d.BlogId > 1) 339 | .Where(d => d.Rating == ratingValue) 340 | .Select(d => d.BlogId) 341 | .Cacheable(TimeSpan.FromMinutes(5)) 342 | .ToList(); 343 | 344 | // shoud hit cache, because second execution 345 | var cachedResult = constantContext.Blogs 346 | .Where(d => d.BlogId > 1) 347 | .Where(d => d.Rating == ratingValue) 348 | .Select(d => d.BlogId) 349 | .Cacheable(TimeSpan.FromMinutes(5)) 350 | .ToList(); 351 | 352 | Assert.AreEqual(result.Count, cachedResult.Count); 353 | } 354 | 355 | // find "cache hit" log entries 356 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 357 | 358 | // cache should hit one time 359 | Assert.IsTrue(logs.Count() == 1); 360 | } 361 | 362 | /// 363 | /// Testing null parameter query . 364 | /// 365 | [TestMethod] 366 | public void GlobalQueryFilterTest() 367 | { 368 | MemoryCacheProvider.ClearCache(); 369 | 370 | var loggerProvider = new DebugLoggerProvider(); 371 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 372 | 373 | var options = new DbContextOptionsBuilder() 374 | .UseLoggerFactory(loggerFactory) 375 | .UseInMemoryDatabase(databaseName: "GlobalQueryFilterTest") 376 | .UseSecondLevelCache() 377 | .Options; 378 | 379 | // create test entries 380 | using (var initContext = new AgnosticBloggingContext(options)) 381 | { 382 | initContext.Blogs.Add(new Blog { BlogId = 1, Url = "http://sample.com/cats" }); 383 | initContext.Blogs.Add(new Blog { BlogId = 2, Url = "http://sample.com/catfish" }); 384 | initContext.Blogs.Add(new Blog { BlogId = 3, Url = "http://sample.com/dogs" }); 385 | initContext.SaveChanges(); 386 | } 387 | 388 | using (var constantContext = new AgnosticBloggingContext(options, minBlogId: 2)) 389 | { 390 | // shoud not hit cache, because no Cacheable call 391 | var rawResult = constantContext.Blogs 392 | .Count(); 393 | 394 | // shoud not hit cache, because first execution 395 | var result = constantContext.Blogs 396 | .Cacheable(TimeSpan.FromMinutes(5)) 397 | .Count(); 398 | 399 | // shoud hit cache, because second execution 400 | var cachedResult = constantContext.Blogs 401 | .Cacheable(TimeSpan.FromMinutes(5)) 402 | .Count(); 403 | 404 | Assert.AreEqual(result, cachedResult); 405 | } 406 | 407 | // find "cache hit" log entries 408 | var logs = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit); 409 | 410 | // cache should hit one time 411 | Assert.IsTrue(logs.Count() == 1); 412 | } 413 | 414 | /// 415 | /// Testing performance of cache functionality. 416 | /// 417 | /// 418 | /// It only tests agains InMemory database, so the test is not expected to be much faster. 419 | /// 420 | [TestMethod] 421 | public void PerformanceTest() 422 | { 423 | MemoryCacheProvider.ClearCache(); 424 | 425 | decimal loopCount = 1000; 426 | 427 | var loggerProvider = new DebugLoggerProvider(); 428 | var loggerFactory = new LoggerFactory(new[] { loggerProvider }); 429 | 430 | var options = new DbContextOptionsBuilder() 431 | .UseLoggerFactory(loggerFactory) 432 | .UseInMemoryDatabase(databaseName: "PerformanceTest") 433 | .UseSecondLevelCache() 434 | .Options; 435 | 436 | // create test entries 437 | using (var initContext = new AgnosticBloggingContext(options)) 438 | { 439 | initContext.ChangeTracker.AutoDetectChangesEnabled = false; 440 | 441 | for (int i = 0; i < 100000; i++) 442 | { 443 | initContext.Blogs.Add(new Blog 444 | { 445 | Url = $"http://sample.com/cat{i}", 446 | 447 | Posts = new List 448 | { 449 | { new Post {Title = $"Post{1}"} } 450 | } 451 | }); 452 | } 453 | initContext.SaveChanges(); 454 | } 455 | 456 | var rawOptions = new DbContextOptionsBuilder() 457 | .UseLoggerFactory(loggerFactory) 458 | .UseInMemoryDatabase(databaseName: "PerformanceTest") 459 | .Options; 460 | 461 | using (var performanceContext = new AgnosticBloggingContext(rawOptions)) 462 | { 463 | Stopwatch watch = new Stopwatch(); 464 | watch.Start(); 465 | 466 | // raw queries 467 | for (int i = 0; i < loopCount; i++) 468 | { 469 | var result = performanceContext.Blogs 470 | .Where(d => d.BlogId >= 0) 471 | .Take(100) 472 | .ToList(); 473 | } 474 | 475 | var rawTimeSpan = watch.Elapsed; 476 | 477 | Debug.WriteLine($"Average default context database query duration [+{TimeSpan.FromTicks((long)(rawTimeSpan.Ticks / loopCount))}]."); 478 | } 479 | 480 | using (var performanceContext = new AgnosticBloggingContext(options)) 481 | { 482 | Stopwatch watch = new Stopwatch(); 483 | watch.Start(); 484 | 485 | // uncached queries 486 | for (int i = 0; i < loopCount; i++) 487 | { 488 | var result = performanceContext.Blogs 489 | .Where(d => d.BlogId >= 0) 490 | .Take(100) 491 | .ToList(); 492 | } 493 | 494 | var uncachedTimeSpan = watch.Elapsed; 495 | 496 | // caching query result 497 | performanceContext.Blogs 498 | .Where(d => d.BlogId >= 0) 499 | .Cacheable(TimeSpan.FromMinutes(10)) 500 | .Take(100) 501 | .ToList(); 502 | 503 | watch.Restart(); 504 | 505 | // cached queries 506 | for (int i = 0; i < loopCount; i++) 507 | { 508 | var result = performanceContext.Blogs 509 | .Where(d => d.BlogId >= 0) 510 | .Cacheable(TimeSpan.FromMinutes(10)) 511 | .Take(100) 512 | .ToList(); 513 | } 514 | 515 | var cachedTimeSpan = watch.Elapsed; 516 | 517 | 518 | // find log entries 519 | var queryResultsCachedCount = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.QueryResultCached).Count(); 520 | var cacheHitsCount = loggerProvider.Entries.Where(e => e.EventId == CacheableEventId.CacheHit).Count(); 521 | 522 | // check cache event counts 523 | Assert.IsTrue(queryResultsCachedCount == 1); 524 | Assert.IsTrue(cacheHitsCount == loopCount); 525 | 526 | Debug.WriteLine($"Average database query duration [+{TimeSpan.FromTicks((long)(uncachedTimeSpan.Ticks / loopCount))}]."); 527 | Debug.WriteLine($"Average cache query duration [+{TimeSpan.FromTicks((long)(cachedTimeSpan.Ticks / loopCount))}]."); 528 | Debug.WriteLine($"Cached queries are x{((Decimal)uncachedTimeSpan.Ticks / (Decimal)cachedTimeSpan.Ticks) - 1:N2} times faster."); 529 | 530 | Assert.IsTrue(cachedTimeSpan < uncachedTimeSpan); 531 | } 532 | } 533 | } 534 | } --------------------------------------------------------------------------------