Open sandboxFocusImprove this doc

Invalidating the cache

As Phil Karlton once famously said, "There are only two hard things in Computer Science: cache invalidation and naming things."

This quote humorously captures the deceptive complexity of cache invalidation. It might seem straightforward, but it's notoriously difficult to get right.

Metalama, unfortunately, cannot completely eliminate this problem from the landscape of software engineering challenges. Cache invalidation is and will remain challenging, especially in the context of distributed systems where multiple caches and data stores need to be kept in sync. However, Metalama simplifies the task of requesting the removal of the correct cache item by exposing a rich programmatic and aspect-oriented API, and eliminating the need to generate consistent caching keys between the cached method and the invalidating logic.

Metalama offers two ways to invalidate the cache:

  • Direct invalidation is when the method that updates the application state (such as a database entity) directly invalidates the cache for all read methods that depend on this entity. The benefit of direct invalidation is that it doesn't require a lot of resources on the caching backend. However, this approach has a significant disadvantage: it exhibits an imperfect separation of concerns. Update methods need to have detailed knowledge of all cached read methods, therefore update methods need to be modified whenever a read method is added. This article will only cover direct invalidation.

  • Indirect invalidation adds a layer of abstraction, named cache dependencies, between the cached method and the invalidating code. Read methods are responsible for adding the proper dependencies to their context, and update methods are responsible for invalidating the dependencies. Therefore, update methods no longer need to know all read methods. For details about this approach, see Working with cache dependencies.

Invalidating cache items declaratively using the [InvalidateCache] aspect

You can add the [InvalidateCache] aspect to a method (referred to as the invalidating method) to cause any call to this method to remove from the cache the value of one or more other methods. Parameters of both methods are matched by name and type.

By default, the [InvalidateCache] aspect looks for the cached method in the current type. You can specify a different type using the alternative constructor of the custom attribute.

For instance, suppose you have the following read method:

25    [Cache]
26    public decimal GetPrice( string productId )

The following code would invalidate this method:

45    [InvalidateCache( nameof(GetPrice) )]
46    public void UpdatePrice( string productId, decimal price )

Example: Using [InvalidateCache]

The following example demonstrates a service named PriceCatalogue with two read methods, GetProducts and GetPrice, and two write methods, AddProduct and UpdatePrice. The write methods use the [InvalidateCache] aspect to trigger invalidation of their respective read methods.

Source Code
1using Metalama.Patterns.Caching.Aspects;
2using System;

3using System.Collections.Generic;

4using System.Linq;
5
6namespace Doc.InvalidateAspect;

