├── .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 | } --------------------------------------------------------------------------------