├── .gitignore
├── Redis101Examples.csproj
├── README.md
├── HllExercises.cs
├── ListExercises.cs
├── SetExercises.cs
├── HashExercises.cs
├── Program.cs
├── SortedSetExercises.cs
├── StringExercises.cs
├── GeoLocationExercises.cs
├── PubSubExercises.cs
└── StreamExercises.cs
/.gitignore:
--------------------------------------------------------------------------------
1 | **/obj/**
2 | **/bin**
3 | **/Session.vim
4 | .idea
5 | .vscode
6 | Folder*
7 |
--------------------------------------------------------------------------------
/Redis101Examples.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net5.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Basic Redis Examples for .NET using StackExchange.Redis
2 |
3 | The code in these example makes a few assumptions:
4 |
5 | - You have a local Redis server running on port 6379.
6 | - You have the StackExchange.Redis (SE.R) package installed locally.
7 |
8 | You can download Redis from [redis.io](https://redis.io/download) or you can run the [Redis Docker image](https://hub.docker.com/_/redis).
9 |
10 | StackExchange.Redis can be installed using NuGet. Use the package name `StackExchange.Redis`.
11 |
12 | To run the code, run:
13 |
14 | `dotnet run`
15 |
16 | from the terminal or through an IDE VS Code, JetBrains Rider, or Visual Studio.
--------------------------------------------------------------------------------
/HllExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using StackExchange.Redis;
3 | using Xunit;
4 |
5 | namespace Redis101Examples
6 | {
7 | // You can do 3 things with HyperLogLog: Add, Union, Merge
8 | // Probabilistic data type: How many unique things are in this set? Cardinality?
9 | // HyperLogLog will always be only 12kb
10 | static class HllExercises
11 | {
12 | public static void Exercises(IDatabase db)
13 | {
14 | Console.WriteLine("Running HyperLogLog exercises...");
15 | // Hyperloglog
16 | db.HyperLogLogAdd("unique:landingpage:hits", "userId:123");
17 | db.HyperLogLogAdd("unique:landingpage:hits", "userId:456");
18 | var cardinality = db.HyperLogLogLength("unique:landingpage:hits");
19 | Assert.Equal(2, cardinality);
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/ListExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xunit;
3 | using StackExchange.Redis;
4 |
5 | namespace Redis101Examples
6 | {
7 | static class ListExercises
8 | {
9 | public static void Exercises(IDatabase db)
10 | {
11 | Console.WriteLine("Running List exercises...");
12 | db.KeyDelete("avengerList");
13 |
14 | // 1. Add an item to a list.
15 | db.ListLeftPush("avengerList", "Iron Man");
16 | db.ListLeftPush("avengerList", new RedisValue[] {"Wasp", "Ant Man"});
17 |
18 | long length = db.ListLength("avengerList");
19 | Assert.Equal(3, length);
20 |
21 | // 2. Pop a value from the right-hand size of the list.
22 | RedisValue avenger = db.ListRightPop("avengerList");
23 | Assert.Equal("Iron Man", avenger);
24 |
25 | // 3. Check the new length of the list.
26 | long newLength = db.ListLength("avengerList");
27 | Assert.Equal(2, newLength);
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/SetExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xunit;
3 | using System.Collections.Generic;
4 | using StackExchange.Redis;
5 |
6 | namespace Redis101Examples
7 | {
8 | static class SetExercises
9 | {
10 | public static void Exercises(IDatabase db)
11 | {
12 | Console.WriteLine("Running Set exercises...");
13 | db.KeyDelete("avengers");
14 |
15 | // 1. Add items to a set.
16 | db.SetAdd("avengers", "Iron Man");
17 | // Initialize a new collection with duplicates
18 | List avengers = new List
19 | {
20 | "Hulk",
21 | "Hulk",
22 | "Hulk",
23 | "Thor",
24 | "Ant Man"
25 | };
26 | long numAdded = db.SetAdd("avengers", avengers.ToArray());
27 | Assert.Equal(3, numAdded);
28 |
29 | // 2. How many members does this set contain?
30 | long setLength = db.SetLength("avengers");
31 | Assert.Equal(4, setLength);
32 |
33 | // 3. Check to see of a set contains a member (a O(1) operation).
34 | bool containsIronMan = db.SetContains("avengers", "Iron Man");
35 | Assert.True(containsIronMan);
36 |
37 | bool containsSuperman = db.SetContains("avengers", "Superman");
38 | Assert.False(containsSuperman);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/HashExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xunit;
3 | using System.Collections.Generic;
4 | using StackExchange.Redis;
5 |
6 | namespace Redis101Examples
7 | {
8 | static class HashExercises
9 | {
10 | public static void Exercises(IDatabase db)
11 | {
12 | Console.WriteLine("Running Hash exercises...");
13 | db.KeyDelete("avenger:1");
14 |
15 | // 1. Create a new Redis hash, with two fields.
16 | db.HashSet("avenger:1", "name", "Tony Stark");
17 | db.HashSet("avenger:1", "age", "41");
18 |
19 | // 2. Get the value of a field from a Hash.
20 | // What's the name of avenger 1?
21 | RedisValue name = db.HashGet("avenger:1", "name");
22 | Assert.Equal("Tony Stark", name);
23 |
24 | //What if I want to update an existing hash element
25 | db.HashSet("avenger:1", "age", "42");
26 |
27 | // 3. Write multiple fields all at once.
28 | List fields = new List
29 | {
30 | new HashEntry("alias", "Iron Man"), new HashEntry("address", "Stark Tower")
31 | };
32 | db.HashSet("avenger:1", fields.ToArray());
33 |
34 |
35 | // 4. Return the entire hash
36 | HashEntry[] entireHashset = db.HashGetAll("avenger:1");
37 | // What are the 4 HashEntry names?
38 | Assert.Equal(4, entireHashset.Length);
39 |
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using StackExchange.Redis;
2 |
3 | namespace Redis101Examples
4 | {
5 | static class Program
6 | {
7 | static void Main()
8 | {
9 | // Configuration options and patterns
10 | string redisConfiguration = "localhost:6379"; // Store *basic* information in a string
11 | var options = ConfigurationOptions.Parse(redisConfiguration); // create a ConfigurationOptions instance
12 | options.AllowAdmin = true; // and set specific details with options
13 | options.Ssl = false;
14 | options.ConnectRetry = 1;
15 | options.HighPrioritySocketThreads = true;
16 |
17 | // Multiplexer is intended to be reused
18 | ConnectionMultiplexer redisMultiplexer = ConnectionMultiplexer.Connect(options);
19 |
20 | // The database reference is a lightweight passthrough object intended to be used and discarded
21 | IDatabase db = redisMultiplexer.GetDatabase();
22 |
23 | // All Redis commands and data types are supported and available through the API
24 |
25 | StringExercises.Exercises(db);
26 |
27 | HashExercises.Exercises(db);
28 |
29 | ListExercises.Exercises(db);
30 |
31 | SetExercises.Exercises(db);
32 |
33 | SortedSetExercises.Exercises(db);
34 |
35 | GeoLocationExercises.Exercises(db);
36 |
37 | HllExercises.Exercises(db);
38 |
39 | StreamExercises.Exercises(db);
40 |
41 | PubSubExercises.Exercises(redisMultiplexer);
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/SortedSetExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xunit;
3 | using System.Collections.Generic;
4 | using StackExchange.Redis;
5 |
6 | namespace Redis101Examples
7 | {
8 | static class SortedSetExercises
9 | {
10 | public static void Exercises(IDatabase db)
11 | {
12 | Console.WriteLine("Running Sorted Set tests...");
13 | db.KeyDelete("avengers:strength");
14 |
15 | // 1. Create a sorted set entry. These entries consist of
16 | // a unique value and a score.
17 | // Suppose we're modeling the "strength" of each Avenger.
18 | db.SortedSetAdd("avengers:strength", "Iron Man", 100);
19 | db.SortedSetAdd("avengers:strength", "Hulk", 250);
20 |
21 | // 2. Add many Avengers at once.
22 | List avengerIssuesList = new List
23 | {
24 | new SortedSetEntry("Spiderman", 95),
25 | new SortedSetEntry("Vision", 47),
26 | new SortedSetEntry("Quicksilver", 118)
27 | };
28 | db.SortedSetAdd("avengers:strength", avengerIssuesList.ToArray());
29 |
30 | // 3. Let's find the strongest Avenger.
31 | // A super efficient operation (O(log n)) even with sets
32 | // containing millions of elements.
33 | // Default sort order is lowest to highest - so we use Order.Descending.
34 | SortedSetEntry[] strongestResults = db.SortedSetRangeByRankWithScores("avengers:strength", 0, 0, order: Order.Descending);
35 | if (strongestResults?[0] != null)
36 | {
37 | var strongest = strongestResults[0];
38 | Assert.Equal("Hulk", strongest.Element);
39 | Assert.Equal(250, strongest.Score);
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/StringExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xunit;
3 | using System.Text;
4 | using StackExchange.Redis;
5 |
6 | namespace Redis101Examples
7 | {
8 | static class StringExercises
9 | {
10 | public static void Exercises(IDatabase db)
11 | {
12 | Console.WriteLine("Running String samples...");
13 |
14 | // 1. Set and get a Redis string.
15 | db.StringSet("planet:0", "Mercury");
16 | RedisValue response = db.StringGet("planet:0");
17 | Assert.Equal("Mercury", response);
18 |
19 | // 2. Represent a string as raw bytes.
20 | byte[] key = Encoding.UTF8.GetBytes("key"), value = Encoding.UTF8.GetBytes("value");
21 | db.StringSet(key, value);
22 | byte[] rawValue = db.StringGet(key);
23 |
24 | // 3. Perform bitwise operations.
25 | db.StringSetBit("bitset1", 16, true); // Set the bit at offset 16 to 1
26 | bool isBitSet = db.StringGetBit("bitset1", 16); // Returns true or false for the value of the bit at offset 16
27 | Assert.True(isBitSet);
28 |
29 | // 4. Create two new bitfields
30 | db.StringSet("bitfield0", new byte[] {3}); // Create new bitfields using binary safe string
31 | db.StringSet("bitfield1", new byte[] {6});
32 | // The available bitwise logical operators are `and`, `or`, `xor`, and `not`.
33 | long resultLength = db.StringBitOperation(Bitwise.And, "resultBitfield", "bitfield0", "bitfield1");
34 | Assert.Equal(1, resultLength);
35 |
36 | byte[] resultValue = (byte[]) db.StringGet("resultBitfield"); // Resulting bitfield can be read back as a byte array
37 | long bitCount = db.StringBitCount("resultBitfield");
38 | Assert.Equal(1, bitCount);
39 | //What should you see when you uncomment the next line
40 | //Console.WriteLine("resultValue byte[]: " + string.Join(",", resultValue));
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/GeoLocationExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using StackExchange.Redis;
4 | using Xunit;
5 |
6 | namespace Redis101Examples
7 | {
8 | static class GeoLocationExercises
9 | {
10 | public static void Exercises(IDatabase db)
11 | {
12 | Console.WriteLine("Running Geo exercises...");
13 | // Geo sets are "Sorted Sets"
14 | // 52-bit integers for geohash
15 | // GeoLocation
16 | db.GeoAdd("avenger:locations", -73.984016, 40.754932, "Stark_Tower"); // Add single location
17 |
18 | List avengerLocations = new List
19 | {
20 | new(-73.968008, 40.771071, "Avengers_Mansion")
21 | };
22 | db.GeoAdd("avenger:locations", avengerLocations.ToArray()); // Add multiple locations
23 |
24 | // Get the distance between the locations in meters
25 | //db.GeoDistance("avenger:locations", "Stark_Tower", "Avengers_Mansion", GeoUnit.Meters);
26 | // GeoUnit.Meters is default, so this result is the same. Other options are Kilometers, Miles, Feet
27 | var metersBetween = db.GeoDistance("avenger:locations", "Stark_Tower", "Avengers_Mansion");
28 | Assert.Equal(2245.3769, metersBetween);
29 |
30 | // Get the longitude and latitude of the requested member. Multiple locations can be returned by providing a RedisValue[] containing a list of members
31 | var geoPosition = db.GeoPosition("avenger:locations", "Stark_Tower");
32 | // Converting from GEOHASH to GEOPOS can provide different outputs than inputs, so we use an approximation
33 | Assert.Contains("-73.98401", Convert.ToDouble(geoPosition.Value.Longitude).ToString());
34 | Assert.Contains("40.75493", Convert.ToDouble(geoPosition.Value.Latitude).ToString());
35 |
36 | // Find all set members within the specified radius using the provided location as the center
37 | var results = db.GeoRadius("avenger:locations", "Stark_Tower", 3, unit: GeoUnit.Kilometers);
38 | foreach (var result in results)
39 | {
40 | Console.WriteLine("In Radius: " +result);
41 | }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/PubSubExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using StackExchange.Redis;
4 | using Xunit;
5 |
6 | namespace Redis101Examples
7 | {
8 | static class PubSubExercises
9 | {
10 | public static void Exercises(ConnectionMultiplexer redisMultiplexer)
11 | {
12 | Console.WriteLine("Running Pub/Sub exercises...");
13 | // PubSub
14 | // Async vs Sync handlers
15 | // Sequential vs Concurrent message handling
16 | // Differences from Streams
17 | // - Pub/Sub has no history, messages are fire and forget
18 |
19 | // Similar to the IDatabase object, this is a lightweight pass-through object to be used and discarded
20 | ISubscriber subscription = redisMultiplexer.GetSubscriber();
21 |
22 | // subscribing to a channel V1
23 | subscription.Subscribe("customer:request:events",
24 | (channel, message) => { Console.WriteLine("Pub/Sub message {0}", message); });
25 |
26 | subscription.Publish("customer:request:events", "New Customer Request 1");
27 |
28 | // subscribing to a channel; v2
29 | // Synchronous - messages are processed in the order received but may delay each other and code hampers scalability
30 | subscription.Subscribe("customer:completed:requests").OnMessage(message =>
31 | {
32 | Console.WriteLine("Do some work when message, {0}, is received.", message);
33 | });
34 |
35 | subscription.Publish("customer:completed:requests", "Completed 1");
36 | subscription.Publish("customer:completed:requests", "Completed 2");
37 |
38 | async Task AsyncMessageTask(ChannelMessage message)
39 | {
40 | Console.WriteLine("Do some work when message, {0}, is received.", message);
41 | await Task.Delay(1000);
42 | return message;
43 | }
44 |
45 | // subscribing to a channel; v2
46 | // Asynchronous - messages are published concurrently and the code is a more scalable
47 | subscription.Subscribe("customer:async:requests").OnMessage( message =>
48 | {
49 | var result = AsyncMessageTask(message);
50 | Assert.NotNull(result);
51 | });
52 |
53 | subscription.Publish("customer:async:requests", "Completed 1");
54 | subscription.Publish("customer:async:requests", "Completed 2");
55 |
56 |
57 | subscription.Publish("customer:request:events", "Sync 1");
58 | subscription.Publish("customer:request:events", "Sync 2");
59 | subscription.Publish("customer:request:events", "Sync 3");
60 | subscription.Publish("customer:request:events", "Sync 4");
61 |
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/StreamExercises.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using StackExchange.Redis;
3 | using Xunit;
4 |
5 | namespace Redis101Examples
6 | {
7 | static class StreamExercises
8 | {
9 | public static void Exercises(IDatabase db)
10 | {
11 | Console.WriteLine("Running Stream exercises...");
12 | db.KeyDelete("hero:organizations");
13 |
14 | // Publish to a stream. Not supplying a streamId causes one to be generated by Redis using the timestamp default.
15 | // If the stream does not exist then it is created when StreamAdd is called.
16 | // Differences from Pub/Sub
17 | // - Every client (consumer) will receive a copy of any item added to the stream
18 | // - All messages appended to a stream will be available for future retrieval unless explicitly deleted
19 | // - Consumer Groups provide process acknowledgement, inspection of pending items, message claiming, and per-client private history, which is unavailable to pub/sub
20 |
21 | // 1. Add an entry with a single name-value pair to the stream
22 | RedisValue firstMessageId = db.StreamAdd("hero:organizations", "name", "Wasp", messageId: "*");
23 | Assert.True(firstMessageId.HasValue);
24 |
25 | // Add an entry with multiple name-value pairs to the stream
26 | var streamPairs1 = new NameValueEntry[]
27 | {
28 | new("name", "Hulk"),
29 | new("affiliation", "Avengers")
30 | };
31 |
32 | var streamPairs2 = new NameValueEntry[]
33 | {
34 | new("name", "Iron Man"),
35 | new("affiliation", "Avengers")
36 | };
37 |
38 | RedisValue secondMessageId = db.StreamAdd("hero:organizations", streamPairs1, messageId: "*");
39 | Assert.True(secondMessageId.HasValue);
40 |
41 | RedisValue thirdMessageId = db.StreamAdd("hero:organizations", streamPairs2, messageId: "*");
42 | Assert.True(thirdMessageId.HasValue);
43 |
44 | // 2. Read from a stream. This will return all messages
45 | // from the 'position' through the end of the stream
46 | // unless a count is given
47 | StreamEntry[] firstMessage = db.StreamRead("hero:organizations", "0", count: 1);
48 | Assert.Equal("Wasp", firstMessage[0]["name"]);
49 | // Returning more than one StreamEntry
50 | StreamEntry[] streamFromZero = db.StreamRead("hero:organizations", "0");
51 | Assert.Equal("Hulk", streamFromZero[1]["name"]);
52 | Assert.Equal("Avengers", streamFromZero[2]["affiliation"]);
53 |
54 | // StreamRange is used to read a consecutive set of IDs.
55 | // The characters `+` and `-` are used to indicate the minimum and maximum possible IDs.
56 | // If no value is passed to the respective parameters the entire stream will be read.
57 | StreamEntry[] rangeOfMessages = db.StreamRange("hero:organizations", minId: "-", maxId: "+");
58 | Assert.Equal(3, rangeOfMessages.Length);
59 |
60 | StreamEntry[] fromSecondMessageId = db.StreamRange("hero:organizations", minId: secondMessageId, maxId: "+");
61 | Assert.Equal(2, fromSecondMessageId.Length);
62 |
63 |
64 | // StreamInfo can be used to gather information about a stream.
65 | StreamInfo info = db.StreamInfo("hero:organizations");
66 | Assert.Equal(3 ,info.Length);
67 |
68 | // ConsumerGroups
69 | // Consumer groups act as an logical endpoint for a stream. The individual consumers are then served by the consumer group.
70 | // Using a consumer group provides certain guarantees
71 | // - Each message will only be sent once to a single client
72 | // - Consumers are identified within the consumer group by a unique case-sensitive string which allows the consumer group to retain state in the event of a consumer disconnect
73 | // - The consumer group maintains a state of the last message ID processed by a consumer which allows pending messages for a consumer to be process in order
74 | // - Consuming pending messages requires an explicit acknowledgement which gives the ability to enforce processing guarantees
75 | // - Consumer groups track pending messages, where pending is defined as, messages delivered to a consumer but know yet acknowledged. This mechanism means that each consumer sees only its own message history
76 |
77 | // CreateConsumerGroup
78 | // If the referenced stream does not exist when the consumer group is created, this will create the stream
79 | // If the first visible message to the new group requires a position other than the beginning of the stream or the first new message after joining,
80 | // position can be specified by giving a specific message ID instead of a `StreamPosition` constant.
81 |
82 | // Synchronous
83 | bool created = db.StreamCreateConsumerGroup("hero:organizations", "hero_consumer_group1", StreamPosition.Beginning);
84 | Assert.True(created);
85 |
86 | // A named consumer is created if it doesn't already exist in the consumer group.
87 | // `>` is a special character that means, "read not delivered to any consumer"
88 | // Count limits the returned messages to the count quantity.
89 | // If a message ID is passed instead of the `>` character, the pending messages for that consumer are
90 | // searched and if a message with a matching ID is found, that StreamEntry is returned.
91 | StreamEntry[] shieldConsumerEntries =
92 | db.StreamReadGroup("hero:organizations", "hero_consumer_group1", "shield", ">", count: 1);
93 | Assert.Equal("Wasp", shieldConsumerEntries[0]["name"]);
94 |
95 |
96 | // Read a message for a second consumer in the same consumer group
97 | StreamEntry[] aimConsumerEntries =
98 | db.StreamReadGroup("hero:organizations", "hero_consumer_group1", "AIM", ">", count: 1);
99 | Assert.Equal("Hulk", aimConsumerEntries[0]["name"]);
100 | Assert.Equal("Avengers", aimConsumerEntries[0]["affiliation"]);
101 |
102 |
103 | // StreamPending
104 | // StreamPending returns high level state information about a given consumers messages
105 | StreamPendingInfo streamPendingInfo = db.StreamPending("hero:organizations", "hero_consumer_group1");
106 | Assert.Equal(2,streamPendingInfo.PendingMessageCount);
107 |
108 | StreamPendingMessageInfo[] aimPendingInfo =
109 | db.StreamPendingMessages("hero:organizations", "hero_consumer_group1", 10, "AIM");
110 | Assert.Single(aimPendingInfo);
111 | Assert.Equal("AIM",aimPendingInfo[0].ConsumerName);
112 | Assert.Equal(secondMessageId,aimPendingInfo[0].MessageId);
113 |
114 | // StreamPendingMessages
115 | // StreamPendingMessages shows detailed message information about pending messages for a given consumer
116 | StreamPendingMessageInfo[] shieldPendingInfo =
117 | db.StreamPendingMessages("hero:organizations", "hero_consumer_group1", 10, "shield");
118 | Assert.Single(shieldPendingInfo);
119 | Assert.Equal("shield",shieldPendingInfo[0].ConsumerName);
120 | Assert.Equal(firstMessageId,shieldPendingInfo[0].MessageId);
121 |
122 | // StreamAcknowledge
123 | // This will send an ACK to the consumer group for the given message ID, removing it from the pending messages for the consumer
124 | var acknowledgedCount = db.StreamAcknowledge("hero:organizations", "hero_consumer_group1", firstMessageId);
125 | Assert.Equal(1, acknowledgedCount);
126 |
127 | StreamPendingInfo pendingInfoAfterAck = db.StreamPending("hero:organizations", "hero_consumer_group1");
128 | Assert.Equal(1,pendingInfoAfterAck.PendingMessageCount);
129 |
130 | // StreamDeleteConsumer
131 | // This will delete the given consumer from the consumer group. There is a separate delete commands for consumer groups, and stream messages
132 | var aimDeletedPending = db.StreamDeleteConsumer("hero:organizations", "hero_consumer_group1", "AIM");
133 | Assert.Equal(1,aimDeletedPending);
134 | var shieldDeletedPending = db.StreamDeleteConsumer("hero:organizations", "hero_consumer_group1", "shield");
135 | Assert.Equal(0,shieldDeletedPending);
136 |
137 | // StreamDeleteConsumerGroup
138 | bool deleted = db.StreamDeleteConsumerGroup("hero:organizations", "hero_consumer_group1");
139 | Assert.True(deleted);
140 | }
141 | }
142 | }
--------------------------------------------------------------------------------