7
8public sealed class ProductCatalogue
9{
10    private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
11
12    public int DbOperationCount { get; private set; }
13
14    [Cache]
15    public string[] GetProducts()
16    {
17        Console.WriteLine( "Getting the product list from database." );
18










19        this.DbOperationCount++;
20
21        return this._dbSimulator.Keys.ToArray();
22    }
23
24    // 
25    [Cache]
26    public decimal GetPrice( string productId )
27    // 
28    {
29        Console.WriteLine( $"Getting the price of {productId} from database." );
30        this.DbOperationCount++;
31











32        return this._dbSimulator[productId];
33    }
34
35    [InvalidateCache( nameof(GetProducts) )]
36    public void AddProduct( string productId, decimal price )
37    {
38        Console.WriteLine( $"Adding the product {productId}." );
39
40        this.DbOperationCount++;
41        this._dbSimulator.Add( productId, price );
42    }
43


44    // 
45    [InvalidateCache( nameof(GetPrice) )]
46    public void UpdatePrice( string productId, decimal price )
47    // 
48    {
49        if ( !this._dbSimulator.ContainsKey( productId ) )
50        {
51            throw new KeyNotFoundException();
52        }

53
54        Console.WriteLine( $"Updating the price of {productId}." );
55
56        this.DbOperationCount++;
57        this._dbSimulator[productId] = price;
58    }
59}
Transformed Code
1using Metalama.Patterns.Caching;
2using Metalama.Patterns.Caching.Aspects;
3using Metalama.Patterns.Caching.Aspects.Helpers;
4using System;
5using System.Collections.Generic;
6using System.Linq;
7using System.Reflection;
8
9namespace Doc.InvalidateAspect;
10
11public sealed class ProductCatalogue
12{
13    private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
14
15    public int DbOperationCount { get; private set; }
16
17    [Cache]
18    public string[] GetProducts()
19    {
20        static object? Invoke(object? instance, object?[] args)
21        {
22            return ((ProductCatalogue)instance).GetProducts_Source();
23        }
24
25        return _cachingService.GetFromCacheOrExecute<string[]>(_cacheRegistration_GetProducts, this, new object[] { }, Invoke);
26    }
27
28    private string[] GetProducts_Source()
29    {
30        Console.WriteLine("Getting the product list from database.");
31
32        this.DbOperationCount++;
33
34        return this._dbSimulator.Keys.ToArray();
35    }
36
37    // 
38    [Cache]
39    public decimal GetPrice(string productId)
40    // 
41    {
42        // 
43        static object? Invoke(object? instance, object?[] args)
44        {
45            return ((ProductCatalogue)instance).GetPrice_Source((string)args[0]);
46        }
47
48        return _cachingService.GetFromCacheOrExecute<decimal>(_cacheRegistration_GetPrice, this, new object[] { productId }, Invoke);
49    }
50
51    private decimal GetPrice_Source(string productId)    // 
52    {
53        Console.WriteLine($"Getting the price of {productId} from database.");
54        this.DbOperationCount++;
55
56        return this._dbSimulator[productId];
57    }
58
59    [InvalidateCache(nameof(GetProducts))]
60    public void AddProduct(string productId, decimal price)
61    {
62        Console.WriteLine($"Adding the product {productId}.");
63
64        this.DbOperationCount++;
65        this._dbSimulator.Add(productId, price);
66        object result = null;
67        _cachingService.Invalidate(_methodsInvalidatedBy_AddProduct_976614B12F3F447F4082EAE1C88C1EE0[0], this, new object[] { });
68    }
69
70    // 
71    [InvalidateCache(nameof(GetPrice))]
72    public void UpdatePrice(string productId, decimal price)
73    // 
74    {
75        // 
76        if (!this._dbSimulator.ContainsKey(productId))
77        {
78            throw new KeyNotFoundException();
79        }
80
81        Console.WriteLine($"Updating the price of {productId}.");
82
83        this.DbOperationCount++;
84        this._dbSimulator[productId] = price;
85        object result = null;
86        _cachingService.Invalidate(_methodsInvalidatedBy_UpdatePrice_DA3C5EB2E8FE3C0C2B256E589481CF14[0], this, new object[] { productId });
87    }
88
89    private static readonly CachedMethodMetadata _cacheRegistration_GetPrice;
90    private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
91    private ICachingService _cachingService;
92    private static MethodInfo[] _methodsInvalidatedBy_AddProduct_976614B12F3F447F4082EAE1C88C1EE0;
93    private static MethodInfo[] _methodsInvalidatedBy_UpdatePrice_DA3C5EB2E8FE3C0C2B256E589481CF14;
94
95    static ProductCatalogue()
96    {
97        _cacheRegistration_GetProducts = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetProducts", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null).ThrowIfMissing("ProductCatalogue.GetProducts()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
98        _cacheRegistration_GetPrice = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetPrice", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null).ThrowIfMissing("ProductCatalogue.GetPrice(string)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, false);
99        _methodsInvalidatedBy_AddProduct_976614B12F3F447F4082EAE1C88C1EE0 = new MethodInfo[] {
100            typeof(ProductCatalogue).GetMethod("GetProducts", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null).ThrowIfMissing("ProductCatalogue.GetProducts()")
101        };
102        _methodsInvalidatedBy_UpdatePrice_DA3C5EB2E8FE3C0C2B256E589481CF14 = new MethodInfo[] {
103            typeof(ProductCatalogue).GetMethod("GetPrice", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null).ThrowIfMissing("ProductCatalogue.GetPrice(string)")
104        };
105    }
106
107    public ProductCatalogue(ICachingService? cachingService = default)
108    {
109        this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
110    }
111}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3using Xunit;
4
5namespace Doc.InvalidateAspect;
6
7public sealed class ConsoleMain : IConsoleMain
8{
9    private readonly ProductCatalogue _catalogue;
10
11    public ConsoleMain( ProductCatalogue catalogue )
12    {
13        this._catalogue = catalogue;
14    }
15
16    private void PrintCatalogue()
17    {
18        var products = this._catalogue.GetProducts();
19
20        foreach ( var product in products )
21        {
22            var price = this._catalogue.GetPrice( product );
23            Console.WriteLine( $"Price of '{product}' is {price}." );
24        }
25    }
26
27    public void Execute()
28    {
29        Console.WriteLine( "Read the price catalogue a first time." );
30        this.PrintCatalogue();
31
32        Console.WriteLine(
33            "Read the price catalogue a second time time. It should be completely performed from cache." );
34
35        var operationsBefore = this._catalogue.DbOperationCount;
36        this.PrintCatalogue();
37        var operationsAfter = this._catalogue.DbOperationCount;
38        Assert.Equal( operationsBefore, operationsAfter );
39
40        // There should be just one product in the catalogue.
41        Assert.Single( this._catalogue.GetProducts() );
42
43        // Adding a product and updating the price.
44        Console.WriteLine( "Updating the catalogue." );
45        this._catalogue.AddProduct( "wheat", 150 );
46        this._catalogue.UpdatePrice( "corn", 110 );
47
48        // Read the catalogue a third time.
49        Assert.Equal( 2, this._catalogue.GetProducts().Length );
50        Assert.Equal( 110, this._catalogue.GetPrice( "corn" ) );
51
52        // Print the catalogue.
53        Console.WriteLine( "Catalogue after changes:" );
54        this.PrintCatalogue();
55    }
56}
Read the price catalogue a first time.
Getting the product list from database.
Getting the price of corn from database.
Price of 'corn' is 100.
Read the price catalogue a second time time. It should be completely performed from cache.
Price of 'corn' is 100.
Updating the catalogue.
Adding the product wheat.
Updating the price of corn.
Getting the product list from database.
Getting the price of corn from database.
Catalogue after changes:
Price of 'corn' is 110.
Getting the price of wheat from database.
Price of 'wheat' is 150.
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.InvalidateAspect;
6
7internal static class Program
8{
9    public static void Main()
10    {
11        var builder = ConsoleApp.CreateBuilder();
12
13        // Add the caching service.
14        builder.Services.AddMetalamaCaching();
15
16        // Add other components as usual, then run the application.
17        builder.Services.AddConsoleMain<ConsoleMain>();
18        builder.Services.AddSingleton<ProductCatalogue>();
19
20        using var app = builder.Build();
21        app.Run();
22    }
23}

