Skip to content

Support Redis URI #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
18 changes: 18 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="NetTopologySuite" Version="2.5.0" />
<PackageVersion Include="System.Text.Json" Version="7.0.2" />
<PackageVersion Include="StackExchange.Redis" Version="2.7.10" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
<PackageVersion Include="coverlet.msbuild" Version="3.2.0" />
<PackageVersion Include="dotenv.net" Version="3.1.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.assert" Version="2.4.2" />
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.2.0" />
</ItemGroup>
</Project>
41 changes: 2 additions & 39 deletions src/NRedisStack/Auxiliary.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
using NRedisStack.Core;
using NRedisStack.RedisStackCommands;
using StackExchange.Redis;

namespace NRedisStack
{
public static class Auxiliary
{
private static string? _libraryName = $"NRedisStack(.NET_v{Environment.Version})";
private static bool _setInfo = true;
public static void ResetInfoDefaults()
{
_setInfo = true;
_libraryName = $"NRedisStack(.NET_v{Environment.Version})";
}
public static List<object> MergeArgs(RedisKey key, params RedisValue[] items)
{
var args = new List<object>(items.Length + 1) { key };
Expand All @@ -34,46 +26,17 @@ public static object[] AssembleNonNullArguments(params object?[] arguments)
return args.ToArray();
}

// 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_v{Environment.Version})";

return _db;
}
// public static IDatabase GetDatabase(this ConnectionMultiplexer redis) => redis.GetDatabase("", "");

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)
{
if (_setInfo)
{
_setInfo = false;
db.SetInfoInPipeline();
}
return db.Execute(command.Command, command.Args);
}

public async static Task<RedisResult> ExecuteAsync(this IDatabaseAsync db, SerializedCommand command)
{
if (_setInfo)
{
_setInfo = false;
((IDatabase)db).SetInfoInPipeline();
}
var compareVersions = db.Multiplexer.GetServer(db.Multiplexer.GetEndPoints()[0]).Version.CompareTo(new Version(7, 1, 242));
return await db.ExecuteAsync(command.Command, command.Args);
}

Expand Down
46 changes: 46 additions & 0 deletions src/NRedisStack/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Dynamic;
using StackExchange.Redis;

namespace NRedisStack
{
public class Configuration
{
public ConfigurationOptions Options { get; set; } = new ConfigurationOptions();

public static Configuration Parse(string redisConnectionString) =>
new Configuration().DoParse(redisConnectionString);

public static Configuration Parse(ConfigurationOptions options) =>
new Configuration().DoParse(options);

private Configuration DoParse(string redisConnectionString)
{
try // Redis URI parsing
{
Options = RedisUriParser.FromUri(redisConnectionString);
}
catch (UriFormatException) // StackExchange.Redis connection string parsing
{
Options = ConfigurationOptions.Parse(redisConnectionString);
}
SetLibName(Options);
return this;
}

private Configuration DoParse(ConfigurationOptions options)
{
Options = options;
SetLibName(Options);
return this;
}

internal static void SetLibName(ConfigurationOptions options)
{
if (options.LibraryName != null) // the user set his own the library name
options.LibraryName = $"NRedisStack({options.LibraryName};.NET_v{Environment.Version})";
else // the default library name and version sending
options.LibraryName = $"NRedisStack(.NET_v{Environment.Version})";
}

}
}
37 changes: 37 additions & 0 deletions src/NRedisStack/ConnectionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using StackExchange.Redis;

namespace NRedisStack
{
public static class ConnectionManager
{
public static IConnectionMultiplexer Connect(string redisConnectionString)
{
return Connect(Configuration.Parse(redisConnectionString));
}

public static async Task<IConnectionMultiplexer> ConnectAsync(string redisConnectionString)
{
return await ConnectAsync(Configuration.Parse(redisConnectionString));
}

public static IConnectionMultiplexer Connect(Configuration configuration)
{
return ConnectionMultiplexer.Connect(configuration.Options);
}

public static async Task<IConnectionMultiplexer> ConnectAsync(Configuration configuration)
{
return await ConnectionMultiplexer.ConnectAsync(configuration.Options);
}

public static IConnectionMultiplexer Connect(ConfigurationOptions options)
{
return Connect(Configuration.Parse(options));
}

public static async Task<IConnectionMultiplexer> ConnectAsync(ConfigurationOptions options)
{
return await ConnectAsync(Configuration.Parse(options));
}
}
}
6 changes: 3 additions & 3 deletions src/NRedisStack/NRedisStack.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="System.Text.Json" Version="7.0.2" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
<PackageReference Include="NetTopologySuite" />
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageReference Include="StackExchange.Redis" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

Expand Down
158 changes: 158 additions & 0 deletions src/NRedisStack/RedisUriParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using StackExchange.Redis;

[assembly: InternalsVisibleTo("NRedisStack.Tests")]

