MetalamaConceptual documentationUsing Metalama PatternsCachingWorking with cache dependencies
Open sandboxFocusImprove this doc

Working with cache dependencies

Cache dependencies serve two primary purposes. Firstly, they act as an intermediary layer between cached methods (typically read methods) and invalidating methods (typically write methods), thereby reducing the coupling between these methods. Secondly, cache dependencies can represent external dependencies, such as file system dependencies or SQL dependencies.

Compared to direct invalidation, the use of dependencies results in lower performance and increased resource consumption in the caching backend due to the need to store and synchronize the graph of dependencies. For more details on direct invalidation, refer to Invalidating the cache.

Adding string dependencies

All dependencies are eventually represented as strings. Although we recommend using one of the strongly-typed methods mentioned below, it's beneficial to understand how string dependencies operate.

To add or invalidate dependencies, you will typically access the ICachingService interface. If you are using dependency injection, you should first declare your class as partial, and the interface will be available under a field named _cachingService. Otherwise, use the Default property.

Within read methods, use the ICachingService.AddDependency* at any time to add a dependency to the method being executed, for the arguments with which it is executed. You can pass an arbitrary string to this method, potentially including the method arguments.

For instance, here is how to add a string dependency:

23            this._cachingService.AddDependency( $"ProductPrice:{productId}" );

Then, in the update methods, use the ICachingService.Invalidate* method and pass the dependency string to remove any cache item that has a dependency on this string.

For instance, the following line invalidates two string dependencies:

72            this._cachingService.Invalidate( $"ProductPrice:{productId}", "PriceList"  );
Note

Dependencies function correctly with recursive method calls. If a cached method A calls another cached method B, all dependencies of B automatically become dependencies of A, even if A was cached when A was being evaluated.

Example: string dependencies

The following code is a variation of our ProductCatalogue example. It has three read methods:

  • GetPrice returns the price of a given product,
  • GetProducts returns a list of products without their prices, and
  • GetPriceList returns both the name and the price of all products.

It has two write methods:

  • AddProduct adds a product, therefore it should affect both GetProducts and GetPriceList, and
  • UpdatePrice changes the price of a given product, and should affect GetPrice for this product and GetPriceList.

We model the dependencies using three string templates:

  • ProductList represents the product list without prices,
  • ProductPrice:{productId} represents the price of a given product, and
  • PriceList represents the complete price list.
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3using System.Diagnostics;
4using Xunit;
5
6namespace Doc.StringDependencies
7{
8    public sealed class ConsoleMain : IConsoleMain
9    {
10        private readonly ProductCatalogue _catalogue;
11
12        public ConsoleMain( ProductCatalogue catalogue )
13        {
14            this._catalogue = catalogue;
15        }
16
17        private void PrintCatalogue()
18        {
19            var products = this._catalogue.GetProducts();
20
21            foreach ( var product in products )
22            {
23                var price = this._catalogue.GetPrice( product );
24                Console.WriteLine( $"Price of '{product}' is {price}." );
25            }
26        }
27
28        public void Execute()
29        {
30            Console.WriteLine( "Read the price catalogue a first time." );
31            this.PrintCatalogue();
32
33            Console.WriteLine( "Read the price catalogue a second time time. It should be completely performed from cache." );
34            var operationsBefore = this._catalogue.DbOperationCount;
35            this.PrintCatalogue();
36            var operationsAfter = this._catalogue.DbOperationCount;
37            Assert.Equal( operationsBefore, operationsAfter );
38
39            // There should be just one product in the catalogue.
40            Assert.Single( this._catalogue.GetProducts() );
41
42            // Adding a product and updating the price.
43            Console.WriteLine( "Updating the catalogue." );
44            this._catalogue.AddProduct( "wheat", 150 );
45            this._catalogue.UpdatePrice( "corn", 110 );
46
47            // Read the catalogue a third time.
48            Assert.Equal( 2, this._catalogue.GetProducts().Length );
49            Assert.Equal( 110, this._catalogue.GetPrice( "corn" ) );
50
51            // Print the catalogue.
52            Console.WriteLine( "Catalogue after changes:" );
53            this.PrintCatalogue();
54        }
55    }
56}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.StringDependencies
6{
7    internal 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            var host = builder.Build();
21            host.Run();
22        }
23    }
24}
Source Code
1using Metalama.Patterns.Caching;
2
3using Metalama.Patterns.Caching.Aspects;
4using System;
5using System.Collections.Generic;