Compile-time validation

One of the most useful features of the [InvalidateCache] aspect is that it verifies that the parameters of the invalidated and invalidating methods match:

  • When the method cannot be found, a compile-time error is reported.

    1using Metalama.Patterns.Caching.Aspects;
    2using System;
    3
    4namespace Doc.InvalidateCompileTimeErrors.BadMethod;
    5
    6public sealed class ProductCatalogue
    7{
    8    [Cache]
    9    public decimal GetPrice( string productId ) => throw new NotImplementedException();
    10
    11    [InvalidateCache( "GetBadPrice" )]
        Error LAMA5106: Invalid [InvalidateCache] aspect on 'ProductCatalogue.UpdatePrice(string, decimal)': there is no method named 'GetBadPrice' in type 'ProductCatalogue'.
    
    12    public void UpdatePrice( string productId, decimal price )
    13        => throw new NotImplementedException();
    14}
    
  • If any parameter of the cached method cannot be matched with a parameter of the invalidating method, a build error will be reported (unless the parameter has the NotCacheKeyAttribute custom attribute). The order of parameters is not considered.

    1using Metalama.Patterns.Caching.Aspects;
    2using System;
    3
    4namespace Doc.InvalidateCompileTimeErrors.BadArgument;
    5
    6public sealed class ProductCatalogue
    7{
    8    [Cache]
    9    public decimal GetPrice( string productId ) => throw new NotImplementedException();
    10
    11    [InvalidateCache( nameof(GetPrice) )]
        Error LAMA5102: The [InvalidateCache] aspect applied to 'ProductCatalogue.GetPrice(string)' cannot invalidate 'ProductCatalogue.GetPrice(string)': the invalidating method does not contain a parameter named 'productId'. Make sure 'ProductCatalogue.GetPrice(string)' contains a parameter named 'productId' or add the [NotCachedKey] attribute to the 'productId' parameter in 'ProductCatalogue.GetPrice(string)'.
    
    12    public void UpdatePrice( string product, decimal price ) => throw new NotImplementedException();
    13}
    
  • When you invalidate a non-static method (unless instance has been excluded from the cache key by setting the IgnoreThisParameter to true), you can do it only from a non-static method of the current type.

    1using Metalama.Patterns.Caching.Aspects;
    2using System;
    3
    4namespace Doc.InvalidateCompileTimeErrors.NonStatic;
    5
    6public sealed class ProductCatalogue
    7{
    8    [Cache]
    9    public decimal GetPrice( string productId ) => throw new NotImplementedException();
    10
    11    [InvalidateCache( nameof(GetPrice) )]
        Error LAMA5101: The [InvalidateCache] aspect applied to 'ProductCatalogue.UpdatePrice(string, decimal)' cannot invalidate 'ProductCatalogue.GetPrice(string)': the 'this' parameter cannot be mapped because (a) 'ProductCatalogue.GetPrice(string)' is an instance method, (b) the IgnoreThisParameter is not enabled and (c) the type ProductCatalogue is either static or not derived from 'ProductCatalogue'.
    
    12    public static void UpdatePrice( string product, decimal price )
    13        => throw new NotImplementedException();
    14}
    
  • If there are more invalidated methods of the same name for one invalidating method, a build error is reported. To enable invalidation of all the matching overloads by the one invalidating methods, set the property AllowMultipleOverloads to true.