namespace NRedisStack
{
/// <summary>
/// URI parsing utility.
/// </summary>
internal static class RedisUriParser
{
internal static string defaultHost = "localhost";
internal static int defaultPort = 6379;

// The options:
internal const string
Timeout = "timeout",
ClientName = "clientname",
Sentinel_primary_name = "sentinel_primary_name",
Endpoint = "endpoint",
AllowAdmin = "allowadmin",
AbortConnect = "abortconnect",
AsyncTimeout = "asynctimeout",
Retry = "retry",
Protocol = "protocol",
LibraryName = "lib_name";
// TODO: add library version when it will be available

/// <summary>
/// Parses a Config options for StackExchange Redis from the URI.
/// </summary>
/// <param name="redisUri">Redis Uri string</param>
/// <returns>A configuration options result for SE.Redis.</returns>
internal static ConfigurationOptions FromUri(string redisUri)
{
var options = new ConfigurationOptions();

if (string.IsNullOrEmpty(redisUri))
{
options.EndPoints.Add($"{defaultHost}:{defaultPort}");
return options;
}

var uri = new Uri(redisUri);
ParseHost(options, uri);
ParseUserInfo(options, uri);
ParseQueryArguments(options, uri);
ParseDefaultDatabase(options, uri);
options.Ssl = uri.Scheme == "rediss";
options.AbortOnConnectFail = false;
return options;
}

private static void ParseDefaultDatabase(ConfigurationOptions options, Uri uri)
{
if (string.IsNullOrEmpty(uri.AbsolutePath))
{
return;
}

var dbNumStr = Regex.Match(uri.AbsolutePath, "[0-9]+").Value;
int dbNum;
if (int.TryParse(dbNumStr, out dbNum))
{
options.DefaultDatabase = dbNum;
}
}

private static IList<KeyValuePair<string, string>> ParseQuery(string query) =>
query.Split('&').Select(x =>
new KeyValuePair<string, string>(x.Split('=').First(), x.Split('=').Last())).ToList();

private static void ParseUserInfo(ConfigurationOptions options, Uri uri)
{
if (string.IsNullOrEmpty(uri.UserInfo))
{
return;
}

var userInfo = uri.UserInfo.Split(':');

if (userInfo.Length > 1)
{
options.User = Uri.UnescapeDataString(userInfo[0]);
options.Password = Uri.UnescapeDataString(userInfo[1]);
}

else
{
throw new FormatException("Username and password must be in the form username:password - if there is no username use the format :password");
}
}


private static void ParseHost(ConfigurationOptions options, Uri uri)
{
var port = uri.Port >= 0 ? uri.Port : defaultPort;
var host = !string.IsNullOrEmpty(uri.Host) ? uri.Host : defaultHost;
options.EndPoints.Add($"{host}:{port}");
}

private static void ParseQueryArguments(ConfigurationOptions options, Uri uri)
{
if (string.IsNullOrEmpty(uri.Query))
{
return;
}

var queryArgs = ParseQuery(uri.Query.Substring(1));

var actions = new Dictionary<string, Action<string>>(StringComparer.OrdinalIgnoreCase)
{
{ Timeout, value => SetTimeoutOptions(options, value) },
{ ClientName, value => options.ClientName = value },
{ Sentinel_primary_name, value => options.ServiceName = value },
{ Endpoint, value => options.EndPoints.Add(value) },
{ AllowAdmin, value => options.AllowAdmin = bool.Parse(value) },
{ AbortConnect, value => options.AbortOnConnectFail = bool.Parse(value) },
{ AsyncTimeout, value => options.AsyncTimeout = int.Parse(value) },
{ Retry, value => options.ConnectRetry = int.Parse(value) },
{ Protocol, value => ParseRedisProtocol(options, value) },
{ LibraryName, value => options.LibraryName = value }
// TODO: add more options (especially the library version when it will be available)
};

foreach (var arg in queryArgs.Where(arg => actions.ContainsKey(arg.Key)))
{
actions[arg.Key.ToLower()](arg.Value);
}
}

private static void ParseRedisProtocol(ConfigurationOptions options, string value)
{
switch (value)
{
case "2":
options.Protocol = RedisProtocol.Resp2;
break;
case "3":
options.Protocol = RedisProtocol.Resp3; ;
break;
default:
throw new FormatException("Invalid protocol specified");
}
}

private static void SetTimeoutOptions(ConfigurationOptions options, string value)
{
var timeout = int.Parse(value);
options.AsyncTimeout = timeout;
options.SyncTimeout = timeout;
options.ConnectTimeout = timeout;
}

}
}
12 changes: 7 additions & 5 deletions tests/Doc/Doc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
<IsPackable>false</IsPackable>
<OutputType>Module</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\NRedisStack\NRedisStack.csproj" />
<ProjectReference Include="..\..\tests\NRedisStack.Tests\NRedisStack.Tests.csproj" />
</ItemGroup>
</Project>
</Project>
Loading