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:

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

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
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}
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
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 = null)
147    {
148        this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
149    }
150}
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 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.