Invalidating cache items imperatively

Instead of annotating invalidating methods with a custom attribute, you can make a call to one of the overloads of the Invalidate or InvalidateAsync extension method of the ICachingService interface.

To access this interface, if you are using dependency injection, you should first make your class partial. Then, the service is available as a field named _cachingService. If you are not using dependency injection in this class, use the CachingService.Default property. The first argument of the Invalidate method should be a delegate to the method to invalidate. This argument must be followed by the list of arguments for which the cache should be invalidated. These arguments will be used to construct the key of the item to be removed from the cache. All arguments must be supplied. Even arguments of parameters that are not part of the cache key will be included.

For instance, suppose you have the following read method:

17    [Cache]
18    public decimal GetPrice( string productId )

Then, the following code would invalidate this method:

61            this._cachingService.Invalidate( this.GetPrice, productId );
62

Example: Imperative invalidation

The following example is an update of the previous one. The PriceCatalogue service has two read methods, GetProducts and GetPrice, and two write methods, AddProduct and UpdatePrice. The write methods use the Invalidate method to trigger invalidation of their respective read methods.

Source Code
1using Metalama.Patterns.Caching;
2
3using Metalama.Patterns.Caching.Aspects;
4using System;
5using System.Collections.Generic;

6using System.Linq;
7
8namespace Doc.ImperativeInvalidate;

