Skip to content
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

Add nx/xx #206

Merged
merged 25 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
47edac0
Update to 2.5.61 version
shacharPash Jun 7, 2022
af08452
Merge branch 'redis:main' into main
shacharPash Jun 8, 2022
e91238c
Fix naming 'TestToLower' insted of 'TestTooLower' & Deleting unnecess…
shacharPash Jun 14, 2022
cfb8fc0
Merge branch 'redis:main' into main
shacharPash Jun 14, 2022
a03939e
Merge branch 'redis:main' into main
shacharPash Jun 29, 2022
70af77a
Merge branch 'redis:main' into main
shacharPash Jun 29, 2022
04670a4
Merge branch 'redis:main' into main
shacharPash Jul 14, 2022
a1f4f7e
Merge branch 'redis:main' into main
shacharPash Jul 19, 2022
f3a8d8b
Merge branch 'redis:main' into main
shacharPash Aug 24, 2022
d8762b2
Merge branch 'redis:main' into main
shacharPash Aug 25, 2022
0454cf3
Merge branch 'main' of github.com:shacharPash/redis-om-dotnet into main
shacharPash Aug 30, 2022
f306c4a
Merge branch 'redis:main' into main
shacharPash Sep 20, 2022
85e3ef9
Add NX/XX features to JsonSet & InsertIfExsist/NotExist
shacharPash Sep 20, 2022
264e909
Fix Test and Add Async Test
shacharPash Sep 20, 2022
b3dfc0c
fixing tests
Sep 21, 2022
538f4a5
Change to When Type Instead of string
shacharPash Oct 12, 2022
76d720b
Merge branch 'AddNX/XX' of github.com:shacharPash/redis-om-dotnet int…
shacharPash Oct 12, 2022
3c1ed22
fixing tests
slorello89 Oct 12, 2022
45ffc86
Create local enum When
shacharPash Oct 13, 2022
b8e71c5
Fixes
shacharPash Oct 13, 2022
1144b6e
Merge branch 'AddNX/XX' of github.com:shacharPash/redis-om-dotnet int…
shacharPash Oct 13, 2022
48e7a33
More fixes
shacharPash Oct 13, 2022
357852b
fixing tests
slorello89 Oct 13, 2022
988956d
adding exists/notexists options for hash, added expiry+exists/not-exi…
slorello89 Oct 13, 2022
ab0330a
moving WhenKey
slorello89 Oct 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 186 additions & 1 deletion src/Redis.OM/RedisCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Threading.Tasks;
using Redis.OM.Contracts;
using Redis.OM.Modeling;
using StackExchange.Redis;

