L1CacheSystem is the application’s internal cache. It was created to solve two problems that IMemoryCache couldn’t address:
- The ability to filter by all cache entities
- Blocking a specific resource by key
Caching is also supported in two modes:
- Entities
- Columns
Columns are more data‑oriented. They can be useful if you often need to scan properties within a class.
The first thing you need to do is to import the packages:
- L1CacheSystem
- L1CacheSystem.Attributes;
- L1CacheSystem.Generator
Then, in the code, you need to declare your cache. Let’s say that, for example, we will cache the MyValue class.
public class MyValue
{
public long Id { get; set; }
public double Value { get; set; }
// other properties
public MyValue Copy()
{
return new MyValue
{
Id = this.Id,
Value = this.Value
};
}
}public partial interface IServiceCacheSystem
{
}
// You can have as many EntityCacheType as you like, the number of different types is not limited.
[EntityCacheType(
cacheType: typeof(MyValue),
interfaceType: typeof(IServiceCacheSystem),
cacheName: "YouCacheName",
propAsKey: nameof(MyValue.Id),
metricCollectFlags: MetricCollectBits.AllBits, // what metrics for this specific cache need to collect
copyMethodName: nameof(MyValue.Copy),
copyMethodType: CopyMethodType.CopyMethodOnType)] // we also support: static method and copy-constructor
[KeyColumnCache(
keyType: typeof(long), // In column mode, the key is a separate entity from the columns.
interfaceType: typeof(IServiceCacheSystem),
cacheName: "YouColumnCacheName"),
ColumnCache(columnType: typeof(long), columnName: nameof(MyValue.Id)),
ColumnCache(columnType: typeof(double), columnName: nameof(MyValue.Value))] // It is recommended to have no more than 32 columns for each KeyColumnCache.
public partial class ServiceCacheSystem : L1CacheSystemBase, IServiceCacheSystem
{
public ServiceCacheSystem(
ILogger<ServiceCacheSystem> logger,
TimeProvider timeProvider,
IMeterFactory meterFactory)
: base(
logger,
timeProvider,
meterFactory)
{
}
}Now you just need create and run system. For example in asp.net we can do this with that way:
public class CacheSystemHostedService : IHostedService
{
private readonly IL1CacheSystemBase _cacheSystem;
public CacheSystemHostedService(IL1CacheSystemBase cacheSystem)
{
_cacheSystem = cacheSystem;
}
public Task StartAsync(CancellationToken cancellationToken)
{
return _cacheSystem.StartAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return _cacheSystem.StopAsync(cancellationToken);
}
}services.AddSingleton<ServiceCacheSystem>();
services.AddSingleton<IL1CacheSystemBase>((provider) => provider.GetRequiredService<ServiceCacheSystem>()); // IL1CacheSystemBase interface providet by L1CacheSystem nuget
services.AddSingleton<IServiceCacheSystem>((provider) => provider.GetRequiredService<ServiceCacheSystem>());Settings is done.
Now usage case (Entity):
public class MyValueService
{
private readonly ILogger<MyValueService> _logger;
private readonly IServiceCacheSystem _casheSystem;
public MyValueService(ILogger<MyValueService> logger, IServiceCacheSystem casheSystem)
{
_logger = logger;
_casheSystem = casheSystem;
}
public CreateValue(long id)
{
await using var lockGuard = new LockGuard(logger: _logger, parent: null);
// Since we are at the root of the call (parent == null), we will either gain ownership of the resource immediately,
// or at the moment when other calls stop owning it. If the waiting should not be infinite, pass a CancellationToken.
await _cacheSystem.GetYouCacheNameLockAsync(id, lockGuard, default);
// do whatever you want: your custom logic
// YouCacheName with Id = id nobody (except you) can change until lockGuard.Dispose not called
var value = new MyValue() { Id = id, Value = 7114 };
// If you need the value and plan to change it after this call, then pass it as `NotTransferred`
// Then CacheSystem will save a copy of it during the call (by copying it using the method you specified earlier, at the configuration stage).
_cacheSystem.SetYouCacheName(value.Id, value, valueOwnership: ValueOwnership.Transferred, expiry: null);
// if "expiry" is provided then casheSystem remove cache value from cache when key is expire
// The Set method is thread‑safe on its own; the lockGuard is only needed if you’re using something more complex than a simple set/get.
}
public DoRelatedWork(long id, ILockGuard? lockGuard = null)
{
// in this method we not shure what poinc its called
// maybe this is root call or maybe recudsive
await using var lockGuard = new LockGuard(_logger, lockGuard);
// If this is not the root (lockGuard != null) and the parent already has a lock on this key,
// then the lock counter inside lockGuard will simply be increased.
await _cacheSystem.GetYouCacheNameLockAsync(id, lockGuard, default);
// your custom logic
// On dispose lockGuard release all locks: decrease lock counter if this lockGuard not the owner of lock or release if is the owner
}
public MyValue GetMyValue(long id)
{
return _cacheSystem.GetYouCacheName(id, ReturnType.Reference); // or ReturnType.Copy
}
public MyValue[] GetAllMyValue(long id, Func<MyValue, bool>? predicate = null)
{
return _cacheSystem.GetAllYouCacheName(predicate, ReturnType.Reference); // or ReturnType.Copy
}
}Now usage case (Column):
public class MyValueService
{
private readonly ILogger<MyValueService> _logger;
private readonly IServiceCacheSystem _casheSystem;
public MyValueService(ILogger<MyValueService> logger, IServiceCacheSystem casheSystem)
{
_logger = logger;
_casheSystem = casheSystem;
}
public CreateValue(long id)
{
await using var lockGuard = new LockGuard(logger: _logger, parent: null);
await _cacheSystem.GetYouCacheNameLockAsync(id, lockGuard, default);
var value = new MyValue() { Id = id, Value = 7114 };
var view = new CacheSystem.SetViewYouColumnCacheName<MyValue> // You can fill it in from anywhere; you just need to create a view and fill in its fields.
{
Id = SetViewOperation.Set,
FuncId = static (source) => source.Id,
Value = SetViewOperation.Set,
FuncValue = static (source) => source.IntProp
};
_casheSystem.SetYouColumnCacheNameFromView(value.Id, view, expiry: null);
// You can also fill in and update not everything at once, but column by column.
// _cacheSystem.SetColumnCacheId(value.Id, value.Id, expiry: null);
// _cacheSystem.SetColumnCacheId(value.Id, value.Value, expiry: null);
}
public double GetValue(long id)
{
var haveValue = _cacheSystem.TryGetYouColumnCacheNameValue(id, out var value);
// haveValue
// There may be no columns at all, since columns are just a promise that they might exist, not a requirement. Each entity may or may not have them.
return value; // We have a simple example, so we’re definitely sure that it’s there.
}
public double SumValue()
{
double sum;
_cacheSystem.ViewInContextYouColumnCacheName((context) =>
{
var span = context.GetValueSpan();
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
});
return sum;
}
}What metrics look like, using Grafana visualization as an example:
