An open API service indexing awesome lists of open source software.

https://github.com/digitalruby/simplecache

Simple yet Powerful L1/L2/L3 caching in .NET. Memory -> Local File -> Redis. I am open to suggestions for enhancements, email support@digitalruby.com.
https://github.com/digitalruby/simplecache

cache caching csharp disk dotnet file io l1 l2 l3 layers performance ram redis

Last synced: 4 months ago
JSON representation

Simple yet Powerful L1/L2/L3 caching in .NET. Memory -> Local File -> Redis. I am open to suggestions for enhancements, email support@digitalruby.com.

Awesome Lists containing this project

README

          

SimpleCache

SimpleCache removes the headache and pain of getting caching right in .NET.

**Features**:
- Simple and intuitive API using generics and tasks.
- Cache storm/stampede prevention (per machine) using `GetOrCreateAsync`. Your factory is guaranteed to execute only once per key, regardless of how many callers stack on it.
- Exceptions are not cached.
- Thread safe.
- Three layers: RAM, disk and redis. Disk and redis can be disabled if desired.
- Null and memory versions of both file and redis caches available for mocking.
- Excellent test coverage.
- Optimized usage of all your resources. Simple cache has three layers to give you maximum performance: RAM, disk and redis.
- Built in json-lz4 serializer for file and redis caching for smaller values and minimal implementation pain.
- You can create your own serializer if you want to use protobuf or other compression options.
- Redis key remove/change/add notifications to keep all your servers in sync.

## Setup and Configuration

```cs
using DigitalRuby.SimpleCache;

// create your builder, add simple cache
var builder = WebApplication.CreateBuilder(args);

// bind to IConfiguration, see the DigitalRuby.SimpleCache.Sandbox project appsettings.json for an example
builder.Services.AddSimpleCache(builder.Configuration);

// you can also create a builder with a strongly typed configuration
builder.Services.AddSimpleCache(new SimpleCacheConfiguration
{
// fill in values here
});
```

The configuration options are:

```json
{
"DigitalRuby.SimpleCache":
{
/*
optional, cache key prefix, by default the entry assembly name is used
you can set this to an empty string to share keys between services that are using the same redis cluster
*/
"KeyPrefix": "sandbox",

/* optional, override max memory size (in megabytes). Default is 1024. */
"MaxMemorySize": 2048,

/* optional redis connection string */
"RedisConnectionString": "localhost:6379",

/*
opptional, override file cache directory, set to empty to not use file cache (recommended if not on SSD)
the default is %temp% which means to use the temp directory
this example assumes running on Windows, for production, use an environment variable or just leave off for default of %temp%.
*/
"FileCacheDirectory": "c:/temp",

/* optional, override the file cache cleanup threshold (0-100 percent). default is 15 */
"FileCacheFreeSpaceThreshold": 10,

/*
optional, override the default json-lz4 serializer with your own class that implements DigitalRuby.SimpleCache.ISerializer
the serializer is used to convert objects to bytes for the file and redis caches
this should be an assembly qualified type name
*/
"SerializerType": "DigitalRuby.SimpleCache.JsonSerializer, DigitalRuby.SimpleCache"
}
}

```

If the `RedisConnectionString` is empty, no redis cache will be used, an no key change notifications will be sent, preventing auto purge of cache values that are modified.

For production usage, you should load this from an environment variable.

## Usage

You can inject the following interface into your constructors to use the layered cache:

```cs
///
/// Layered cache interface. A layered cache aggregates multiple caches, such as memory, file and distributed cache (redis, etc.).

/// Internally, keys are prefixed with the entry assembyly name and the type full name. You can change the entry assembly by specifying a KeyPrefix in the configuration.

///
public interface ILayeredCache : IDisposable
{
///
/// Get or create an item from the cache.
///
/// Type of item
/// Cache key
/// Factory method to create the item if no item is in the cache for the key. This factory is guaranteed to execute only one per key.

/// Inside your factory, you should set the CacheParameters on the GetOrCreateAsyncContext to a duration and size tuple: (TimeSpan duration, int size)
/// Cancel token
/// Task of return of type T
Task GetOrCreateAsync(string key, Func> factory, CancellationToken cancelToken = default);

///
/// Attempts to retrieve value of T by key.
///
/// Type of object to get
/// Cache key
/// Cancel token
/// Result of type T or null if nothing found for the key
Task GetAsync(string key, CancellationToken cancelToken = default);

///
/// Sets value T by key.
///
/// Type of object
/// Cache key to set
/// Value to set
/// Cache parameters
/// Cancel token
/// Task
Task SetAsync(string key, T value, CacheParameters cacheParam, CancellationToken cancelToken = default);

///
/// Attempts to delete an entry of T type by key. If there is no key found, nothing happens.
///
/// The type of object to delete
/// The key to delete
/// Cancel token
/// Task
Task DeleteAsync(string key, CancellationToken cancelToken = default);
}
```

