Skip to content

Commit 812d633

Browse files
authored
Add nx/xx (#206)
1 parent 6745e2b commit 812d633

File tree

7 files changed

+568
-3
lines changed

7 files changed

+568
-3
lines changed

src/Redis.OM/RedisCommands.cs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Threading.Tasks;
88
using Redis.OM.Contracts;
99
using Redis.OM.Modeling;
10-
using StackExchange.Redis;
1110

1211
namespace Redis.OM
1312
{
@@ -187,6 +186,48 @@ public static async Task<bool> JsonSetAsync(this IRedisConnection connection, st
187186
return result;
188187
}
189188

189+
/// <summary>
190+
/// Sets a value as JSON in redis.
191+
/// </summary>
192+
/// <param name="connection">the connection.</param>
193+
/// <param name="key">the key for the object.</param>
194+
/// <param name="path">the path within the json to set.</param>
195+
/// <param name="json">the json.</param>
196+
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
197+
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
198+
/// <returns>whether the operation succeeded.</returns>
199+
public static async Task<bool> JsonSetAsync(this IRedisConnection connection, string key, string path, string json, WhenKey when, TimeSpan? timeSpan = null)
200+
{
201+
var argList = new List<string> { timeSpan != null ? ((long)timeSpan.Value.TotalMilliseconds).ToString() : "-1", path, json };
202+
switch (when)
203+
{
204+
case WhenKey.Exists:
205+
argList.Add("XX");
206+
break;
207+
case WhenKey.NotExists:
208+
argList.Add("NX");
209+
break;
210+
}
211+
212+
return await connection.CreateAndEvalAsync(nameof(Scripts.JsonSetWithExpire), new[] { key }, argList.ToArray()) == 1;
213+
}
214+
215+
/// <summary>
216+
/// Sets a value as JSON in redis.
217+
/// </summary>
218+
/// <param name="connection">the connection.</param>
219+
/// <param name="key">the key for the object.</param>
220+
/// <param name="path">the path within the json to set.</param>
221+
/// <param name="obj">the object to serialize to json.</param>
222+
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
223+
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
224+
/// <returns>whether the operation succeeded.</returns>
225+
public static async Task<bool> JsonSetAsync(this IRedisConnection connection, string key, string path, object obj, WhenKey when, TimeSpan? timeSpan = null)
226+
{
227+
var json = JsonSerializer.Serialize(obj, Options);
228+
return await connection.JsonSetAsync(key, path, json, when, timeSpan);
229+
}
230+
190231
/// <summary>
191232
/// Set's values in a hash.
192233
/// </summary>
@@ -286,6 +327,48 @@ public static bool JsonSet(this IRedisConnection connection, string key, string
286327
return connection.JsonSet(key, path, json, timeSpan);
287328
}
288329

330+
/// <summary>
331+
/// Sets a value as JSON in redis.
332+
/// </summary>
333+
/// <param name="connection">the connection.</param>
334+
/// <param name="key">the key for the object.</param>
335+
/// <param name="path">the path within the json to set.</param>
336+
/// <param name="json">the json.</param>
337+
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
338+
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
339+
/// <returns>whether the operation succeeded.</returns>
340+
public static bool JsonSet(this IRedisConnection connection, string key, string path, string json, WhenKey when, TimeSpan? timeSpan = null)
341+
{
342+
var argList = new List<string> { timeSpan != null ? ((long)timeSpan.Value.TotalMilliseconds).ToString() : "-1", path, json };
343+
switch (when)
344+
{
345+
case WhenKey.Exists:
346+
argList.Add("XX");
347+
break;
348+
case WhenKey.NotExists:
349+
argList.Add("NX");
350+
break;
351+
}
352+
353+
return connection.CreateAndEval(nameof(Scripts.JsonSetWithExpire), new[] { key }, argList.ToArray()) == 1;
354+
}
355+
356+
/// <summary>
357+
/// Sets a value as JSON in redis.
358+
/// </summary>
359+
/// <param name="connection">the connection.</param>
360+
/// <param name="key">the key for the object.</param>
361+
/// <param name="path">the path within the json to set.</param>
362+
/// <param name="obj">the object to serialize to json.</param>
363+
/// <param name="when">XX - set if exist, NX - set if not exist.</param>
364+
/// <param name="timeSpan">the the timespan to set for your (TTL).</param>
365+
/// <returns>whether the operation succeeded.</returns>
366+
public static bool JsonSet(this IRedisConnection connection, string key, string path, object obj, WhenKey when, TimeSpan? timeSpan = null)
367+
{
368+
var json = JsonSerializer.Serialize(obj, Options);
369+
return connection.JsonSet(key, path, json, when, timeSpan);
370+
}
371+
289372
/// <summary>
290373
/// Serializes an object to either hash or json (depending on how it's decorated), and saves it in redis.
291374
/// </summary>
@@ -315,6 +398,108 @@ public static string Set(this IRedisConnection connection, object obj)
315398
return id;
316399
}
317400

401+
/// <summary>
402+
/// Serializes an object to either hash or json (depending on how it's decorated, and saves it to redis conditionally based on the WhenKey,
403+
/// NOTE: <see cref="WhenKey.Exists"/> will replace the object in redis if it exists.
404+
/// </summary>
405+
/// <param name="connection">The connection to redis.</param>
406+
/// <param name="obj">The object to save.</param>
407+
/// <param name="when">The condition for when to set the object.</param>
408+
/// <param name="timespan">The length of time before the key expires.</param>
409+
/// <returns>the key for the object, null if nothing was set.</returns>
410+
public static string? Set(this IRedisConnection connection, object obj, WhenKey when, TimeSpan? timespan = null)
411+
{
412+
var id = obj.SetId();
413+
var type = obj.GetType();
414+
415+
if (Attribute.GetCustomAttribute(type, typeof(DocumentAttribute)) is not DocumentAttribute attr || attr.StorageType == StorageType.Hash)
416+
{
417+
if (when == WhenKey.Always)
418+
{
419+
if (timespan.HasValue)
420+
{
421+
return connection.Set(obj, timespan.Value);
422+
}
423+
424+
return connection.Set(obj);
425+
}
426+
427+
var kvps = obj.BuildHashSet();
428+
var argsList = new List<string>();
429+
int? res = null;
430+
argsList.Add(timespan != null ? ((long)timespan.Value.TotalMilliseconds).ToString() : "-1");
431+
foreach (var kvp in kvps)
432+
{
433+
argsList.Add(kvp.Key);
434+
argsList.Add(kvp.Value);
435+
}
436+
437+
if (when == WhenKey.Exists)
438+
{
439+
res = connection.CreateAndEval(nameof(Scripts.ReplaceHashIfExists), new[] { id }, argsList.ToArray());
440+
}
441+
else if (when == WhenKey.NotExists)
442+
{
443+
res = connection.CreateAndEval(nameof(Scripts.HsetIfNotExists), new[] { id }, argsList.ToArray());
444+
}
445+
446+
return res == 1 ? id : null;
447+
}
448+
449+
return connection.JsonSet(id, "$", obj, when, timespan) ? id : null;
450+
}
451+
452+
/// <summary>
453+
/// Serializes an object to either hash or json (depending on how it's decorated, and saves it to redis conditionally based on the WhenKey,
454+
/// NOTE: <see cref="WhenKey.Exists"/> will replace the object in redis if it exists.
455+
/// </summary>
456+
/// <param name="connection">The connection to redis.</param>
457+
/// <param name="obj">The object to save.</param>
458+
/// <param name="when">The condition for when to set the object.</param>
459+
/// <param name="timespan">The length of time before the key expires.</param>
460+
/// <returns>the key for the object, null if nothing was set.</returns>
461+
public static async Task<string?> SetAsync(this IRedisConnection connection, object obj, WhenKey when, TimeSpan? timespan = null)
462+
{
463+
var id = obj.SetId();
464+
var type = obj.GetType();
465+
466+
if (Attribute.GetCustomAttribute(type, typeof(DocumentAttribute)) is not DocumentAttribute attr || attr.StorageType == StorageType.Hash)
467+
{
468+
if (when == WhenKey.Always)
469+
{
470+
if (timespan.HasValue)
471+
{
472+
return await connection.SetAsync(obj, timespan.Value);
473+
}
474+
475+
return await connection.SetAsync(obj);
476+
}
477+
478+
var kvps = obj.BuildHashSet();
479+
var argsList = new List<string>();
480+
int? res = null;
481+
argsList.Add(timespan != null ? ((long)timespan.Value.TotalMilliseconds).ToString() : "-1");
482+
foreach (var kvp in kvps)
483+
{
484+
argsList.Add(kvp.Key);
485+
argsList.Add(kvp.Value);
486+
}
487+
488+
if (when == WhenKey.Exists)
489+
{
490+
res = await connection.CreateAndEvalAsync(nameof(Scripts.ReplaceHashIfExists), new[] { id }, argsList.ToArray());
491+
}
492+
else if (when == WhenKey.NotExists)
493+
{
494+
res = await connection.CreateAndEvalAsync(nameof(Scripts.HsetIfNotExists), new[] { id }, argsList.ToArray());
495+
}
496+
497+
return res == 1 ? id : null;
498+
}
499+
500+
return await connection.JsonSetAsync(id, "$", obj, when, timespan) ? id : null;
501+
}
502+
318503
/// <summary>
319504
/// Serializes an object to either hash or json (depending on how it's decorated), and saves it in redis.
320505
/// </summary>

src/Redis.OM/RedisReply.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public static implicit operator int(RedisReply v)
198198
/// </summary>
199199
/// <param name="v">The redis reply.</param>
200200
/// <returns>the integer.</returns>
201-
public static implicit operator int?(RedisReply v) => v._internalInt;
201+
public static implicit operator int?(RedisReply v) => v._internalInt ?? (int?)v._internalLong;
202202

203203
/// <summary>
204204
/// Converts an integer to a reply.

src/Redis.OM/Scripts.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,68 @@ local second_op
8383
redis.call('UNLINK', KEYS[1])
8484
redis.call('JSON.SET', KEYS[1], '.', ARGV[1])
8585
return 0
86+
";
87+
88+
/// <summary>
89+
/// Conditionally calls a hset if a key doesn't exist.
90+
/// </summary>
91+
internal const string HsetIfNotExists = @"
92+
local exists = redis.call('EXISTS', KEYS[1])
93+
if exists ~= 1 then
94+
local hashArgs = {}
95+
local expiry = tonumber(ARGV[1])
96+
for i = 2, table.getn(ARGV) do
97+
hashArgs[i-1] = ARGV[i]
98+
end
99+
redis.call('HSET', KEYS[1], unpack(hashArgs))
100+
if expiry > 0 then
101+
redis.call('PEXPIRE', KEYS[1], expiry)
102+
end
103+
return 1
104+
end
105+
return 0
106+
";
107+
108+
/// <summary>
109+
/// replaces hash if key exists.
110+
/// </summary>
111+
internal const string ReplaceHashIfExists = @"
112+
local exists = redis.call('EXISTS', KEYS[1])
113+
if exists == 1 then
114+
local hashArgs = {}
115+
local expiry = tonumber(ARGV[1])
116+
for i = 2, table.getn(ARGV) do
117+
hashArgs[i-1] = ARGV[i]
118+
end
119+
redis.call('UNLINK', KEYS[1])
120+
redis.call('HSET', KEYS[1], unpack(hashArgs))
121+
if expiry > 0 then
122+
redis.call('PEXPIRE', KEYS[1], expiry)
123+
end
124+
return 1
125+
end
126+
return 0
127+
";
128+
129+
/// <summary>
130+
/// Sets a Json object, if the object is set, and there is an expiration, also set expiration.
131+
/// </summary>
132+
internal const string JsonSetWithExpire = @"
133+
local expiry = tonumber(ARGV[1])
134+
local jsonArgs = {}
135+
for i = 2, table.getn(ARGV) do
136+
jsonArgs[i-1] = ARGV[i]
137+
end
138+
local wasAdded = redis.call('JSON.SET', KEYS[1], unpack(jsonArgs))
139+
if wasAdded ~= false then
140+
if expiry > 0 then
141+
redis.call('PEXPIRE', KEYS[1], expiry)
142+
else
143+
redis.call('PERSIST', KEYS[1])
144+
end
145+
return 1
146+
end
147+
return 0
86148
";
87149

88150
/// <summary>
@@ -95,6 +157,9 @@ local second_op
95157
{ nameof(Unlink), Unlink },
96158
{ nameof(UnlinkAndSetHash), UnlinkAndSetHash },
97159
{ nameof(UnlinkAndSendJson), UnlinkAndSendJson },
160+
{ nameof(HsetIfNotExists), HsetIfNotExists },
161+
{ nameof(ReplaceHashIfExists), ReplaceHashIfExists },
162+
{ nameof(JsonSetWithExpire), JsonSetWithExpire },
98163
};
99164

100165
/// <summary>

src/Redis.OM/Searching/IRedisCollection.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
6969
/// <returns>the key.</returns>
7070
Task<string> InsertAsync(T item, TimeSpan timeSpan);
7171

72+
/// <summary>
73+
/// Inserts an item into redis.
74+
/// </summary>
75+
/// <param name="item">The item.</param>
76+
/// <param name="when">Condition to insert the document under.</param>
77+
/// <param name="timeSpan">The expiration time of the document (TTL).</param>
78+
/// <returns>the Id of the newly inserted item, or null if not inserted.</returns>
79+
Task<string?> InsertAsync(T item, WhenKey when, TimeSpan? timeSpan = null);
80+
81+
/// <summary>
82+
/// Inserts an item into redis.
83+
/// </summary>
84+
/// <param name="item">The item.</param>
85+
/// <param name="when">Condition to insert the document under.</param>
86+
/// <param name="timeSpan">The expiration time of the document (TTL).</param>
87+
/// <returns>the Id of the newly inserted item, or null if not inserted.</returns>
88+
string? Insert(T item, WhenKey when, TimeSpan? timeSpan = null);
89+
7290
/// <summary>
7391
/// finds an item by it's ID or keyname.
7492
/// </summary>

src/Redis.OM/Searching/RedisCollection.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,18 @@ public async Task<string> InsertAsync(T item, TimeSpan timeSpan)
584584
return await ((RedisQueryProvider)Provider).Connection.SetAsync(item, timeSpan);
585585
}
586586

587+
/// <inheritdoc/>
588+
public Task<string?> InsertAsync(T item, WhenKey when, TimeSpan? timeSpan = null)
589+
{
590+
return ((RedisQueryProvider)Provider).Connection.SetAsync(item, when, timeSpan);
591+
}
592+
593+
/// <inheritdoc/>
594+
public string? Insert(T item, WhenKey when, TimeSpan? timeSpan = null)
595+
{
596+
return ((RedisQueryProvider)Provider).Connection.Set(item, when, timeSpan);
597+
}
598+
587599
/// <inheritdoc/>
588600
public T? FindById(string id)
589601
{

src/Redis.OM/WhenKey.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Redis.OM
2+
{
3+
/// <summary>
4+
/// Indicates when this operation should be performed (only some variations are legal in a given context).
5+
/// </summary>
6+
public enum WhenKey
7+
{
8+
/// <summary>
9+
/// The operation should occur whether or not there is an existing value.
10+
/// </summary>
11+
Always,
12+
13+
/// <summary>
14+
/// The operation should only occur when there is an existing value.
15+
/// </summary>
16+
Exists,
17+
18+
/// <summary>
19+
/// The operation should only occur when there is not an existing value.
20+
/// </summary>
21+
NotExists,
22+
}
23+
}

0 commit comments

Comments
 (0)