diff --git a/src/NRedisStack/Auxiliary.cs b/src/NRedisStack/Auxiliary.cs index 75f1fd5d..207f39ac 100644 --- a/src/NRedisStack/Auxiliary.cs +++ b/src/NRedisStack/Auxiliary.cs @@ -1,3 +1,5 @@ +using System.Xml.Linq; +using NRedisStack.Core; using NRedisStack.RedisStackCommands; using StackExchange.Redis; @@ -5,6 +7,13 @@ namespace NRedisStack { public static class Auxiliary { + private static string? _libraryName = $"NRedisStack;.NET-{Environment.Version}"; + private static bool _setInfo = true; + public static void ResetInfoDefaults() + { + _setInfo = true; + _libraryName = $"NRedisStack;.NET-{Environment.Version}"; + } public static List MergeArgs(RedisKey key, params RedisValue[] items) { var args = new List(items.Length + 1) { key }; @@ -26,20 +35,57 @@ public static object[] AssembleNonNullArguments(params object?[] arguments) return args.ToArray(); } + // public static IDatabase GetDatabase(this ConnectionMultiplexer redis) => redis.GetDatabase("", ""); + + // TODO: add all the signatures of GetDatabase + public static IDatabase GetDatabase(this ConnectionMultiplexer redis, + string? LibraryName) + { + var _db = redis.GetDatabase(); + if (LibraryName == null) // the user wants to disable the library name and version sending + _setInfo = false; + + else // the user set his own the library name + _libraryName = $"NRedisStack({LibraryName});.NET-{Environment.Version})"; + + return _db; + } + + private static void SetInfoInPipeline(this IDatabase db) + { + if (_libraryName == null) return; + Pipeline pipeline = new Pipeline(db); + _ = pipeline.Db.ClientSetInfoAsync(SetInfoAttr.LibraryName, _libraryName!); + _ = pipeline.Db.ClientSetInfoAsync(SetInfoAttr.LibraryVersion, GetNRedisStackVersion()); + pipeline.Execute(); + } + public static RedisResult Execute(this IDatabase db, SerializedCommand command) { + var compareVersions = db.Multiplexer.GetServer(db.Multiplexer.GetEndPoints()[0]).Version.CompareTo(new Version(7, 1, 242)); + if (_setInfo && compareVersions >= 0) + { + _setInfo = false; + db.SetInfoInPipeline(); + } return db.Execute(command.Command, command.Args); } public async static Task ExecuteAsync(this IDatabaseAsync db, SerializedCommand command) { + var compareVersions = db.Multiplexer.GetServer(db.Multiplexer.GetEndPoints()[0]).Version.CompareTo(new Version(7, 1, 242)); + if (_setInfo && compareVersions >= 0) + { + _setInfo = false; + ((IDatabase)db).SetInfoInPipeline(); + } return await db.ExecuteAsync(command.Command, command.Args); } - public static List ExecuteBroadcast(this IDatabaseAsync db, string command) + public static List ExecuteBroadcast(this IDatabase db, string command) => db.ExecuteBroadcast(new SerializedCommand(command)); - public static List ExecuteBroadcast(this IDatabaseAsync db, SerializedCommand command) + public static List ExecuteBroadcast(this IDatabase db, SerializedCommand command) { var redis = db.Multiplexer; var endpoints = redis.GetEndPoints(); @@ -83,5 +129,26 @@ public async static Task> ExecuteBroadcastAsync(this IDatabase } return results; } + + public static string GetNRedisStackVersion() + { + XDocument csprojDocument = GetCsprojDocument(); + + // Find the Version element and get its value. + var versionElement = csprojDocument.Root! + .Descendants("Version") + .FirstOrDefault(); + + return versionElement!.Value; + } + + private static XDocument GetCsprojDocument() + { + string csprojFilePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "src", "NRedisStack", "NRedisStack.csproj"); + + // Load the .csproj file. + var csprojDocument = XDocument.Load(csprojFilePath); + return csprojDocument; + } } } \ No newline at end of file diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs new file mode 100644 index 00000000..9677824d --- /dev/null +++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs @@ -0,0 +1,22 @@ +using NRedisStack.RedisStackCommands; +using NRedisStack.Core.Literals; +using NRedisStack.Core; + +namespace NRedisStack +{ + + public static class CoreCommandBuilder + { + public static SerializedCommand ClientSetInfo(SetInfoAttr attr, string value) + { + string attrValue = attr switch + { + SetInfoAttr.LibraryName => CoreArgs.lib_name, + SetInfoAttr.LibraryVersion => CoreArgs.lib_ver, + _ => throw new System.NotImplementedException(), + }; + + return new SerializedCommand(RedisCoreCommands.CLIENT, RedisCoreCommands.SETINFO, attrValue, value); + } + } +} diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs new file mode 100644 index 00000000..0040c2bc --- /dev/null +++ b/src/NRedisStack/CoreCommands/CoreCommands.cs @@ -0,0 +1,20 @@ +using NRedisStack.Core; +using StackExchange.Redis; +namespace NRedisStack +{ + + public static class CoreCommands + { + /// + /// Sets information specific to the client or connection. + /// + /// which attribute to set + /// the attribute value + /// if the attribute name was successfully set, Error otherwise. + /// + public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string value) + { + return db.Execute(CoreCommandBuilder.ClientSetInfo(attr, value)).OKtoBoolean(); + } + } +} diff --git a/src/NRedisStack/CoreCommands/CoreCommandsAsync.cs b/src/NRedisStack/CoreCommands/CoreCommandsAsync.cs new file mode 100644 index 00000000..688d30db --- /dev/null +++ b/src/NRedisStack/CoreCommands/CoreCommandsAsync.cs @@ -0,0 +1,20 @@ +using NRedisStack.Core; +using StackExchange.Redis; +namespace NRedisStack +{ + + public static class CoreCommandsAsync //: ICoreCommandsAsync + { + /// + /// Sets information specific to the client or connection. + /// + /// which attribute to set + /// the attribute value + /// if the attribute name was successfully set, Error otherwise. + /// + public static async Task ClientSetInfoAsync(this IDatabaseAsync db, SetInfoAttr attr, string value) + { + return (await db.ExecuteAsync(CoreCommandBuilder.ClientSetInfo(attr, value))).OKtoBoolean(); + } + } +} diff --git a/src/NRedisStack/CoreCommands/Enums/SetInfoAttr.cs b/src/NRedisStack/CoreCommands/Enums/SetInfoAttr.cs new file mode 100644 index 00000000..16f10a9a --- /dev/null +++ b/src/NRedisStack/CoreCommands/Enums/SetInfoAttr.cs @@ -0,0 +1,12 @@ +namespace NRedisStack.Core; +public enum SetInfoAttr +{ + /// + /// meant to hold the name of the client library that's in use. + /// + LibraryName, + /// + /// meant to hold the client library's version. + /// + LibraryVersion +} diff --git a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs new file mode 100644 index 00000000..8d9aaf4f --- /dev/null +++ b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs @@ -0,0 +1,8 @@ +namespace NRedisStack.Core.Literals +{ + internal class CoreArgs + { + public const string lib_name = "LIB-NAME"; + public const string lib_ver = "LIB-VER"; + } +} diff --git a/src/NRedisStack/CoreCommands/Literals/Commands.cs b/src/NRedisStack/CoreCommands/Literals/Commands.cs new file mode 100644 index 00000000..aaebaef6 --- /dev/null +++ b/src/NRedisStack/CoreCommands/Literals/Commands.cs @@ -0,0 +1,11 @@ +namespace NRedisStack.Core.Literals +{ + /// + /// Redis Core command literals + /// + internal class RedisCoreCommands + { + public const string CLIENT = "CLIENT"; + public const string SETINFO = "SETINFO"; + } +} diff --git a/src/NRedisStack/Graph/IGraphCommands.cs b/src/NRedisStack/Graph/IGraphCommands.cs index 2ce0f289..3052cb23 100644 --- a/src/NRedisStack/Graph/IGraphCommands.cs +++ b/src/NRedisStack/Graph/IGraphCommands.cs @@ -3,7 +3,6 @@ namespace NRedisStack { - [Obsolete("RedisGraph support is deprecated as of Redis Stack 7.2 (https://redis.com/blog/redisgraph-eol/)")] public interface IGraphCommands { diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs new file mode 100644 index 00000000..d403dec2 --- /dev/null +++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs @@ -0,0 +1,149 @@ +using Xunit; +using NRedisStack.Core; +using NRedisStack; +using static NRedisStack.Auxiliary; +using StackExchange.Redis; +using System.Xml.Linq; +using System.Reflection; +using NRedisStack.RedisStackCommands; + + +namespace NRedisStack.Tests.Core; + +public class CoreTests : AbstractNRedisStackTest, IDisposable +{ + public CoreTests(RedisFixture redisFixture) : base(redisFixture) { } + + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public void TestSimpleSetInfo() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + + db.ClientSetInfo(SetInfoAttr.LibraryName, "TestLibraryName"); + db.ClientSetInfo(SetInfoAttr.LibraryVersion, "1.2.3"); + + var info = db.Execute("CLIENT", "INFO").ToString(); + Assert.EndsWith($"lib-name=TestLibraryName lib-ver=1.2.3\n", info); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public async Task TestSimpleSetInfoAsync() + { + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + + await db.ClientSetInfoAsync(SetInfoAttr.LibraryName, "TestLibraryName"); + await db.ClientSetInfoAsync(SetInfoAttr.LibraryVersion, "1.2.3"); + + var info = db.Execute("CLIENT", "INFO").ToString(); + Assert.EndsWith($"lib-name=TestLibraryName lib-ver=1.2.3\n", info); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public void TestSetInfoDefaultValue() + { + ResetInfoDefaults(); // demonstrate first connection + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + + db.Execute(new SerializedCommand("PING")); // only the extension method of Execute (which is used for all the commands of Redis Stack) will set the library name and version. + + var info = db.Execute("CLIENT", "INFO").ToString(); + Assert.EndsWith($"lib-name=NRedisStack;.NET-{Environment.Version} lib-ver={GetNRedisStackVersion()}\n", info); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public async Task TestSetInfoDefaultValueAsync() + { + ResetInfoDefaults(); // demonstrate first connection + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + + await db.ExecuteAsync(new SerializedCommand("PING")); // only the extension method of Execute (which is used for all the commands of Redis Stack) will set the library name and version. + + var info = (await db.ExecuteAsync("CLIENT", "INFO")).ToString(); + Assert.EndsWith($"lib-name=NRedisStack;.NET-{Environment.Version} lib-ver={GetNRedisStackVersion()}\n", info); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public void TestSetInfoWithValue() + { + ResetInfoDefaults(); // demonstrate first connection + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase("MyLibraryName;v1.0.0"); + db.Execute("FLUSHALL"); + + db.Execute(new SerializedCommand("PING")); // only the extension method of Execute (which is used for all the commands of Redis Stack) will set the library name and version. + + var info = db.Execute("CLIENT", "INFO").ToString(); + Assert.EndsWith($"NRedisStack(MyLibraryName;v1.0.0);.NET-{Environment.Version}) lib-ver={GetNRedisStackVersion()}\n", info); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public async Task TestSetInfoWithValueAsync() + { + ResetInfoDefaults(); // demonstrate first connection + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase("MyLibraryName;v1.0.0"); + db.Execute("FLUSHALL"); + + await db.ExecuteAsync(new SerializedCommand("PING")); // only the extension method of Execute (which is used for all the commands of Redis Stack) will set the library name and version. + + var info = (await db.ExecuteAsync("CLIENT", "INFO")).ToString(); + Assert.EndsWith($"NRedisStack(MyLibraryName;v1.0.0);.NET-{Environment.Version}) lib-ver={GetNRedisStackVersion()}\n", info); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public void TestSetInfoNull() + { + ResetInfoDefaults(); // demonstrate first connection + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase(null); + + db.Execute("FLUSHALL"); + var infoBefore = db.Execute("CLIENT", "INFO").ToString(); + db.Execute(new SerializedCommand("PING")); // only the extension method of Execute (which is used for all the commands of Redis Stack) will set the library name and version. + + var infoAfter = db.Execute("CLIENT", "INFO").ToString(); + // Find the indices of "lib-name=" in the strings + int infoAfterLibNameIndex = infoAfter!.IndexOf("lib-name="); + int infoBeforeLibNameIndex = infoBefore!.IndexOf("lib-name="); + + // Extract the sub-strings starting from "lib-name=" + string infoAfterLibNameToEnd = infoAfter.Substring(infoAfterLibNameIndex); + string infoBeforeLibNameToEnd = infoBefore.Substring(infoBeforeLibNameIndex); + + // Assert that the extracted sub-strings are equal + Assert.Equal(infoAfterLibNameToEnd, infoBeforeLibNameToEnd); + } + + [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.1.242")] + public async Task TestSetInfoNullAsync() + { + ResetInfoDefaults(); // demonstrate first connection + var redis = ConnectionMultiplexer.Connect("localhost"); + var db = redis.GetDatabase(null); + + db.Execute("FLUSHALL"); + var infoBefore = (await db.ExecuteAsync("CLIENT", "INFO")).ToString(); + await db.ExecuteAsync(new SerializedCommand("PING")); // only the extension method of Execute (which is used for all the commands of Redis Stack) will set the library name and version. + + var infoAfter = (await db.ExecuteAsync("CLIENT", "INFO")).ToString(); + // Find the indices of "lib-name=" in the strings + int infoAfterLibNameIndex = infoAfter!.IndexOf("lib-name="); + int infoBeforeLibNameIndex = infoBefore!.IndexOf("lib-name="); + + // Extract the sub-strings starting from "lib-name=" + string infoAfterLibNameToEnd = infoAfter.Substring(infoAfterLibNameIndex); + string infoBeforeLibNameToEnd = infoBefore.Substring(infoBeforeLibNameIndex); + + // Assert that the extracted sub-strings are equal + Assert.Equal(infoAfterLibNameToEnd, infoBeforeLibNameToEnd); + } +} \ No newline at end of file