Caching with H.Necessaire
Caching is often used in many scenarios for performance improvement and resource de-congestion.
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
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>
asImACacher<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
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
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.