Caching with H.Necessaire

Caching is often used in many scenarios for performance improvement and resource de-congestion.

Caching with H.Necessaire
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

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:

  1. Easiest, register your own, concrete implementation of ImACacher<T> as ImACacher<T>, thus overriding the default InMemoryCacher for the given type.
  2. 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.