**IMPORTANT**
Do not recursively call cache methods. A cache call should not make other caching calls inside of the factory method.

Your cache key will be modified by the type parameter, ``. This means you can have duplicate `key` parameters for different types.

Cache keys are also prefixed by the entry assembly name by default. This can be changed in the configuration.

The `CacheParameters` struct can be simplified by just passing a `TimeSpan` if you don't know the size. You can also pass a tuple of `(TimeSpan, int)` for a duration, size pair.

If you do know the approximate size of your object, you should specify the size to assist the memory compaction background task to be more accurate.

`GetOrCreateAsync` example:

```cs
var result = await cache.GetOrCreateAsync(key, duration, async context =>
{
// if you need the key, you can use context.Key to avoid capturing the key parameter, saving performance
var value = await MyExpensiveFunctionThatReturnsAStringAsync();

// set the cache duration and size, this is an important step to not miss
// the tuple is minutes, size
context.CacheParameters = (0.5, value.Length * 2);

// you can also set individually
context.Duration = TimeSpan.FromMinutes(0.5);
context.Size = value.Length * 2;

// the context also has a CancelToken property if you need it

return value;
}, stoppingToken);
```

## Serialization

The configuration options mention a serializer. The default serializer is a json-lz4 serializer that gives a balance of ease of use, performance and smaller cache value sizes.

You can create your own serializer if desired, or use the json serializer that does not compress, as is shown in the configuration example.

When implementing your own serializer, inherit and complete the following interface:

```cs
///
/// Interface for serializing cache objects to/from bytes
///
public interface ISerializer
{
///
/// Deserialize
///
/// Bytes to deserialize
/// Type of object to deserialize to
/// Deserialized object or null if bytes is null or empty
object? Deserialize(byte[]? bytes, Type type);

///
/// Deserialize using generic type parameter
///
/// Type of object to deserialize
/// Bytes
/// Deserialized object or null if bytes is null or empty
T? Deserialize(byte[]? bytes) => (T?)Deserialize(bytes, typeof(T));

///
/// Serialize an object
///
/// Object to serialize
/// Serialized bytes or null if obj is null
byte[]? Serialize(object? obj);

///
/// Serialize using generic type parameter
///
/// Type of object
/// Object to serialize
/// Serialized bytes or null if obj is null
byte[]? Serialize(T? obj) => Serialize(obj);

///
/// Get a short description for the serializer, i.e. json or json-lz4.
///
string Description { get; }
}
```

## Layers

Simple cache uses layers, just like a modern CPU. Modern CPU's have multiple layers of cache just like simple cache.

Using multiple layers allows ever increasing amounts of data to be stored at slightly slower retrieval times.

### Memory cache

The first layer (L1), the memory cache portion of simple cache uses IMemoryCache. This will be registered for you automatically in the services collection.

.NET will compact the memory cache based on your settings from the configuration.

### File cache

The second layer (L2), the file cache portion of simple cache uses the temp directory by default. You can override this.

Keys are hashed using Blake2B and converted to base64.

A background file cleanup task runs to ensure you do not overrun disk space.

If you are not running on an SSD, it is recommended to disable the file cache by specifying an empty string for the file cache directory.

### Redis cache

The third and final layer, the redis cache uses StackExchange.Redis nuget package.

The redis layer detects when there is a failover and failback in a cluster and handles this gracefully.

Keyspace notifications are sent to keep cache in sync between machines. Run `CONFIG SET notify-keyspace-events KEA` on your redis servers for this to take effect. Simple cache will attempt to do this as well.

Sometimes you need to purge your entire cache, do this with caution. To cause simple cache to clear memory and file caches, set a redis key that equals `__flushall__` with any value, then wait a second then execute a `FLUSHALL` or `FLUSHDB` command.

As a bonus, a distributed lock factory is provided to acquire locks that need to be synchronized accross machines.

You can inject this interface into your constructors for distributed locking:

```cs
///
/// Interface for distributed locks
///
public interface IDistributedLockFactory
{
///
/// Attempt to acquire a distributed lock
///
/// Lock key
/// Duration to hold the lock before it auto-expires. Set this to the maximum possible duration you think your code might hold the lock.
/// Time out to acquire the lock or default to only make one attempt to acquire the lock
/// The lock or null if the lock could not be acquired
Task TryAcquireLockAsync(string key, TimeSpan lockTime, TimeSpan timeout = default);
}
```

## ISystemClock
Simple cache uses TimeProvider as of version 2.0.0.

## Exceptions and null

Simple cache does not cache exceptions and does not cache null. If you must cache these types of objects, please wrap them in an object that can go in the cache.

---

Thanks for reading!

-- Jeff

https://www.digitalruby.com