diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj index a9135d9..618d8ce 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj +++ b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj @@ -1,7 +1,7 @@ Entity Framework Core Second Level Caching Library. - 4.1.1 + 4.1.2 Vahid Nasiri net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;net462;netcoreapp3.1; true diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFSqlCommandsProcessor.cs b/src/EFCoreSecondLevelCacheInterceptor/EFSqlCommandsProcessor.cs index f9b141c..e222564 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFSqlCommandsProcessor.cs +++ b/src/EFCoreSecondLevelCacheInterceptor/EFSqlCommandsProcessor.cs @@ -13,30 +13,39 @@ namespace EFCoreSecondLevelCacheInterceptor; /// public class EFSqlCommandsProcessor : IEFSqlCommandsProcessor { - private static readonly string[] CrudMarkers = { "MERGE ", "insert ", "update ", "delete ", "create " }; + private static readonly string[] CrudMarkers = + { + "MERGE ", "insert ", "update ", "delete ", "create " + }; private static readonly Type IEntityType = Type.GetType("Microsoft.EntityFrameworkCore.Metadata.IEntityType, Microsoft.EntityFrameworkCore") ?? throw new TypeLoadException("Couldn't load Microsoft.EntityFrameworkCore.Metadata.IEntityType"); - private static readonly PropertyInfo ClrTypePropertyInfo = - IEntityType.GetInterfaces() - .Union(new[] { IEntityType }) - .Select(i => i.GetProperty("ClrType", BindingFlags.Public | BindingFlags.Instance)) - .Distinct() - .FirstOrDefault(propertyInfo => propertyInfo != null) ?? - throw new KeyNotFoundException("Couldn't find `ClrType` on IEntityType."); + private static readonly PropertyInfo ClrTypePropertyInfo = IEntityType.GetInterfaces().Union(new[] + { + IEntityType + }).Select(i => i.GetProperty("ClrType", + BindingFlags.Public | BindingFlags.Instance)) + .Distinct() + .FirstOrDefault(propertyInfo + => propertyInfo != null) ?? + throw new KeyNotFoundException( + "Couldn't find `ClrType` on IEntityType."); private static readonly Type RelationalEntityTypeExtensionsType = Type.GetType( - "Microsoft.EntityFrameworkCore.RelationalEntityTypeExtensions, Microsoft.EntityFrameworkCore.Relational") ?? + "Microsoft.EntityFrameworkCore.RelationalEntityTypeExtensions, Microsoft.EntityFrameworkCore.Relational") ?? throw new TypeLoadException("Couldn't load Microsoft.EntityFrameworkCore.RelationalEntityTypeExtensions"); private static readonly MethodInfo GetTableNameMethodInfo = - RelationalEntityTypeExtensionsType.GetMethod("GetTableName", BindingFlags.Static | BindingFlags.Public) - ?? throw new KeyNotFoundException("Couldn't find `GetTableName()` on RelationalEntityTypeExtensions."); + RelationalEntityTypeExtensionsType.GetMethod("GetTableName", BindingFlags.Static | BindingFlags.Public) ?? + throw new KeyNotFoundException("Couldn't find `GetTableName()` on RelationalEntityTypeExtensions."); - private static readonly string[] Separator = { "." }; + private static readonly string[] Separator = + { + "." + }; private readonly ConcurrentDictionary>> _commandTableNames = new(StringComparer.OrdinalIgnoreCase); @@ -47,8 +56,8 @@ public class EFSqlCommandsProcessor : IEFSqlCommandsProcessor /// /// SqlCommands Utils /// - public EFSqlCommandsProcessor(IEFHashProvider hashProvider) => - _hashProvider = hashProvider ?? throw new ArgumentNullException(nameof(hashProvider)); + public EFSqlCommandsProcessor(IEFHashProvider hashProvider) + => _hashProvider = hashProvider ?? throw new ArgumentNullException(nameof(hashProvider)); /// /// Is `insert`, `update` or `delete`? @@ -86,9 +95,8 @@ public IList GetAllTableNames(DbContext context) } return _contextTableNames.GetOrAdd(context.GetType(), - _ => new Lazy>(() => getTableNames(context), - LazyThreadSafetyMode - .ExecutionAndPublication)).Value; + _ => new Lazy>(() => getTableNames(context), + LazyThreadSafetyMode.ExecutionAndPublication)).Value; } /// @@ -98,11 +106,8 @@ public SortedSet GetSqlCommandTableNames(string commandText) { var commandTextKey = $"{_hashProvider.ComputeHash(commandText):X}"; return _commandTableNames.GetOrAdd(commandTextKey, - _ => - new - Lazy>(() => getRawSqlCommandTableNames(commandText), - LazyThreadSafetyMode.ExecutionAndPublication)).Value; + _ => new Lazy>(() => getRawSqlCommandTableNames(commandText), + LazyThreadSafetyMode.ExecutionAndPublication)).Value; } /// @@ -112,8 +117,7 @@ public IList GetSqlCommandEntityTypes(string commandText, IList commandTableNames.Contains(entityType.TableName)) - .Select(entityType => entityType.ClrType) - .ToList(); + .Select(entityType => entityType.ClrType).ToList(); } private static List getTableNames(DbContext context) @@ -122,21 +126,21 @@ private static List getTableNames(DbContext context) foreach (var entityType in context.Model.GetEntityTypes()) { var clrType = getClrType(entityType); - tableNames.Add( - new TableEntityInfo - { - ClrType = clrType, - TableName = getTableName(entityType) ?? clrType.ToString(), - }); + tableNames.Add(new TableEntityInfo + { + ClrType = clrType, + TableName = getTableName(entityType) ?? clrType.ToString() + }); } return tableNames; } private static string? getTableName(object entityType) - { - return GetTableNameMethodInfo.Invoke(null, new[] { entityType }) as string; - } + => GetTableNameMethodInfo.Invoke(null, new[] + { + entityType + }) as string; private static Type getClrType(object entityType) { @@ -147,12 +151,17 @@ private static Type getClrType(object entityType) private static SortedSet getRawSqlCommandTableNames(string commandText) { - string[] tableMarkers = { "FROM", "JOIN", "INTO", "UPDATE", "MERGE" }; + string[] tableMarkers = + { + "FROM", "JOIN", "INTO", "UPDATE", "MERGE" + }; var tables = new SortedSet(StringComparer.OrdinalIgnoreCase); - var sqlItems = commandText.Split(new[] { " ", "\r\n", Environment.NewLine, "\n" }, - StringSplitOptions.RemoveEmptyEntries); + var sqlItems = commandText.Split(new[] + { + " ", "\r\n", Environment.NewLine, "\n" + }, StringSplitOptions.RemoveEmptyEntries); var sqlItemsLength = sqlItems.Length; for (var i = 0; i < sqlItemsLength; i++) { @@ -166,7 +175,7 @@ private static SortedSet getRawSqlCommandTableNames(string commandText) ++i; if (i >= sqlItemsLength) { - continue; + break; } var tableName = string.Empty; @@ -187,10 +196,8 @@ private static SortedSet getRawSqlCommandTableNames(string commandText) } tableName = tableName.Replace("[", "", StringComparison.Ordinal) - .Replace("]", "", StringComparison.Ordinal) - .Replace("'", "", StringComparison.Ordinal) - .Replace("`", "", StringComparison.Ordinal) - .Replace("\"", "", StringComparison.Ordinal); + .Replace("]", "", StringComparison.Ordinal).Replace("'", "", StringComparison.Ordinal) + .Replace("`", "", StringComparison.Ordinal).Replace("\"", "", StringComparison.Ordinal); tables.Add(tableName); } } diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCacheDependenciesProcessorTests.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCacheDependenciesProcessorTests.cs index 6587d56..5b747ee 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCacheDependenciesProcessorTests.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCacheDependenciesProcessorTests.cs @@ -18,12 +18,20 @@ FROM [Users] AS [u] WHERE [u].[Id] = @__user1_Id_0]"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Users" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Users" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -38,12 +46,21 @@ WHERE [p].[post_type] IN (N'post_base', N'post_page') AND ([p].[Id] > @__param1_ ORDER BY [p].[Id]"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Posts", CacheKeyPrefix + "Users" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Posts", + CacheKeyPrefix + "Users" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -58,12 +75,21 @@ WHERE [p].[post_type] IN (N'post_base', N'post_page') AND ([p].[Id] > @__param1_ ORDER BY [p].[Id]"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Posts", CacheKeyPrefix + "Users" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Posts", + CacheKeyPrefix + "Users" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } @@ -79,12 +105,21 @@ WHERE [p].[post_type] IN (N'post_base', N'post_page') AND ([p].[Id] > @__param1_ ORDER BY [p].[Id]"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Posts", CacheKeyPrefix + "Users" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Posts", + CacheKeyPrefix + "Users" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -99,12 +134,21 @@ WHERE [p].[post_type] IN (N'post_base', N'post_page') AND ([p].[Id] > @__param1_ ORDER BY [p].[Id]"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Posts", CacheKeyPrefix + "Users" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Posts", + CacheKeyPrefix + "Users" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -118,12 +162,20 @@ FROM [Products] WHERE @@ROWCOUNT = 1 AND [ProductId] = scope_identity();"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Products" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Products" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -137,12 +189,20 @@ public void TestGetCacheDependenciesWorksForInsertsWithBacktick() WHERE @@ROWCOUNT = 1 AND `ProductId` = scope_identity();"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Products" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Products" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -154,12 +214,20 @@ DELETE FROM [Products] SELECT @@ROWCOUNT;"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Products" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Products" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -171,12 +239,20 @@ public void TestGetCacheDependenciesWorksForUpdates() SELECT @@ROWCOUNT;"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Posts", "Users", "Products" }, - commandText); + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Users" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Users" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } @@ -199,11 +275,19 @@ WHEN NOT MATCHED THEN INTO @inserted2;"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { "Blogs", "Posts" }, commandText); + new SortedSet + { + "Blogs", + "Posts" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "Blogs" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Blogs" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } [TestMethod] @@ -213,12 +297,44 @@ public void TestGetCacheDependenciesWorksForBulkInsertOrUpdate() @"MERGE [dbo].[People] WITH (HOLDLOCK) AS T USING (SELECT TOP 2 * FROM [dbo].[PeopleTemp94f5cba8] ORDER BY [Id]) AS S ON T.[Id] = S.[Id] WHEN NOT MATCHED BY TARGET THEN INSERT ([Name]) VALUES (S.[Name]) WHEN MATCHED AND EXISTS (SELECT S.[Name] EXCEPT SELECT T.[Name]) THEN UPDATE SET T.[Name] = S.[Name];"; var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), - new SortedSet - { - "People", - }, commandText); + new SortedSet + { + "People" + }, + commandText); - var inUseTableNames = new SortedSet { CacheKeyPrefix + "People" }; - CollectionAssert.AreEqual(inUseTableNames, cacheDependencies); + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "People" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); + } + + [TestMethod] + public void TestGetCacheDependenciesWorksForQueryHints() + { + const string commandText = @"SET NOCOUNT ON; + INSERT INTO [Products] ([IsActive], [Notes], [ProductName], [ProductNumber], [UserId]) + VALUES (@p0, @p1, @p2, @p3, @p4); + SELECT [ProductId] + FROM [Products] + WHERE @@ROWCOUNT = 1 AND [ProductId] = scope_identity() FOR UPDATE"; + var cacheDependenciesProcessor = EFServiceProvider.GetRequiredService(); + var cacheDependencies = cacheDependenciesProcessor.GetCacheDependencies(new EFCachePolicy(), + new SortedSet + { + "Posts", + "Users", + "Products" + }, + commandText); + + var inUseTableNames = new SortedSet + { + CacheKeyPrefix + "Products" + }; + CollectionAssert.AreEqual(inUseTableNames, + cacheDependencies); } } \ No newline at end of file