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'll typically access the ICachingService interface. If you're using dependency injection, first declare your class as partial, and the interface is 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:
24 this._cachingService.AddDependency( $"ProductPrice:{productId}" );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:
74 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:
GetPricereturns the price of a given product,GetProductsreturns a list of products without their prices, andGetPriceListreturns both the name and the price of all products.
It has two write methods:
AddProductadds a product, therefore it should affect bothGetProductsandGetPriceList, andUpdatePricechanges the price of a given product, and should affectGetPricefor this product andGetPriceList.
We model the dependencies using three string templates:
ProductListrepresents the product list without prices,ProductPrice:{productId}represents the price of a given product, andPriceListrepresents the complete price list.
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
11public sealed partial 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
23 //
24 this._cachingService.AddDependency( $"ProductPrice:{productId}" );
25 //
26 return this._dbSimulator[productId];
27 }
28
29 [Cache]
30 public string[] GetProducts()
31 {
32 Console.WriteLine( "Getting the product list from database." );
33
34 this.DbOperationCount++;
35
36 this._cachingService.AddDependency( "ProductList" );
37
38 return this._dbSimulator.Keys.ToArray();
39 }
40
41 [Cache]
42 public ImmutableDictionary<string, decimal> GetPriceList()
43 {
44 this.DbOperationCount++;
45
46 this._cachingService.AddDependency( "PriceList" );
47
48 return this._dbSimulator.ToImmutableDictionary();
49 }
50
51 public void AddProduct( string productId, decimal price )
52 {
53 Console.WriteLine( $"Adding the product {productId}." );
54
55 this.DbOperationCount++;
56 this._dbSimulator.Add( productId, price );
57
58 this._cachingService.Invalidate( "ProductList", "PriceList" );
59 }
60
61 public void UpdatePrice( string productId, decimal price )
62 {
63 if ( !this._dbSimulator.ContainsKey( productId ) )
64 {
65 throw new KeyNotFoundException();
66 }
67
68 Console.WriteLine( $"Updating the price of {productId}." );
69
70 this.DbOperationCount++;
71 this._dbSimulator[productId] = price;
72
73 //
74 this._cachingService.Invalidate( $"ProductPrice:{productId}", "PriceList" );
75 //
76 }
77}
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
13public sealed partial 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 //
36 this._cachingService.AddDependency($"ProductPrice:{productId}");
37 //
38 return this._dbSimulator[productId];
39 }
40
41 [Cache]
42 public string[] GetProducts()
43 {
44 static object? Invoke(object? instance, object?[] args)
45 {
46 return ((ProductCatalogue)instance).GetProducts_Source();
47 }
48
49 return _cachingService.GetFromCacheOrExecute<string[]>(_cacheRegistration_GetProducts, this, new object[] { }, Invoke);
50 }
51
52 private string[] GetProducts_Source()
53 {
54 Console.WriteLine("Getting the product list from database.");
55
56 this.DbOperationCount++;
57
58 this._cachingService.AddDependency("ProductList");
59
60 return this._dbSimulator.Keys.ToArray();
61 }
62
63 [Cache]
64 public ImmutableDictionary<string, decimal> GetPriceList()
65 {
66 static object? Invoke(object? instance, object?[] args)
67 {
68 return ((ProductCatalogue)instance).GetPriceList_Source();
69 }
70
71 return _cachingService.GetFromCacheOrExecute<ImmutableDictionary<string, decimal>>(_cacheRegistration_GetPriceList, this, new object[] { }, Invoke);
72 }
73
74 private ImmutableDictionary<string, decimal> GetPriceList_Source()
75 {
76 this.DbOperationCount++;
77
78 this._cachingService.AddDependency("PriceList");
79
80 return this._dbSimulator.ToImmutableDictionary();
81 }
82
83 public void AddProduct(string productId, decimal price)
84 {
85 Console.WriteLine($"Adding the product {productId}.");
86
87 this.DbOperationCount++;
88 this._dbSimulator.Add(productId, price);
89
90 this._cachingService.Invalidate("ProductList", "PriceList");
91 }
92
93 public void UpdatePrice(string productId, decimal price)
94 {
95 if (!this._dbSimulator.ContainsKey(productId))
96 {
97 throw new KeyNotFoundException();
98 }
99
100 Console.WriteLine($"Updating the price of {productId}.");
101
102 this.DbOperationCount++;
103 this._dbSimulator[productId] = price;
104
105 //
106 this._cachingService.Invalidate($"ProductPrice:{productId}", "PriceList");
107 //
108 }
109
110 private static readonly CachedMethodMetadata _cacheRegistration_GetPrice;
111 private static readonly CachedMethodMetadata _cacheRegistration_GetPriceList;
112 private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
113 private ICachingService _cachingService;
114
115 static ProductCatalogue()
116 {
117 _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);
118 _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);
119 _cacheRegistration_GetPriceList = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetPriceList", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null).ThrowIfMissing("ProductCatalogue.GetPriceList()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
120 }
121
122 public ProductCatalogue(ICachingService? cachingService = null)
123 {
124 this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
125 }
126}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3using Xunit;
4
5namespace Doc.StringDependencies;
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.StringDependencies;
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 var host = builder.Build();
21 host.Run();
22 }
23}
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
stringrepresentation 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:
- The most practical option is often to implement the ICacheDependency in your domain objects.
- Alternatively, create a parallel object model implementing ICacheDependency — just to represent dependencies.
- 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.
- 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.Patterns.Caching;
2using Metalama.Patterns.Caching.Aspects;
3using Metalama.Patterns.Caching.Dependencies;
4using System;
5using System.Collections.Generic;
6using System.Linq;
7
8namespace Doc.ObjectDependencies;
9
10internal static class GlobalDependencies
11{
12 public static ICacheDependency ProductCatalogue =
13 new StringDependency( nameof(ProductCatalogue) );
14
15 public static ICacheDependency ProductList = new StringDependency( nameof(ProductList) );
16}
17
18public 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; } =
24 new[] { GlobalDependencies.ProductCatalogue };
25}
26
27public sealed partial class ProductCatalogue
28{
29 private readonly Dictionary<string, Product> _dbSimulator =
30 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 Console.WriteLine( $"Getting the price of {productId} from database." );
38 this.DbOperationCount++;
39
40 var product = this._dbSimulator[productId];
41
42 //
43 this._cachingService.AddDependency( product );
44 //
45 return product;
46 }
47
48 [Cache]
49 public string[] GetProducts()
50 {
51 Console.WriteLine( "Getting the product list from database." );
52
53 this.DbOperationCount++;
54
55 this._cachingService.AddDependency( GlobalDependencies.ProductList );
56
57 return this._dbSimulator.Keys.ToArray();
58 }
59
60 [Cache]
61 public IReadOnlyCollection<Product> GetPriceList()
62 {
63 this.DbOperationCount++;
64
65 this._cachingService.AddDependency( GlobalDependencies.ProductCatalogue );
66
67 return this._dbSimulator.Values;
68 }
69
70 public void AddProduct( Product product )
71 {
72 Console.WriteLine( $"Adding the product {product.Name}." );
73
74 this.DbOperationCount++;
75
76 this._dbSimulator.Add( product.Name, product );
77
78 this._cachingService.Invalidate( product );
79 this._cachingService.Invalidate( GlobalDependencies.ProductList );
80 }
81
82 public void UpdateProduct( Product product )
83 {
84 if ( !this._dbSimulator.ContainsKey( product.Name ) )
85 {
86 throw new KeyNotFoundException();
87 }
88
89 Console.WriteLine( $"Updating the price of {product.Name}." );
90
91 this.DbOperationCount++;
92 this._dbSimulator[product.Name] = product;
93
94 //
95 this._cachingService.Invalidate( product );
96
97 //
98 }
99}
1using Metalama.Patterns.Caching;
2using Metalama.Patterns.Caching.Aspects;
3using Metalama.Patterns.Caching.Aspects.Helpers;
4using Metalama.Patterns.Caching.Dependencies;
5using System;
6using System.Collections.Generic;
7using System.Linq;
8using System.Reflection;
9
10namespace Doc.ObjectDependencies;
11
12internal static class GlobalDependencies
13{
14 public static ICacheDependency ProductCatalogue =
15 new StringDependency(nameof(ProductCatalogue));
16
17 public static ICacheDependency ProductList = new StringDependency(nameof(ProductList));
18}
19
20public record Product(string Name, decimal Price) : ICacheDependency
21{
22 string ICacheDependency.GetCacheKey(ICachingService cachingService)
23 {
24 if (cachingService == null!)
25 {
26 throw new ArgumentNullException("cachingService", "The 'cachingService' parameter must not be null.");
27 }
28
29 return this.Name;
30 }
31
32 // Means that when we invalidate the current product in cache, we should also invalidate the product catalogue.
33 IReadOnlyCollection<ICacheDependency> ICacheDependency.CascadeDependencies { get; } =
34 new[] { GlobalDependencies.ProductCatalogue };
35}
36
37public sealed partial class ProductCatalogue
38{
39 private readonly Dictionary<string, Product> _dbSimulator =
40 new() { ["corn"] = new Product("corn", 100) };
41
42 public int DbOperationCount { get; private set; }
43
44 [Cache]
45 public Product GetProduct(string productId)
46 {
47 static object? Invoke(object? instance, object?[] args)
48 {
49 return ((ProductCatalogue)instance).GetProduct_Source((string)args[0]);
50 }
51
52 return _cachingService.GetFromCacheOrExecute<Product>(_cacheRegistration_GetProduct, this, new object[] { productId }, Invoke);
53 }
54
55 private Product GetProduct_Source(string productId)
56 {
57 Console.WriteLine($"Getting the price of {productId} from database.");
58 this.DbOperationCount++;
59
60 var product = this._dbSimulator[productId];
61
62 //
63 this._cachingService.AddDependency(product);
64 //
65 return product;
66 }
67
68 [Cache]
69 public string[] GetProducts()
70 {
71 static object? Invoke(object? instance, object?[] args)
72 {
73 return ((ProductCatalogue)instance).GetProducts_Source();
74 }
75
76 return _cachingService.GetFromCacheOrExecute<string[]>(_cacheRegistration_GetProducts, this, new object[] { }, Invoke);
77 }
78
79 private string[] GetProducts_Source()
80 {
81 Console.WriteLine("Getting the product list from database.");
82
83 this.DbOperationCount++;
84
85 this._cachingService.AddDependency(GlobalDependencies.ProductList);
86
87 return this._dbSimulator.Keys.ToArray();
88 }
89
90 [Cache]
91 public IReadOnlyCollection<Product> GetPriceList()
92 {
93 static object? Invoke(object? instance, object?[] args)
94 {
95 return ((ProductCatalogue)instance).GetPriceList_Source();
96 }
97
98 return _cachingService.GetFromCacheOrExecute<IReadOnlyCollection<Product>>(_cacheRegistration_GetPriceList, this, new object[] { }, Invoke);
99 }
100
101 private IReadOnlyCollection<Product> GetPriceList_Source()
102 {
103 this.DbOperationCount++;
104
105 this._cachingService.AddDependency(GlobalDependencies.ProductCatalogue);
106
107 return this._dbSimulator.Values;
108 }
109
110 public void AddProduct(Product product)
111 {
112 Console.WriteLine($"Adding the product {product.Name}.");
113
114 this.DbOperationCount++;
115
116 this._dbSimulator.Add(product.Name, product);
117
118 this._cachingService.Invalidate(product);
119 this._cachingService.Invalidate(GlobalDependencies.ProductList);
120 }
121
122 public void UpdateProduct(Product product)
123 {
124 if (!this._dbSimulator.ContainsKey(product.Name))
125 {
126 throw new KeyNotFoundException();
127 }
128
129 Console.WriteLine($"Updating the price of {product.Name}.");
130
131 this.DbOperationCount++;
132 this._dbSimulator[product.Name] = product;
133
134 //
135 this._cachingService.Invalidate(product);
136
137 //
138 }
139
140 private static readonly CachedMethodMetadata _cacheRegistration_GetPriceList;
141 private static readonly CachedMethodMetadata _cacheRegistration_GetProduct;
142 private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
143 private ICachingService _cachingService;
144
145 static ProductCatalogue()
146 {
147 _cacheRegistration_GetProduct = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetProduct", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null).ThrowIfMissing("ProductCatalogue.GetProduct(string)"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
148 _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);
149 _cacheRegistration_GetPriceList = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetPriceList", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null).ThrowIfMissing("ProductCatalogue.GetPriceList()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
150 }
151
152 public ProductCatalogue(ICachingService? cachingService = null)
153 {
154 this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
155 }
156}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3using Xunit;
4
5namespace Doc.ObjectDependencies;
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.GetProduct( 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 var corn = this._catalogue.GetProduct( "corn" );
44
45 // Adding a product and updating the price.
46 Console.WriteLine( "Updating the catalogue." );
47
48 this._catalogue.AddProduct( new Product( "wheat", 150 ) );
49 this._catalogue.UpdateProduct( corn with { Price = 110 } );
50
51 // Read the catalogue a third time.
52 Assert.Equal( 2, this._catalogue.GetProducts().Length );
53 Assert.Equal( 110, this._catalogue.GetProduct( "corn" ).Price );
54
55 // Print the catalogue.
56 Console.WriteLine( "Catalogue after changes:" );
57 this.PrintCatalogue();
58 }
59}
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 }.1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.ObjectDependencies;
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}
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 wasn't 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) isn't desirable. For instance, if the parent method runs an asynchronous child task using Task.Run and doesn't wait for its completion, then the dependencies of methods called in the child task probably shouldn't be propagated to the parent. This is because the child task could be considered a side effect of the parent method and shouldn't affect caching. Undesired dependencies wouldn't 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.