6using System.Collections.Immutable;
7using System.Linq;
8
9namespace Doc.StringDependencies

10{
11    public 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 decimal GetPrice( string productId )
19        {
20            Console.WriteLine( $"Getting the price of {productId} from database." );
21            this.DbOperationCount++;










22
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

23            this._cachingService.AddDependency( $"ProductPrice:{productId}" );
24
25            return this._dbSimulator[productId];
26        }
27
28        [Cache]
29        public string[] GetProducts()
30        {
31            Console.WriteLine( "Getting the product list from database." );
32
33            this.DbOperationCount++;






34




            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

35            this._cachingService.AddDependency( "ProductList" );
36
37            return this._dbSimulator.Keys.ToArray();
38        }
39
40        [Cache]
41        public ImmutableDictionary<string, decimal> GetPriceList()
42        {
43            this.DbOperationCount++;
44










            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

45            this._cachingService.AddDependency( "PriceList" );
46
47            return this._dbSimulator.ToImmutableDictionary();
48        }
49
50        public void AddProduct( string productId, decimal price )
51        {
52            Console.WriteLine( $"Adding the product {productId}." );
53
54            this.DbOperationCount++;
55            this._dbSimulator.Add( productId, price );
56
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

57            this._cachingService.Invalidate( "ProductList", "PriceList" );
58        }
59
60        public void UpdatePrice( string productId, decimal price )
61        {
62            if ( !this._dbSimulator.ContainsKey( productId ) )
63            {
64                throw new KeyNotFoundException();
65            }
66
67            Console.WriteLine( $"Updating the price of {productId}." );
68
69            this.DbOperationCount++;
70            this._dbSimulator[productId] = price;
71
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

72            this._cachingService.Invalidate( $"ProductPrice:{productId}", "PriceList"  );
73
74        }
75    }



















