In theory, only immutable types should be cached. However, in practice, there are some problematic types that we might still want to cache:
- Stream-like types such as the IEnumerator<T> interface or the Stream class cannot be directly cached because the position of the enumerator or stream can be altered by the caller.
- Interfaces like IEnumerable<T> cannot be cached because the actual value might be a query rather than the result of this query, which would be pointless to cache.
- Some types, like List<T> or arrays, are mutable, and the caller may modify the instance stored in the cache.
So, how can we safely cache these problematic types?
Metalama addresses this issue through the concept of a value adapter. A value adapter allows you to store a different type than the one returned by the cached method. The method return value is referred to as the exposed value because this is the value exposed by your API. The exposed value must be type-compatible with the method return type. The value that is actually stored in the cache is called the stored value. For instance, for a method returning a Stream, the stored value is an array of bytes and the exposed value is a MemoryStream.
Standard value adapters
By default, the following value adapters are used automatically:
Return type | Stored type | Exposed type | Comments |
---|---|---|---|
IEnumerable<T> | List<T> | List<T> | |
IEnumerator<T> | List<T> | List<T>.Enumerator | The Reset() method is not supported by the exposed value. |
Stream | byte [] | MemoryStream |
Implementing a custom value adapter
To implement a custom value adapter:
Create a class implementing the IValueAdapter<T> interface or the non-generic IValueAdapter interface.
Go back to the code that initialized the Metalama Caching by calling serviceCollection.AddMetalamaCaching or CachingService.Create. Call the AddValueAdapter method, then pass an instance of your IValueAdapter<T>.
16 caching => 17 caching.AddValueAdapter( new StringBuilderAdapter() ) );
Note
Null values are automatically handled outside of the value adapters.
Example: Caching a StringBuilder
Let's say you are maintaining a legacy service that implements the unusual practice of returning a StringBuilder
instead of a string
. You are responsible for improving the performance of this API, so you want to cache the result of this method. However, you cannot cache mutable objects, as this would mean that if a caller modifies the StringBuilder
, the next caller would receive the modified copy. Therefore, you decide to cache the string
instead of the StringBuilder
, and return a new StringBuilder
every time the value is fetched from the cache.
1using Metalama.Patterns.Caching.Aspects;
2using System;
3using System.Collections.Generic;
4using System.Linq;
5using System.Text;
6
7namespace Doc.ValueAdapter;
8
9public sealed class ProductCatalogue
10{
11 private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
12
13 public int DbOperationCount { get; private set; }
14
15 // Very weird API but suppose it's legacy and we need to keep it, but cache it.
16 [Cache]
17 public StringBuilder GetProductsAsStringBuilder()
18 {
19 Console.WriteLine( "Getting the product list from database." );
20
21 this.DbOperationCount++;
22
23 var stringBuilder = new StringBuilder();
24
25 foreach ( var productId in this._dbSimulator.Keys )
26 {
27 if ( stringBuilder.Length > 0 )
28 {
29 stringBuilder.Append( "," );
30 }
31
32 stringBuilder.Append( productId );
33 }
34
35 return stringBuilder;
36 }
37}
1using Metalama.Patterns.Caching;
2using Metalama.Patterns.Caching.Aspects;
3using Metalama.Patterns.Caching.Aspects.Helpers;
4using System;
5using System.Collections.Generic;
6using System.Linq;
7using System.Reflection;
8using System.Text;
9
10namespace Doc.ValueAdapter;
11
12public sealed class ProductCatalogue
13{
14 private readonly Dictionary<string, decimal> _dbSimulator = new() { ["corn"] = 100 };
15
16 public int DbOperationCount { get; private set; }
17
18 // Very weird API but suppose it's legacy and we need to keep it, but cache it.
19 [Cache]
20 public StringBuilder GetProductsAsStringBuilder()
21 {
22 static object? Invoke(object? instance, object?[] args)
23 {
24 return ((ProductCatalogue)instance).GetProductsAsStringBuilder_Source();
25 }
26
27 return _cachingService.GetFromCacheOrExecute<StringBuilder>(_cacheRegistration_GetProductsAsStringBuilder, this, new object[] { }, Invoke);
28 }
29
30 private StringBuilder GetProductsAsStringBuilder_Source()
31 {
32 Console.WriteLine("Getting the product list from database.");
33
34 this.DbOperationCount++;
35
36 var stringBuilder = new StringBuilder();
37
38 foreach (var productId in this._dbSimulator.Keys)
39 {
40 if (stringBuilder.Length > 0)
41 {
42 stringBuilder.Append(",");
43 }
44
45 stringBuilder.Append(productId);
46 }
47
48 return stringBuilder;
49 }
50
51 private static readonly CachedMethodMetadata _cacheRegistration_GetProductsAsStringBuilder;
52 private ICachingService _cachingService;
53
54 static ProductCatalogue()
55 {
56 _cacheRegistration_GetProductsAsStringBuilder = CachedMethodMetadata.Register(typeof(ProductCatalogue).GetMethod("GetProductsAsStringBuilder", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null).ThrowIfMissing("ProductCatalogue.GetProductsAsStringBuilder()"), new CachedMethodConfiguration() { AbsoluteExpiration = null, AutoReload = null, IgnoreThisParameter = null, Priority = null, ProfileName = (string?)null, SlidingExpiration = null }, true);
57 }
58
59 public ProductCatalogue(ICachingService? cachingService = default)
60 {
61 this._cachingService = cachingService ?? throw new System.ArgumentNullException(nameof(cachingService));
62 }
63}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3
4namespace Doc.ValueAdapter;
5
6public sealed class ConsoleMain : IConsoleMain
7{
8 private readonly ProductCatalogue _catalogue;
9
10 public ConsoleMain( ProductCatalogue catalogue )
11 {
12 this._catalogue = catalogue;
13 }
14
15 public void Execute()
16 {
17 for ( var i = 0; i < 2; i++ )
18 {
19 // Get the StringBuilder through the cache.
20 var products = this._catalogue.GetProductsAsStringBuilder();
21
22 // Modify the StringBuilder. Without the ValueAdapter, we would receive the
23 // mutated instance and we would have our prefix twice.
24 products.Insert( 0, "The list of products is: " );
25
26 // Print.
27 Console.WriteLine( products );
28 }
29 }
30}
Getting the product list from database. The list of products is: corn The list of products is: corn
1using Metalama.Documentation.Helpers.ConsoleApp;
2using Metalama.Patterns.Caching.Building;
3using Microsoft.Extensions.DependencyInjection;
4
5namespace Doc.ValueAdapter;
6
7internal static class Program
8{
9 public static void Main()
10 {
11 var builder = ConsoleApp.CreateBuilder();
12
13 // Add the caching service and register out ValueAdapter.
14 builder.Services.AddMetalamaCaching(
15 //
16 caching =>
17 caching.AddValueAdapter( new StringBuilderAdapter() ) );
18 //
19
20 // Add other components as usual, then run the application.
21 builder.Services.AddConsoleMain<ConsoleMain>();
22 builder.Services.AddSingleton<ProductCatalogue>();
23
24 using var app = builder.Build();
25 app.Run();
26 }
27}
1using Metalama.Patterns.Caching.ValueAdapters;
2using System.Text;
3
4namespace Doc.ValueAdapter;
5
6internal class StringBuilderAdapter : ValueAdapter<StringBuilder>
7{
8 public override StringBuilder? GetExposedValue( object? storedValue )
9 => storedValue == null ? null : new StringBuilder( (string) storedValue );
10
11 public override object? GetStoredValue( StringBuilder? value ) => value?.ToString();
12}