/* * Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.VNCache * File: EntityCacheExtensions.cs * * EntityCacheExtensions.cs is part of VNLib.Plugins.Extensions.VNCache * which is part of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.Threading; using System.Threading.Tasks; using System.Runtime.CompilerServices; using VNLib.Data.Caching; namespace VNLib.Plugins.Extensions.VNCache.DataModel { /// /// Provides cache extensions for entity caching /// public static class EntityCacheExtensions { /// /// Gets a new that is backed by the current cache provider /// and generates 1:1 keys from the /// /// /// The instance that generates unique keys for a given entity id /// The new instance public static ScopedCache GetScopedCache(this IGlobalCacheProvider cache, ICacheKeyGenerator cacheKeyGenerator) => new ScopedCacheImpl(cache, cacheKeyGenerator); /// /// Deletes an from the cache from its id /// /// /// /// The entity to delete from the store /// A token to cancel the operation /// A task that completes when the delete operation has compelted /// public static Task RemoveAsync(this IEntityCache cache, T entity, CancellationToken cancellation) where T: class, ICacheEntity { _ = entity ?? throw new ArgumentNullException(nameof(entity)); _ = cache ?? throw new ArgumentNullException(nameof(entity)); //Delete by its id return cache.RemoveAsync(entity.Id, cancellation); } /// /// Asynchronously sets (or updates) a cached value in the backing cache store /// /// /// /// A token to cancel the async operation /// The entity to set at the given key /// A task that completes when the add/update operation has compelted /// public static Task UpsertAsync(this IEntityCache cache, T entity, CancellationToken cancellation) where T: class, ICacheEntity { _ = entity ?? throw new ArgumentNullException(nameof(entity)); _ = cache ?? throw new ArgumentNullException(nameof(cache)); //Add/update with its id return cache.UpsertAsync(entity.Id, entity, cancellation); } /// /// Creates an wrapper using the current global cache provider. /// Understand this will share the same cache store as other stores. Consider creating a scoped cache /// to avoid key collisions /// /// /// /// The entity data serializer /// The entity data deserializer /// The new wrapper instance /// public static IEntityCache CreateEntityCache(this IGlobalCacheProvider cache, ICacheObjectSerializer serialier, ICacheObjectDeserializer deserializer) where T: class { _ = cache ?? throw new ArgumentNullException(nameof(cache)); _ = serialier ?? throw new ArgumentNullException(nameof(serialier)); _ = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); return new EntityCacheImpl(cache, deserializer, serialier); } /// /// Creates an wrapper using the current global cache provider, /// with a Json serializer/deserializer /// /// /// /// The default serializer buffer size /// The new wrapper using json serialization /// public static IEntityCache CreateJsonEntityCache(this IGlobalCacheProvider cache, int bufferSize) where T: class { _ = cache ?? throw new ArgumentNullException(nameof(cache)); JsonCacheObjectSerializer json = new(bufferSize); return CreateEntityCache(cache, json, json); } /// /// Attemts to recover an entity from cache if possible, if a miss occurs, the /// factory function is called to produce a value from a backing store. If the store /// returns a result it is writen back to the cache before this method returns /// /// /// /// The id of the entity to get or laod /// The factory callback function to produce a value when a cache miss occurs /// A token to cancel the operation /// A task that completes by returning the entity /// public static async Task GetOrLoadAsync(this IEntityCache cache, string id, Func> factory, CancellationToken cancellation = default) where T : class { _ = cache ?? throw new ArgumentNullException(nameof(cache)); _ = id ?? throw new ArgumentNullException(nameof(id)); _ = factory ?? throw new ArgumentNullException(nameof(factory)); //try to load the value from cache T? record = await cache.GetAsync(id, cancellation); //If record was not found in cache, load it from the factory if (record is null) { record = await factory(id); //If new record found, write to cache if (record is not null) { await cache.UpsertAsync(id, record, cancellation); } } return record; } /// /// Attemts to recover an entity from cache if possible, if a miss occurs, the /// factory function is called to produce a value from a backing store. If the store /// returns a result it is writen back to the cache before this method returns /// /// /// /// The id of the entity to get or laod /// The factory callback function to produce a value when a cache miss occurs /// A token to cancel the operation /// A task that completes by returning the entity /// /// public static async Task GetOrLoadAsync(this IEntityCache cache, string id, Func> factory, CancellationToken cancellation = default) where T : class { ArgumentNullException.ThrowIfNull(cache); ArgumentNullException.ThrowIfNull(factory); ArgumentException.ThrowIfNullOrWhiteSpace(id); //try to load the value from cache T? record = await cache.GetAsync(id, cancellation); //If record was not found in cache, load it from the factory if (record is null) { record = await factory(id, cancellation); //If new record found, write to cache if(record is not null) { await cache.UpsertAsync(id, record, cancellation); } } return record; } private sealed class EntityCacheImpl : IEntityCache where T : class { private readonly IGlobalCacheProvider _cacheProvider; private readonly ICacheObjectDeserializer _cacheObjectDeserialzer; private readonly ICacheObjectSerializer _cacheObjectSerialzer; public EntityCacheImpl(IGlobalCacheProvider cache, ICacheObjectDeserializer deserializer, ICacheObjectSerializer serializer) { _cacheProvider = cache; _cacheObjectDeserialzer = deserializer; _cacheObjectSerialzer = serializer; } /// public Task GetAsync(string id, CancellationToken token = default) => _cacheProvider.GetAsync(id, _cacheObjectDeserialzer, token); /// public Task RemoveAsync(string id, CancellationToken token = default) => _cacheProvider.DeleteAsync(id, token); /// public Task UpsertAsync(string id, T entity, CancellationToken token = default) => _cacheProvider.AddOrUpdateAsync(id, null, entity, _cacheObjectSerialzer, token); } private sealed class ScopedCacheImpl: ScopedCache { private readonly IGlobalCacheProvider Cache; /// public override bool IsConnected { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Cache.IsConnected; } /// protected override ICacheKeyGenerator KeyGen { get; } /// public override ICacheObjectDeserializer DefaultDeserializer => Cache.DefaultDeserializer; /// public override ICacheObjectSerializer DefaultSerializer => Cache.DefaultSerializer; public ScopedCacheImpl(IGlobalCacheProvider cache, ICacheKeyGenerator keyGen) { this.Cache = cache; KeyGen = keyGen; } /// public override Task DeleteAsync(string key, CancellationToken cancellation) { ArgumentException.ThrowIfNullOrWhiteSpace(key); //Compute the key for the id string scoped = KeyGen.ComputedKey(key); return Cache.DeleteAsync(scoped, cancellation); } /// public override Task GetAsync(string key, ICacheObjectDeserializer deserializer, CancellationToken cancellation) { ArgumentException.ThrowIfNullOrWhiteSpace(key); //Compute the key for the id string scoped = KeyGen.ComputedKey(key); return Cache.GetAsync(scoped, deserializer, cancellation); } /// public override Task AddOrUpdateAsync(string key, string? newKey, T value, ICacheObjectSerializer serialzer, CancellationToken cancellation) { ArgumentException.ThrowIfNullOrWhiteSpace(key); //Compute primary key from id string primary = KeyGen.ComputedKey(key); //If newkey exists, compute the secondary key string? secondary = newKey != null ? KeyGen.ComputedKey(newKey) : null; return Cache.AddOrUpdateAsync(primary, secondary, value, serialzer, cancellation); } /// public override Task GetAsync(string key, ObjectDataSet callback, T state, CancellationToken cancellation) { ArgumentException.ThrowIfNullOrWhiteSpace(key); //Compute the key for the id string scoped = KeyGen.ComputedKey(key); return Cache.GetAsync(scoped, callback, state, cancellation); } /// public override Task AddOrUpdateAsync(string key, string? newKey, ObjectDataGet callback, T state, CancellationToken cancellation) { ArgumentException.ThrowIfNullOrWhiteSpace(key); //Compute primary key from id string primary = KeyGen.ComputedKey(key); //If newkey exists, compute the secondary key string? secondary = newKey != null ? KeyGen.ComputedKey(newKey) : null; return Cache.AddOrUpdateAsync(primary, secondary, callback, state, cancellation); } /// public override object GetUnderlyingStore() => Cache.GetUnderlyingStore(); } } }