76}
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.Collections.Immutable;
8using System.Linq;
9using System.Reflection;
10
11namespace Doc.StringDependencies
12{
13    public sealed class ProductCatalogue
14    {
15        private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
16
17        public int DbOperationCount { get; private set; }
18
19        [Cache]
20        public decimal GetPrice(string productId)
21        {
22            static object? Invoke(object? instance, object?[] args)
23            {
24                return ((ProductCatalogue)instance).GetPrice_Source((string)args[0]);
25            }
26
27            return _cachingService!.GetFromCacheOrExecute<decimal>(_cacheRegistration_GetPrice!, this, new object[] { productId }, Invoke);
28        }
29
30        private decimal GetPrice_Source(string productId)
31        {
32            Console.WriteLine($"Getting the price of {productId} from database.");
33            this.DbOperationCount++;
34
35            this._cachingService.AddDependency($"ProductPrice:{productId}");  
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            this._cachingService.AddDependency("ProductList");
58
59            return this._dbSimulator.Keys.ToArray();
60        }
61
62        [Cache]
63        public ImmutableDictionary<string, decimal> GetPriceList()
64        {
65            static object? Invoke(object? instance, object?[] args)
66            {
67                return ((ProductCatalogue)instance).GetPriceList_Source();
68            }
69
70            return _cachingService!.GetFromCacheOrExecute<ImmutableDictionary<string, decimal>>(_cacheRegistration_GetPriceList!, this, new object[] { }, Invoke);
71        }
72
73        private ImmutableDictionary<string, decimal> GetPriceList_Source()
74        {
75            this.DbOperationCount++;
76
77            this._cachingService.AddDependency("PriceList");
78
79            return this._dbSimulator.ToImmutableDictionary();
80        }
81
82        public void AddProduct(string productId, decimal price)
83        {
84            Console.WriteLine($"Adding the product {productId}.");
85
86            this.DbOperationCount++;
87            this._dbSimulator.Add(productId, price);
88
89            this._cachingService.Invalidate("ProductList", "PriceList");
90        }
91
92        public void UpdatePrice(string productId, decimal price)
93        {
94            if (!this._dbSimulator.ContainsKey(productId))
95            {
96                throw new KeyNotFoundException();
97            }
98
99            Console.WriteLine($"Updating the price of {productId}.");
100
101            this.DbOperationCount++;
102            this._dbSimulator[productId] = price;
103
104            this._cachingService.Invalidate($"ProductPrice:{productId}", "PriceList");
105
106        }
107
108        private static readonly CachedMethodMetadata _cacheRegistration_GetPrice;
109        private static readonly CachedMethodMetadata _cacheRegistration_GetPriceList;
110        private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
111        private ICachingService _cachingService;
112
113        static ProductCatalogue
114        ()
115        {
116            ProductCatalogue._cacheRegistration_GetPrice = CachedMethodMetadata.Register(RunTimeHelpers.ThrowIfMissing(typeof(ProductCatalogue).GetMethod("GetPrice", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null)!, "ProductCatalogue.GetPrice(string)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, false);
117            ProductCatalogue._cacheRegistration_GetProducts = CachedMethodMetadata.Register(RunTimeHelpers.ThrowIfMissing(typeof(ProductCatalogue).GetMethod("GetProducts", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null)!, "ProductCatalogue.GetProducts()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
118            ProductCatalogue._cacheRegistration_GetPriceList = CachedMethodMetadata.Register(RunTimeHelpers.ThrowIfMissing(typeof(ProductCatalogue).GetMethod("GetPriceList", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null)!, "ProductCatalogue.GetPriceList()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
119        }
120
121        public ProductCatalogue
122        (ICachingService? cachingService = default)
123        {
124            this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
125        }
126    }
127}
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.

Adding object-oriented dependencies through the ICacheDependency interface

As previously mentioned, working with string dependencies can be error-prone as the code generating the string is duplicated in both the read and the write methods. A more efficient approach is to encapsulate the cache key generation logic, i.e., represent the cache dependency as an object and add some key-generation logic to this object.

For this reason, Metalama Caching allows you to work with strongly-typed, object-oriented dependencies through the ICacheDependency interface.

This interface has two members:

  • GetCacheKey should return the string representation of the caching key,
  • CascadeDependencies, an optional property, can return a list of dependencies that should be recursively invalidated when the current dependency is invalidated.

How and where you implement ICacheDependency is entirely up to you. You have the following options:

  1. The most practical option is often to implement the ICacheDependency in your domain objects.
  2. Alternatively, you can create a parallel object model implementing ICacheDependency -- just to represent dependencies.
  3. If you have types that can already be used in cache keys, e.g., thanks to the [CacheKey] aspect or another mechanism (see Customizing cache keys), you can turn these objects into dependencies by wrapping them into an ObjectDependency. You can also use the AddObjectDependency and InvalidateObject methods to avoid creating a wrapper.
  4. To represent singleton dependencies, it can be convenient to assign them a constant string and wrap this string into a StringDependency object.

Example: object-oriented Dependencies

Let's revamp our previous example using object-oriented dependencies.

Instead of just working with primitive types like string and decimal, we create a new type record Product( string Name, decimal Price) and make this type implement the ICacheDependency interface. To represent dependencies of the global collections ProductList and PriceList, we use instances of the StringDependency class rather than creating new classes for each. These instances are exposed as static properties of the GlobalDependencies static class.

To ensure the entire PriceList is invalidated whenever a Product is updated, we return the global PriceList dependency instance from the CascadeDependencies property of the Product class.

1using Metalama.Documentation.Helpers.ConsoleApp;
2using Microsoft.Extensions.Hosting;
3using System;
4using Xunit;
5
6namespace Doc.ObjectDependencies
7{
8    public sealed class ConsoleMain : IConsoleMain
9    {
10        private readonly ProductCatalogue _catalogue;
11
12        public ConsoleMain( ProductCatalogue catalogue )
13        {
14            this._catalogue = catalogue;
15        }
16
17        private void PrintCatalogue()
18        {
19            var products = this._catalogue.GetProducts();
20
21            foreach ( var product in products )
22            {
23                var price = this._catalogue.GetProduct( product );
24                Console.WriteLine( $"Price of '{product}' is {price}." );
25            }
26        }
27
28        public void Execute()
29        {
30            Console.WriteLine( "Read the price catalogue a first time." );
31            this.PrintCatalogue();
32
33            Console.WriteLine( "Read the price catalogue a second time time. It should be completely performed from cache." );
34            var operationsBefore = this._catalogue.DbOperationCount;
35            this.PrintCatalogue();
36            var operationsAfter = this._catalogue.DbOperationCount;
37            Assert.Equal( operationsBefore, operationsAfter );
38
39            // There should be just one product in the catalogue.
40            Assert.Single( this._catalogue.GetProducts() );
41
42            var corn = this._catalogue.GetProduct( "corn" );
43
44            // Adding a product and updating the price.
45            Console.WriteLine( "Updating the catalogue." );
46
47            this._catalogue.AddProduct( new Product( "wheat", 150 ) );
48            this._catalogue.UpdateProduct( corn with { Price = 110 } );
49
50            // Read the catalogue a third time.
51            Assert.Equal( 2, this._catalogue.GetProducts().Length );
52            Assert.Equal( 110, this._catalogue.GetProduct( "corn" ).Price );
53
54            // Print the catalogue.
55            Console.WriteLine( "Catalogue after changes:" );
56            this.PrintCatalogue();
57        }
58    }
59}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.ObjectDependencies
6{
7    internal 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    }
24}
Source Code
1using Metalama.Patterns.Caching;
2
3using Metalama.Patterns.Caching;
4using Metalama.Patterns.Caching.Aspects;
5using Metalama.Patterns.Caching.Dependencies;
6using System;

7using System.Collections.Generic;
8using System.Linq;
9
10namespace Doc.ObjectDependencies

11{
12    internal static class GlobalDependencies
13    {
14        public static ICacheDependency ProductCatalogue = new StringDependency( nameof(ProductCatalogue) );
15        public static ICacheDependency ProductList = new StringDependency( nameof(ProductList) );
16    }
17
18    public record Product( string Name, decimal Price ) : ICacheDependency
19    {
20        string ICacheDependency.GetCacheKey( ICachingService cachingService ) => this.Name;
21
22        // Means that when we invalidate the current product in cache, we should also invalidate the product catalogue.
23        IReadOnlyCollection<ICacheDependency> ICacheDependency.CascadeDependencies { get; } = new[] { GlobalDependencies.ProductCatalogue };
24    }
25
26    public sealed class ProductCatalogue
27    {
28        private readonly Dictionary<string, Product> _dbSimulator = new() { ["corn"] = new Product( "corn", 100 ) };
29
30        public int DbOperationCount { get; private set; }
31
32        [Cache]
33        public Product GetProduct( string productId )
34        {
35            Console.WriteLine( $"Getting the price of {productId} from database." );
36            this.DbOperationCount++;










37
38            var product = this._dbSimulator[productId];
39
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

40            this._cachingService.AddDependency( product );
41
42            return product;
43        }
44
45        [Cache]
46        public string[] GetProducts()
47        {
48            Console.WriteLine( "Getting the product list from database." );
49










50            this.DbOperationCount++;
51
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

52            this._cachingService.AddDependency( GlobalDependencies.ProductList );
53
54            return this._dbSimulator.Keys.ToArray();
55        }
56
57        [Cache]
58        public IReadOnlyCollection<Product> GetPriceList()
59        {
60            this.DbOperationCount++;
61










            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

62            this._cachingService.AddDependency( GlobalDependencies.ProductCatalogue );
63
64            return this._dbSimulator.Values;
65        }
66
67        public void AddProduct( Product product )
68        {
69            Console.WriteLine( $"Adding the product {product.Name}." );
70
71            this.DbOperationCount++;
72
73            this._dbSimulator.Add( product.Name, product );
74
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

75            this._cachingService.Invalidate( product );
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

76            this._cachingService.Invalidate( GlobalDependencies.ProductList );
77        }
78
79        public void UpdateProduct( Product product )
80        {
81            if ( !this._dbSimulator.ContainsKey( product.Name ) )
82            {
83                throw new KeyNotFoundException();
84            }
85
86            Console.WriteLine( $"Updating the price of {product.Name}." );
87
88            this.DbOperationCount++;
89            this._dbSimulator[product.Name] = product;
90
            Error CS1061: 'ProductCatalogue' does not contain a definition for '_cachingService' and no accessible extension method '_cachingService' accepting a first argument of type 'ProductCatalogue' could be found (are you missing a using directive or an assembly reference?)

91            this._cachingService.Invalidate( product  );
92
93        }
94    }



















95}
Transformed Code
1using Metalama.Patterns.Caching;
2
3using Metalama.Patterns.Caching;
4using Metalama.Patterns.Caching.Aspects;
5using Metalama.Patterns.Caching.Aspects.Helpers;
6using Metalama.Patterns.Caching.Dependencies;
7using System;
8using System.Collections.Generic;
9using System.Linq;
10using System.Reflection;
11
12namespace Doc.ObjectDependencies
13{
14    internal static class GlobalDependencies
15    {
16        public static ICacheDependency ProductCatalogue = new StringDependency(nameof(ProductCatalogue));
17        public static ICacheDependency ProductList = new StringDependency(nameof(ProductList));
18    }
19
20    public record Product(string Name, decimal Price) : ICacheDependency
21    {
22        string ICacheDependency.GetCacheKey(ICachingService cachingService) => this.Name;
23
24        // Means that when we invalidate the current product in cache, we should also invalidate the product catalogue.
25        IReadOnlyCollection<ICacheDependency> ICacheDependency.CascadeDependencies { get; } = new[] { GlobalDependencies.ProductCatalogue };
26    }
27
28    public sealed class ProductCatalogue
29    {
30        private readonly Dictionary<string, Product> _dbSimulator = new() { ["corn"] = new Product("corn", 100) };
31
32        public int DbOperationCount { get; private set; }
33
34        [Cache]
35        public Product GetProduct(string productId)
36        {
37            static object? Invoke(object? instance, object?[] args)
38            {
39                return ((ProductCatalogue)instance).GetProduct_Source((string)args[0]);
40            }
41
42            return _cachingService!.GetFromCacheOrExecute<Product>(_cacheRegistration_GetProduct!, this, new object[] { productId }, Invoke);
43        }
44
45        private Product GetProduct_Source(string productId)
46        {
47            Console.WriteLine($"Getting the price of {productId} from database.");
48            this.DbOperationCount++;
49
50            var product = this._dbSimulator[productId];
51
52            this._cachingService.AddDependency(product);  
53            
54            return product;
55        }
56
57        [Cache]
58        public string[] GetProducts()
59        {
60            static object? Invoke(object? instance, object?[] args)
61            {
62                return ((ProductCatalogue)instance).GetProducts_Source();
63            }
64
65            return _cachingService!.GetFromCacheOrExecute<string[]>(_cacheRegistration_GetProducts!, this, new object[] { }, Invoke);
66        }
67
68        private string[] GetProducts_Source()
69        {
70            Console.WriteLine("Getting the product list from database.");
71
72            this.DbOperationCount++;
73
74            this._cachingService.AddDependency(GlobalDependencies.ProductList);
75
76            return this._dbSimulator.Keys.ToArray();
77        }
78
79        [Cache]
80        public IReadOnlyCollection<Product> GetPriceList()
81        {
82            static object? Invoke(object? instance, object?[] args)
83            {
84                return ((ProductCatalogue)instance).GetPriceList_Source();
85            }
86
87            return _cachingService!.GetFromCacheOrExecute<IReadOnlyCollection<Product>>(_cacheRegistration_GetPriceList!, this, new object[] { }, Invoke);
88        }
89
90        private IReadOnlyCollection<Product> GetPriceList_Source()
91        {
92            this.DbOperationCount++;
93
94            this._cachingService.AddDependency(GlobalDependencies.ProductCatalogue);
95
96            return this._dbSimulator.Values;
97        }
98
99        public void AddProduct(Product product)
100        {
101            Console.WriteLine($"Adding the product {product.Name}.");
102
103            this.DbOperationCount++;
104
105            this._dbSimulator.Add(product.Name, product);
106
107            this._cachingService.Invalidate(product);
108            this._cachingService.Invalidate(GlobalDependencies.ProductList);
109        }
110
111        public void UpdateProduct(Product product)
112        {
113            if (!this._dbSimulator.ContainsKey(product.Name))
114            {
115                throw new KeyNotFoundException();
116            }
117
118            Console.WriteLine($"Updating the price of {product.Name}.");
119
120            this.DbOperationCount++;
121            this._dbSimulator[product.Name] = product;
122
123            this._cachingService.Invalidate(product);
124
125        }
126
127        private static readonly CachedMethodMetadata _cacheRegistration_GetPriceList;
128        private static readonly CachedMethodMetadata _cacheRegistration_GetProduct;
129        private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
130        private ICachingService _cachingService;
131
132        static ProductCatalogue
133        ()
134        {
135            ProductCatalogue._cacheRegistration_GetProduct = CachedMethodMetadata.Register(RunTimeHelpers.ThrowIfMissing(typeof(ProductCatalogue).GetMethod("GetProduct", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null)!, "ProductCatalogue.GetProduct(string)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
136            ProductCatalogue._cacheRegistration_GetProducts = CachedMethodMetadata.Register(RunTimeHelpers.ThrowIfMissing(typeof(ProductCatalogue).GetMethod("GetProducts", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null)!, "ProductCatalogue.GetProducts()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
137            ProductCatalogue._cacheRegistration_GetPriceList = CachedMethodMetadata.Register(RunTimeHelpers.ThrowIfMissing(typeof(ProductCatalogue).GetMethod("GetPriceList", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null)!, "ProductCatalogue.GetPriceList()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
138        }
139
140        public ProductCatalogue
141        (ICachingService? cachingService = default)
142        {
143            this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
144        }
145    }
146}
Read the price catalogue a first time.
Getting the product list from database.
Getting the price of corn from database.
Price of 'corn' is Product { Name = corn, Price = 100 }.
Read the price catalogue a second time time. It should be completely performed from cache.
Price of 'corn' is Product { Name = corn, Price = 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 Product { Name = corn, Price = 110 }.
Getting the price of wheat from database.
Price of 'wheat' is Product { Name = wheat, Price = 150 }.

Suspending the Collection of Cache Dependencies

A new caching context is created for each cached method. The caching context is propagated along all invoked methods and is implemented using AsyncLocal<T>.

When a parent cached method calls a child cached method, the dependencies of the child methods are automatically added to the parent method, even if the child method was not executed because its result was found in the cache. Therefore, invalidating a child method automatically invalidates the parent method, which is often an intuitive and desirable behavior.

However, there are cases where propagating the caching context from the parent to the child methods (and thereby the collection of child dependencies into the parent context) is not desirable. For instance, if the parent method runs an asynchronous child task using Task.Run and does not wait for its completion, then it is likely that the dependencies of methods called in the child task should not be propagated to the parent. This is because the child task could be considered a side effect of the parent method and should not affect caching. Undesired dependencies would not compromise the program's correctness, but they would make it less efficient.

To suspend the collection of dependencies in the current context and in all child contexts, use the _cachingService.SuspendDependencyPropagation method within a using construct.