A .NET 8 helper library for common Redis client functionality.
This package aims to build upon StackExchange.Redis and NRedisStack in order to provide all the functionality needed to get up and running with a new project utilizing Redis as fast as possible.
With RedisKit you can add multiple named Redis connections within the Dependency Injection container (beneficial in hybrid environments where Redis server setups could be different e.g. persistent vs non-persistent), then configure each of these connections with features such as .NET Data Protection keys persistence, an ITicketStore
implementation to store large Claims Principals from your application's authentication cookies (very useful within Blazor Server applications where no HTTP Context is available), individual health checks to each Redis server, message consumer and producer implementations to make working with Redis Streams more simple as well as many other helpful methods and extensions for working with the RedisJSON and RediSearch modules.
To get started with this library, you will first need to download and install either the .NET 8 SDK or Microsoft Visual Studio 2022 which comes bundled with the SDK.
- .github - Files needed by GitHub are contained in the
.github
directory. For example; theworkflows
directory contains any Build, Test or Package configurations needed by GitHub Actions. - src - The Source directory contains all the source code for the library.
- test - Contains all unit and integration tests relating to the src directory. These tests are implemented in XUnit and can be run using the
dotnet test
CLI. These tests will also be run as part of the Build & Test workflow in GitHub Actions.
Once you have installed the desired version of RedisKit you can configure this during service registration to be used by an application in the following ways.
You can add a named Redis connection to the Dependency Injection container and configure it by passing in an Action
of RedisConnectionOptions
.
using RedisKit.DependencyInjection.Extensions;
services.AddRedisConnection("cloud-cache", options =>
{
options.ClientName = "MyCompany.MyProduct";
options.ConnectionString = "redislabs.com:6379,allowAdmin=true,ssl=false";
});
The ConnectionString
property can be provided in either of two ways:
- Using the
StackExchange.Redis
format with a comma-separated list of arguments as outlined here. For example:redis0:6379,redis1:6380,allowAdmin=true,ssl=false
- Using the Redis CLI URI style with the
redis://
protocol prefix. For example:redis://username:[email protected]:1234
This can then be used later anywhere in the application via the Singleton IRedisConnectionProvider
that retrieves named Redis connections.
using RedisKit;
using RedisKit.DependencyInjection.Abstractions;
private readonly IRedisConnection _redis;
public MyClassConstructor(IRedisConnectionProvider provider)
{
_redis = provider.GetRequiredConnection("cloud-cache");
}
public async Task DoSomethingAsync()
{
await _redis.Db.StringSetAsync("key", "value");
}
If only a single Redis connection is being used within the application, this can then easily be retrieved directly from DI, rather than going via the provider.
using RedisKit.Abstractions;
private readonly IRedisConnection _redis;
public MyClassConstructor(IRedisConnection redis)
{
_redis = redis
}
public Task<T> GetSomethingAsync()
{
return _redis.Db.HashGetAsync<T>("key");
}
You can continue to use a connection in exactly the same ways that you would otherwise use NRedisStack or StackExchange.Redis directly.
using StackExchange.Redis;
using NRedisStack;
using NRedisStack.RedisStackCommands;
using RedisKit.DependencyInjection.Abstractions;
private readonly IRedisConnection _redis;
public MyClassConstructor(IRedisConnectionProvider provider)
{
_redis = provider.GetRequiredConnection("enterprise-db");
}
public async Task<bool> ExpireKeyAsync()
{
IDatabase db = _redis.Db;
return await db.KeyExpireAsync("key", TimeSpan.FromMinutes(5));
}
public async Task<double> IncrementNumberAsync()
{
JsonCommands json = _redis.Json;
return await json.NumberIncrbyAsync("key", "$.number", 1.2);
}
The main benefit of configuring these named connections is to be able to connect to multiple Redis servers from a single application when the requirements of that connection differ. For example; a caching instance that does not have persistence or high availability enabled, and therefore reduces costs. And a Messaging instance, where both persistence and AOF writing may be needed.
using RedisKit.DependencyInjection.Extensions;
services.AddRedisConnection("enterprise-cache", options =>
{
options.ClientName = "MyCompany.MyApplication.Caching";
options.ConnectionString = "redislabs.com:18526,allowAdmin=false,ssl=true";
});
services.AddRedisConnection("onprem-message-broker", options =>
{
options.ClientName = "MyProduct.Messaging";
options.ConnectionString = "redis://username:password@localhost:6379";
});
These can then individually both be retrieved and used in the same way as detailed above in the Using A Named Connection section.
The return type of IServiceCollection.AddRedisConnection()
is a new instance of IRedisConnectionBuilder
which can be used to chain further configuration onto this connection via it's Fluent API.
For example, to add the .NET Data Protection middleware to the application that utilizes your named Redis connection:
using RedisKit.DependencyInjection.Extensions;
services
.AddRedisConnection("elasticache", options...)
.AddRedisDataProtection();
RedisKit is also able to help abstract Redis JSON document usage within the application. You can use your own custom JSON converters for (de)serialization by configuring it in the following way.
using RedisKit.DependencyInjection.Extensions;
services.ConfigureRedisJson(options =>
{
options.Serializer.Converters.Add(new MyCustomJsonConverter());
});
Then pass the RedisJsonOptions
into the available JsonCommands
methods.
using RedisKit.DependencyInjection.Abstractions;
private readonly JsonCommands _json;
private readonly RedisJsonOptions _options;
public MyClassConstructor(IRedisConnectionProvider provider, IOptions<RedisJsonOptions> options)
{
IRedisConnection redis = provider.GetRequiredConnection("document-db");
_json = redis.Json;
_options = options.Value;
}
public Task<MyCustomClass> GetSomethingAsync()
{
return _json.GetAsync<MyCustomClass>("key", _options);
}
There are IMessageConsumer<T>
and IMessageProducer<T>
abstractions available for interacting with Redis as a message broker.
This utilizes the Redis Streams data type on the server and is implemented in the RedisStreamsConsumer<TMessage>
and RedisStreamsProducer<TMessage>
services respectively.
In order to get started simply chain either one or both of the methods below during service registration.
Note: These do not need to be chained to the same connection. For example you could Consume messages from an internal Redis server and then Produce these out to an entirely different public server.
using RedisKit.DependencyInjection.Extensions;
services
.AddRedisConnection("message-broker", options...)
.AddRedisStreamsConsumer(options =>
{
options.ConsumerGroupName = "Project.ClientApp.Consumer";
}).AddRedisStreamsProducer();
You can then request these abstractions from the Dependency Injection container and use them to produce or consume messages.
using RedisKit.Messaging.Abstractions;
private readonly IMessageConsumer<MyMessage> _consumer;
private readonly IMessageProducer<MyMessage> _producer;
public MyMessagingClass(IMessageConsumer<MyMessage> consumer, IMessageProducer<MyMessage> producer)
{
_consumer = consumer;
_producer = producer
}
public Task ConsumeSomethingAsync()
{
MessageResult<ICollection<MyMessage>> results = await _consumer.ConsumeAsync(5);
// omitted for brevity
}
public Task ProduceSomethingAsync(MyMessage message)
{
bool success = await _producer.SendAsync("key", message)
// omitted for brevity
}
Note: Unfortunately there is a limitation in the
StackExchange.Redis
library that prevents blocking reads, due to the fact that all commands leverage a singleConnectionMultiplexer
instance. Therefore, message consumers will need to periodically request messages as appropriate. See more informaion here.
The .NET Data Protection providers can be configured to use your named Redis connection and save the keys under a specified location like the following:
using RedisKit.DependencyInjection.Extensions;
services
.AddRedisConnection("redis-persistent", options...)
.AddRedisDataProtection(options =>
{
options.KeyName = "my-application:data-protection:keys";
});
If using cookie authentication, there is a provided implementation of ITicketStore
that can be configured on CookieAuthenticationOptions
in order
to utilize the named Redis connection to store instances of AuthenticationTicket
within the server as JSON. This then easily allows for distributed
authentication sessions, and removes the reliance on browsers storing very large ClaimsPrincipal
payloads and from purging this data when they are expected to.
Note: You MUST ensure that Data Protection has been configured for this to work correctly.
using RedisKit.DependencyInjection.Extensions;
using RedisKit.Json.Converters;
services
.AddRedisConnection("redis-persistent", options...)
.AddRedisDataProtection(options...)
.AddRedisTicketStore(options =>
{
options.KeyPrefix = "client-app:authentication:tickets:";
options.CookieSchemeName = "My Cookie Scheme";
});
You can configure the built in .NET Health Check framework to test connectivity to your named Redis connection as part of it's configured Health Checks.
using RedisKit.DependencyInjection.Extensions;
services
.AddRedisConnection("redis-server", options...)
.AddHealthCheck();