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'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:

  • 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.

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
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}
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
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 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, 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.

Source Code
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}
Transformed Code
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.