Almost called yamc, this is yet another memory cache.
This library is a thin layer of abstraction over Microsoft.Extensions.Caching.Memory.MemoryCache
(.NET Standard 2.0) that allows to cache an element using more than one key.
It achieves that by creating a different memory cache for each key of an element, so by caching the item multiple times. MKCache
can also be used as a single-key cache.
Given the following type:
class Country
{
// Id unique in the database.
public int Id { get; set; }
public string Name { get; set; }
// ISO 3166-2 standard, unique identifier.
public string ISOCode { get; set; }
}
allow the cache consumers to find the cached item either by Id
or ISOCode
,
since they both identify uniquely the entity, and their values don't overlap,
meaning that no Id
will never equal to an ISOCode
.
// This cache doesn't have any multi-key logic.
// It behaves exactly like a MemoryCache.
var cache = new MKCache<Country>();
var cache = new MKCache<Country>(
// Cache the items using both
// their Id and their ISOCode.
c => c.Id,
c => c.ISOCode);
// The delegate that will retrieve the country,
// if it's not found in the cache.
async static Task<Country> countryResolver()
{
return await _countriesService.ResolveAsync("US");
}
var countryCacheExpiration = TimeSpan.FromMinutes(30);
// Set the item in cache,
// fetched via the countryResolver delegate.
var country = await cache.GetOrCreateAsync(
"US",
countryResolver,
countryCacheExpiration);
// Now the country can be found in the cache
// using both its Id and its ISOCode
var countryFoundById = cache.Get(country.Id);
Assert(countryFoundById != null);
var countryFoundByISO = cache.Get("US");
Assert(countryFoundByISO != null);
Assert(countryFoundById == countryFoundByISO);
It may happen that the cache is requested to resolve an item with the same key multiple times concurrently.
If the cache doesn't have a reference to the item yet, it would cause it to execute as many item-resolution delegates (fetchers)
as the cache invocations.
In order to mitigate this, MKCache
reuses the tasks created by the delegates, which are stored in a ConcurrentDictionary
.
var people = new[]
{
new Person { Name = "Thomas", CountryISOCode = "US", },
new Person { Name = "Astrid", CountryISOCode = "NO", },
new Person { Name = "Elizabeth", CountryISOCode = "US", },
};
// The country "US" will be requested two times,
// and if the cache doesn't hold the country's reference
// _countriesService.ResolveAsync("US") would be invoked two times
// if it wasn't for the ReuseRunningAsyncFetchers property.
cache.ReuseRunningAsyncFetchers = false;
var allCountriesTasks = people.Select(async p =>
{
return await cache.GetOrCreateAsync(
p.CountryISOCode,
() => _countriesService.ResolveAsync(p.CountryISOCode),
TimeSpan.FromMinutes(30));
});
var allCountries = await Task.WhenAll(allCountriesTasks);
var allCountryNames = allCountries.Select(c => c.Name);
var uniqueCountryNames = allCountryNames.Distinct();
This won't ensure that two or more concurrent requests with the same key will never be executed, because there's no lock in play, but in general it will greatly improve the use of resources and performances, proportionally to the amount of "twin" requests executed concurrently.
This behavior can be disabled by setting cache.ReuseRunningAsyncFetchers = false;
(default is true
).
For a running demo, check out the sample project.