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:
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?)
24 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:
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?)
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:
GetPrice
returns the price of a given product,GetProducts
returns a list of products without their prices, andGetPriceList
returns both the name and the price of all products.
It has two write methods:
AddProduct
adds a product, therefore it should affect bothGetProducts
andGetPriceList
, andUpdatePrice
changes the price of a given product, and should affectGetPrice
for this product andGetPriceList
.
We model the dependencies using three string templates:
ProductList
represents the product list without prices,ProductPrice:{productId}
represents the price of a given product, andPriceList
represents 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 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 //
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?)
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
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?)
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
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?)
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
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?)
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 //
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?)
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 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 = default)
123 {
124 this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
125 }
126}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3using System.Diagnostics;
4using Xunit;
5
6namespace Doc.StringDependencies;
7
8public 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(
34 "Read the price catalogue a second time time. It should be completely performed from cache." );
35
36 var operationsBefore = this._catalogue.DbOperationCount;
37 this.PrintCatalogue();
38 var operationsAfter = this._catalogue.DbOperationCount;
39 Assert.Equal( operationsBefore, operationsAfter );
40
41 // There should be just one product in the catalogue.
42 Assert.Single( this._catalogue.GetProducts() );
43
44 // Adding a product and updating the price.
45 Console.WriteLine( "Updating the catalogue." );
46 this._catalogue.AddProduct( "wheat", 150 );
47 this._catalogue.UpdatePrice( "corn", 110 );
48
49 // Read the catalogue a third time.
50 Assert.Equal( 2, this._catalogue.GetProducts().Length );
51 Assert.Equal( 110, this._catalogue.GetPrice( "corn" ) );
52
53 // Print the catalogue.
54 Console.WriteLine( "Catalogue after changes:" );
55 this.PrintCatalogue();
56 }
57}
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
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:
- The most practical option is often to implement the ICacheDependency in your domain objects.
- Alternatively, you can 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;
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
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 ) => 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; } =
26 new[] { GlobalDependencies.ProductCatalogue };
27}
28
29public sealed class ProductCatalogue
30{
31 private readonly Dictionary<string, Product> _dbSimulator =
32 new() { ["corn"] = new Product( "corn", 100 ) };
33
34 public int DbOperationCount { get; private set; }
35
36 [Cache]
37 public Product GetProduct( string productId )
38 {
39 Console.WriteLine( $"Getting the price of {productId} from database." );
40 this.DbOperationCount++;
41
42 var product = this._dbSimulator[productId];
43
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( product );
46 //
47 return product;
48 }
49
50 [Cache]
51 public string[] GetProducts()
52 {
53 Console.WriteLine( "Getting the product list from database." );
54
55 this.DbOperationCount++;
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.AddDependency( GlobalDependencies.ProductList );
58
59 return this._dbSimulator.Keys.ToArray();
60 }
61
62 [Cache]
63 public IReadOnlyCollection<Product> GetPriceList()
64 {
65 this.DbOperationCount++;
66
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?)
67 this._cachingService.AddDependency( GlobalDependencies.ProductCatalogue );
68
69 return this._dbSimulator.Values;
70 }
71
72 public void AddProduct( Product product )
73 {
74 Console.WriteLine( $"Adding the product {product.Name}." );
75
76 this.DbOperationCount++;
77
78 this._dbSimulator.Add( product.Name, product );
79
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?)
80 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?)
81 this._cachingService.Invalidate( GlobalDependencies.ProductList );
82 }
83
84 public void UpdateProduct( Product product )
85 {
86 if ( !this._dbSimulator.ContainsKey( product.Name ) )
87 {
88 throw new KeyNotFoundException();
89 }
90
91 Console.WriteLine( $"Updating the price of {product.Name}." );
92
93 this.DbOperationCount++;
94 this._dbSimulator[product.Name] = product;
95
96 //
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?)
97 this._cachingService.Invalidate( product );
98
99 //
100 }
101}
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
14internal static class GlobalDependencies
15{
16 public static ICacheDependency ProductCatalogue =
17 new StringDependency(nameof(ProductCatalogue));
18
19 public static ICacheDependency ProductList = new StringDependency(nameof(ProductList));
20}
21
22public record Product(string Name, decimal Price) : ICacheDependency
23{
24 string ICacheDependency.GetCacheKey(ICachingService cachingService) => this.Name;
25
26 // Means that when we invalidate the current product in cache, we should also invalidate the product catalogue.
27 IReadOnlyCollection<ICacheDependency> ICacheDependency.CascadeDependencies { get; } =
28 new[] { GlobalDependencies.ProductCatalogue };
29}
30
31public sealed class ProductCatalogue
32{
33 private readonly Dictionary<string, Product> _dbSimulator =
34 new() { ["corn"] = new Product("corn", 100) };
35
36 public int DbOperationCount { get; private set; }
37
38 [Cache]
39 public Product GetProduct(string productId)
40 {
41 static object? Invoke(object? instance, object?[] args)
42 {
43 return ((ProductCatalogue)instance).GetProduct_Source((string)args[0]);
44 }
45
46 return _cachingService.GetFromCacheOrExecute<Product>(_cacheRegistration_GetProduct, this, new object[] { productId }, Invoke);
47 }
48
49 private Product GetProduct_Source(string productId)
50 {
51 Console.WriteLine($"Getting the price of {productId} from database.");
52 this.DbOperationCount++;
53
54 var product = this._dbSimulator[productId];
55
56 //
57 this._cachingService.AddDependency(product);
58 //
59 return product;
60 }
61
62 [Cache]
63 public string[] GetProducts()
64 {
65 static object? Invoke(object? instance, object?[] args)
66 {
67 return ((ProductCatalogue)instance).GetProducts_Source();
68 }
69
70 return _cachingService.GetFromCacheOrExecute<string[]>(_cacheRegistration_GetProducts, this, new object[] { }, Invoke);
71 }
72
73 private string[] GetProducts_Source()
74 {
75 Console.WriteLine("Getting the product list from database.");
76
77 this.DbOperationCount++;
78
79 this._cachingService.AddDependency(GlobalDependencies.ProductList);
80
81 return this._dbSimulator.Keys.ToArray();
82 }
83
84 [Cache]
85 public IReadOnlyCollection<Product> GetPriceList()
86 {
87 static object? Invoke(object? instance, object?[] args)
88 {
89 return ((ProductCatalogue)instance).GetPriceList_Source();
90 }
91
92 return _cachingService.GetFromCacheOrExecute<IReadOnlyCollection<Product>>(_cacheRegistration_GetPriceList, this, new object[] { }, Invoke);
93 }
94
95 private IReadOnlyCollection<Product> GetPriceList_Source()
96 {
97 this.DbOperationCount++;
98
99 this._cachingService.AddDependency(GlobalDependencies.ProductCatalogue);
100
101 return this._dbSimulator.Values;
102 }
103
104 public void AddProduct(Product product)
105 {
106 Console.WriteLine($"Adding the product {product.Name}.");
107
108 this.DbOperationCount++;
109
110 this._dbSimulator.Add(product.Name, product);
111
112 this._cachingService.Invalidate(product);
113 this._cachingService.Invalidate(GlobalDependencies.ProductList);
114 }
115
116 public void UpdateProduct(Product product)
117 {
118 if (!this._dbSimulator.ContainsKey(product.Name))
119 {
120 throw new KeyNotFoundException();
121 }
122
123 Console.WriteLine($"Updating the price of {product.Name}.");
124
125 this.DbOperationCount++;
126 this._dbSimulator[product.Name] = product;
127
128 //
129 this._cachingService.Invalidate(product);
130
131 //
132 }
133
134 private static readonly CachedMethodMetadata _cacheRegistration_GetPriceList;
135 private static readonly CachedMethodMetadata _cacheRegistration_GetProduct;
136 private static readonly CachedMethodMetadata _cacheRegistration_GetProducts;
137 private ICachingService _cachingService;
138
139 static ProductCatalogue()
140 {
141 _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);
142 _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);
143 _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);
144 }
145
146 public ProductCatalogue(ICachingService? cachingService = default)
147 {
148 this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
149 }
150}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Microsoft.Extensions.Hosting;
3using System;
4using Xunit;
5
6namespace Doc.ObjectDependencies;
7
8public 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(
34 "Read the price catalogue a second time time. It should be completely performed from cache." );
35
36 var operationsBefore = this._catalogue.DbOperationCount;
37 this.PrintCatalogue();
38 var operationsAfter = this._catalogue.DbOperationCount;
39 Assert.Equal( operationsBefore, operationsAfter );
40
41 // There should be just one product in the catalogue.
42 Assert.Single( this._catalogue.GetProducts() );
43
44 var corn = this._catalogue.GetProduct( "corn" );
45
46 // Adding a product and updating the price.
47 Console.WriteLine( "Updating the catalogue." );
48
49 this._catalogue.AddProduct( new Product( "wheat", 150 ) );
50 this._catalogue.UpdateProduct( corn with { Price = 110 } );
51
52 // Read the catalogue a third time.
53 Assert.Equal( 2, this._catalogue.GetProducts().Length );
54 Assert.Equal( 110, this._catalogue.GetProduct( "corn" ).Price );
55
56 // Print the catalogue.
57 Console.WriteLine( "Catalogue after changes:" );
58 this.PrintCatalogue();
59 }
60}
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 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.