namespace Redis.OM
{
Expand Down Expand Up @@ -187,6 +186,48 @@ public static async Task<bool> JsonSetAsync(this IRedisConnection connection, st
return result;
}

/// <summary>
/// Sets a value as JSON in redis.
/// </summary>
/// <param name="connection">the connection.</param>
/// <param name="key">the key for the object.</param>
/// <param name="path">the path within the json to set.</param>
/// <param name="json">the json.</param>
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
/// <returns>whether the operation succeeded.</returns>
public static async Task<bool> JsonSetAsync(this IRedisConnection connection, string key, string path, string json, WhenKey when, TimeSpan? timeSpan = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm of two sides here.

On the client side, it's very clear that we have to expose based on name XX vs NX. But here.. I think it's a bad variable name. Ideally, to help users we could just name ifExists or similar. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key here is that we are NOT in fact just exposing XX/NX, we are exposing whether we care to take any special action WRT to caring whether or not the object has existed WhenKey uses the same semantics as the When enum from StackExchange.Redis. We explicitly didn't want to expose that part of StackExchange.Redis' API, so we used a similarly named enum with the same semantics.

{
var argList = new List<string> { timeSpan != null ? ((long)timeSpan.Value.TotalMilliseconds).ToString() : "-1", path, json };
switch (when)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use a switch instead of an if/else? If/else is better for boolean values, as traditionally it's a single block passed to the compiler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a switch here over an if/else because the semantic isn't to see whether or not the key exists, it's whether or not the user wants to add the XX/NX options (or their equivalent), you could use an if/else-if. The perf difference between an if/else-if and switch here is going to be negligible on a tiny set(might even be 0 depending on how the IL is compiled).

{
case WhenKey.Exists:
argList.Add("XX");
break;
case WhenKey.NotExists:
argList.Add("NX");
break;
}

return await connection.CreateAndEvalAsync(nameof(Scripts.JsonSetWithExpire), new[] { key }, argList.ToArray()) == 1;
}

/// <summary>
/// Sets a value as JSON in redis.
/// </summary>
/// <param name="connection">the connection.</param>
/// <param name="key">the key for the object.</param>
/// <param name="path">the path within the json to set.</param>
/// <param name="obj">the object to serialize to json.</param>
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why timeSpan and not just seconds or evevn TTL? When I see timeSpan, as a user I assume I need seconds.. but maybe these are milliseconds?

I think the API should be more explict.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a fair bit of precedent for .NET users for using a TimeSpan over straight up using seconds or millisecond in situations like this. TimeSpan's conveniently have a fair number of functions for simply producing these, e.g. TimeSpan.From(Millisecond/Hours/Day/Weeks etc. . .). Also when performing arithmetic on two DateTime or DateTimeOffset structs, the result is a timespan. So it's just easier (and what people are used to)

/// <returns>whether the operation succeeded.</returns>
public static async Task<bool> JsonSetAsync(this IRedisConnection connection, string key, string path, object obj, WhenKey when, TimeSpan? timeSpan = null)
{
var json = JsonSerializer.Serialize(obj, Options);
return await connection.JsonSetAsync(key, path, json, when, timeSpan);
}

/// <summary>
/// Set's values in a hash.
/// </summary>
Expand Down Expand Up @@ -286,6 +327,48 @@ public static bool JsonSet(this IRedisConnection connection, string key, string
return connection.JsonSet(key, path, json, timeSpan);
}

/// <summary>
/// Sets a value as JSON in redis.
/// </summary>
/// <param name="connection">the connection.</param>
/// <param name="key">the key for the object.</param>
/// <param name="path">the path within the json to set.</param>
/// <param name="json">the json.</param>
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for both when + timespan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

/// <returns>whether the operation succeeded.</returns>
public static bool JsonSet(this IRedisConnection connection, string key, string path, string json, WhenKey when, TimeSpan? timeSpan = null)
{
var argList = new List<string> { timeSpan != null ? ((long)timeSpan.Value.TotalMilliseconds).ToString() : "-1", path, json };
switch (when)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question re if/else

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

{
case WhenKey.Exists:
argList.Add("XX");
break;
case WhenKey.NotExists:
argList.Add("NX");
break;
}

return connection.CreateAndEval(nameof(Scripts.JsonSetWithExpire), new[] { key }, argList.ToArray()) == 1;
}

/// <summary>
/// Sets a value as JSON in redis.
/// </summary>
/// <param name="connection">the connection.</param>
/// <param name="key">the key for the object.</param>
/// <param name="path">the path within the json to set.</param>
/// <param name="obj">the object to serialize to json.</param>
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And again

/// <returns>whether the operation succeeded.</returns>
public static bool JsonSet(this IRedisConnection connection, string key, string path, object obj, WhenKey when, TimeSpan? timeSpan = null)
{
var json = JsonSerializer.Serialize(obj, Options);
return connection.JsonSet(key, path, json, when, timeSpan);
}

/// <summary>
/// Serializes an object to either hash or json (depending on how it's decorated), and saves it in redis.
/// </summary>
Expand Down Expand Up @@ -315,6 +398,108 @@ public static string Set(this IRedisConnection connection, object obj)
return id;
}

/// <summary>
/// Serializes an object to either hash or json (depending on how it's decorated, and saves it to redis conditionally based on the WhenKey,
/// NOTE: <see cref="WhenKey.Exists"/> will replace the object in redis if it exists.
/// </summary>
/// <param name="connection">The connection to redis.</param>
/// <param name="obj">The object to save.</param>
/// <param name="when">The condition for when to set the object.</param>
/// <param name="timespan">The length of time before the key expires.</param>
/// <returns>the key for the object, null if nothing was set.</returns>
public static string? Set(this IRedisConnection connection, object obj, WhenKey when, TimeSpan? timespan = null)
{
var id = obj.SetId();
var type = obj.GetType();

if (Attribute.GetCustomAttribute(type, typeof(DocumentAttribute)) is not DocumentAttribute attr || attr.StorageType == StorageType.Hash)
{
if (when == WhenKey.Always)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these be combined into a single branch? if when == && timespan.HasValue?

Branching multiple portions in the same if statement (rather than sub) is usually more performant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can, but you'd be adding an extra comparison:

if(when == WhenKey.Always && timeSpan.HasValue)
{
// set with timespan
}
else if (when == WhenKey.Always)
{
// set without timespan
}

The perf difference again would be negligible here. It's probably faster as is because in the case where when == WhenKey.Always you would always be performing two comparisons, versus with the if/else-if where you could be performing as many as 3. And then for the cases where when != WhenKey.Always in the current implementation you'd only perform 1 comparison, whereas with the if/else-if you'd be performing 3. Again, this is a super tiny thing, IMO it's better as is.

{
if (timespan.HasValue)
{
return connection.Set(obj, timespan.Value);
}

return connection.Set(obj);
}

var kvps = obj.BuildHashSet();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kvps == key value properties?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kvps in this content is referring to KeyValuePair<TKey,TValue> The response is actually a Dictionary<TKey,TValue> (equivalent of a HashMap/Dict) - but if you see it's usage in the foreach below, it's enumerator is being accessed, meaning we're effectively using it as a collection of KeyValuePair<TKey,TValue> (that's the type of enumerator a Dictionary exposes)

var argsList = new List<string>();
int? res = null;
argsList.Add(timespan != null ? ((long)timespan.Value.TotalMilliseconds).ToString() : "-1");
foreach (var kvp in kvps)
{
argsList.Add(kvp.Key);
argsList.Add(kvp.Value);
}

if (when == WhenKey.Exists)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this become a straight boolean?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

{
res = connection.CreateAndEval(nameof(Scripts.ReplaceHashIfExists), new[] { id }, argsList.ToArray());
}
else if (when == WhenKey.NotExists)
{
res = connection.CreateAndEval(nameof(Scripts.HsetIfNotExists), new[] { id }, argsList.ToArray());
}

return res == 1 ? id : null;
}

return connection.JsonSet(id, "$", obj, when, timespan) ? id : null;
}

/// <summary>
/// Serializes an object to either hash or json (depending on how it's decorated, and saves it to redis conditionally based on the WhenKey,
/// NOTE: <see cref="WhenKey.Exists"/> will replace the object in redis if it exists.
/// </summary>
/// <param name="connection">The connection to redis.</param>
/// <param name="obj">The object to save.</param>
/// <param name="when">The condition for when to set the object.</param>
/// <param name="timespan">The length of time before the key expires.</param>
/// <returns>the key for the object, null if nothing was set.</returns>
public static async Task<string?> SetAsync(this IRedisConnection connection, object obj, WhenKey when, TimeSpan? timespan = null)
{
var id = obj.SetId();
var type = obj.GetType();

if (Attribute.GetCustomAttribute(type, typeof(DocumentAttribute)) is not DocumentAttribute attr || attr.StorageType == StorageType.Hash)
{
if (when == WhenKey.Always)
{
if (timespan.HasValue)
{
return await connection.SetAsync(obj, timespan.Value);
}

return await connection.SetAsync(obj);
}

var kvps = obj.BuildHashSet();
var argsList = new List<string>();
int? res = null;
argsList.Add(timespan != null ? ((long)timespan.Value.TotalMilliseconds).ToString() : "-1");
foreach (var kvp in kvps)
{
argsList.Add(kvp.Key);
argsList.Add(kvp.Value);
}

if (when == WhenKey.Exists)
{
res = await connection.CreateAndEvalAsync(nameof(Scripts.ReplaceHashIfExists), new[] { id }, argsList.ToArray());
}
else if (when == WhenKey.NotExists)
{
res = await connection.CreateAndEvalAsync(nameof(Scripts.HsetIfNotExists), new[] { id }, argsList.ToArray());
}

return res == 1 ? id : null;
}

return await connection.JsonSetAsync(id, "$", obj, when, timespan) ? id : null;
}

/// <summary>
/// Serializes an object to either hash or json (depending on how it's decorated), and saves it in redis.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Redis.OM/RedisReply.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public static implicit operator int(RedisReply v)
/// </summary>
/// <param name="v">The redis reply.</param>
/// <returns>the integer.</returns>
public static implicit operator int?(RedisReply v) => v._internalInt;
public static implicit operator int?(RedisReply v) => v._internalInt ?? (int?)v._internalLong;

/// <summary>
/// Converts an integer to a reply.
Expand Down
65 changes: 65 additions & 0 deletions src/Redis.OM/Scripts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,68 @@ local second_op
redis.call('UNLINK', KEYS[1])
redis.call('JSON.SET', KEYS[1], '.', ARGV[1])
return 0
";

/// <summary>
/// Conditionally calls a hset if a key doesn't exist.
/// </summary>
internal const string HsetIfNotExists = @"
local exists = redis.call('EXISTS', KEYS[1])
if exists ~= 1 then
local hashArgs = {}
local expiry = tonumber(ARGV[1])
for i = 2, table.getn(ARGV) do
hashArgs[i-1] = ARGV[i]
end
redis.call('HSET', KEYS[1], unpack(hashArgs))
if expiry > 0 then
redis.call('PEXPIRE', KEYS[1], expiry)
end
return 1
end
return 0
";

/// <summary>
/// replaces hash if key exists.
/// </summary>
internal const string ReplaceHashIfExists = @"
local exists = redis.call('EXISTS', KEYS[1])
if exists == 1 then
local hashArgs = {}
local expiry = tonumber(ARGV[1])
for i = 2, table.getn(ARGV) do
hashArgs[i-1] = ARGV[i]
end
redis.call('UNLINK', KEYS[1])
redis.call('HSET', KEYS[1], unpack(hashArgs))
if expiry > 0 then
redis.call('PEXPIRE', KEYS[1], expiry)
end
return 1
end
return 0
";

/// <summary>
/// Sets a Json object, if the object is set, and there is an expiration, also set expiration.
/// </summary>
internal const string JsonSetWithExpire = @"
local expiry = tonumber(ARGV[1])
local jsonArgs = {}
for i = 2, table.getn(ARGV) do
jsonArgs[i-1] = ARGV[i]
end
local wasAdded = redis.call('JSON.SET', KEYS[1], unpack(jsonArgs))
if wasAdded ~= false then
if expiry > 0 then
redis.call('PEXPIRE', KEYS[1], expiry)
else
redis.call('PERSIST', KEYS[1])
end
return 1
end
return 0
";

/// <summary>
Expand All @@ -95,6 +157,9 @@ local second_op
{ nameof(Unlink), Unlink },
{ nameof(UnlinkAndSetHash), UnlinkAndSetHash },
{ nameof(UnlinkAndSendJson), UnlinkAndSendJson },
{ nameof(HsetIfNotExists), HsetIfNotExists },
{ nameof(ReplaceHashIfExists), ReplaceHashIfExists },
{ nameof(JsonSetWithExpire), JsonSetWithExpire },
};

/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions src/Redis.OM/Searching/IRedisCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
/// <returns>the key.</returns>
Task<string> InsertAsync(T item, TimeSpan timeSpan);

/// <summary>
/// Inserts an item into redis.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="when">Condition to insert the document under.</param>
/// <param name="timeSpan">The expiration time of the document (TTL).</param>
/// <returns>the Id of the newly inserted item, or null if not inserted.</returns>
Task<string?> InsertAsync(T item, WhenKey when, TimeSpan? timeSpan = null);

/// <summary>
/// Inserts an item into redis.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="when">Condition to insert the document under.</param>
/// <param name="timeSpan">The expiration time of the document (TTL).</param>
/// <returns>the Id of the newly inserted item, or null if not inserted.</returns>
string? Insert(T item, WhenKey when, TimeSpan? timeSpan = null);

/// <summary>
/// finds an item by it's ID or keyname.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Redis.OM/Searching/RedisCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,18 @@ public async Task<string> InsertAsync(T item, TimeSpan timeSpan)
return await ((RedisQueryProvider)Provider).Connection.SetAsync(item, timeSpan);
}

/// <inheritdoc/>
public Task<string?> InsertAsync(T item, WhenKey when, TimeSpan? timeSpan = null)
{
return ((RedisQueryProvider)Provider).Connection.SetAsync(item, when, timeSpan);
}

/// <inheritdoc/>
public string? Insert(T item, WhenKey when, TimeSpan? timeSpan = null)
{
return ((RedisQueryProvider)Provider).Connection.Set(item, when, timeSpan);
}

/// <inheritdoc/>
public T? FindById(string id)
{
Expand Down
23 changes: 23 additions & 0 deletions src/Redis.OM/WhenKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Redis.OM
{
/// <summary>
/// Indicates when this operation should be performed (only some variations are legal in a given context).
/// </summary>
public enum WhenKey
{
/// <summary>
/// The operation should occur whether or not there is an existing value.
/// </summary>
Always,

/// <summary>
/// The operation should only occur when there is an existing value.
/// </summary>
Exists,

/// <summary>
/// The operation should only occur when there is not an existing value.
/// </summary>
NotExists,
}
}
Loading