By H.Necessaire
in
core
—
03 Jan 2025
Caching with H.Necessaire
Caching is often used in many scenarios for performance improvement and resource de-congestion.
Photo by NASA / Unsplash
Caching is often used in many scenarios for performance improvement and resource de-congestion . H.Necessaire offers an out-of-the-box, easy to use and easy to extend, caching mechanism with a default in-memory implementation.
Usage dotnet add package H.Necessaire
ImACacher<SampleData> cacher = depsProvider.GetCacher<SampleData>();
SampleData data = await cacher.GetOrAdd("SomeData", id => new SampleData { Name = "My Sample Data" }.ToCacheableItem(id).AsTask());
In-memory caching in H.Necessaire
ImADependencyProvider depsProvider
= H.Necessaire.IoC
.NewDependencyRegistry()
.Register<HNecessaireDependencyGroup>(() => new HNecessaireDependencyGroup())
;
Creating a new IoC-DI container with H.Necessaire Core dependencies
H.Necessaire.Usage.Samples/Src/H.Necessaire.Samples/H.Necessaire.Samples.Caching/Program.cs at master · hinteadan/H.Necessaire.Usage.Samples
Usage Samples for H.Necessaire Docs WebSite. Contribute to hinteadan/H.Necessaire.Usage.Samples development by creating an account on GitHub.
Behind the scenes The usage, though simple, has a thread-safe implementation under the hood, presented in the following flowchart, useful to understand the extension points , marked with 🧬.
flowchart TD
depsProvider[depsProvider : **ImADependencyProvider**] --> |GetCacher|CacherManager[CacherManager : **ImACacherFactory**]
CacherManager --> |BuildCacher|ResolveCacher{Resolve **Cacher**}
ResolveCacher --> |**default** to|InMemoryCacher[_InMemoryCacher⟨T⟩_ : **ImACacher⟨T⟩**]
ResolveCacher --> |ID **not** provided|GetImACacher[🧬 **Get** registered **ImACacher⟨T⟩** from _IoC_]
ResolveCacher --> |ID **provided**|BuildImACacher[🧬 **Build** registered **ImACacher⟨T⟩** with **ID** from _IoC_]
GetImACacher --> |Resolved Cacher|IsResolvedCacherNull{Is NULL?}
BuildImACacher --> |Resolved Cacher|IsResolvedCacherNull
IsResolvedCacherNull --> |Yes|InMemoryCacher
IsResolvedCacherNull --> |No|ConcreteCacher[_ConcreteCacher⟨T⟩_ : **ImACacher⟨T⟩**]
InMemoryCacher --> |Add cacher to registry|CacherManagerAsRegistry[CacherManager : **ImACacherRegistry**]
ConcreteCacher --> |Add cacher to registry|CacherManagerAsRegistry
CacherManagerAsRegistry --> |Start Housekeeping Periodic Job|Done[✅ Return **ImACacher⟨T⟩**]
Extending As you can see in the presented flow chart, there are two extension points (🧬) that you can leverage, in order to implement your own custom cacher:
Easiest , register your own, concrete implementation of ImACacher<T>
as ImACacher<T>
, thus overriding the default InMemoryCacher for the given type .More customized , register a concrete implementation of ImACacher<T>
as its own type , having a custom ID ( ID attribute ) or Alias ( Alias attribute ) , which can be resolved accordingly via the cacherID parameter. ImADependencyProvider depsProvider
= IoC.NewDependencyRegistry()
.Register<HNecessaireDependencyGroup>(() => new HNecessaireDependencyGroup())
.Register<ImACacher<SampleData>>(() => new CustomCacher<SampleData>())
;
//cacher will be CustomCacher<SampleData>
ImACacher<SampleData> cacher = depsProvider.GetCacher<SampleData>();
Easiest extension via ImACacher<T>
registration
ImADependencyProvider depsProvider
= IoC.NewDependencyRegistry()
.Register<HNecessaireDependencyGroup>(() => new HNecessaireDependencyGroup())
.Register<StringCacher>(() => new StringCacher())
;
//cacher will be StringCacher
ImACacher<string> stringCacher = depsProvider.GetCacher<string>("string");
Customized extension via MyCustomCacher<T> :
ImACacher<T>
registration with ID or Alias
Custom cacher sample implementations Below are two samples of custom cacher implementations. One generic, one non-generic.
Generic implementation internal class CustomCacher<T> : ImADependency, ImACacher<T>
{
#region Construct
ImALogger logger;
ImACacher<T> baseCacher;
public void ReferDependencies(ImADependencyProvider dependencyProvider)
{
this.logger = dependencyProvider.GetLogger<CustomCacher<T>>();
this.baseCacher = dependencyProvider.GetCacher<T>(cacherID: "InMemory");
}
#endregion
public async Task<T> AddOrUpdate(string id, Func<string, Task<ImCachebale<T>>> cacheableItemFactory)
{
using (new TimeMeasurement(x => Log(nameof(AddOrUpdate), x)))
{
return await baseCacher.AddOrUpdate(id, cacheableItemFactory);
}
}
public async Task Clear(params string[] ids)
{
using (new TimeMeasurement(x => Log(nameof(Clear), x)))
{
await baseCacher.Clear(ids);
}
}
public async Task ClearAll()
{
using (new TimeMeasurement(x => Log(nameof(ClearAll), x)))
{
await baseCacher.ClearAll();
}
}
public async Task<T> GetOrAdd(string id, Func<string, Task<ImCachebale<T>>> cacheableItemFactory)
{
using (new TimeMeasurement(x => Log(nameof(GetOrAdd), x)))
{
return await baseCacher.GetOrAdd(id, cacheableItemFactory);
}
}
public async Task RunHousekeepingSession()
{
using (new TimeMeasurement(x => Log(nameof(RunHousekeepingSession), x)))
{
await baseCacher.RunHousekeepingSession();
}
}
public async Task<OperationResult<T>> TryGet(string id)
{
using (new TimeMeasurement(x => Log(nameof(TryGet), x)))
{
return await baseCacher.TryGet(id);
}
}
void Log(string actionName, TimeSpan duration)
{
logger
.LogInfo($"{nameof(CustomCacher<T>)} operation [{actionName}] took {duration}")
.ConfigureAwait(continueOnCapturedContext: false)
.GetAwaiter()
.GetResult()
;
}
}
Generic custom cacher implementation
H.Necessaire.Usage.Samples/Src/H.Necessaire.Samples/H.Necessaire.Samples.Caching/CustomCacher.cs at master · hinteadan/H.Necessaire.Usage.Samples
Usage Samples for H.Necessaire Docs WebSite. Contribute to hinteadan/H.Necessaire.Usage.Samples development by creating an account on GitHub.
Non-Generic implementation internal class StringCacher : ImADependency, ImACacher<string>
{
#region Construct
ImALogger logger;
ImACacher<string> baseCacher;
public void ReferDependencies(ImADependencyProvider dependencyProvider)
{
this.logger = dependencyProvider.GetLogger<StringCacher>();
this.baseCacher = dependencyProvider.GetCacher<string>(cacherID: "InMemory");
}
#endregion
public async Task<string> AddOrUpdate(string id, Func<string, Task<ImCachebale<string>>> cacheableItemFactory)
{
using (new TimeMeasurement(x => Log(nameof(AddOrUpdate), x)))
{
return await baseCacher.AddOrUpdate(id, cacheableItemFactory);
}
}
public async Task Clear(params string[] ids)
{
using (new TimeMeasurement(x => Log(nameof(Clear), x)))
{
await baseCacher.Clear(ids);
}
}
public async Task ClearAll()
{
using (new TimeMeasurement(x => Log(nameof(ClearAll), x)))
{
await baseCacher.ClearAll();
}
}
public async Task<string> GetOrAdd(string id, Func<string, Task<ImCachebale<string>>> cacheableItemFactory)
{
using (new TimeMeasurement(x => Log(nameof(GetOrAdd), x)))
{
return await baseCacher.GetOrAdd(id, cacheableItemFactory);
}
}
public async Task RunHousekeepingSession()
{
using (new TimeMeasurement(x => Log(nameof(RunHousekeepingSession), x)))
{
await baseCacher.RunHousekeepingSession();
}
}
public async Task<OperationResult<string>> TryGet(string id)
{
using (new TimeMeasurement(x => Log(nameof(TryGet), x)))
{
return await baseCacher.TryGet(id);
}
}
void Log(string actionName, TimeSpan duration)
{
logger
.LogInfo($"{nameof(StringCacher)} operation [{actionName}] took {duration}")
.ConfigureAwait(continueOnCapturedContext: false)
.GetAwaiter()
.GetResult()
;
}
}
Non-Generic custom cacher implementation
H.Necessaire.Usage.Samples/Src/H.Necessaire.Samples/H.Necessaire.Samples.Caching/StringCacher.cs at master · hinteadan/H.Necessaire.Usage.Samples
Usage Samples for H.Necessaire Docs WebSite. Contribute to hinteadan/H.Necessaire.Usage.Samples development by creating an account on GitHub.
Runtime Config CachingHousekeepingIntervalInSeconds
optional , defaults to 60 seconds .
Determines how often the internal Cache Manager runs its housekeeping tasks for each cacher.
Can be a floating point value, parsed via double.TryParse
, thus parsed using system culture . If the value cannot be parsed, the default 60 seconds will be used.