9
10public sealed partial class ProductCatalogue
11{
12    private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
13
14    public int DbOperationCount { get; private set; }
15
16    // 
17    [Cache]
18    public decimal GetPrice( string productId )
19    // 
20    {


21        Console.WriteLine( $"Getting the price of {productId} from database." );
22        this.DbOperationCount++;









23
24        return this._dbSimulator[productId];
25    }
26
27    [Cache]
28    public string[] GetProducts()
29    {
30        Console.WriteLine( "Getting the product list from database." );
31










32        this.DbOperationCount++;
33
34        return this._dbSimulator.Keys.ToArray();
35    }
36
37    public void AddProduct( string productId, decimal price )
38    {
39        Console.WriteLine( $"Adding the product {productId}." );
40
41        this.DbOperationCount++;
42        this._dbSimulator.Add( productId, price );
43
44            this._cachingService.Invalidate( this.GetProducts );
45
46    }
47
48    public void UpdatePrice( string productId, decimal price )
49    {
50        if ( !this._dbSimulator.ContainsKey( productId ) )
51        {
52            throw new KeyNotFoundException();
53        }
54
55        Console.WriteLine( $"Updating the price of {productId}." );
56
57        this.DbOperationCount++;
58        this._dbSimulator[productId] = price;
59
60            // 
61            this._cachingService.Invalidate( this.GetPrice, productId );
62
63                                                                         // 
64    }
65}
Transformed Code
1using Metalama.Patterns.Caching;
2
3using Metalama.Patterns.Caching.Aspects;
4using Metalama.Patterns.Caching.Aspects.Helpers;
5using System;
6using System.Collections.Generic;
7using System.Linq;
8using System.Reflection;
9
10namespace Doc.ImperativeInvalidate;
11
12public sealed partial class ProductCatalogue
13{
14    private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
15
16    public int DbOperationCount { get; private set; }
17
18    // 
19    [Cache]
20    public decimal GetPrice(string productId)
21    // 
22    {
23        // 
24        static object? Invoke(object? instance, object?[] args)
25        {
26            return ((ProductCatalogue)instance).GetPrice_Source((string)args[0]);
27        }
28
29        return _cachingService.GetFromCacheOrExecute<decimal>(_cacheRegistration_GetPrice, this, new object[] { productId }, Invoke);
30    }
31
32    private decimal GetPrice_Source(string productId)    // 
33    {
34        Console.WriteLine($"Getting the price of {productId} from database.");
35        this.DbOperationCount++;
36
37        return this._dbSimulator[productId];
38    }
39
40    [Cache]
41    public string[] GetProducts()
42    {
43        static object? Invoke(object? instance, object?[] args)
44        {
45            return ((ProductCatalogue)instance).GetProducts_Source();
46        }
47
48        return _cachingService.GetFromCacheOrExecute<string[]>(_cacheRegistration_GetProducts, this, new object[] { }, Invoke);
49    }
50
51    private string[] GetProducts_Source()
52    {
53        Console.WriteLine("Getting the product list from database.");
54
55        this.DbOperationCount++;
56
57        return this._dbSimulator.Keys.ToArray();
58    }
59
60    public void AddProduct(string productId, decimal price)
61    {
62        Console.WriteLine($"Adding the product {productId}.");
63
64        this.DbOperationCount++;
65        this._dbSimulator.Add(productId, price);
66
67        this._cachingService.Invalidate(this.GetProducts);
68
69    }
70
71    public void UpdatePrice(string productId, decimal price)
72    {
73        if (!this._dbSimulator.ContainsKey(productId))
74        {
75            throw new KeyNotFoundException();
76        }
77
78        Console.WriteLine($"Updating the price of {productId}.");
79
80        this.DbOperationCount++;
81        this._dbSimulator[productId] = price;
82
83        // 
84        this._cachingService.Invalidate(this.GetPrice, productId);
85
86        // 
87    }
88
89    private static readonly CachedMethodMetadata _cacheRegistration_GetPrice;
90    private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
91    private ICachingService _cachingService;
92
93    static ProductCatalogue()
94    {
95        _cacheRegistration_GetPrice = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetPrice", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null).ThrowIfMissing("ProductCatalogue.GetPrice(string)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, false);
96        _cacheRegistration_GetProducts = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetProducts", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null).ThrowIfMissing("ProductCatalogue.GetProducts()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
97    }
98
99    public ProductCatalogue(ICachingService? cachingService = default)
100    {
101        this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
102    }
103}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3using Xunit;
4
5namespace Doc.ImperativeInvalidate;
6
7public sealed class ConsoleMain : IConsoleMain
8{
9    private readonly ProductCatalogue _catalogue;
10
11    public ConsoleMain( ProductCatalogue catalogue )
12    {
13        this._catalogue = catalogue;
14    }
15
16    private void PrintCatalogue()
17    {
18        var products = this._catalogue.GetProducts();
19
20        foreach ( var product in products )
21        {
22            var price = this._catalogue.GetPrice( product );
23            Console.WriteLine( $"Price of '{product}' is {price}." );
24        }
25    }
26
27    public void Execute()
28    {
29        Console.WriteLine( "Read the price catalogue a first time." );
30        this.PrintCatalogue();
31
32        Console.WriteLine(
33            "Read the price catalogue a second time time. It should be completely performed from cache." );
34
35        var operationsBefore = this._catalogue.DbOperationCount;
36        this.PrintCatalogue();
37        var operationsAfter = this._catalogue.DbOperationCount;
38        Assert.Equal( operationsBefore, operationsAfter );
39
40        // There should be just one product in the catalogue.
41        Assert.Single( this._catalogue.GetProducts() );
42
43        // Adding a product and updating the price.
44        Console.WriteLine( "Updating the catalogue." );
45        this._catalogue.AddProduct( "wheat", 150 );
46        this._catalogue.UpdatePrice( "corn", 110 );
47
48        // Read the catalogue a third time.
49        Assert.Equal( 2, this._catalogue.GetProducts().Length );
50        Assert.Equal( 110, this._catalogue.GetPrice( "corn" ) );
51
52        // Print the catalogue.
53        Console.WriteLine( "Catalogue after changes:" );
54        this.PrintCatalogue();
55    }
56}
Read the price catalogue a first time.
Getting the product list from database.
Getting the price of corn from database.
Price of 'corn' is 100.
Read the price catalogue a second time time. It should be completely performed from cache.
Price of 'corn' is 100.
Updating the catalogue.
Adding the product wheat.
Updating the price of corn.
Getting the product list from database.
Getting the price of corn from database.
Catalogue after changes:
Price of 'corn' is 110.
Getting the price of wheat from database.
Price of 'wheat' is 150.
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.ImperativeInvalidate;
6
7internal static class Program
8{
9    public static void Main()
10    {
11        var builder = ConsoleApp.CreateBuilder();
12
13        // Add the caching service.
14        builder.Services.AddMetalamaCaching();
15
16        // Add other components as usual.
17        builder.Services.AddConsoleMain<ConsoleMain>();
18        builder.Services.AddSingleton<ProductCatalogue>();
19
20        // Run the application.
21        using var app = builder.Build();
22        app.Run();
23    }
24}

Forcing the cache to refresh

If you want to call a method while skipping the cache, instead of calling _cachingService.Invalidate and then the method, you can simply call the _cachingService.Refresh method. The new result of the method will be stored in the cache.

Contrary to Invalidate, Refresh will cause methods in the calling context to skip the cache.