Skip to content

Commit

Permalink
Add nx/xx (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
shacharPash authored Dec 9, 2022
1 parent 6745e2b commit 812d633
Show file tree
Hide file tree
Showing 7 changed files with 568 additions and 3 deletions.
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)
{
var argList = new List<string> { timeSpan != null ? ((long)timeSpan.Value.TotalMilliseconds).ToString() : "-1", path, json };
switch (when)
{
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>
/// <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>
/// <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)
{
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>
/// <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)
{
if (timespan.HasValue)
{
return connection.Set(obj, timespan.Value);
}

return connection.Set(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 = 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 @@ -198,7 +198,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 @@ -584,6 +584,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

0 comments on commit 812d633

Please sign in to comment.