.NET Redis client library based on StackExchange.Redis adding some interesting features like an extensible serialization strategy, a tagging mechanism to group keys, hash fields and set members, and a fetching mechanism to support atomic add/get operations, all being cluster-compatible.
- .NET Framework and .NET Core support (Net Standard 1.5)
- Typed cache: any serializable object can be used as a cache value.
- Fetching mechanism: shortcut cache methods for atomic add/get operations (cache-aside pattern).
- Tagging mechanism: cache items can be tagged allowing to retrieve or invalidate keys or members by tag.
- Time-To-Live mechanism: each key can be associated to a value defining its time-to-live.
- Lexicographically sorted sets: for fast string matching and auto-complete suggestion.
- Pub/Sub support: Publish-Subscribe implementation with strongly typed messages.
- Geospatial indexes: with radius queries support.
- HyperLogLog support: to count unique things.
- Configurable Serialization: a compressed binary serializer by default, or provide your own serialization.
- Redis data types as .NET collections: List, Set, Sorted Set, Hash and Bitmap support as managed collections.
- Redis Keyspace Notifications: Subscribe to Pub/Sub channels in order to receive events affecting the Redis data set.
- Fully compatible with Redis Cluster: all commands are cluster-safe.
To install the package run the following command on the Package Manager Console:
PM> Install-Package CachingFramework.Redis
The RedisContext
class provides all the functionality divided into five categories, each of which is exposed as a property with the following names:
- Cache
- Collections
- GeoSpatial
- PubSub
- KeyEvents
Connect to Redis on localhost port 6379:
var context = new RedisContext();
var context = new RedisContext("10.0.0.1:7000, 10.0.0.2:7000, connectRetry=10, abortConnect=false, allowAdmin=true");
The constructor parameter must be a valid StackExchange.Redis connection string. Check this for more information about StackExchange.Redis configuration options.
You can inject your own multiplexer by using the appropiate constructor overload:
public class PooledConnectionMultiplexer : IConnectionMultiplexer
{
// ...
}
var myMultiplexer = new PooledConnectionMultiplexer(Common.Config);
var context = new RedisContext(myMultiplexer);
The RedisContext
object should be shared and reused between callers. It is not recommended to create a RedisContext
per operation. Please check StackExchange.Redis documentation for more information.
Different serialization mechanisms are provided:
Serializer | Data | Configuration |
---|---|---|
BinarySerializer |
All types are serialized using the .NET BinaryFormatter and GZIP compressed. |
Default for .Net Framework |
JsonSerializer |
Data is stored as Json using System.Text.Json . Serialization can be configured with JsonSerializerOptions . |
Default for .Net Core |
RawSerializer |
The simple types are serialized as UTF-8 strings. Any other type is serialized using the default serializer. | Serialization can be set-up per type using SetSerializerFor() |
NewtonsoftJsonSerializer |
Data is stored as Json using Newtonsoft.Json . Serialization can be configured with JsonSerializerSettings . |
NuGet Package CachingFramework.Redis.NewtonsoftJson |
MsgPackSerializer |
Data is stored as MessagePack via MsgPack.Cli . |
NuGet Package CachingFramework.Redis.MsgPack |
The RedisContext
class has constructor overloads to supply the serialization mechanism, for example:
var context = new RedisContext("localhost:6379", new JsonSerializer());
You can change the default serialization mechanism by setting the static property RedisContext.DefaultSerializer
.
This default is used when creating RedisContext
without an explicit serializer.
Of course you must do this before any context creation, for example on your application startup:
RedisContext.DefaultSerializer = new JsonSerializer();
NOTE: If you don't explicitly set the serializer, it will default depending on the framework: .NET Framework version will default to
BinarySerializer
and .NET Core toJsonSerializer
.
If you plan to consume data from different framework versions, make sure all of them are using the same serialization method.
To provide a custom serialization mechanism, implement the ISerializer
interface. For example:
public class MySerializer : ISerializer
{
public RedisValue Serialize<T>(T value)
{
return value.ToString();
}
public T Deserialize<T>(RedisValue value)
{
return (T)Convert.ChangeType(value.ToString(), typeof(T));
}
}
The RawSerializer
allows to dynamically override the serialization/deserialization logic per type with the method SetSerializerFor<T>()
.
For example, to allow the serialization of a StringBuilder
as an UTF-8 encoded string:
// On your startup logic:
RedisContext.DefaultSerializer = new RawSerializer()
.SetSerializerFor<StringBuilder>
(
sb => Encoding.UTF8.GetBytes(sb.ToString()),
b => new StringBuilder(Encoding.UTF8.GetString(b))
);
Any primitive type or serializable class can be used as a cache value.
For example:
[Serializable]
public class User
{
public int Id { get; set; }
public string UserName { get; set; } ...
}
Note: The Serializable attribute is needed by the default serialization method binary serializer. If you use, for example, the json serializer, this attribute becomes unnecessary. See Serialization section for more information.
string redisKey = "user:1";
User value = new User() { Id = 1 }; // any serializable object
context.Cache.SetObject(redisKey, value);
Add a single object to the cache with a Time-To-Live of 1 day:
context.Cache.SetObject(redisKey, value, TimeSpan.FromDays(1));
User user = context.Cache.GetObject<User>(redisKey);
context.Cache.Remove(redisKey);
Shortcut methods are provided for atomic add/get operations (see Cache-Aside pattern).
Try to get an object from the cache, inserting it to the cache if it does not exists:
var user = context.Cache.FetchObject<User>(redisKey, () => GetUserFromDatabase(id));
The method GetUserFromDatabase
will only be called when the value is not present on the cache, in which case will be added to the cache before returning it.
Fetch an object with a time-to-live:
var user = context.Cache.FetchObject<User>(redisKey, () => GetUserFromDatabase(id), TimeSpan.FromDays(1));
The TTL value is set only when the value is not present on the cache.
Hashes are maps composed of fields associated with values, like .NET dictionaries.
Set an object on a redis key indexed by a field key (sub-key):
void InsertUser(User user)
{
var redisKey = "users:hash";
var fieldKey = "user:id:" + user.Id;
context.Cache.SetHashed(redisKey, fieldKey, user);
}
Get an object by the redis key and a field key:
User u = context.Cache.GetHashed<User>("users:hash", "user:id:1");
IDictionary<string, User> users = context.Cache.GetHashedAll<User>("users:hash");
Objects within a hash can be of different types.
Incrementally iterate over the hash members by matching a glob-style pattern with the field names.
For example, to iterate over the members of a hash whose field names starts with "user:".
var scan = context.Cache.ScanHashed<User>("users:hash", "user:*");
foreach (var item in scan)
{
string key = item.Key;
User value = item.Value;
// ...
}
context.Cache.RemoveHashed("users:hash", "user:id:1");
var user = context.Cache.FetchHashed<User>("users:hash", "user:id:1", () => GetUser(1));
The method GetUser
will only be called when the value is not present on the hash, in which case will be added to the hash before returning it.
Hashes can be handled as .NET Dictionaries by using the GetRedisDictionary
method on RedisContext.Collections
, for example:
var dict = context.Collections.GetRedisDictionary<string, User>("users:hash");
dict.Add("user:id:1", user);
For more information about collections, please see COLLECTIONS.md.
Cluster compatible tagging mechanism where tags are used to group keys, hash fields, set members, sorted set members and geospatial members, so they can be retrieved or invalidated at the same time. A tag can be related to any number of keys, hash fields, or set members.
Add a single object to the cache and associate it with tags red and blue:
context.Cache.SetObject("user:1", user, new[] { "red", "blue" });
Tags can point to a field in a hash.
context.Cache.SetHashed("users:hash", "user:id:1", value, new[] { "red" });
Add a single member to a redis set and associate the member to the tag red:
context.Cache.AddToSet("users:set", value, new[] { "red" });
Add a single member to a redis sorted set and associate the member to the tag blue:
context.Cache.AddToSortedSet("users:sortedset", 100.00, value, new[] { "blue" });
Relate the key to the green tag:
context.Cache.AddTagsToKey("user:1", new [] { "green" });
Relate the hash field to the green tag:
context.Cache.AddTagsToHashField("users:hash", "user:id:1", new[] {"green"});
Relate a set member to the blue tag:
context.Cache.AddTagsToSetMember("users:set", "user:id:1", new[] { "blue" });
The same method can be used to relate tags to Sorted Set members and GeoSpatial index members.
Remove the relation between the key and the tag green:
context.Cache.RemoveTagsFromKey("user:1", new [] { "green" });
Remove the relation between the hash field and the tag green:
context.Cache.RemoveTagsFromHashField("users:hash", "user:id:1", new [] { "green" });
Remove the relation between a set member and the tag green:
context.Cache.RemoveTagsFromSetMember("users:set", "user:id:1", new[] { "green" });
The same method can be used to remove tags from Sorted Set members and GeoSpatial index members.
Get all the objects related to red and/or green:
IEnumerable<User> users = context.Cache.GetObjectsByTag<User>("red", "green");
This assumes all the keys related to the tags are of the same type.
Determines whether a redis string key is included on a given tag:
bool x = context.Cache.IsStringKeyInTag("key", "blue");
Determines whether a redis hash field is included on a given tag:
bool x = context.Cache.IsHashFieldInTag("users:hash", "user:id:1", "blue");
Determines whether a redis set member is included on a given tag:
bool x = context.Cache.IsSetMemberInTag("users:set", user, "red");
Remove all the keys, hash fields, set members and sorted set members related to blue and/or green tags:
context.Cache.InvalidateKeysByTag("blue", "green");
Get all the members (keys, hash fields and set members) related to a particular tag:
IEnumerable<TagMember> members = context.Cache.GetMembersByTag("blue");
foreach (TagMember member in members)
{
var key = member.Key;
var type = member.MemberType;
var user = member.GetMemberAs<User>();
}
TagMember
contains the Redis Key on its Key
property and the member type on MemberType
property.
If the member type is not a redis string, you can get the member value pointed by the tag
by calling the GetMemberAs<T>
method.
The MemberType
is one of StringKey
, HashField
, SetMember
or SortedSetMember
.
Get all the keys, hash fields and set members related to the given tags:
ISet<string> keys = context.Cache.GetKeysByTag(new [] { "green" });
If the tag is related to a hash field, the string returned will be in the form:
{hashKey}:$_->_$:{field}
If the tag is related to a set, sorted set or geospatial index the string returned will be in the form:
{setKey}:$_-S>_$:{member}
For example:
users:hash:$_->_$:user:id:1
means the field user:id:1
of hash users:hash
.
users:set:$_-S>_$:user:id:2
means the member user:id:2
of set users:set
.
A strongly typed Publish/Subscribe mechanism is provided.
Listen for messages of type User
on the channel users:
context.PubSub.Subscribe<User>("users", user => Console.WriteLine(user.Id));
Publishes a messages of type User
to the channel users:
context.PubSub.Publish<User>("users", new User() { Id = 1 });
context.PubSub.Unsubscribe("users");
Redis Pub/Sub supports pattern matching in which clients may subscribe to glob-style patterns to receive all the messages sent to channel names matching a given pattern.
context.PubSub.Subscribe<User>("users.*", user => Console.WriteLine(user.Id));
This will listen to any channel whose name starts with "users.".
context.PubSub.Unsubscribe("users.*");
static void Main()
{
var context = new RedisContext("10.0.0.1:7000");
context.PubSub.Subscribe<string>("chat", m => Console.WriteLine(m));
while (true)
{
context.PubSub.Publish("chat", Console.ReadLine());
}
}
The Geospatial Redis API consists of a set of commands that add support for storing and querying pairs of longitude/latitude coordinates into Redis keys.
The Geospatial API is available from Redis version >= 3.2.0.
Add a user to a geospatial index by its coordinates:
string redisKey = "users:geo";
context.GeoSpatial.GeoAdd<User>(redisKey, new GeoCoordinate(20.637, -103.402), user);
Add a user to a geospatial index and relate it to a tag:
string redisKey = "users:geo";
context.GeoSpatial.GeoAdd<User>(redisKey, new GeoCoordinate(20.637, -103.402), user, new[] { "tag" });
GeoCoordinate coord = context.GeoSpatial.GeoPosition(redisKey, user);
var lat = coord.Latitude;
var lon = coord.Longitude;
Get the distance in Kilometers between two user items:
double dist = context.GeoSpatial.GeoDistance(redisKey, user1, user2, Unit.Kilometers);
Get the users within a 100 Km radius:
string redisKey = "users:geo";
var center = new GeoCoordinate(20.553, -102.925);
double radius = 100;
var results = context.GeoSpatial.GeoRadius<User>(redisKey, center, radius, Unit.Kilometers);
The results includes the position and the distance from the center:
foreach (var r in results)
{
double dist = r.DistanceToCenter;
GeoCoordinate pos = r.Position;
User user = r.Value;
...
}
Get the distance (in kilometers) between two addresses by using GoogleMaps.LocationServices:
private Context _context = new RedisContext();
private GoogleLocationService _location = new GoogleLocationService();
public double Distance(string address1, string address2)
{
var redisKey = "dist";
var loc1 = _location.GetLatLongFromAddress(address1);
var loc2 = _location.GetLatLongFromAddress(address2);
_context.GeoSpatial.GeoAdd(redisKey, new[]
{
new GeoMember<string>(loc1.Latitude, loc1.Longitude, address1),
new GeoMember<string>(loc2.Latitude, loc2.Longitude, address2)
});
return _context.GeoSpatial.GeoDistance(redisKey, address1, address2, Unit.Kilometers);
}
For example:
double km = Distance("London", "Buenos Aires");
The Redis HyperLogLog implementation provides a very good approximation of the cardinality of a set using a very small amount of memory.
To add elements to the HLL, use the HyperLogLogAdd
method:
bool result = context.Cache.HyperLogLogAdd<string>("key", "10.0.0.1");
The method returns True
if the underlying HLL count was modified.
To get the cardinality (the count of unique elements) use the HyperLogLogCount
method:
long count = context.Cache.HyperLogLogCount("key");
Considering a unique login as the Username + IP address combination.
Each time a user login, add the element to the HLL with the HyperLogLogAdd
method:
public void OnLogin(string userName, string ipAddress)
{
var info = new LoginInfo(userName, ipAddress);
var key = "logins:" + DateTime.Now.ToString("yyyyMMdd");
context.Cache.HyperLogLogAdd(key, info);
}
To get the unique login count for a specific date, use the HyperLogLogCount
method:
public long GetLoginCount(DateTime date)
{
var key = "logins:" + date.ToString("yyyyMMdd");
return context.Cache.HyperLogLogCount(key);
}
Subscribe to keyspace events to receive events affecting the Redis data. See the Redis notification documentation.
By default keyspace events notifications are disabled. To enable notifications use the notify-keyspace-events of redis.conf or via the CONFIG SET, for example:
redis> CONFIG SET notify-keyspace-events KEA
To access the Keyspace Notifications API, use the Subscribe
/Unsubscribe
methods on the context's KeyEvents
property.
The subscribe method callback in an Action<string, KeyEvent>
where the first parameter is the Redis key affected, and the second is the operation performed.
Receive all the commands affecting a specific key:
context.KeyEvents.Subscribe("user:1", (string key, KeyEvent cmd) =>
{
if (cmd == KeyEvent.Delete)
{
//Key "user:1" was deleted
}
Console.WriteLine("command " + cmd);
});
Receive a specific command affecting any key:
context.KeyEvents.Subscribe(KeyEvent.PushLeft, (key, cmd) =>
{
Console.WriteLine("key {0} received an LPUSH", key);
});
Receive any command affecting any key:
context.KeyEvents.Subscribe(KeyEventSubscriptionType.All, (key, cmd) =>
{
Console.WriteLine("key {0} - command {1}", key, cmd);
});
Stop receiving all commands affecting the given key:
context.KeyEvents.Unsubscribe("user:1");
Stop receiving LPUSH commands affecting any key:
context.KeyEvents.Unsubscribe(KeyEvent.PushLeft);
Some Redis commands were omitted by design falling into these two categories:
-
Commands that operates on multiple keys are not included because they are incompatible with a cluster topology. (i.e. MGET, SINTER, SUNION)
-
Commands that assumes a format on the Redis value were omitted because the library doesn't make assumptions on the serialization method. (i.e. INCRBY, APPEND.) (Except for the collections RedisBitmap, RedisLexicographicSet and RedisString)
You can still call these commands via StackExchange.Redis
API, accesing the ConnectionMultiplexer
by calling the GetConnectionMultiplexer()
method on the RedisContext
(see next section).
To use the StackExchange.Redis
API, call the GetConnectionMultiplexer()
method on the RedisContext
.
For example:
var context = new RedisContext();
var multiplexer = context.GetConnectionMultiplexer(); // SE.Redis Connection Multiplexer
multiplexer.GetDatabase().StringIncrement("key", 1); // SE.Redis API
You can handle Redis Lists as IList<T>
, Hashes as IDictionary<K, V>
, Sets, Lex Sets and Bitmaps as ICollection<T>
, and more.
Access these objects by the Collections
property on RedisContext
.
For example:
var hash = context.Collections.GetRedisDictionary<int, User>("users:hash");
hash.Add(1, new User() { Id = 1 }, new [] { "tag" });
For details please see COLLECTIONS.md documentation file
If you like this project please contribute in any of the following ways:
- Star this project on GitHub.
- Request a new feature or expose any bug you found by creating a new issue.
- Ask any questions about the library on StackOverflow.
- Subscribe to and use the Gitter CachingFramework.Redis channel.
- Spread the word by blogging about it, or sharing it on social networks: