@@ -284,6 +284,20 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value)
284284 /// <param name="member">The member the sorted set must not contain.</param>
285285 public static Condition SortedSetNotContains ( RedisKey key , RedisValue member ) => new ExistsCondition ( key , RedisType . SortedSet , member , false ) ;
286286
287+ /// <summary>
288+ /// Enforces that the given sorted set contains a member that ist starting with the start-sequence
289+ /// </summary>
290+ /// <param name="key">The key of the sorted set to check.</param>
291+ /// <param name="memberStartSequence">a byte array: the set must contain at least one member, that starts with the byte-sequence.</param>
292+ public static Condition SortedSetStartsWith ( RedisKey key , byte [ ] memberStartSequence ) => new StartsWithCondition ( key , memberStartSequence , true ) ;
293+
294+ /// <summary>
295+ /// Enforces that the given sorted set does not contain a member that ist starting with the start-sequence
296+ /// </summary>
297+ /// <param name="key">The key of the sorted set to check.</param>
298+ /// <param name="memberStartSequence">a byte array: the set must not contain any members, that start with the byte-sequence.</param>
299+ public static Condition SortedSetNotStartsWith ( RedisKey key , byte [ ] memberStartSequence ) => new StartsWithCondition ( key , memberStartSequence , false ) ;
300+
287301 /// <summary>
288302 /// Enforces that the given sorted set member must have the specified score.
289303 /// </summary>
@@ -370,6 +384,9 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl
370384 public static Message CreateMessage ( Condition condition , int db , CommandFlags flags , RedisCommand command , in RedisKey key , in RedisValue value , in RedisValue value1 ) =>
371385 new ConditionMessage ( condition , db , flags , command , key , value , value1 ) ;
372386
387+ public static Message CreateMessage ( Condition condition , int db , CommandFlags flags , RedisCommand command , in RedisKey key , in RedisValue value , in RedisValue value1 , in RedisValue value2 , in RedisValue value3 , in RedisValue value4 ) =>
388+ new ConditionMessage ( condition , db , flags , command , key , value , value1 , value2 , value3 , value4 ) ;
389+
373390 [ System . Diagnostics . CodeAnalysis . SuppressMessage ( "Style" , "IDE0071:Simplify interpolation" , Justification = "Allocations (string.Concat vs. string.Format)" ) ]
374391 protected override bool SetResultCore ( PhysicalConnection connection , Message message , in RawResult result )
375392 {
@@ -389,6 +406,9 @@ private class ConditionMessage : Message.CommandKeyBase
389406 public readonly Condition Condition ;
390407 private readonly RedisValue value ;
391408 private readonly RedisValue value1 ;
409+ private readonly RedisValue value2 ;
410+ private readonly RedisValue value3 ;
411+ private readonly RedisValue value4 ;
392412
393413 public ConditionMessage ( Condition condition , int db , CommandFlags flags , RedisCommand command , in RedisKey key , in RedisValue value )
394414 : base ( db , flags , command , key )
@@ -403,6 +423,15 @@ public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCo
403423 this . value1 = value1 ; // note no assert here
404424 }
405425
426+ // Message with 3 or 4 values not used, therefore not implemented
427+ public ConditionMessage ( Condition condition , int db , CommandFlags flags , RedisCommand command , in RedisKey key , in RedisValue value , in RedisValue value1 , in RedisValue value2 , in RedisValue value3 , in RedisValue value4 )
428+ : this ( condition , db , flags , command , key , value , value1 )
429+ {
430+ this . value2 = value2 ; // note no assert here
431+ this . value3 = value3 ; // note no assert here
432+ this . value4 = value4 ; // note no assert here
433+ }
434+
406435 protected override void WriteImpl ( PhysicalConnection physical )
407436 {
408437 if ( value . IsNull )
@@ -412,19 +441,25 @@ protected override void WriteImpl(PhysicalConnection physical)
412441 }
413442 else
414443 {
415- physical . WriteHeader ( command , value1 . IsNull ? 2 : 3 ) ;
444+ physical . WriteHeader ( command , value1 . IsNull ? 2 : value2 . IsNull ? 3 : value3 . IsNull ? 4 : value4 . IsNull ? 5 : 6 ) ;
416445 physical . Write ( Key ) ;
417446 physical . WriteBulkString ( value ) ;
418447 if ( ! value1 . IsNull )
419- {
420448 physical . WriteBulkString ( value1 ) ;
421- }
449+ if ( ! value2 . IsNull )
450+ physical . WriteBulkString ( value2 ) ;
451+ if ( ! value3 . IsNull )
452+ physical . WriteBulkString ( value3 ) ;
453+ if ( ! value4 . IsNull )
454+ physical . WriteBulkString ( value4 ) ;
422455 }
423456 }
424- public override int ArgCount => value . IsNull ? 1 : value1 . IsNull ? 2 : 3 ;
457+ public override int ArgCount => value . IsNull ? 1 : value1 . IsNull ? 2 : value2 . IsNull ? 3 : value3 . IsNull ? 4 : value4 . IsNull ? 5 : 6 ;
425458 }
426459 }
427460
461+
462+
428463 internal class ExistsCondition : Condition
429464 {
430465 private readonly bool expectedResult ;
@@ -501,6 +536,90 @@ internal override bool TryValidate(in RawResult result, out bool value)
501536 }
502537 }
503538
539+ internal class StartsWithCondition : Condition
540+ {
541+ // only usable for RedisType.SortedSet, members of SortedSets are always byte-arrays, expectedStartValue therefore is a byte-array
542+ // any Encoding and Conversion for the search-sequence has to be executed in calling application
543+ // working with byte arrays should prevent any encoding within this class, that could distort the comparison
544+
545+ private readonly bool expectedResult ;
546+ private readonly RedisValue expectedStartValue ;
547+ private readonly RedisKey key ;
548+
549+ internal override Condition MapKeys ( Func < RedisKey , RedisKey > map ) =>
550+ new StartsWithCondition ( map ( key ) , expectedStartValue , expectedResult ) ;
551+
552+ public StartsWithCondition ( in RedisKey key , in RedisValue expectedStartValue , bool expectedResult )
553+ {
554+ if ( key . IsNull ) throw new ArgumentNullException ( nameof ( key ) ) ;
555+ if ( expectedStartValue . IsNull ) throw new ArgumentNullException ( nameof ( expectedStartValue ) ) ;
556+ this . key = key ;
557+ this . expectedStartValue = expectedStartValue ; // array with length 0 returns true condition
558+ this . expectedResult = expectedResult ;
559+ }
560+
561+ public override string ToString ( ) =>
562+ ( expectedStartValue . IsNull ? key . ToString ( ) : ( ( string ? ) key ) + " " + RedisType . SortedSet + " > " + expectedStartValue )
563+ + ( expectedResult ? " starts with" : " does not start with" ) ;
564+
565+ internal override void CheckCommands ( CommandMap commandMap ) => commandMap . AssertAvailable ( RedisCommand . ZRANGEBYLEX ) ;
566+
567+ internal override IEnumerable < Message > CreateMessages ( int db , IResultBox ? resultBox )
568+ {
569+ yield return Message . Create ( db , CommandFlags . None , RedisCommand . WATCH , key ) ;
570+
571+ #pragma warning disable CS8600 , CS8604 // expectedStartValue is checked to be not null in Constructor and must be a byte[] because of API-parameters
572+ var message = ConditionProcessor . CreateMessage ( this , db , CommandFlags . None , RedisCommand . ZRANGEBYLEX , key ,
573+ CombineBytes ( 91 , ( byte [ ] ) expectedStartValue . Box ( ) ) , "+" , "LIMIT" , "0" , "1" ) ; // prepends '[' to startValue for inclusive search in CombineBytes
574+ #pragma warning disable CS8600 , CS8604
575+ message . SetSource ( ConditionProcessor . Default , resultBox ) ;
576+ yield return message ;
577+ }
578+
579+ internal override int GetHashSlot ( ServerSelectionStrategy serverSelectionStrategy ) => serverSelectionStrategy . HashSlot ( key ) ;
580+
581+ internal override bool TryValidate ( in RawResult result , out bool value )
582+ {
583+ RedisValue [ ] ? r = result . GetItemsAsValues ( ) ;
584+ if ( result . ItemsCount == 0 ) value = false ; // false, if empty list -> read after end of memberlist / itemsCout > 1 is impossible due to 'LIMIT 0 1'
585+ #pragma warning disable CS8600 , CS8604 // warnings on StartsWith can be ignored because of ItemsCount-check in then preceding command!!
586+ else value = r != null && r . Length > 0 && StartsWith ( ( byte [ ] ) r [ 0 ] . Box ( ) , expectedStartValue ) ;
587+ #pragma warning disable CS8600 , CS8604
588+
589+ #pragma warning disable CS8602 // warning for r[0] can be ignored because of null-check in then same command-line !!
590+ if ( ! expectedResult ) value = ! value ;
591+ ConnectionMultiplexer . TraceWithoutContext ( "actual: " + r == null ? "null" : r . Length == 0 ? "empty" : r [ 0 ] . ToString ( )
592+ + "; expected: " + expectedStartValue . ToString ( )
593+ + "; wanted: " + ( expectedResult ? "StartsWith" : "NotStartWith" )
594+ + "; voting: " + value ) ;
595+ #pragma warning restore CS8602
596+ return true ;
597+ }
598+
599+ private static byte [ ] CombineBytes ( byte b1 , byte [ ] a1 ) // combines b1 and a1 to new array
600+ {
601+ byte [ ] newArray = new byte [ a1 . Length + 1 ] ;
602+ newArray [ 0 ] = b1 ;
603+ System . Buffer . BlockCopy ( a1 , 0 , newArray , 1 , a1 . Length ) ;
604+ return newArray ;
605+ }
606+
607+ internal bool StartsWith ( byte [ ] result , byte [ ] searchfor )
608+ {
609+ if ( searchfor . Length > result . Length ) return false ;
610+
611+ for ( int i = 0 ; i < searchfor . Length ; i ++ )
612+ {
613+ if ( result [ i ] != searchfor [ i ] ) return false ;
614+ }
615+
616+ return true ;
617+ }
618+
619+
620+ }
621+
622+
504623 internal class EqualsCondition : Condition
505624 {
506625 internal override Condition MapKeys ( Func < RedisKey , RedisKey > map ) =>
0 commit comments