├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── nuget └── pack.bat └── src ├── .editorconfig ├── CloudStructures.slnx ├── CloudStructures ├── CloudStructures.csproj ├── ConnectionEventArgs.cs ├── Converters │ ├── IRedisValueConverter.cs │ ├── IValueConverter.cs │ ├── PrimitiveConverter.cs │ ├── SystemTextJsonConverter.cs │ └── ValueConverter.cs ├── IConnectionEventHandler.cs ├── Internals │ ├── EnumerableExtensions.cs │ └── RedisOperationHelpers.cs ├── RedisConfig.cs ├── RedisConnection.cs ├── RedisResult.cs └── Structures │ ├── IRedisStructure.cs │ ├── RedisBit.cs │ ├── RedisDictionary.cs │ ├── RedisGeo.cs │ ├── RedisHashSet.cs │ ├── RedisHyperLogLog.cs │ ├── RedisList.cs │ ├── RedisLock.cs │ ├── RedisLua.cs │ ├── RedisSet.cs │ ├── RedisSortedSet.cs │ └── RedisString.cs ├── Directory.Build.props ├── Directory.Packages.props └── NuGet.config /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Takaaki Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudStructures 2 | CloudStructures is the [Redis](https://redis.io/) client based on [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis). 3 | 4 | StackExchange.Redis is very pure and low level library. It's Redis driver like ADO.NET. It's difficult to use it as raw. CloudStructures provides simple O/R (Object / Redis) mapper like [Dapper](https://github.com/StackExchange/Dapper) for ADO.NET. 5 | 6 | 7 | [![Releases](https://img.shields.io/github/release/neuecc/CloudStructures.svg)](https://github.com/neuecc/CloudStructures/releases) 8 | 9 | 10 | 11 | # Support framework 12 | - .NET 8+ 13 | - .NET Standard 2.0+ 14 | - .NET Framework 4.6.2+ 15 | 16 | 17 | 18 | # Installation 19 | ``` 20 | dotnet add package CloudStructures 21 | ``` 22 | 23 | 24 | 25 | # Data structures of Redis 26 | CloudStructures supports these Redis data types. All methods are async. 27 | 28 | | Structure | Description | 29 | | --- | --- | 30 | | `RedisBit` | Bits API | 31 | | `RedisDictionary` | Hashes API with constrained value type | 32 | | `RedisGeo` | Geometries API | 33 | | `RedisHashSet` | like `RedisDictionary` | 34 | | `RedisHyperLogLog` | HyperLogLogs API | 35 | | `RedisList` | Lists API | 36 | | `RedisLua` | Lua eval API | 37 | | `RedisSet` | Sets API | 38 | | `RedisSortedSet` | SortedSets API | 39 | | `RedisString` | Strings API | 40 | 41 | 42 | 43 | # Getting started 44 | Following code is simple sample. 45 | 46 | ```cs 47 | // RedisConnection have to be held as static. 48 | public static class RedisServer 49 | { 50 | public static RedisConnection Connection { get; } 51 | public static RedisServer() 52 | { 53 | var config = new RedisConfig("name", "connectionString"); 54 | Connection = new RedisConnection(config); 55 | } 56 | } 57 | 58 | // A certain data class 59 | public class Person 60 | { 61 | public string Name { get; set; } 62 | public int Age { get; set; } 63 | } 64 | 65 | // 1. Create redis structure 66 | var key = "test-key"; 67 | var defaultExpiry = TimeSpan.FromDays(1); 68 | var redis = new RedisString(RedisServer.Connection, key, defaultExpiry) 69 | 70 | // 2. Call command 71 | var neuecc = new Person("neuecc", 35); 72 | await redis.SetAsync(neuecc); 73 | var result = await redis.GetAsync(); 74 | ``` 75 | 76 | 77 | 78 | # ValueConverter 79 | If you use this library, you *should* implement `IValueConverter` to serialize your original class. Unless you pass custom `IValueConverter` to `RedisConnection` ctor, fallback to `SystemTextJsonConverter` automatically that is default converter we provide. 80 | 81 | 82 | ## How to implement custom `IValueConverter` 83 | 84 | ```cs 85 | using CloudStructures.Converters; 86 | using Utf8Json; 87 | using Utf8Json.Resolvers; 88 | 89 | namespace HowToImplement_CustomValueConverter 90 | { 91 | public sealed class Utf8JsonConverter : IValueConverter 92 | { 93 | public byte[] Serialize(T value) 94 | => JsonSerializer.Serialize(value, StandardResolver.AllowPrivate); 95 | 96 | public T Deserialize(byte[] value) 97 | => JsonSerializer.Deserialize(value, StandardResolver.AllowPrivate); 98 | } 99 | } 100 | ``` 101 | 102 | ```cs 103 | using CloudStructures.Converters; 104 | using MessagePack; 105 | using MessagePack.Resolvers; 106 | 107 | namespace HowToImplement_CustomValueConverter 108 | { 109 | public sealed class MessagePackConverter : IValueConverter 110 | { 111 | private MessagePackSerializerOptions Options { get; } 112 | 113 | public MessagePackConverter(MessagePackSerializerOptions options) 114 | => this.Options = options; 115 | 116 | public byte[] Serialize(T value) 117 | => MessagePackSerializer.Serialize(value, this.Options); 118 | 119 | public T Deserialize(byte[] value) 120 | => MessagePackSerializer.Deserialize(value, this.Options); 121 | } 122 | } 123 | ``` 124 | 125 | 126 | 127 | # Authors 128 | - Yoshifumi Kawai (a.k.a [@neuecc](https://twitter.com/neuecc)) 129 | - Takaaki Suzuki (a.k.a [@xin9le](https://twitter.com/xin9le)) 130 | 131 | Yoshifumi Kawai is software developer in Tokyo, Japan. Awarded Microsoft MVP (C#) since April, 2011. He's the original owner of this project. 132 | 133 | Takaaki Suzuki is software developer in Fukui, Japan. Awarded Microsoft MVP (C#) since July, 2012. He's a contributer who led the .NET Standard support. 134 | 135 | 136 | 137 | # License 138 | This library is under the MIT License. 139 | -------------------------------------------------------------------------------- /nuget/pack.bat: -------------------------------------------------------------------------------- 1 | dotnet pack ../src/CloudStructures/CloudStructures.csproj -c Release -o ./packages 2 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig style reference 2 | # https://docs.microsoft.com/ja-jp/visualstudio/ide/editorconfig-code-style-settings-reference 3 | 4 | 5 | 6 | # top-most EditorConfig file 7 | root = true 8 | 9 | # Don't use tabs for indentation. 10 | [*] 11 | indent_style = space 12 | trim_trailing_whitespace = true 13 | 14 | # Code files 15 | [*.{cs,csx,vb,vbx,ts,js,css,less}] 16 | indent_size = 4 17 | insert_final_newline = true 18 | charset = utf-8-bom 19 | 20 | # Razor files 21 | [*.{cshtml,razor}] 22 | indent_size = 4 23 | charset = utf-8-bom 24 | 25 | # Xml project files 26 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 27 | indent_size = 4 28 | 29 | # Xml config files 30 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 31 | indent_size = 4 32 | 33 | # JSON files 34 | [*.json] 35 | indent_size = 4 36 | 37 | # CSharp 38 | [*.cs] 39 | dotnet_code_quality_unused_parameters = all:silent 40 | dotnet_code_quality.ca1822.api_surface = private 41 | 42 | dotnet_diagnostic.CA1304.severity = error 43 | dotnet_diagnostic.CA1305.severity = error 44 | dotnet_diagnostic.CA1307.severity = error 45 | dotnet_diagnostic.CA1310.severity = error 46 | dotnet_diagnostic.CA1848.severity = error 47 | dotnet_diagnostic.CA2016.severity = error 48 | dotnet_diagnostic.CA2254.severity = error 49 | dotnet_diagnostic.CS9124.severity = error 50 | dotnet_diagnostic.IDE0004.severity = error 51 | dotnet_diagnostic.IDE0005.severity = warning 52 | dotnet_diagnostic.IDE0009.severity = error 53 | dotnet_diagnostic.IDE0028.severity = error 54 | dotnet_diagnostic.IDE0034.severity = error 55 | dotnet_diagnostic.IDE0041.severity = error 56 | dotnet_diagnostic.IDE0044.severity = error 57 | dotnet_diagnostic.IDE0052.severity = warning 58 | dotnet_diagnostic.IDE0079.severity = error 59 | dotnet_diagnostic.IDE0080.severity = error 60 | dotnet_diagnostic.IDE0082.severity = error 61 | dotnet_diagnostic.IDE0090.severity = error 62 | dotnet_diagnostic.IDE0100.severity = error 63 | dotnet_diagnostic.IDE0120.severity = error 64 | dotnet_diagnostic.IDE0161.severity = error 65 | dotnet_diagnostic.IDE0161.severity = error 66 | dotnet_diagnostic.IDE0170.severity = silent 67 | dotnet_diagnostic.IDE0230.severity = error 68 | dotnet_diagnostic.IDE0240.severity = error 69 | dotnet_diagnostic.IDE0241.severity = error 70 | dotnet_diagnostic.IDE0250.severity = error 71 | dotnet_diagnostic.IDE0251.severity = error 72 | dotnet_diagnostic.IDE0270.severity = silent 73 | dotnet_diagnostic.IDE0290.severity = silent 74 | dotnet_diagnostic.IDE0300.severity = error 75 | 76 | dotnet_sort_system_directives_first = true 77 | dotnet_style_qualification_for_field = true 78 | dotnet_style_qualification_for_property = true 79 | dotnet_style_qualification_for_method = true 80 | dotnet_style_qualification_for_event = true 81 | 82 | csharp_prefer_simple_using_statement = false:none 83 | csharp_style_deconstructed_variable_declaration = false:none 84 | csharp_style_pattern_matching_over_as_with_null_check = false:none 85 | csharp_style_namespace_declarations = file_scoped:suggestion 86 | -------------------------------------------------------------------------------- /src/CloudStructures.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/CloudStructures/CloudStructures.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | true 6 | CloudStructures 7 | 3.4.1.0 8 | Redis Client based on StackExchange.Redis. 9 | https://github.com/neuecc/CloudStructures 10 | Redis, Redis Client, O/R Mapping 11 | true 12 | MIT 13 | $(PackageProjectUrl) 14 | Git 15 | 16 | neuecc, xin9le 17 | Copyright© neuecc, xin9le 18 | 19 | README.md 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/CloudStructures/ConnectionEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CloudStructures; 4 | 5 | 6 | 7 | /// 8 | /// Contains information about a server connection establishment. 9 | /// 10 | public sealed class ConnectionOpenedEventArgs : EventArgs 11 | { 12 | /// 13 | /// Gets the elapsed time to establish connection. 14 | /// 15 | public TimeSpan Elapsed { get; } 16 | 17 | 18 | /// 19 | /// Creates instance. 20 | /// 21 | /// 22 | internal ConnectionOpenedEventArgs(TimeSpan elapsed) 23 | => this.Elapsed = elapsed; 24 | } 25 | -------------------------------------------------------------------------------- /src/CloudStructures/Converters/IRedisValueConverter.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace CloudStructures.Converters; 4 | 5 | 6 | 7 | /// 8 | /// Provides conversion function to . 9 | /// 10 | /// Data type 11 | internal interface IRedisValueConverter 12 | { 13 | /// 14 | /// Serialize to . 15 | /// 16 | /// 17 | /// 18 | RedisValue Serialize(T value); 19 | 20 | 21 | /// 22 | /// Deserialize from . 23 | /// 24 | /// 25 | /// 26 | T Deserialize(RedisValue value); 27 | } 28 | -------------------------------------------------------------------------------- /src/CloudStructures/Converters/IValueConverter.cs: -------------------------------------------------------------------------------- 1 | namespace CloudStructures.Converters; 2 | 3 | 4 | 5 | /// 6 | /// Provides data conversion function. 7 | /// 8 | public interface IValueConverter 9 | { 10 | /// 11 | /// Serialize to byte array. 12 | /// 13 | /// Data type 14 | /// 15 | /// 16 | byte[] Serialize(T value); 17 | 18 | 19 | /// 20 | /// Deserialize from byte array. 21 | /// 22 | /// Data type 23 | /// 24 | /// 25 | T Deserialize(byte[] value); 26 | } 27 | -------------------------------------------------------------------------------- /src/CloudStructures/Converters/PrimitiveConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StackExchange.Redis; 3 | 4 | namespace CloudStructures.Converters; 5 | 6 | 7 | 8 | /// 9 | /// Provides conversion function. 10 | /// 11 | internal sealed class BooleanConverter : IRedisValueConverter 12 | { 13 | public RedisValue Serialize(bool value) => value; 14 | public bool Deserialize(RedisValue value) => (bool)value; 15 | } 16 | 17 | 18 | 19 | /// 20 | /// Provides conversion function. 21 | /// 22 | internal sealed class NullableBooleanConverter : IRedisValueConverter 23 | { 24 | public RedisValue Serialize(bool? value) => value; 25 | public bool? Deserialize(RedisValue value) => (bool?)value; 26 | } 27 | 28 | 29 | 30 | /// 31 | /// Provides conversion function. 32 | /// 33 | internal sealed class CharConverter : IRedisValueConverter 34 | { 35 | public RedisValue Serialize(char value) => (int)value; 36 | public char Deserialize(RedisValue value) => (char)(int)value; 37 | } 38 | 39 | 40 | 41 | /// 42 | /// Provides conversion function. 43 | /// 44 | internal sealed class NullableCharConverter : IRedisValueConverter 45 | { 46 | public RedisValue Serialize(char? value) => (int?)value; 47 | public char? Deserialize(RedisValue value) => (char?)(int?)value; 48 | } 49 | 50 | 51 | 52 | /// 53 | /// Provides conversion function. 54 | /// 55 | internal sealed class SByteConverter : IRedisValueConverter 56 | { 57 | public RedisValue Serialize(sbyte value) => value; 58 | public sbyte Deserialize(RedisValue value) => (sbyte)value; 59 | } 60 | 61 | 62 | 63 | /// 64 | /// Provides conversion function. 65 | /// 66 | internal sealed class NullableSByteConverter : IRedisValueConverter 67 | { 68 | public RedisValue Serialize(sbyte? value) => value; 69 | public sbyte? Deserialize(RedisValue value) => (sbyte?)value; 70 | } 71 | 72 | 73 | 74 | /// 75 | /// Provides conversion function. 76 | /// 77 | internal sealed class ByteConverter : IRedisValueConverter 78 | { 79 | public RedisValue Serialize(byte value) => (uint)value; 80 | public byte Deserialize(RedisValue value) => (byte)(uint)value; 81 | } 82 | 83 | 84 | 85 | /// 86 | /// Provides conversion function. 87 | /// 88 | internal sealed class NullableByteConverter : IRedisValueConverter 89 | { 90 | public RedisValue Serialize(byte? value) => (uint?)value; 91 | public byte? Deserialize(RedisValue value) => (byte?)(uint?)value; 92 | } 93 | 94 | 95 | 96 | /// 97 | /// Provides conversion function. 98 | /// 99 | internal sealed class Int16Converter : IRedisValueConverter 100 | { 101 | public RedisValue Serialize(short value) => value; 102 | public short Deserialize(RedisValue value) => (short)value; 103 | } 104 | 105 | 106 | 107 | /// 108 | /// Provides conversion function. 109 | /// 110 | internal sealed class NullableInt16Converter : IRedisValueConverter 111 | { 112 | public RedisValue Serialize(short? value) => value; 113 | public short? Deserialize(RedisValue value) => (short?)value; 114 | } 115 | 116 | 117 | 118 | /// 119 | /// Provides conversion function. 120 | /// 121 | internal sealed class UInt16Converter : IRedisValueConverter 122 | { 123 | public RedisValue Serialize(ushort value) => (uint)value; 124 | public ushort Deserialize(RedisValue value) => (ushort)(uint)value; 125 | } 126 | 127 | 128 | 129 | /// 130 | /// Provides conversion function. 131 | /// 132 | internal sealed class NullableUInt16Converter : IRedisValueConverter 133 | { 134 | public RedisValue Serialize(ushort? value) => (uint?)value; 135 | public ushort? Deserialize(RedisValue value) => (ushort?)(uint?)value; 136 | } 137 | 138 | 139 | 140 | /// 141 | /// Provides conversion function. 142 | /// 143 | internal sealed class Int32Converter : IRedisValueConverter 144 | { 145 | public RedisValue Serialize(int value) => value; 146 | public int Deserialize(RedisValue value) => (int)value; 147 | } 148 | 149 | 150 | 151 | /// 152 | /// Provides conversion function. 153 | /// 154 | internal sealed class NullableInt32Converter : IRedisValueConverter 155 | { 156 | public RedisValue Serialize(int? value) => value; 157 | public int? Deserialize(RedisValue value) => (int?)value; 158 | } 159 | 160 | 161 | 162 | /// 163 | /// Provides conversion function. 164 | /// 165 | internal sealed class UInt32Converter : IRedisValueConverter 166 | { 167 | public RedisValue Serialize(uint value) => value; 168 | public uint Deserialize(RedisValue value) => (uint)value; 169 | } 170 | 171 | 172 | 173 | /// 174 | /// Provides conversion function. 175 | /// 176 | internal sealed class NullableUInt32Converter : IRedisValueConverter 177 | { 178 | public RedisValue Serialize(uint? value) => value; 179 | public uint? Deserialize(RedisValue value) => (uint?)value; 180 | } 181 | 182 | 183 | 184 | /// 185 | /// Provides conversion function. 186 | /// 187 | internal sealed class Int64Converter : IRedisValueConverter 188 | { 189 | public RedisValue Serialize(long value) => value; 190 | public long Deserialize(RedisValue value) => (long)value; 191 | } 192 | 193 | 194 | 195 | /// 196 | /// Provides conversion function. 197 | /// 198 | internal sealed class NullableInt64Converter : IRedisValueConverter 199 | { 200 | public RedisValue Serialize(long? value) => value; 201 | public long? Deserialize(RedisValue value) => (long?)value; 202 | } 203 | 204 | 205 | 206 | /// 207 | /// Provides conversion function. 208 | /// 209 | internal sealed class UInt64Converter : IRedisValueConverter 210 | { 211 | public RedisValue Serialize(ulong value) => value; 212 | public ulong Deserialize(RedisValue value) => (ulong)value; 213 | } 214 | 215 | 216 | 217 | /// 218 | /// Provides conversion function. 219 | /// 220 | internal sealed class NullableUInt64Converter : IRedisValueConverter 221 | { 222 | public RedisValue Serialize(ulong? value) => value; 223 | public ulong? Deserialize(RedisValue value) => (ulong?)value; 224 | } 225 | 226 | 227 | 228 | /// 229 | /// Provides conversion function. 230 | /// 231 | internal sealed class SingleConverter : IRedisValueConverter 232 | { 233 | public RedisValue Serialize(float value) => value; 234 | public float Deserialize(RedisValue value) => (float)value; 235 | } 236 | 237 | 238 | 239 | /// 240 | /// Provides conversion function. 241 | /// 242 | internal sealed class NullableSingleConverter : IRedisValueConverter 243 | { 244 | public RedisValue Serialize(float? value) => value; 245 | public float? Deserialize(RedisValue value) => (float?)value; 246 | } 247 | 248 | 249 | 250 | /// 251 | /// Provides conversion function. 252 | /// 253 | internal sealed class DoubleConverter : IRedisValueConverter 254 | { 255 | public RedisValue Serialize(double value) => value; 256 | public double Deserialize(RedisValue value) => (double)value; 257 | } 258 | 259 | 260 | 261 | /// 262 | /// Provides conversion function. 263 | /// 264 | internal sealed class NullableDoubleConverter : IRedisValueConverter 265 | { 266 | public RedisValue Serialize(double? value) => value; 267 | public double? Deserialize(RedisValue value) => (double?)value; 268 | } 269 | 270 | 271 | 272 | /// 273 | /// Provides conversion function. 274 | /// 275 | internal sealed class StringConverter : IRedisValueConverter 276 | { 277 | public RedisValue Serialize(string? value) => value; 278 | public string? Deserialize(RedisValue value) => value; 279 | } 280 | 281 | 282 | 283 | /// 284 | /// Provides [] conversion function. 285 | /// 286 | internal sealed class ByteArrayConverter : IRedisValueConverter 287 | { 288 | public RedisValue Serialize(byte[]? value) => value; 289 | public byte[]? Deserialize(RedisValue value) => value; 290 | } 291 | 292 | 293 | 294 | /// 295 | /// Provides (= T is ) conversion function. 296 | /// 297 | internal sealed class MemoryByteConverter : IRedisValueConverter> 298 | { 299 | public RedisValue Serialize(Memory value) => value; 300 | public Memory Deserialize(RedisValue value) => (byte[]?)value; 301 | } 302 | 303 | 304 | 305 | /// 306 | /// Provides (= T is ) conversion function. 307 | /// 308 | internal sealed class ReadOnlyMemoryByteConverter : IRedisValueConverter> 309 | { 310 | public RedisValue Serialize(ReadOnlyMemory value) => value; 311 | public ReadOnlyMemory Deserialize(RedisValue value) => value; 312 | } 313 | -------------------------------------------------------------------------------- /src/CloudStructures/Converters/SystemTextJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace CloudStructures.Converters; 4 | 5 | 6 | 7 | /// 8 | /// Provides value converter using System.Text.Json. 9 | /// 10 | public sealed class SystemTextJsonConverter : IValueConverter 11 | { 12 | /// 13 | /// Serialize value to binary. 14 | /// 15 | /// Data type 16 | /// 17 | /// 18 | public byte[] Serialize(T value) 19 | => JsonSerializer.SerializeToUtf8Bytes(value); 20 | 21 | 22 | /// 23 | /// Deserialize value from binary. 24 | /// 25 | /// Data type 26 | /// 27 | /// 28 | public T Deserialize(byte[] value) 29 | => JsonSerializer.Deserialize(value)!; // forgive 30 | } 31 | -------------------------------------------------------------------------------- /src/CloudStructures/Converters/ValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using StackExchange.Redis; 4 | 5 | namespace CloudStructures.Converters; 6 | 7 | 8 | 9 | /// 10 | /// Provides data conversion function. 11 | /// 12 | internal sealed class ValueConverter(IValueConverter? customConverter) 13 | { 14 | #region Properties 15 | /// 16 | /// Gets custom conversion function. 17 | /// 18 | private IValueConverter CustomConverter { get; } = customConverter ?? new SystemTextJsonConverter(); 19 | #endregion 20 | 21 | 22 | #region Serialization 23 | /// 24 | /// Serialize to . 25 | /// 26 | /// Data type 27 | /// 28 | /// 29 | public RedisValue Serialize(T value) 30 | { 31 | var converter = PrimitiveConverterCache.Converter; 32 | return converter is null 33 | ? this.CustomConverter.Serialize(value) 34 | : converter.Serialize(value); 35 | } 36 | 37 | 38 | /// 39 | /// Deserialize from . 40 | /// 41 | /// Data type 42 | /// 43 | /// 44 | public T Deserialize(RedisValue value) 45 | { 46 | var converter = PrimitiveConverterCache.Converter; 47 | return converter is null 48 | ? this.CustomConverter.Deserialize(value!) // forgive 49 | : converter.Deserialize(value); 50 | } 51 | #endregion 52 | 53 | 54 | #region Cache 55 | /// 56 | /// Provides primitive value converter cache mecanism. 57 | /// 58 | private static class PrimitiveConverterCache 59 | { 60 | /// 61 | /// Hold type and converter mapping table. 62 | /// 63 | public static IReadOnlyDictionary Map { get; } = new Dictionary 64 | { 65 | [typeof(bool)] = new BooleanConverter(), 66 | [typeof(bool?)] = new NullableBooleanConverter(), 67 | [typeof(char)] = new CharConverter(), 68 | [typeof(char?)] = new NullableCharConverter(), 69 | [typeof(sbyte)] = new SByteConverter(), 70 | [typeof(sbyte?)] = new NullableSByteConverter(), 71 | [typeof(byte)] = new ByteConverter(), 72 | [typeof(byte?)] = new NullableByteConverter(), 73 | [typeof(short)] = new Int16Converter(), 74 | [typeof(short?)] = new NullableInt16Converter(), 75 | [typeof(ushort)] = new UInt16Converter(), 76 | [typeof(ushort?)] = new NullableUInt16Converter(), 77 | [typeof(int)] = new Int32Converter(), 78 | [typeof(int?)] = new NullableInt32Converter(), 79 | [typeof(uint)] = new UInt32Converter(), 80 | [typeof(uint?)] = new NullableUInt32Converter(), 81 | [typeof(long)] = new Int64Converter(), 82 | [typeof(long?)] = new NullableInt64Converter(), 83 | [typeof(ulong)] = new UInt64Converter(), 84 | [typeof(ulong?)] = new NullableUInt64Converter(), 85 | [typeof(float)] = new SingleConverter(), 86 | [typeof(float?)] = new NullableSingleConverter(), 87 | [typeof(double)] = new DoubleConverter(), 88 | [typeof(double?)] = new NullableDoubleConverter(), 89 | [typeof(string)] = new StringConverter(), 90 | [typeof(byte[])] = new ByteArrayConverter(), 91 | [typeof(Memory)] = new MemoryByteConverter(), 92 | [typeof(ReadOnlyMemory)] = new ReadOnlyMemoryByteConverter(), 93 | }; 94 | } 95 | 96 | 97 | /// 98 | /// Provides cache mecanism. 99 | /// 100 | /// Data type 101 | private static class PrimitiveConverterCache 102 | { 103 | /// 104 | /// Gets converter. 105 | /// 106 | public static IRedisValueConverter? Converter { get; } 107 | 108 | 109 | /// 110 | /// 111 | /// 112 | static PrimitiveConverterCache() 113 | { 114 | Converter 115 | = PrimitiveConverterCache.Map.TryGetValue(typeof(T), out var converter) 116 | ? (IRedisValueConverter)converter 117 | : null; 118 | } 119 | } 120 | #endregion 121 | } 122 | -------------------------------------------------------------------------------- /src/CloudStructures/IConnectionEventHandler.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | using StackExchange.Redis.Maintenance; 3 | 4 | namespace CloudStructures; 5 | 6 | 7 | 8 | /// 9 | /// Provides connection event handling function. 10 | /// 11 | public interface IConnectionEventHandler 12 | { 13 | /// 14 | /// Raised when configuration changes are detected 15 | /// 16 | /// 17 | /// 18 | void OnConfigurationChanged(RedisConnection sender, EndPointEventArgs e); 19 | 20 | 21 | /// 22 | /// Raised when nodes are explicitly requested to reconfigure via broadcast; 23 | /// this usually means master/slave changes 24 | /// 25 | /// 26 | /// 27 | void OnConfigurationChangedBroadcast(RedisConnection sender, EndPointEventArgs e); 28 | 29 | 30 | /// 31 | /// Raised whenever a physical connection fails 32 | /// 33 | /// 34 | /// 35 | void OnConnectionFailed(RedisConnection sender, ConnectionFailedEventArgs e); 36 | 37 | 38 | /// 39 | /// Raised whenever a physical connection is opened 40 | /// 41 | /// 42 | /// 43 | void OnConnectionOpened(RedisConnection sender, ConnectionOpenedEventArgs e); 44 | 45 | 46 | /// 47 | /// Raised whenever a physical connection is established 48 | /// 49 | /// 50 | /// 51 | void OnConnectionRestored(RedisConnection sender, ConnectionFailedEventArgs e); 52 | 53 | 54 | /// 55 | /// A server replied with an error message; 56 | /// 57 | /// 58 | /// 59 | void OnErrorMessage(RedisConnection sender, RedisErrorEventArgs e); 60 | 61 | 62 | /// 63 | /// Raised when a hash-slot has been relocated 64 | /// 65 | /// 66 | /// 67 | void OnHashSlotMoved(RedisConnection sender, HashSlotMovedEventArgs e); 68 | 69 | 70 | /// 71 | /// Raised whenever an internal error occurs (this is primarily for debugging) 72 | /// 73 | /// 74 | /// 75 | void OnInternalError(RedisConnection sender, InternalErrorEventArgs e); 76 | 77 | 78 | /// 79 | /// Raised when server indicates a maintenance event is going to happen. 80 | /// 81 | /// 82 | /// 83 | void OnServerMaintenanceEvent(RedisConnection sender, ServerMaintenanceEvent e); 84 | } 85 | -------------------------------------------------------------------------------- /src/CloudStructures/Internals/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace CloudStructures.Internals; 6 | 7 | 8 | 9 | /// 10 | /// Provides extension methods for . 11 | /// 12 | internal static class EnumerableExtensions 13 | { 14 | /// 15 | /// Returns if collection is empty. 16 | /// 17 | /// Element type 18 | /// 19 | /// 20 | public static bool IsEmpty(this IEnumerable source) 21 | => !source.Any(); 22 | 23 | 24 | /// 25 | /// Projects each element of a sequance into new form with state. 26 | /// 27 | /// Element type 28 | /// State type 29 | /// Result type 30 | /// 31 | /// 32 | /// 33 | /// 34 | public static IEnumerable Select(this IEnumerable source, TState state, Func selector) 35 | { 36 | foreach (var x in source) 37 | yield return selector(x, state); 38 | } 39 | 40 | 41 | /// 42 | /// If state of source is lazy, returns materialized collection. if materialized already, it does nothing and returns itself. 43 | /// 44 | /// Element type 45 | /// 46 | /// If true, returns empty sequence when source collection is null. If false, throws 47 | /// 48 | public static IEnumerable Materialize(this IEnumerable? source, bool nullToEmpty = true) 49 | { 50 | if (source is null) 51 | { 52 | if (nullToEmpty) 53 | return []; 54 | 55 | #if NET6_0_OR_GREATER 56 | ArgumentNullException.ThrowIfNull(source); 57 | #else 58 | throw new ArgumentNullException(nameof(source)); 59 | #endif 60 | } 61 | if (source is ICollection) return source; 62 | if (source is IReadOnlyCollection) return source; 63 | return source.ToArray(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CloudStructures/Internals/RedisOperationHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CloudStructures.Structures; 4 | using StackExchange.Redis; 5 | 6 | namespace CloudStructures.Internals; 7 | 8 | 9 | 10 | /// 11 | /// Provides helper methods for Redis operation. 12 | /// 13 | internal static class RedisOperationHelpers 14 | { 15 | /// 16 | /// Execute specified command with expiration time. 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | public static async Task ExecuteWithExpiryAsync(this TRedis structure, Func command, TState state, TimeSpan? expiry, CommandFlags flags) 27 | where TRedis : IRedisStructure 28 | { 29 | if (expiry.HasValue) 30 | { 31 | //--- Execute multiple commands in tracsaction 32 | var t = structure.Connection.Transaction; 33 | _ = command(t, state); // forget 34 | _ = t.KeyExpireAsync(structure.Key, expiry.Value, flags); // forget 35 | 36 | //--- commit 37 | await t.ExecuteAsync(flags).ConfigureAwait(false); 38 | } 39 | else 40 | { 41 | var database = structure.Connection.Database; 42 | await command(database, state).ConfigureAwait(false); 43 | } 44 | } 45 | 46 | 47 | /// 48 | /// Execute specified command with expiration time. 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | public static async Task ExecuteWithExpiryAsync(this TRedis structure, Func> command, TState state, TimeSpan? expiry, CommandFlags flags) 60 | where TRedis : IRedisStructure 61 | { 62 | if (expiry.HasValue) 63 | { 64 | //--- Execute multiple commands in tracsaction 65 | var t = structure.Connection.Transaction; 66 | var result = command(t, state); 67 | _ = t.KeyExpireAsync(structure.Key, expiry.Value, flags); // forget 68 | 69 | //--- commit 70 | await t.ExecuteAsync(flags).ConfigureAwait(false); 71 | 72 | //--- gets result value 73 | return await result.ConfigureAwait(false); 74 | } 75 | else 76 | { 77 | var database = structure.Connection.Database; 78 | return await command(database, state).ConfigureAwait(false); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/CloudStructures/RedisConfig.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace CloudStructures; 4 | 5 | 6 | 7 | /// 8 | /// Represents connection configuration. 9 | /// 10 | public sealed class RedisConfig 11 | { 12 | #region Properties 13 | /// 14 | /// Gets name. 15 | /// 16 | public string Name { get; } 17 | 18 | 19 | /// 20 | /// Gets configuration options. 21 | /// 22 | /// 23 | /// How to write configuration: 24 | /// https://stackexchange.github.io/StackExchange.Redis/Configuration.html 25 | /// 26 | public ConfigurationOptions Options { get; } 27 | 28 | 29 | /// 30 | /// Gets logical database index. 31 | /// 32 | public int? Database { get; } 33 | #endregion 34 | 35 | 36 | #region Constructors 37 | /// 38 | /// Creates instance. 39 | /// 40 | /// 41 | /// 42 | /// 43 | public RedisConfig(string name, string connectionString, int? database = default) 44 | : this(name, ConfigurationOptions.Parse(connectionString), database) 45 | { } 46 | 47 | 48 | /// 49 | /// Creates instance. 50 | /// 51 | /// 52 | /// 53 | /// 54 | public RedisConfig(string name, ConfigurationOptions options, int? database = default) 55 | { 56 | this.Name = name; 57 | this.Options = options; 58 | this.Database = database ?? options.DefaultDatabase; 59 | } 60 | #endregion 61 | } 62 | -------------------------------------------------------------------------------- /src/CloudStructures/RedisConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using CloudStructures.Converters; 7 | using CloudStructures.Internals; 8 | using StackExchange.Redis; 9 | using StackExchange.Redis.Maintenance; 10 | 11 | namespace CloudStructures; 12 | 13 | 14 | 15 | /// 16 | /// Provides connection to the server. 17 | /// 18 | /// This connection needs to be used w/o destroying. Please hold as static field or static property. 19 | public sealed class RedisConnection( 20 | RedisConfig config, 21 | IValueConverter? converter = null, 22 | IConnectionEventHandler? handler = null, 23 | TextWriter? logger = null) : IDisposable 24 | { 25 | #region Properties 26 | /// 27 | /// Gets configuration. 28 | /// 29 | public RedisConfig Config { get; } = config; 30 | 31 | 32 | /// 33 | /// Gets value converter. 34 | /// 35 | internal ValueConverter Converter { get; } = new(converter); 36 | 37 | 38 | /// 39 | /// Gets connection event handler. 40 | /// 41 | private IConnectionEventHandler? Handler { get; } = handler; 42 | 43 | 44 | /// 45 | /// Gets logger. 46 | /// 47 | private TextWriter? Logger { get; } = logger; 48 | 49 | 50 | /// 51 | /// Gets an interactive connection to a database inside redis. 52 | /// 53 | /// 54 | /// This object has already been disposed. 55 | /// 56 | internal IDatabaseAsync Database 57 | { 58 | get 59 | { 60 | this.CheckDisposed(); 61 | 62 | return this.Config.Database.HasValue 63 | ? this.GetConnection().GetDatabase(this.Config.Database.Value) 64 | : this.GetConnection().GetDatabase(); 65 | } 66 | } 67 | 68 | 69 | /// 70 | /// Gets a transaction. 71 | /// 72 | /// 73 | /// This object has already been disposed. 74 | /// 75 | internal ITransaction Transaction 76 | { 77 | get 78 | { 79 | this.CheckDisposed(); 80 | 81 | return ((IDatabase)this.Database).CreateTransaction(); 82 | } 83 | } 84 | 85 | 86 | /// 87 | /// Gets target servers. 88 | /// 89 | /// 90 | /// This object has already been disposed. 91 | /// 92 | internal IServer[] Servers 93 | { 94 | get 95 | { 96 | this.CheckDisposed(); 97 | 98 | return this.Config.Options 99 | .EndPoints 100 | .Select(this.GetConnection(), static (x, c) => c.GetServer(x)) 101 | .ToArray(); 102 | } 103 | } 104 | #endregion 105 | 106 | 107 | #region Connection management 108 | /// 109 | /// Gets underlying connection. 110 | /// 111 | /// 112 | /// 113 | /// This object has already been disposed. 114 | /// 115 | public ConnectionMultiplexer GetConnection() 116 | { 117 | this.CheckDisposed(); 118 | 119 | lock (this._gate) 120 | { 121 | if (this._connection is not null) 122 | return this._connection; 123 | 124 | ConnectionMultiplexer? connection = null; 125 | try 126 | { 127 | //--- create inner connection 128 | var stopwatch = Stopwatch.StartNew(); 129 | connection = ConnectionMultiplexer.Connect(this.Config.Options, this.Logger); 130 | stopwatch.Stop(); 131 | 132 | if (this.Handler is not null) 133 | { 134 | this.Handler.OnConnectionOpened(this, new(stopwatch.Elapsed)); 135 | 136 | //--- attach events 137 | connection.ConfigurationChanged += this.OnConfigurationChanged; 138 | connection.ConfigurationChangedBroadcast += this.OnConfigurationChangedBroadcast; 139 | connection.ConnectionFailed += this.OnConnectionFailed; 140 | connection.ConnectionRestored += this.OnConnectionRestored; 141 | connection.ErrorMessage += this.OnErrorMessage; 142 | connection.HashSlotMoved += this.OnHashSlotMoved; 143 | connection.InternalError += this.OnInternalError; 144 | connection.ServerMaintenanceEvent += this.OnServerMaintenanceEvent; 145 | } 146 | } 147 | catch 148 | { 149 | connection?.Dispose(); 150 | throw; 151 | } 152 | 153 | this._connection = connection; 154 | return this._connection; 155 | } 156 | } 157 | 158 | 159 | /// 160 | /// The internal connection is destroyed without destroying this object. 161 | /// 162 | /// 163 | /// This object has already been disposed. 164 | /// 165 | /// 166 | /// The internal connection will be recreated the next time the method is called. 167 | /// 168 | [EditorBrowsable(EditorBrowsableState.Advanced)] 169 | public void ReleaseConnection() 170 | { 171 | this.CheckDisposed(); 172 | 173 | lock (this._gate) 174 | { 175 | var connection = this._connection; 176 | if (connection is null) 177 | return; 178 | 179 | connection.ConfigurationChanged -= this.OnConfigurationChanged; 180 | connection.ConfigurationChangedBroadcast -= this.OnConfigurationChangedBroadcast; 181 | connection.ConnectionFailed -= this.OnConnectionFailed; 182 | connection.ConnectionRestored -= this.OnConnectionRestored; 183 | connection.ErrorMessage -= this.OnErrorMessage; 184 | connection.HashSlotMoved -= this.OnHashSlotMoved; 185 | connection.InternalError -= this.OnInternalError; 186 | connection.ServerMaintenanceEvent -= this.OnServerMaintenanceEvent; 187 | 188 | connection.Dispose(); 189 | this._connection = null; 190 | } 191 | } 192 | 193 | 194 | #if NET9_0_OR_GREATER 195 | private readonly System.Threading.Lock _gate = new(); 196 | #else 197 | private readonly object _gate = new(); 198 | #endif 199 | private ConnectionMultiplexer? _connection; 200 | #endregion 201 | 202 | 203 | #region IDisposable 204 | /// 205 | void IDisposable.Dispose() 206 | { 207 | this.ReleaseConnection(); 208 | this._disposed = true; 209 | } 210 | 211 | 212 | private void CheckDisposed() 213 | { 214 | #if NET7_0_OR_GREATER 215 | ObjectDisposedException.ThrowIf(this._disposed, this); 216 | #else 217 | if (this._disposed) 218 | { 219 | throw new ObjectDisposedException(this.GetType().FullName); 220 | } 221 | #endif 222 | } 223 | 224 | 225 | private bool _disposed; 226 | #endregion 227 | 228 | 229 | #region Event handlers 230 | private void OnConfigurationChanged(object? sender, EndPointEventArgs e) 231 | => this.Handler?.OnConfigurationChanged(this, e); 232 | 233 | 234 | private void OnConfigurationChangedBroadcast(object? sender, EndPointEventArgs e) 235 | => this.Handler?.OnConfigurationChangedBroadcast(this, e); 236 | 237 | 238 | private void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) 239 | => this.Handler?.OnConnectionFailed(this, e); 240 | 241 | 242 | private void OnConnectionRestored(object? sender, ConnectionFailedEventArgs e) 243 | => this.Handler?.OnConnectionRestored(this, e); 244 | 245 | 246 | private void OnErrorMessage(object? sender, RedisErrorEventArgs e) 247 | => this.Handler?.OnErrorMessage(this, e); 248 | 249 | 250 | private void OnHashSlotMoved(object? sender, HashSlotMovedEventArgs e) 251 | => this.Handler?.OnHashSlotMoved(this, e); 252 | 253 | 254 | private void OnInternalError(object? sender, InternalErrorEventArgs e) 255 | => this.Handler?.OnInternalError(this, e); 256 | 257 | 258 | private void OnServerMaintenanceEvent(object? sender, ServerMaintenanceEvent e) 259 | => this.Handler?.OnServerMaintenanceEvent(this, e); 260 | #endregion 261 | } 262 | -------------------------------------------------------------------------------- /src/CloudStructures/RedisResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CloudStructures.Converters; 3 | using StackExchange.Redis; 4 | 5 | namespace CloudStructures; 6 | 7 | 8 | 9 | /// 10 | /// Represents generics version of . 11 | /// 12 | /// Data type 13 | public readonly struct RedisResult 14 | { 15 | #region Properties 16 | /// 17 | /// Gets default value. 18 | /// 19 | public static RedisResult Default { get; } = default; 20 | 21 | 22 | /// 23 | /// Gets If value exists. 24 | /// 25 | public bool HasValue { get; } 26 | 27 | 28 | /// 29 | /// Gets value. 30 | /// 31 | public T Value 32 | => this.HasValue 33 | ? this.value 34 | : throw new InvalidOperationException("has no value."); 35 | private readonly T value; 36 | #endregion 37 | 38 | 39 | #region Constructors 40 | /// 41 | /// Creates instance. 42 | /// 43 | /// 44 | internal RedisResult(T value) 45 | { 46 | this.HasValue = true; 47 | this.value = value; 48 | } 49 | #endregion 50 | 51 | 52 | #region override 53 | /// 54 | /// Converts to string. 55 | /// 56 | /// 57 | public override string? ToString() 58 | => this.HasValue ? this.Value?.ToString() : null; 59 | #endregion 60 | 61 | 62 | #region Gets 63 | /// 64 | /// Gets value. Returns null if value doesn't exists. 65 | /// 66 | /// 67 | public object? GetValueOrNull() 68 | => this.HasValue ? this.Value : null; 69 | 70 | 71 | /// 72 | /// Gets value. Returns default value if value doesn't exists. 73 | /// 74 | /// 75 | /// 76 | public T? GetValueOrDefault(T? @default = default) 77 | => this.HasValue ? this.Value : @default; 78 | 79 | 80 | /// 81 | /// Gets value. Returns value which returned from delegate if value doesn't exists. 82 | /// 83 | /// 84 | /// 85 | public T? GetValueOrDefault(Func valueFactory) 86 | => this.HasValue ? this.Value : valueFactory(); 87 | #endregion 88 | } 89 | 90 | 91 | 92 | /// 93 | /// Represents generics version of with expiration time. 94 | /// 95 | /// Data type 96 | public readonly struct RedisResultWithExpiry 97 | { 98 | #region Properties 99 | /// 100 | /// Gets default value. 101 | /// 102 | public static RedisResultWithExpiry Default { get; } = default; 103 | 104 | 105 | /// 106 | /// Gets If value exists. 107 | /// 108 | public bool HasValue { get; } 109 | 110 | 111 | /// 112 | /// Gets value. 113 | /// 114 | public T Value 115 | => this.HasValue 116 | ? this.value 117 | : throw new InvalidOperationException("has no value."); 118 | private readonly T value; 119 | 120 | 121 | /// 122 | /// Gets expiration time. 123 | /// 124 | public TimeSpan? Expiry { get; } 125 | #endregion 126 | 127 | 128 | #region Constructors 129 | /// 130 | /// Creates instance. 131 | /// 132 | /// 133 | /// 134 | internal RedisResultWithExpiry(T value, TimeSpan? expiry) 135 | { 136 | this.HasValue = true; 137 | this.value = value; 138 | this.Expiry = expiry; 139 | } 140 | #endregion 141 | 142 | 143 | #region override 144 | /// 145 | /// Converts to string. 146 | /// 147 | /// 148 | public override string? ToString() 149 | => this.HasValue ? this.Value?.ToString() : null; 150 | #endregion 151 | 152 | 153 | #region Gets 154 | /// 155 | /// Gets value. Returns null if value doesn't exists. 156 | /// 157 | /// 158 | public object? GetValueOrNull() 159 | => this.HasValue ? this.Value : null; 160 | 161 | 162 | /// 163 | /// Gets value. Returns default value if value doesn't exists. 164 | /// 165 | /// 166 | /// 167 | public T? GetValueOrDefault(T? @default = default) 168 | => this.HasValue ? this.Value : @default; 169 | 170 | 171 | /// 172 | /// Gets value. Returns value which returned from delegate if value doesn't exists. 173 | /// 174 | /// 175 | /// 176 | public T? GetValueOrDefault(Func valueFactory) 177 | => this.HasValue ? this.Value : valueFactory(); 178 | #endregion 179 | } 180 | 181 | 182 | 183 | /// 184 | /// Provides extension methods for and . 185 | /// 186 | internal static class RedisResultExtensions 187 | { 188 | /// 189 | /// Converts to . 190 | /// 191 | /// Data type 192 | /// 193 | /// 194 | /// 195 | public static RedisResult ToResult(this in RedisValue value, ValueConverter converter) 196 | { 197 | if (value.IsNull) 198 | return RedisResult.Default; 199 | 200 | var converted = converter.Deserialize(value); 201 | return new(converted); 202 | } 203 | 204 | 205 | /// 206 | /// Converts to . 207 | /// 208 | /// Data type 209 | /// 210 | /// 211 | /// 212 | public static RedisResultWithExpiry ToResult(this in RedisValueWithExpiry value, ValueConverter converter) 213 | { 214 | if (value.Value.IsNull) 215 | return RedisResultWithExpiry.Default; 216 | 217 | var converted = converter.Deserialize(value.Value); 218 | return new(converted, value.Expiry); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/IRedisStructure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using StackExchange.Redis; 5 | 6 | namespace CloudStructures.Structures; 7 | 8 | 9 | 10 | /// 11 | /// Represents a base interface for Redis data structure. 12 | /// 13 | public interface IRedisStructure 14 | { 15 | #region Properties 16 | /// 17 | /// Gets connection. 18 | /// 19 | RedisConnection Connection { get; } 20 | 21 | 22 | /// 23 | /// Gets key. 24 | /// 25 | RedisKey Key { get; } 26 | #endregion 27 | } 28 | 29 | 30 | 31 | /// 32 | /// Represents a interface for Redis data structure with default expiration time. 33 | /// 34 | public interface IRedisStructureWithExpiry : IRedisStructure 35 | { 36 | #region Properties 37 | /// 38 | /// Gets default expiration time. 39 | /// 40 | TimeSpan? DefaultExpiry { get; } 41 | #endregion 42 | } 43 | 44 | 45 | 46 | /// 47 | /// Provides extension methods for and . 48 | /// 49 | public static class RedisStructureExtensions 50 | { 51 | #region Commands 52 | //- [] DebugObjectAsync 53 | //- [] ExecuteAsync 54 | //- [] IdentifyEndpointAsync 55 | //- [x] IsConnected 56 | //- [x] KeyDeleteAsync 57 | //- [x] KeyDumpAsync 58 | //- [x] KeyExistsAsync 59 | //- [x] KeyExpireAsync 60 | //- [x] KeyMigrateAsync 61 | //- [x] KeyMoveAsync 62 | //- [x] KeyPersistAsync 63 | //- [x] KeyRenameAsync 64 | //- [] KeyRestoreAsync 65 | //- [x] KeyTimeToLiveAsync 66 | //- [x] KeyTypeAsync 67 | //- [] PublishAsync 68 | 69 | 70 | /// 71 | /// Indicates whether the instance can communicate with the server (resolved using the supplied key and optional flags). 72 | /// 73 | public static bool IsConnected(this T redis, CommandFlags flags = CommandFlags.None) 74 | where T : IRedisStructure 75 | => redis.Connection.Database.IsConnected(redis.Key, flags); 76 | 77 | 78 | /// 79 | /// DEL : 80 | /// 81 | public static Task DeleteAsync(this T redis, CommandFlags flags = CommandFlags.None) 82 | where T : IRedisStructure 83 | => redis.Connection.Database.KeyDeleteAsync(redis.Key, flags); 84 | 85 | 86 | /// 87 | /// DUMP : 88 | /// 89 | public static Task DumpAsync(this T redis, CommandFlags flags = CommandFlags.None) 90 | where T : IRedisStructure 91 | => redis.Connection.Database.KeyDumpAsync(redis.Key, flags); 92 | 93 | 94 | /// 95 | /// EXISTS : 96 | /// 97 | public static Task ExistsAsync(this T redis, CommandFlags flags = CommandFlags.None) 98 | where T : IRedisStructure 99 | => redis.Connection.Database.KeyExistsAsync(redis.Key, flags); 100 | 101 | 102 | /// 103 | /// SETEX :
104 | /// PSETEX : 105 | ///
106 | public static Task ExpireAsync(this T redis, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) 107 | where T : IRedisStructure 108 | => redis.Connection.Database.KeyExpireAsync(redis.Key, expiry, flags); 109 | 110 | 111 | /// 112 | /// MOVE : 113 | /// 114 | public static Task MoveAsync(this T redis, int database, CommandFlags flags = CommandFlags.None) 115 | where T : IRedisStructure 116 | => redis.Connection.Database.KeyMoveAsync(redis.Key, database, flags); 117 | 118 | 119 | /// 120 | /// MIGRATE : 121 | /// 122 | public static Task MigrateAsync(this T redis, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) 123 | where T : IRedisStructure 124 | => redis.Connection.Database.KeyMigrateAsync(redis.Key, toServer, toDatabase, timeoutMilliseconds, migrateOptions, flags); 125 | 126 | 127 | /// 128 | /// PERSIST : 129 | /// 130 | public static Task PersistAsync(this T redis, CommandFlags flags = CommandFlags.None) 131 | where T : IRedisStructure 132 | => redis.Connection.Database.KeyPersistAsync(redis.Key, flags); 133 | 134 | 135 | /// 136 | /// RENAME : 137 | /// 138 | public static Task RenameAsync(this T redis, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) 139 | where T : IRedisStructure 140 | => redis.Connection.Database.KeyRenameAsync(redis.Key, newKey, when, flags); 141 | 142 | 143 | /// 144 | /// TTL : 145 | /// 146 | public static Task TimeToLiveAsync(this T redis, CommandFlags flags = CommandFlags.None) 147 | where T : IRedisStructure 148 | => redis.Connection.Database.KeyTimeToLiveAsync(redis.Key, flags); 149 | 150 | 151 | /// 152 | /// TYPE : 153 | /// 154 | public static Task TypeAsync(this T redis, CommandFlags flags = CommandFlags.None) 155 | where T : IRedisStructure 156 | => redis.Connection.Database.KeyTypeAsync(redis.Key, flags); 157 | #endregion 158 | } 159 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisBit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Internals; 6 | using StackExchange.Redis; 7 | 8 | namespace CloudStructures.Structures; 9 | 10 | 11 | 12 | /// 13 | /// Provides bit related commands. 14 | /// 15 | public readonly struct RedisBit(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 16 | { 17 | #region IRedisStructureWithExpiry implementations 18 | /// 19 | /// Gets connection. 20 | /// 21 | public RedisConnection Connection { get; } = connection; 22 | 23 | 24 | /// 25 | /// Gets key. 26 | /// 27 | public RedisKey Key { get; } = key; 28 | 29 | 30 | /// 31 | /// Gets default expiration time. 32 | /// 33 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 34 | #endregion 35 | 36 | 37 | #region Commands 38 | //- [x] StringBitCountAsync 39 | //- [x] StringBitOperationAsync 40 | //- [x] StringBitPositionAsync 41 | //- [x] StringGetBitAsync 42 | //- [x] StringSetBitAsync 43 | 44 | 45 | /// 46 | /// BITCOUNT : 47 | /// 48 | public Task CountAsync(long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) 49 | => this.Connection.Database.StringBitCountAsync(this.Key, start, end, flags); 50 | 51 | 52 | /// 53 | /// BITOP : 54 | /// 55 | public Task OperationAsync(Bitwise operation, RedisBit first, RedisBit? second = null, CommandFlags flags = CommandFlags.None) 56 | { 57 | var firstKey = first.Key; 58 | var secondKey = second?.Key ?? default; 59 | return this.Connection.Database.StringBitOperationAsync(operation, this.Key, firstKey, secondKey, flags); 60 | } 61 | 62 | 63 | /// 64 | /// BITOP : 65 | /// 66 | public Task OperationAsync(Bitwise operation, IReadOnlyCollection bits, CommandFlags flags = CommandFlags.None) 67 | { 68 | if (bits.Count == 0) 69 | throw new ArgumentException("bits length is 0."); 70 | 71 | var keys = bits.Select(static x => x.Key).ToArray(); 72 | return this.Connection.Database.StringBitOperationAsync(operation, this.Key, keys, flags); 73 | } 74 | 75 | 76 | /// 77 | /// BITPOSITION : 78 | /// 79 | public Task PositionAsync(bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) 80 | => this.Connection.Database.StringBitPositionAsync(this.Key, bit, start, end, flags); 81 | 82 | 83 | /// 84 | /// GETBIT : 85 | /// 86 | public Task GetAsync(long offset, CommandFlags flags = CommandFlags.None) 87 | => this.Connection.Database.StringGetBitAsync(this.Key, offset, flags); 88 | 89 | 90 | /// 91 | /// SETBIT : 92 | /// 93 | public Task SetAsync(long offset, bool bit, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 94 | { 95 | expiry ??= this.DefaultExpiry; 96 | return this.ExecuteWithExpiryAsync 97 | ( 98 | static (db, state) => db.StringSetBitAsync(state.key, state.offset, state.bit, state.flags), 99 | state: (key: this.Key, offset, bit, flags), 100 | expiry, 101 | flags 102 | ); 103 | } 104 | #endregion 105 | } 106 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisDictionary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Internals; 6 | using StackExchange.Redis; 7 | 8 | namespace CloudStructures.Structures; 9 | 10 | 11 | 12 | /// 13 | /// Provides dictionary related commands. 14 | /// 15 | /// Key type 16 | /// Value type 17 | public readonly struct RedisDictionary(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 18 | where TKey : notnull 19 | { 20 | #region IRedisStructureWithExpiry implementations 21 | /// 22 | /// Gets connection. 23 | /// 24 | public RedisConnection Connection { get; } = connection; 25 | 26 | 27 | /// 28 | /// Gets key. 29 | /// 30 | public RedisKey Key { get; } = key; 31 | 32 | 33 | /// 34 | /// Gets default expiration time. 35 | /// 36 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 37 | #endregion 38 | 39 | 40 | #region Commands 41 | //- [x] HashDecrementAsync 42 | //- [x] HashDeleteAsync 43 | //- [x] HashExistsAsync 44 | //- [x] HashGetAllAsync 45 | //- [x] HashGetAsync 46 | //- [x] HashIncrementAsync 47 | //- [x] HashKeysAsync 48 | //- [x] HashLengthAsync 49 | //- [x] HashSetAsync 50 | //- [x] HashValuesAsync 51 | 52 | 53 | /// 54 | /// HINCRBY : 55 | /// 56 | public Task DecrementAsync(TKey field, long value = 1, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 57 | { 58 | expiry ??= this.DefaultExpiry; 59 | var hashField = this.Connection.Converter.Serialize(field); 60 | return this.ExecuteWithExpiryAsync 61 | ( 62 | static (db, state) => db.HashDecrementAsync(state.key, state.hashField, state.value, state.flags), 63 | state: (key: this.Key, hashField, value, flags), 64 | expiry, 65 | flags 66 | ); 67 | } 68 | 69 | 70 | /// 71 | /// HINCRBYFLOAT : 72 | /// 73 | public Task DecrementAsync(TKey field, double value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 74 | { 75 | expiry ??= this.DefaultExpiry; 76 | var hashField = this.Connection.Converter.Serialize(field); 77 | return this.ExecuteWithExpiryAsync 78 | ( 79 | static (db, state) => db.HashDecrementAsync(state.key, state.hashField, state.value, state.flags), 80 | state: (key: this.Key, hashField, value, flags), 81 | expiry, 82 | flags 83 | ); 84 | } 85 | 86 | 87 | /// 88 | /// HDEL : 89 | /// 90 | public Task DeleteAsync(TKey field, CommandFlags flags = CommandFlags.None) 91 | { 92 | var hashField = this.Connection.Converter.Serialize(field); 93 | return this.Connection.Database.HashDeleteAsync(this.Key, hashField, flags); 94 | } 95 | 96 | 97 | /// 98 | /// HDEL : https://redis.io/commands/hdel 99 | /// 100 | public Task DeleteAsync(IEnumerable fields, CommandFlags flags = CommandFlags.None) 101 | { 102 | var hashFields = fields.Select(this.Connection.Converter.Serialize).ToArray(); 103 | return this.Connection.Database.HashDeleteAsync(this.Key, hashFields, flags); 104 | } 105 | 106 | 107 | /// 108 | /// HEXISTS : https://redis.io/commands/hexists 109 | /// 110 | public Task ExistsAsync(TKey field, CommandFlags flags = CommandFlags.None) 111 | { 112 | var hashField = this.Connection.Converter.Serialize(field); 113 | return this.Connection.Database.HashExistsAsync(this.Key, hashField, flags); 114 | } 115 | 116 | 117 | /// 118 | /// HGETALL : https://redis.io/commands/hgetall 119 | /// 120 | public async Task> GetAllAsync(IEqualityComparer? dictionaryEqualityComparer = null, CommandFlags flags = CommandFlags.None) 121 | { 122 | var comparer = dictionaryEqualityComparer ?? EqualityComparer.Default; 123 | var entries = await this.Connection.Database.HashGetAllAsync(this.Key, flags).ConfigureAwait(false); 124 | return entries 125 | .Select(this.Connection.Converter, static (x, c) => 126 | { 127 | var field = c.Deserialize(x.Name); 128 | var value = c.Deserialize(x.Value); 129 | return (field, value); 130 | }) 131 | .ToDictionary(static x => x.field, static x => x.value, comparer); 132 | } 133 | 134 | 135 | /// 136 | /// HGET : https://redis.io/commands/hget 137 | /// 138 | public async Task> GetAsync(TKey field, CommandFlags flags = CommandFlags.None) 139 | { 140 | var hashField = this.Connection.Converter.Serialize(field); 141 | var value = await this.Connection.Database.HashGetAsync(this.Key, hashField, flags).ConfigureAwait(false); 142 | return value.ToResult(this.Connection.Converter); 143 | } 144 | 145 | 146 | /// 147 | /// HMGET : https://redis.io/commands/hmget 148 | /// 149 | public async Task> GetAsync(IEnumerable fields, IEqualityComparer? dictionaryEqualityComparer = null, CommandFlags flags = CommandFlags.None) 150 | { 151 | fields = fields.Materialize(false); 152 | var comparer = dictionaryEqualityComparer ?? EqualityComparer.Default; 153 | var hashFields = fields.Select(this.Connection.Converter.Serialize).ToArray(); 154 | var values = await this.Connection.Database.HashGetAsync(this.Key, hashFields, flags).ConfigureAwait(false); 155 | return fields 156 | .Zip(values, static (f, v) => (field: f, value: v)) 157 | .Select(this.Connection.Converter, static (x, c) => 158 | { 159 | var result = x.value.ToResult(c); 160 | return (x.field, result); 161 | }) 162 | .Where(static x => x.result.HasValue) 163 | .ToDictionary(static x => x.field, static x => x.result.Value, comparer); 164 | } 165 | 166 | 167 | /// 168 | /// HINCRBY : https://redis.io/commands/hincrby 169 | /// 170 | public Task IncrementAsync(TKey field, long value = 1, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 171 | { 172 | expiry ??= this.DefaultExpiry; 173 | var hashField = this.Connection.Converter.Serialize(field); 174 | return this.ExecuteWithExpiryAsync 175 | ( 176 | static (db, state) => db.HashIncrementAsync(state.key, state.hashField, state.value, state.flags), 177 | state: (key: this.Key, hashField, value, flags), 178 | expiry, 179 | flags 180 | ); 181 | } 182 | 183 | 184 | /// 185 | /// HINCRBYFLOAT : https://redis.io/commands/hincrbyfloat 186 | /// 187 | public Task IncrementAsync(TKey field, double value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 188 | { 189 | expiry ??= this.DefaultExpiry; 190 | var hashField = this.Connection.Converter.Serialize(field); 191 | return this.ExecuteWithExpiryAsync 192 | ( 193 | static (db, state) => db.HashIncrementAsync(state.key, state.hashField, state.value, state.flags), 194 | state: (key: this.Key, hashField, value, flags), 195 | expiry, 196 | flags 197 | ); 198 | } 199 | 200 | 201 | /// 202 | /// HKEYS : https://redis.io/commands/hkeys 203 | /// 204 | public async Task KeysAsync(CommandFlags flags = CommandFlags.None) 205 | { 206 | var keys = await this.Connection.Database.HashKeysAsync(this.Key, flags).ConfigureAwait(false); 207 | return keys.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 208 | } 209 | 210 | 211 | /// 212 | /// HLEN : https://redis.io/commands/hlen 213 | /// 214 | public Task LengthAsync(CommandFlags flags = CommandFlags.None) 215 | => this.Connection.Database.HashLengthAsync(this.Key, flags); 216 | 217 | 218 | /// 219 | /// HSET : https://redis.io/commands/hset 220 | /// 221 | public Task SetAsync(TKey field, TValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 222 | { 223 | expiry ??= this.DefaultExpiry; 224 | var f = this.Connection.Converter.Serialize(field); 225 | var v = this.Connection.Converter.Serialize(value); 226 | return this.ExecuteWithExpiryAsync 227 | ( 228 | static (db, state) => db.HashSetAsync(state.key, state.f, state.v, state.when, state.flags), 229 | state: (key: this.Key, f, v, when, flags), 230 | expiry, 231 | flags 232 | ); 233 | } 234 | 235 | 236 | /// 237 | /// HMSET : https://redis.io/commands/hmset 238 | /// 239 | public Task SetAsync(IEnumerable> entries, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 240 | { 241 | expiry ??= this.DefaultExpiry; 242 | var hashEntries 243 | = entries 244 | .Select(this.Connection.Converter, static (x, c) => 245 | { 246 | var field = c.Serialize(x.Key); 247 | var value = c.Serialize(x.Value); 248 | return new HashEntry(field, value); 249 | }) 250 | .ToArray(); 251 | 252 | if (hashEntries.Length == 0) 253 | return Task.CompletedTask; 254 | 255 | return this.ExecuteWithExpiryAsync 256 | ( 257 | static (db, state) => db.HashSetAsync(state.key, state.hashEntries, state.flags), 258 | state: (key: this.Key, hashEntries, flags), 259 | expiry, 260 | flags 261 | ); 262 | } 263 | 264 | 265 | /// 266 | /// HVALS : https://redis.io/commands/hvals 267 | /// 268 | public async Task ValuesAsync(CommandFlags flags = CommandFlags.None) 269 | { 270 | var values = await this.Connection.Database.HashValuesAsync(this.Key, flags).ConfigureAwait(false); 271 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 272 | } 273 | #endregion 274 | 275 | 276 | #region Custom Commands 277 | /// 278 | /// HGET : https://redis.io/commands/hget 279 | /// HSET : https://redis.io/commands/hset 280 | /// 281 | public async Task GetOrSetAsync(TKey field, Func valueFactory, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 282 | { 283 | var result = await this.GetAsync(field, flags).ConfigureAwait(false); 284 | if (result.HasValue) 285 | { 286 | return result.Value; 287 | } 288 | else 289 | { 290 | var newValue = valueFactory(field); 291 | await this.SetAsync(field, newValue, expiry, When.Always, flags).ConfigureAwait(false); 292 | return newValue; 293 | } 294 | } 295 | 296 | 297 | /// 298 | /// HGET : https://redis.io/commands/hget 299 | /// HSET : https://redis.io/commands/hset 300 | /// 301 | public async Task GetOrSetAsync(TKey field, Func> valueFactory, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 302 | { 303 | var result = await this.GetAsync(field, flags).ConfigureAwait(false); 304 | if (result.HasValue) 305 | { 306 | return result.Value; 307 | } 308 | else 309 | { 310 | var newValue = await valueFactory(field).ConfigureAwait(false); 311 | await this.SetAsync(field, newValue, expiry, When.Always, flags).ConfigureAwait(false); 312 | return newValue; 313 | } 314 | } 315 | 316 | 317 | /// 318 | /// HMGET : https://redis.io/commands/hmget 319 | /// HMSET : https://redis.io/commands/hmset 320 | /// 321 | public async Task> GetOrSetAsync(IEnumerable fields, Func, IEnumerable>> valueFactory, TimeSpan? expiry = null, IEqualityComparer? dictionaryEqualityComparer = null, CommandFlags flags = CommandFlags.None) 322 | { 323 | var comparer = dictionaryEqualityComparer ?? EqualityComparer.Default; 324 | fields = fields.Materialize(false); 325 | if (fields.IsEmpty()) 326 | return new Dictionary(comparer); 327 | 328 | //--- get 329 | var hashFields = fields.Select(this.Connection.Converter.Serialize).ToArray(); 330 | var values = await this.Connection.Database.HashGetAsync(this.Key, hashFields, flags).ConfigureAwait(false); 331 | 332 | //--- divides cached / non cached 333 | var cached = new Dictionary(comparer); 334 | var notCached = new LinkedList(); 335 | foreach (var x in fields.Zip(values, static (f, v) => (f, v))) 336 | { 337 | var result = x.v.ToResult(this.Connection.Converter); 338 | if (result.HasValue) 339 | cached[x.f] = result.Value; 340 | else 341 | notCached.AddLast(x.f); 342 | } 343 | 344 | //--- load if non cached key exists 345 | if (notCached.Count > 0) 346 | { 347 | var loaded = valueFactory(notCached).Materialize(); 348 | await this.SetAsync(loaded, expiry, flags).ConfigureAwait(false); 349 | foreach (var x in loaded) 350 | cached[x.Key] = x.Value; 351 | } 352 | return cached; 353 | } 354 | 355 | 356 | /// 357 | /// HMGET : https://redis.io/commands/hmget 358 | /// HMSET : https://redis.io/commands/hmset 359 | /// 360 | public async Task> GetOrSetAsync(IEnumerable fields, Func, Task>>> valueFactory, TimeSpan? expiry = null, IEqualityComparer? dictionaryEqualityComparer = null, CommandFlags flags = CommandFlags.None) 361 | { 362 | var comparer = dictionaryEqualityComparer ?? EqualityComparer.Default; 363 | fields = fields.Materialize(false); 364 | if (fields.IsEmpty()) 365 | return new Dictionary(comparer); 366 | 367 | //--- get 368 | var hashFields = fields.Select(this.Connection.Converter.Serialize).ToArray(); 369 | var values = await this.Connection.Database.HashGetAsync(this.Key, hashFields, flags).ConfigureAwait(false); 370 | 371 | //--- divides cached / non cached 372 | var cached = new Dictionary(comparer); 373 | var notCached = new LinkedList(); 374 | foreach (var x in fields.Zip(values, static (f, v) => (f, v))) 375 | { 376 | var result = x.v.ToResult(this.Connection.Converter); 377 | if (result.HasValue) 378 | cached[x.f] = result.Value; 379 | else 380 | notCached.AddLast(x.f); 381 | } 382 | 383 | //--- load if non cached key exists 384 | if (notCached.Count > 0) 385 | { 386 | var loaded = (await valueFactory(notCached).ConfigureAwait(false)).Materialize(); 387 | await this.SetAsync(loaded, expiry, flags).ConfigureAwait(false); 388 | foreach (var x in loaded) 389 | cached[x.Key] = x.Value; 390 | } 391 | return cached; 392 | } 393 | 394 | 395 | /// 396 | /// HGET : https://redis.io/commands/hget 397 | /// HDEL : https://redis.io/commands/hdel 398 | /// 399 | public async Task> GetAndDeleteAsync(TKey field, CommandFlags flags = CommandFlags.None) 400 | { 401 | //--- GetAsync 402 | var hashField = this.Connection.Converter.Serialize(field); 403 | var value = await this.Connection.Database.HashGetAsync(this.Key, hashField, flags).ConfigureAwait(false); 404 | var result = value.ToResult(this.Connection.Converter); 405 | 406 | //--- DeleteAsync 407 | if (result.HasValue) 408 | await this.Connection.Database.HashDeleteAsync(this.Key, hashField, flags).ConfigureAwait(false); 409 | 410 | return result; 411 | } 412 | 413 | 414 | /// 415 | /// HMGET : https://redis.io/commands/hmget 416 | /// HDEL : https://redis.io/commands/hdel 417 | /// 418 | public async Task> GetAndDeleteAsync(IEnumerable fields, IEqualityComparer? dictionaryEqualityComparer = null, CommandFlags flags = CommandFlags.None) 419 | { 420 | //--- GetAsync 421 | fields = fields.Materialize(false); 422 | var comparer = dictionaryEqualityComparer ?? EqualityComparer.Default; 423 | var hashFields = fields.Select(this.Connection.Converter.Serialize).ToArray(); 424 | var values = await this.Connection.Database.HashGetAsync(this.Key, hashFields, flags).ConfigureAwait(false); 425 | var result 426 | = fields 427 | .Zip(values, static (f, v) => (field: f, value: v)) 428 | .Select(this.Connection.Converter, static (x, c) => 429 | { 430 | var result = x.value.ToResult(c); 431 | return (x.field, result); 432 | }) 433 | .Where(static x => x.result.HasValue) 434 | .ToDictionary(static x => x.field, static x => x.result.Value, comparer); 435 | 436 | //--- DeleteAsync 437 | if (0 < result.Count) 438 | await this.Connection.Database.HashDeleteAsync(this.Key, hashFields, flags).ConfigureAwait(false); 439 | 440 | return result; 441 | } 442 | #endregion 443 | } 444 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisGeo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Converters; 6 | using CloudStructures.Internals; 7 | using StackExchange.Redis; 8 | 9 | namespace CloudStructures.Structures; 10 | 11 | 12 | 13 | /// 14 | /// Provides geometry related commands. 15 | /// 16 | /// Data type 17 | public readonly struct RedisGeo(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 18 | { 19 | #region IRedisStructureWithExpiry implementations 20 | /// 21 | /// Gets connection. 22 | /// 23 | public RedisConnection Connection { get; } = connection; 24 | 25 | 26 | /// 27 | /// Gets key. 28 | /// 29 | public RedisKey Key { get; } = key; 30 | 31 | 32 | /// 33 | /// Gets default expiration time. 34 | /// 35 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 36 | #endregion 37 | 38 | 39 | #region Commands 40 | //- [x] GeoAddAsync 41 | //- [x] GeoDistanceAsync 42 | //- [x] GeoHashAsync 43 | //- [x] GeoPositionAsync 44 | //- [x] GeoRadiusAsync 45 | //- [x] GeoRemoveAsync 46 | 47 | 48 | /// 49 | /// GEOADD : 50 | /// 51 | public Task AddAsync(RedisGeoEntry value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 52 | { 53 | expiry ??= this.DefaultExpiry; 54 | var entry = value.ToNonGenerics(this.Connection.Converter); 55 | return this.ExecuteWithExpiryAsync 56 | ( 57 | static (db, state) => db.GeoAddAsync(state.key, state.entry, state.flags), 58 | state: (key: this.Key, entry, flags), 59 | expiry, 60 | flags 61 | ); 62 | } 63 | 64 | 65 | /// 66 | /// GEOADD : 67 | /// 68 | public Task AddAsync(IEnumerable> values, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 69 | { 70 | expiry ??= this.DefaultExpiry; 71 | var entries = values.Select(this.Connection.Converter, static (x, c) => x.ToNonGenerics(c)).ToArray(); 72 | return this.ExecuteWithExpiryAsync 73 | ( 74 | static (db, state) => db.GeoAddAsync(state.key, state.entries, state.flags), 75 | state: (key: this.Key, entries, flags), 76 | expiry, 77 | flags 78 | ); 79 | } 80 | 81 | 82 | /// 83 | /// GEOADD : 84 | /// 85 | public Task AddAsync(double longitude, double latitude, T member, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 86 | { 87 | expiry ??= this.DefaultExpiry; 88 | var entry = new RedisGeoEntry(longitude, latitude, member); 89 | return this.AddAsync(entry, expiry, flags); 90 | } 91 | 92 | 93 | /// 94 | /// GEODIST : 95 | /// 96 | public Task DistanceAsync(T member1, T member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) 97 | { 98 | var value1 = this.Connection.Converter.Serialize(member1); 99 | var value2 = this.Connection.Converter.Serialize(member2); 100 | return this.Connection.Database.GeoDistanceAsync(this.Key, value1, value2, unit, flags); 101 | } 102 | 103 | 104 | /// 105 | /// GEOHASH : 106 | /// 107 | public Task HashAsync(T member, CommandFlags flags = CommandFlags.None) 108 | { 109 | var value = this.Connection.Converter.Serialize(member); 110 | return this.Connection.Database.GeoHashAsync(this.Key, value, flags); 111 | } 112 | 113 | 114 | /// 115 | /// GEOHASH : 116 | /// 117 | public Task HashAsync(IEnumerable members, CommandFlags flags = CommandFlags.None) 118 | { 119 | var values = members.Select(this.Connection.Converter.Serialize).ToArray(); 120 | return this.Connection.Database.GeoHashAsync(this.Key, values, flags); 121 | } 122 | 123 | 124 | /// 125 | /// GEOPOS : 126 | /// 127 | public Task PositionAsync(T member, CommandFlags flags = CommandFlags.None) 128 | { 129 | var value = this.Connection.Converter.Serialize(member); 130 | return this.Connection.Database.GeoPositionAsync(this.Key, value, flags); 131 | } 132 | 133 | 134 | /// 135 | /// GEOPOS : 136 | /// 137 | public Task PositionAsync(IEnumerable members, CommandFlags flags = CommandFlags.None) 138 | { 139 | var values = members.Select(this.Connection.Converter.Serialize).ToArray(); 140 | return this.Connection.Database.GeoPositionAsync(this.Key, values, flags); 141 | } 142 | 143 | 144 | /// 145 | /// GEORADIUS : 146 | /// 147 | public async Task[]> RadiusAsync(double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) 148 | { 149 | var results = await this.Connection.Database.GeoRadiusAsync(this.Key, longitude, latitude, radius, unit, count, order, options, flags).ConfigureAwait(false); 150 | return results.Select(this.Connection.Converter, static (x, c) => x.ToGenerics(c)).ToArray(); 151 | } 152 | 153 | 154 | /// 155 | /// GEORADIUSBYMEMBER : 156 | /// 157 | public async Task[]> RadiusAsync(T member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) 158 | { 159 | var value = this.Connection.Converter.Serialize(member); 160 | var results = await this.Connection.Database.GeoRadiusAsync(this.Key, value, radius, unit, count, order, options, flags).ConfigureAwait(false); 161 | return results.Select(this.Connection.Converter, static (x, c) => x.ToGenerics(c)).ToArray(); 162 | } 163 | 164 | 165 | /// 166 | /// ZREM : 167 | /// 168 | /// There is no GEODEL command. 169 | public Task RemoveAsync(T member, CommandFlags flags = CommandFlags.None) 170 | { 171 | var value = this.Connection.Converter.Serialize(member); 172 | return this.Connection.Database.GeoRemoveAsync(this.Key, value, flags); 173 | } 174 | #endregion 175 | } 176 | 177 | 178 | 179 | /// 180 | /// Represents element. 181 | /// 182 | /// Data type 183 | public readonly struct RedisGeoEntry(double longitude, double latitude, T member) 184 | { 185 | #region Properties 186 | /// 187 | /// Gets longitude. 188 | /// 189 | public double Longitude { get; } = longitude; 190 | 191 | 192 | /// 193 | /// Gets latitude. 194 | /// 195 | public double Latitude { get; } = latitude; 196 | 197 | 198 | /// 199 | /// Gets member. 200 | /// 201 | public T Member { get; } = member; 202 | #endregion 203 | } 204 | 205 | 206 | 207 | /// 208 | /// Provides extension methods for . 209 | /// 210 | internal static class RedisGeoEntryExtensions 211 | { 212 | /// 213 | /// Converts to . 214 | /// 215 | /// Data type 216 | /// 217 | /// 218 | /// 219 | public static GeoEntry ToNonGenerics(this in RedisGeoEntry entry, ValueConverter converter) 220 | { 221 | var member = converter.Serialize(entry.Member); 222 | return new GeoEntry(entry.Longitude, entry.Latitude, member); 223 | } 224 | } 225 | 226 | 227 | 228 | /// 229 | /// Represents .RadiusAsync result. 230 | /// 231 | /// Data type 232 | public readonly struct RedisGeoRadiusResult(in RedisResult member, double? distance, long? hash, GeoPosition? position) 233 | { 234 | #region Properties 235 | /// 236 | /// Gets member. 237 | /// 238 | public RedisResult Member { get; } = member; 239 | 240 | 241 | /// 242 | /// Gets distance. 243 | /// 244 | public double? Distance { get; } = distance; 245 | 246 | 247 | /// 248 | /// Gets hash. 249 | /// 250 | public long? Hash { get; } = hash; 251 | 252 | 253 | /// 254 | /// Gets position. 255 | /// 256 | public GeoPosition? Position { get; } = position; 257 | #endregion 258 | } 259 | 260 | 261 | 262 | /// 263 | /// Provides extension methods for . 264 | /// 265 | internal static class RedisGeoRadiusResultExtensions 266 | { 267 | /// 268 | /// Converts to . 269 | /// 270 | /// Data type 271 | /// 272 | /// 273 | /// 274 | public static RedisGeoRadiusResult ToGenerics(this in GeoRadiusResult result, ValueConverter converter) 275 | { 276 | var member = result.Member.ToResult(converter); 277 | return new RedisGeoRadiusResult(member, result.Distance, result.Hash, result.Position); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisHashSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Internals; 6 | using StackExchange.Redis; 7 | 8 | namespace CloudStructures.Structures; 9 | 10 | 11 | 12 | /// 13 | /// Provides hash set related commands. 14 | /// Like RedisDictionary<TKey, bool>. 15 | /// 16 | /// Data type 17 | public readonly struct RedisHashSet(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 18 | where T : notnull 19 | { 20 | #region IRedisStructureWithExpiry implementations 21 | /// 22 | /// Gets connection. 23 | /// 24 | public RedisConnection Connection { get; } = connection; 25 | 26 | 27 | /// 28 | /// Gets key. 29 | /// 30 | public RedisKey Key { get; } = key; 31 | 32 | 33 | /// 34 | /// Gets default expiration time. 35 | /// 36 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 37 | #endregion 38 | 39 | 40 | #region Commands 41 | /// 42 | /// Deletes specified element. 43 | /// 44 | public Task DeleteAsync(T value, CommandFlags flags = CommandFlags.None) 45 | { 46 | // HDEL 47 | // https://redis.io/commands/hdel 48 | 49 | var hashField = this.Connection.Converter.Serialize(value); 50 | return this.Connection.Database.HashDeleteAsync(this.Key, hashField, flags); 51 | } 52 | 53 | 54 | /// 55 | /// Deletes specified elements. 56 | /// 57 | public Task DeleteAsync(IEnumerable values, CommandFlags flags = CommandFlags.None) 58 | { 59 | // HDEL 60 | // https://redis.io/commands/hdel 61 | 62 | var hashFields = values.Select(this.Connection.Converter.Serialize).ToArray(); 63 | return this.Connection.Database.HashDeleteAsync(this.Key, hashFields, flags); 64 | } 65 | 66 | 67 | /// 68 | /// Checks specified element existence. 69 | /// 70 | public async Task ContainsAsync(T value, CommandFlags flags = CommandFlags.None) 71 | { 72 | // HGET 73 | // https://redis.io/commands/hget 74 | 75 | var hashField = this.Connection.Converter.Serialize(value); 76 | var element = await this.Connection.Database.HashGetAsync(this.Key, hashField, flags).ConfigureAwait(false); 77 | return !element.IsNull; 78 | } 79 | 80 | 81 | /// 82 | /// Checks specified elements existence. 83 | /// 84 | public async Task> ContainsAsync(IEnumerable values, CommandFlags flags = CommandFlags.None) 85 | { 86 | // HMGET 87 | // https://redis.io/commands/hmget 88 | 89 | values = values.Materialize(false); 90 | var hashFields = values.Select(this.Connection.Converter.Serialize).ToArray(); 91 | var elements = await this.Connection.Database.HashGetAsync(this.Key, hashFields, flags).ConfigureAwait(false); 92 | return values 93 | .Zip(elements, static (k, v) => (key: k, value: v)) 94 | .ToDictionary(static x => x.key, static x => !x.value.IsNull); 95 | } 96 | 97 | 98 | /// 99 | /// Gets all elements. 100 | /// 101 | public async Task ValuesAsync(CommandFlags flags = CommandFlags.None) 102 | { 103 | // HKEYS で OK 104 | // https://redis.io/commands/hkeys 105 | 106 | var elements = await this.Connection.Database.HashKeysAsync(this.Key, flags).ConfigureAwait(false); 107 | return elements.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 108 | } 109 | 110 | 111 | /// 112 | /// Gets length. 113 | /// 114 | public Task LengthAsync(CommandFlags flags = CommandFlags.None) 115 | => this.Connection.Database.HashLengthAsync(this.Key, flags); // HLEN https://redis.io/commands/hlen 116 | 117 | 118 | /// 119 | /// Adds value. 120 | /// 121 | public Task AddAsync(T value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 122 | { 123 | // HSET 124 | // https://redis.io/commands/hset 125 | 126 | expiry ??= this.DefaultExpiry; 127 | var f = this.Connection.Converter.Serialize(value); 128 | var v = this.Connection.Converter.Serialize(true); 129 | return this.ExecuteWithExpiryAsync 130 | ( 131 | static (db, state) => db.HashSetAsync(state.key, state.f, state.v, state.when, state.flags), 132 | state: (key: this.Key, f, v, when, flags), 133 | expiry, 134 | flags 135 | ); 136 | } 137 | 138 | 139 | /// 140 | /// Adds values. 141 | /// 142 | public Task AddAsync(IEnumerable values, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 143 | { 144 | // HMSET 145 | // https://redis.io/commands/hmset 146 | 147 | expiry ??= this.DefaultExpiry; 148 | var hashEntries 149 | = values 150 | .Select(this.Connection.Converter, static (x, c) => 151 | { 152 | var f = c.Serialize(x); 153 | var v = c.Serialize(true); 154 | return new HashEntry(f, v); 155 | }) 156 | .ToArray(); 157 | return this.ExecuteWithExpiryAsync 158 | ( 159 | static (db, state) => db.HashSetAsync(state.key, state.hashEntries, state.flags), 160 | state: (key: this.Key, hashEntries, flags), 161 | expiry, 162 | flags 163 | ); 164 | } 165 | #endregion 166 | } 167 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisHyperLogLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Internals; 6 | using StackExchange.Redis; 7 | 8 | namespace CloudStructures.Structures; 9 | 10 | 11 | 12 | /// 13 | /// Provides HyperLogLog related commands. 14 | /// 15 | /// Data type 16 | public readonly struct RedisHyperLogLog(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 17 | { 18 | #region IRedisStructureWithExpiry implementations 19 | /// 20 | /// Gets connection. 21 | /// 22 | public RedisConnection Connection { get; } = connection; 23 | 24 | 25 | /// 26 | /// Gets key. 27 | /// 28 | public RedisKey Key { get; } = key; 29 | 30 | 31 | /// 32 | /// Gets default expiration time. 33 | /// 34 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 35 | #endregion 36 | 37 | 38 | #region Commands 39 | //- [x] HyperLogLogAddAsync 40 | //- [x] HyperLogLogLengthAsync 41 | //- [x] HyperLogLogMergeAsync 42 | 43 | 44 | /// 45 | /// PFADD : 46 | /// 47 | public Task AddAsync(T value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 48 | { 49 | expiry ??= this.DefaultExpiry; 50 | var serialized = this.Connection.Converter.Serialize(value); 51 | return this.ExecuteWithExpiryAsync 52 | ( 53 | static (db, state) => db.HyperLogLogAddAsync(state.key, state.serialized, state.flags), 54 | state: (key: this.Key, serialized, flags), 55 | expiry, 56 | flags 57 | ); 58 | } 59 | 60 | 61 | /// 62 | /// PFADD : 63 | /// 64 | public Task AddAsync(IEnumerable values, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 65 | { 66 | expiry ??= this.DefaultExpiry; 67 | var serialized = values.Select(this.Connection.Converter.Serialize).ToArray(); 68 | return this.ExecuteWithExpiryAsync 69 | ( 70 | static (db, state) => db.HyperLogLogAddAsync(state.key, state.serialized, state.flags), 71 | state: (key: this.Key, serialized, flags), 72 | expiry, 73 | flags 74 | ); 75 | } 76 | 77 | 78 | /// 79 | /// PFCOUNT : 80 | /// 81 | public Task LengthAsync(CommandFlags flags = CommandFlags.None) 82 | => this.Connection.Database.HyperLogLogLengthAsync(this.Key, flags); 83 | 84 | 85 | /// 86 | /// PFMERGE : 87 | /// 88 | public Task MergeAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) 89 | => this.Connection.Database.HyperLogLogMergeAsync(this.Key, first, second, flags); 90 | 91 | 92 | /// 93 | /// PFMERGE : 94 | /// 95 | public Task MergeAsync(RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) 96 | => this.Connection.Database.HyperLogLogMergeAsync(this.Key, sourceKeys, flags); 97 | #endregion 98 | } 99 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Internals; 6 | using StackExchange.Redis; 7 | 8 | namespace CloudStructures.Structures; 9 | 10 | 11 | 12 | /// 13 | /// Provides list related commands. 14 | /// 15 | /// Data type 16 | public readonly struct RedisList(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 17 | { 18 | #region IRedisStructureWithExpiry implementations 19 | /// 20 | /// Gets connection. 21 | /// 22 | public RedisConnection Connection { get; } = connection; 23 | 24 | 25 | /// 26 | /// Gets key. 27 | /// 28 | public RedisKey Key { get; } = key; 29 | 30 | 31 | /// 32 | /// Gets default expiration time. 33 | /// 34 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 35 | #endregion 36 | 37 | 38 | #region Commands 39 | //- [x] ListGetByIndexAsync 40 | //- [x] ListInsertAfterAsync 41 | //- [x] ListInsertBeforeAsync 42 | //- [x] ListLeftPopAsync 43 | //- [x] ListLeftPushAsync 44 | //- [x] ListLengthAsync 45 | //- [x] ListRangeAsync 46 | //- [x] ListRemoveAsync 47 | //- [x] ListRightPopAsync 48 | //- [x] ListRightPopLeftPushAsync 49 | //- [x] ListRightPushAsync 50 | //- [x] ListSetByIndexAsync 51 | //- [x] ListTrimAsync 52 | //- [x] SortAndStoreAsync 53 | //- [x] SortAsync 54 | 55 | 56 | /// 57 | /// LINDEX : 58 | /// 59 | public async Task> GetByIndexAsync(long index, CommandFlags flags = CommandFlags.None) 60 | { 61 | var value = await this.Connection.Database.ListGetByIndexAsync(this.Key, index, flags).ConfigureAwait(false); 62 | return value.ToResult(this.Connection.Converter); 63 | } 64 | 65 | 66 | /// 67 | /// LINSERT : 68 | /// 69 | public Task InsertAfterAsync(T pivot, T value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 70 | { 71 | expiry ??= this.DefaultExpiry; 72 | var p = this.Connection.Converter.Serialize(pivot); 73 | var v = this.Connection.Converter.Serialize(value); 74 | return this.ExecuteWithExpiryAsync 75 | ( 76 | static (db, state) => db.ListInsertAfterAsync(state.key, state.p, state.v, state.flags), 77 | state: (key: this.Key, p, v, flags), 78 | expiry, 79 | flags 80 | ); 81 | } 82 | 83 | 84 | /// 85 | /// LINSERT : 86 | /// 87 | public Task InsertBeforeAsync(T pivot, T value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 88 | { 89 | expiry ??= this.DefaultExpiry; 90 | var p = this.Connection.Converter.Serialize(pivot); 91 | var v = this.Connection.Converter.Serialize(value); 92 | return this.ExecuteWithExpiryAsync 93 | ( 94 | static (db, state) => db.ListInsertBeforeAsync(state.key, state.p, state.v, state.flags), 95 | state: (key: this.Key, p, v, flags), 96 | expiry, 97 | flags 98 | ); 99 | } 100 | 101 | 102 | /// 103 | /// LPOP : 104 | /// 105 | public async Task> LeftPopAsync(CommandFlags flags = CommandFlags.None) 106 | { 107 | var value = await this.Connection.Database.ListLeftPopAsync(this.Key, flags).ConfigureAwait(false); 108 | return value.ToResult(this.Connection.Converter); 109 | } 110 | 111 | 112 | /// 113 | /// LPOP : 114 | /// 115 | public async Task LeftPopAsync(long count, CommandFlags flags = CommandFlags.None) 116 | { 117 | var values = await this.Connection.Database.ListLeftPopAsync(this.Key, count, flags).ConfigureAwait(false); 118 | return values?.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 119 | } 120 | 121 | 122 | /// 123 | /// LPUSH : 124 | /// 125 | public Task LeftPushAsync(T value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 126 | { 127 | expiry ??= this.DefaultExpiry; 128 | var serialized = this.Connection.Converter.Serialize(value); 129 | return this.ExecuteWithExpiryAsync 130 | ( 131 | static (db, state) => db.ListLeftPushAsync(state.key, state.serialized, state.when, state.flags), 132 | state: (key: this.Key, serialized, when, flags), 133 | expiry, 134 | flags 135 | ); 136 | } 137 | 138 | 139 | /// 140 | /// LPUSH : 141 | /// 142 | public Task LeftPushAsync(IEnumerable values, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 143 | { 144 | expiry ??= this.DefaultExpiry; 145 | var serialized = values.Select(this.Connection.Converter.Serialize).ToArray(); 146 | return this.ExecuteWithExpiryAsync 147 | ( 148 | static (db, state) => db.ListLeftPushAsync(state.key, state.serialized, state.flags), 149 | state: (key: this.Key, serialized, flags), 150 | expiry, 151 | flags 152 | ); 153 | } 154 | 155 | 156 | /// 157 | /// LLEN : 158 | /// 159 | public Task LengthAsync(CommandFlags flags = CommandFlags.None) 160 | => this.Connection.Database.ListLengthAsync(this.Key, flags); 161 | 162 | 163 | /// 164 | /// LRANGE : 165 | /// 166 | public async Task RangeAsync(long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) 167 | { 168 | var values = await this.Connection.Database.ListRangeAsync(this.Key, start, stop, flags).ConfigureAwait(false); 169 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 170 | } 171 | 172 | 173 | /// 174 | /// LREM : 175 | /// 176 | /// Value to be deleted 177 | /// Number of items to be deleted 178 | /// 179 | /// - count > 0 : Delete while searching from the beginning to the end.
180 | /// - count < 0 : Delete while searching from the end to the beginning.
181 | /// - count = 0 : Delete all matches. 182 | ///
183 | /// 184 | /// 185 | public Task RemoveAsync(T value, long count = 0, CommandFlags flags = CommandFlags.None) 186 | { 187 | var serialized = this.Connection.Converter.Serialize(value); 188 | return this.Connection.Database.ListRemoveAsync(this.Key, serialized, count, flags); 189 | } 190 | 191 | 192 | /// 193 | /// RPOP : 194 | /// 195 | public async Task> RightPopAsync(CommandFlags flags = CommandFlags.None) 196 | { 197 | var value = await this.Connection.Database.ListRightPopAsync(this.Key, flags).ConfigureAwait(false); 198 | return value.ToResult(this.Connection.Converter); 199 | } 200 | 201 | 202 | /// 203 | /// RPOP : 204 | /// 205 | public async Task RightPopAsync(long count, CommandFlags flags = CommandFlags.None) 206 | { 207 | var values = await this.Connection.Database.ListRightPopAsync(this.Key, count, flags).ConfigureAwait(false); 208 | return values?.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 209 | } 210 | 211 | 212 | /// 213 | /// RPOPLPUSH : 214 | /// 215 | public async Task> RightPopLeftPushAsync(RedisList destination, CommandFlags flags = CommandFlags.None) 216 | { 217 | var value = await this.Connection.Database.ListRightPopLeftPushAsync(this.Key, destination.Key, flags).ConfigureAwait(false); 218 | return value.ToResult(this.Connection.Converter); 219 | } 220 | 221 | 222 | /// 223 | /// RPUSH : 224 | /// 225 | public Task RightPushAsync(T value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 226 | { 227 | expiry ??= this.DefaultExpiry; 228 | var serialized = this.Connection.Converter.Serialize(value); 229 | return this.ExecuteWithExpiryAsync 230 | ( 231 | static (db, state) => db.ListRightPushAsync(state.key, state.serialized, state.when, state.flags), 232 | state: (key: this.Key, serialized, when, flags), 233 | expiry, 234 | flags 235 | ); 236 | } 237 | 238 | 239 | /// 240 | /// RPUSH : 241 | /// 242 | public Task RightPushAsync(IEnumerable values, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 243 | { 244 | expiry ??= this.DefaultExpiry; 245 | var serialized = values.Select(this.Connection.Converter.Serialize).ToArray(); 246 | return this.ExecuteWithExpiryAsync 247 | ( 248 | static (db, state) => db.ListRightPushAsync(state.key, state.serialized, state.flags), 249 | state: (key: this.Key, serialized, flags), 250 | expiry, 251 | flags 252 | ); 253 | } 254 | 255 | 256 | /// 257 | /// LSET : 258 | /// 259 | public Task SetByIndexAsync(long index, T value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 260 | { 261 | expiry ??= this.DefaultExpiry; 262 | var serialized = this.Connection.Converter.Serialize(value); 263 | return this.ExecuteWithExpiryAsync 264 | ( 265 | static (db, state) => db.ListSetByIndexAsync(state.key, state.index, state.serialized, state.flags), 266 | state: (key: this.Key, index, serialized, flags), 267 | expiry, 268 | flags 269 | ); 270 | } 271 | 272 | 273 | /// 274 | /// LTRIM : 275 | /// 276 | public Task TrimAsync(long start, long stop, CommandFlags flags = CommandFlags.None) 277 | => this.Connection.Database.ListTrimAsync(this.Key, start, stop, flags); 278 | 279 | 280 | /// 281 | /// SORT : 282 | /// 283 | public Task SortAndStoreAsync(RedisList destination, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, /*RedisValue by = default, RedisValue[] get = null,*/ CommandFlags flags = CommandFlags.None) 284 | { 285 | //--- I don't know if serialization is necessary or not, so I will fix the default value. 286 | RedisValue by = default; 287 | RedisValue[]? get = default; 288 | return this.Connection.Database.SortAndStoreAsync(destination.Key, this.Key, skip, take, order, sortType, by, get, flags); 289 | } 290 | 291 | 292 | /// 293 | /// SORT : 294 | /// 295 | public async Task SortAsync(long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, /*RedisValue by = default, RedisValue[] get = null,*/ CommandFlags flags = CommandFlags.None) 296 | { 297 | //--- I don't know if serialization is necessary or not, so I will fix the default value. 298 | RedisValue by = default; 299 | RedisValue[]? get = default; 300 | var values = await this.Connection.Database.SortAsync(this.Key, skip, take, order, sortType, by, get, flags).ConfigureAwait(false); 301 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 302 | } 303 | #endregion 304 | 305 | 306 | #region Custom Commands 307 | /// 308 | /// First LPUSH, then LTRIM to the specified list length. 309 | /// 310 | public async Task FixedLengthLeftPushAsync(T value, long length, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 311 | { 312 | expiry ??= this.DefaultExpiry; 313 | var serialized = this.Connection.Converter.Serialize(value); 314 | 315 | //--- execute multiple commands in transaction 316 | var t = this.Connection.Transaction; 317 | var leftPush = t.ListLeftPushAsync(this.Key, serialized, when, flags); 318 | _ = t.ListTrimAsync(this.Key, 0, length - 1, flags); // forget 319 | if (expiry.HasValue) 320 | _ = t.KeyExpireAsync(this.Key, expiry.Value, flags); // forget 321 | 322 | //--- commit 323 | await t.ExecuteAsync(flags).ConfigureAwait(false); 324 | 325 | //--- get result 326 | var pushLength = await leftPush.ConfigureAwait(false); 327 | return Math.Min(pushLength, length); 328 | } 329 | 330 | 331 | /// 332 | /// First LPUSH, then LTRIM to the specified list length. 333 | /// 334 | public async Task FixedLengthLeftPushAsync(IEnumerable values, long length, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 335 | { 336 | expiry ??= this.DefaultExpiry; 337 | var serialized = values.Select(this.Connection.Converter.Serialize).ToArray(); 338 | 339 | //--- execute multiple commands in transaction 340 | var t = this.Connection.Transaction; 341 | var leftPush = t.ListLeftPushAsync(this.Key, serialized, flags); 342 | _ = t.ListTrimAsync(this.Key, 0, length - 1, flags); // forget 343 | if (expiry.HasValue) 344 | _ = t.KeyExpireAsync(this.Key, expiry.Value, flags); // forget 345 | 346 | //--- commit 347 | await t.ExecuteAsync(flags).ConfigureAwait(false); 348 | 349 | //--- get result 350 | var pushLength = await leftPush.ConfigureAwait(false); 351 | return Math.Min(pushLength, length); 352 | } 353 | #endregion 354 | } 355 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using StackExchange.Redis; 4 | 5 | namespace CloudStructures.Structures; 6 | 7 | 8 | 9 | /// 10 | /// Provides lock related commands. 11 | /// 12 | /// Data type 13 | public readonly struct RedisLock(RedisConnection connection, RedisKey key) : IRedisStructure 14 | { 15 | #region IRedisStructure implementations 16 | /// 17 | /// Gets connection. 18 | /// 19 | public RedisConnection Connection { get; } = connection; 20 | 21 | 22 | /// 23 | /// Gets key. 24 | /// 25 | public RedisKey Key { get; } = key; 26 | #endregion 27 | 28 | 29 | #region Commands 30 | //- [x] LockExtendAsync 31 | //- [x] LockQueryAsync 32 | //- [x] LockReleaseAsync 33 | //- [x] LockTakeAsync 34 | 35 | 36 | /// 37 | /// Extends a lock, if the token value is correct. 38 | /// 39 | public Task ExtendAsync(T value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) 40 | { 41 | var serialized = this.Connection.Converter.Serialize(value); 42 | return this.Connection.Database.LockExtendAsync(this.Key, serialized, expiry, flags); 43 | } 44 | 45 | 46 | /// 47 | /// Queries the token held against a lock. 48 | /// 49 | public async Task> QueryAsync(CommandFlags flags = CommandFlags.None) 50 | { 51 | var value = await this.Connection.Database.LockQueryAsync(this.Key, flags).ConfigureAwait(false); 52 | return value.ToResult(this.Connection.Converter); 53 | } 54 | 55 | 56 | /// 57 | /// Releases a lock, if the token value is correct. 58 | /// 59 | public Task ReleaseAsync(T value, CommandFlags flags = CommandFlags.None) 60 | { 61 | var serialized = this.Connection.Converter.Serialize(value); 62 | return this.Connection.Database.LockReleaseAsync(this.Key, serialized, flags); 63 | } 64 | 65 | 66 | /// 67 | /// Takes a lock (specifying a token value) if it is not already taken. 68 | /// 69 | public Task TakeAsync(T value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) 70 | { 71 | var serialized = this.Connection.Converter.Serialize(value); 72 | return this.Connection.Database.LockTakeAsync(this.Key, serialized, expiry, flags); 73 | } 74 | #endregion 75 | } 76 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisLua.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using StackExchange.Redis; 3 | 4 | namespace CloudStructures.Structures; 5 | 6 | 7 | 8 | /// 9 | /// Provides Lua scripting related commands. 10 | /// 11 | public readonly struct RedisLua(RedisConnection connection, RedisKey key) : IRedisStructure 12 | { 13 | #region IRedisStructure implementations 14 | /// 15 | /// Gets connection. 16 | /// 17 | public RedisConnection Connection { get; } = connection; 18 | 19 | 20 | /// 21 | /// Gets key. 22 | /// 23 | public RedisKey Key { get; } = key; 24 | #endregion 25 | 26 | 27 | #region Commands 28 | // - [x] ScriptEvaluateAsync 29 | 30 | 31 | /// 32 | /// EVALSHA : 33 | /// 34 | public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) 35 | => this.Connection.Database.ScriptEvaluateAsync(script, keys, values, flags); 36 | 37 | 38 | /// 39 | /// EVALSHA : 40 | /// 41 | public async Task> ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) 42 | { 43 | var result = await this.Connection.Database.ScriptEvaluateAsync(script, keys, values, flags).ConfigureAwait(false); 44 | if (result.IsNull) 45 | { 46 | return RedisResult.Default; 47 | } 48 | else 49 | { 50 | var v = (RedisValue)result; 51 | return v.ToResult(this.Connection.Converter); 52 | } 53 | } 54 | #endregion 55 | } 56 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Internals; 6 | using StackExchange.Redis; 7 | 8 | namespace CloudStructures.Structures; 9 | 10 | 11 | 12 | /// 13 | /// Provides set related commands. 14 | /// 15 | /// Data type 16 | public readonly struct RedisSet(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 17 | { 18 | #region IRedisStructureWithExpiry implementations 19 | /// 20 | /// Gets connection. 21 | /// 22 | public RedisConnection Connection { get; } = connection; 23 | 24 | 25 | /// 26 | /// Gets key. 27 | /// 28 | public RedisKey Key { get; } = key; 29 | 30 | 31 | /// 32 | /// Gets default expiration time. 33 | /// 34 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 35 | #endregion 36 | 37 | 38 | #region Commands 39 | //- [x] SetAddAsync 40 | //- [x] SetCombineAndStoreAsync 41 | //- [x] SetCombineAsync 42 | //- [x] SetContainsAsync 43 | //- [x] SetLengthAsync 44 | //- [x] SetMembersAsync 45 | //- [x] SetMoveAsync 46 | //- [x] SetPopAsync 47 | //- [x] SetRandomMemberAsync 48 | //- [x] SetRandomMembersAsync 49 | //- [x] SetRemoveAsync 50 | //- [x] SortAndStoreAsync 51 | //- [x] SortAsync 52 | 53 | 54 | /// 55 | /// SADD : 56 | /// 57 | public Task AddAsync(T value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 58 | { 59 | expiry ??= this.DefaultExpiry; 60 | var serialised = this.Connection.Converter.Serialize(value); 61 | return this.ExecuteWithExpiryAsync 62 | ( 63 | static (db, state) => db.SetAddAsync(state.key, state.serialised, state.flags), 64 | state: (key: this.Key, serialised, flags), 65 | expiry, 66 | flags 67 | ); 68 | } 69 | 70 | 71 | /// 72 | /// SADD : 73 | /// 74 | public Task AddAsync(IEnumerable values, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 75 | { 76 | expiry ??= this.DefaultExpiry; 77 | var serialised = values.Select(this.Connection.Converter.Serialize).ToArray(); 78 | return this.ExecuteWithExpiryAsync 79 | ( 80 | static (db, state) => db.SetAddAsync(state.key, state.serialised, state.flags), 81 | state: (key: this.Key, serialised, flags), 82 | expiry, 83 | flags 84 | ); 85 | } 86 | 87 | 88 | /// 89 | /// SDIFFSTORE :
90 | /// SINTERSTORE :
91 | /// SUNIONSTORE : 92 | ///
93 | /// 94 | /// Combine self and other, then save it to the destination. 95 | /// It does not work unless you pass keys located the same server. 96 | /// 97 | public Task CombineAndStoreAsync(SetOperation operation, RedisSet destination, RedisSet other, CommandFlags flags = CommandFlags.None) 98 | => this.Connection.Database.SetCombineAndStoreAsync(operation, destination.Key, this.Key, other.Key, flags); 99 | 100 | 101 | /// 102 | /// SDIFFSTORE :
103 | /// SINTERSTORE :
104 | /// SUNIONSTORE : 105 | ///
106 | /// 107 | /// Combine self and other, then save it to the destination. 108 | /// It does not work unless you pass keys located the same server. 109 | /// 110 | public Task CombineAndStoreAsync(SetOperation operation, RedisSet destination, IReadOnlyCollection> others, CommandFlags flags = CommandFlags.None) 111 | { 112 | if (others.Count == 0) 113 | throw new ArgumentException("others length is 0."); 114 | 115 | #if NETSTANDARD2_1 || NET5_0_OR_GREATER 116 | var keys = others.Select(static x => x.Key).Append(this.Key).ToArray(); 117 | #else 118 | var keys = others.Select(static x => x.Key).Concat([this.Key]).ToArray(); 119 | #endif 120 | return this.Connection.Database.SetCombineAndStoreAsync(operation, destination.Key, keys, flags); 121 | } 122 | 123 | 124 | /// 125 | /// SDIFF :
126 | /// SINTER :
127 | /// SUNION : 128 | ///
129 | /// It does not work unless you pass keys located the same server. 130 | public async Task CombineAsync(SetOperation operation, RedisSet other, CommandFlags flags = CommandFlags.None) 131 | { 132 | var values = await this.Connection.Database.SetCombineAsync(operation, this.Key, other.Key, flags).ConfigureAwait(false); 133 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 134 | } 135 | 136 | 137 | /// 138 | /// SDIFF :
139 | /// SINTER :
140 | /// SUNION : 141 | ///
142 | /// It does not work unless you pass keys located the same server. 143 | public async Task CombineAsync(SetOperation operation, IReadOnlyCollection> others, CommandFlags flags = CommandFlags.None) 144 | { 145 | if (others.Count == 0) 146 | throw new ArgumentException("others length is 0."); 147 | 148 | #if NETSTANDARD2_1 || NET5_0_OR_GREATER 149 | var keys = others.Select(static x => x.Key).Append(this.Key).ToArray(); 150 | #else 151 | var keys = others.Select(static x => x.Key).Concat([this.Key]).ToArray(); 152 | #endif 153 | var values = await this.Connection.Database.SetCombineAsync(operation, keys, flags).ConfigureAwait(false); 154 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 155 | } 156 | 157 | 158 | /// 159 | /// SISMEMBER : 160 | /// 161 | public Task ContainsAsync(T value, CommandFlags flags = CommandFlags.None) 162 | { 163 | var serialized = this.Connection.Converter.Serialize(value); 164 | return this.Connection.Database.SetContainsAsync(this.Key, serialized, flags); 165 | } 166 | 167 | 168 | /// 169 | /// SCARD : 170 | /// 171 | public Task LengthAsync(CommandFlags flags = CommandFlags.None) 172 | => this.Connection.Database.SetLengthAsync(this.Key, flags); 173 | 174 | 175 | /// 176 | /// SMEMBERS : 177 | /// 178 | public async Task MembersAsync(CommandFlags flags = CommandFlags.None) 179 | { 180 | var members = await this.Connection.Database.SetMembersAsync(this.Key, flags).ConfigureAwait(false); 181 | return members 182 | .Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)) 183 | .ToArray(); 184 | } 185 | 186 | 187 | /// 188 | /// SMOVE : 189 | /// 190 | public Task MoveAsync(RedisSet destination, T value, CommandFlags flags = CommandFlags.None) 191 | { 192 | var serialized = this.Connection.Converter.Serialize(value); 193 | return this.Connection.Database.SetMoveAsync(this.Key, destination.Key, serialized, flags); 194 | } 195 | 196 | 197 | /// 198 | /// SPOP : 199 | /// 200 | public async Task> PopAsync(CommandFlags flags = CommandFlags.None) 201 | { 202 | var value = await this.Connection.Database.SetPopAsync(this.Key, flags).ConfigureAwait(false); 203 | return value.ToResult(this.Connection.Converter); 204 | } 205 | 206 | 207 | /// 208 | /// SRANDMEMBER : 209 | /// 210 | public async Task> RandomMemberAsync(CommandFlags flags = CommandFlags.None) 211 | { 212 | var value = await this.Connection.Database.SetRandomMemberAsync(this.Key, flags).ConfigureAwait(false); 213 | return value.ToResult(this.Connection.Converter); 214 | } 215 | 216 | 217 | /// 218 | /// SRANDMEMBER : 219 | /// 220 | public async Task RandomMemberAsync(long count, CommandFlags flags = CommandFlags.None) 221 | { 222 | var values = await this.Connection.Database.SetRandomMembersAsync(this.Key, count, flags).ConfigureAwait(false); 223 | return values 224 | .Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)) 225 | .ToArray(); 226 | } 227 | 228 | 229 | /// 230 | /// SREM : 231 | /// 232 | public Task RemoveAsync(T value, CommandFlags flags = CommandFlags.None) 233 | { 234 | var serialized = this.Connection.Converter.Serialize(value); 235 | return this.Connection.Database.SetRemoveAsync(this.Key, serialized, flags); 236 | } 237 | 238 | 239 | /// 240 | /// SORT : 241 | /// 242 | public Task SortAndStoreAsync(RedisSet destination, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, /*RedisValue by = default, RedisValue[] get = null,*/ CommandFlags flags = CommandFlags.None) 243 | { 244 | //--- I don't know if serialization is necessary or not, so I will fix the default value. 245 | RedisValue by = default; 246 | RedisValue[]? get = default; 247 | return this.Connection.Database.SortAndStoreAsync(destination.Key, this.Key, skip, take, order, sortType, by, get, flags); 248 | } 249 | 250 | 251 | /// 252 | /// SORT : 253 | /// 254 | public async Task SortAsync(long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, /*RedisValue by = default, RedisValue[] get = null,*/ CommandFlags flags = CommandFlags.None) 255 | { 256 | //--- I don't know if serialization is necessary or not, so I will fix the default value. 257 | RedisValue by = default; 258 | RedisValue[]? get = default; 259 | var values = await this.Connection.Database.SortAsync(this.Key, skip, take, order, sortType, by, get, flags).ConfigureAwait(false); 260 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 261 | } 262 | #endregion 263 | } 264 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisSortedSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CloudStructures.Converters; 6 | using CloudStructures.Internals; 7 | using StackExchange.Redis; 8 | 9 | namespace CloudStructures.Structures; 10 | 11 | 12 | 13 | /// 14 | /// Provides sorted set related commands. 15 | /// 16 | /// Data type 17 | public readonly struct RedisSortedSet(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 18 | { 19 | #region IRedisStructureWithExpiry implementations 20 | /// 21 | /// Gets connection. 22 | /// 23 | public RedisConnection Connection { get; } = connection; 24 | 25 | 26 | /// 27 | /// Gets key. 28 | /// 29 | public RedisKey Key { get; } = key; 30 | 31 | 32 | /// 33 | /// Gets default expiration time. 34 | /// 35 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 36 | #endregion 37 | 38 | 39 | #region Commands 40 | //- [x] SortedSetAddAsync 41 | //- [x] SortedSetCombineAndStoreAsync 42 | //- [x] SortedSetDecrementAsync 43 | //- [x] SortedSetIncrementAsync 44 | //- [x] SortedSetLengthAsync 45 | //- [x] SortedSetLengthByValueAsync 46 | //- [x] SortedSetRangeByRankAsync 47 | //- [x] SortedSetRangeByRankWithScoresAsync 48 | //- [x] SortedSetRangeByScoreAsync 49 | //- [x] SortedSetRangeByScoreWithScoresAsync 50 | //- [x] SortedSetRangeByValueAsync 51 | //- [x] SortedSetRankAsync 52 | //- [x] SortedSetRemoveAsync 53 | //- [x] SortedSetRemoveRangeByRankAsync 54 | //- [x] SortedSetRemoveRangeByScoreAsync 55 | //- [x] SortedSetRemoveRangeByValueAsync 56 | //- [x] SortedSetScoreAsync 57 | //- [x] SortAndStoreAsync 58 | //- [x] SortAsync 59 | 60 | 61 | /// 62 | /// ZADD : 63 | /// 64 | public Task AddAsync(T value, double score, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 65 | { 66 | expiry ??= this.DefaultExpiry; 67 | var serialized = this.Connection.Converter.Serialize(value); 68 | return this.ExecuteWithExpiryAsync 69 | ( 70 | static (db, state) => db.SortedSetAddAsync(state.key, state.serialized, state.score, state.when, state.flags), 71 | state: (key: this.Key, serialized, score, when, flags), 72 | expiry, 73 | flags 74 | ); 75 | } 76 | 77 | 78 | /// 79 | /// ZADD : 80 | /// 81 | public Task AddAsync(IEnumerable> entries, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 82 | { 83 | expiry ??= this.DefaultExpiry; 84 | var values 85 | = entries 86 | .Select(this.Connection.Converter, static (x, c) => x.ToNonGenerics(c)) 87 | .ToArray(); 88 | return this.ExecuteWithExpiryAsync 89 | ( 90 | static (db, state) => db.SortedSetAddAsync(state.key, state.values, state.when, state.flags), 91 | state: (key: this.Key, values, when, flags), 92 | expiry, 93 | flags 94 | ); 95 | } 96 | 97 | 98 | /// 99 | /// ZUNIONSTORE :
100 | /// ZINTERSTORE : 101 | ///
102 | public Task CombineAndStoreAsync(SetOperation operation, RedisSortedSet destination, RedisSortedSet other, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) 103 | => this.Connection.Database.SortedSetCombineAndStoreAsync(operation, destination.Key, this.Key, other.Key, aggregate, flags); 104 | 105 | 106 | /// 107 | /// ZUNIONSTORE :
108 | /// ZINTERSTORE : 109 | ///
110 | public Task CombineAndStoreAsync(SetOperation operation, RedisSortedSet destination, IReadOnlyCollection> others, double[]? weights = default, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) 111 | { 112 | if (others.Count == 0) 113 | throw new ArgumentException("others length is 0."); 114 | 115 | #if NETSTANDARD2_1 || NET5_0_OR_GREATER 116 | var keys = others.Select(static x => x.Key).Append(this.Key).ToArray(); 117 | #else 118 | var keys = others.Select(static x => x.Key).Concat([this.Key]).ToArray(); 119 | #endif 120 | return this.Connection.Database.SortedSetCombineAndStoreAsync(operation, destination.Key, keys, weights, aggregate, flags); 121 | } 122 | 123 | 124 | /// 125 | /// ZINCRBY : 126 | /// 127 | public Task DecrementAsync(T member, double value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 128 | { 129 | expiry ??= this.DefaultExpiry; 130 | var serialized = this.Connection.Converter.Serialize(member); 131 | return this.ExecuteWithExpiryAsync 132 | ( 133 | static (db, state) => db.SortedSetDecrementAsync(state.key, state.serialized, state.value, state.flags), 134 | state: (key: this.Key, serialized, value, flags), 135 | expiry, 136 | flags 137 | ); 138 | } 139 | 140 | 141 | /// 142 | /// ZINCRBY : 143 | /// 144 | public Task IncrementAsync(T member, double value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 145 | { 146 | expiry ??= this.DefaultExpiry; 147 | var serialized = this.Connection.Converter.Serialize(member); 148 | return this.ExecuteWithExpiryAsync 149 | ( 150 | static (db, state) => db.SortedSetIncrementAsync(state.key, state.serialized, state.value, state.flags), 151 | state: (key: this.Key, serialized, value, flags), 152 | expiry, 153 | flags 154 | ); 155 | } 156 | 157 | 158 | /// 159 | /// ZCARD :
160 | /// ZCOUNT : 161 | ///
162 | public Task LengthAsync(double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) 163 | => this.Connection.Database.SortedSetLengthAsync(this.Key, min, max, exclude, flags); 164 | 165 | 166 | /// 167 | /// ZCARD :
168 | /// ZCOUNT : 169 | ///
170 | public Task LengthByValueAsync(T min, T max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) 171 | { 172 | var serializedMin = this.Connection.Converter.Serialize(min); 173 | var serializedMax = this.Connection.Converter.Serialize(max); 174 | return this.Connection.Database.SortedSetLengthByValueAsync(this.Key, serializedMin, serializedMax, exclude, flags); 175 | } 176 | 177 | 178 | /// 179 | /// ZRANGE :
180 | /// ZREVRANGE : 181 | ///
182 | public async Task RangeByRankAsync(long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) 183 | { 184 | var values = await this.Connection.Database.SortedSetRangeByRankAsync(this.Key, start, stop, order, flags).ConfigureAwait(false); 185 | return values 186 | .Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)) 187 | .ToArray(); 188 | } 189 | 190 | 191 | /// 192 | /// ZRANGE :
193 | /// ZREVRANGE : 194 | ///
195 | public async Task[]> RangeByRankWithScoresAsync(long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) 196 | { 197 | var values = await this.Connection.Database.SortedSetRangeByRankWithScoresAsync(this.Key, start, stop, order, flags).ConfigureAwait(false); 198 | return values 199 | .Select(this.Connection.Converter, static (x, c) => x.ToGenerics(c)) 200 | .ToArray(); 201 | } 202 | 203 | 204 | /// 205 | /// ZRANGEBYSCORE :
206 | /// ZREVRANGEBYSCORE : 207 | ///
208 | public async Task RangeByScoreAsync(double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) 209 | { 210 | var values = await this.Connection.Database.SortedSetRangeByScoreAsync(this.Key, start, stop, exclude, order, skip, take, flags).ConfigureAwait(false); 211 | return values 212 | .Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)) 213 | .ToArray(); 214 | } 215 | 216 | 217 | /// 218 | /// ZRANGEBYSCORE :
219 | /// ZREVRANGEBYSCORE : 220 | ///
221 | public async Task[]> RangeByScoreWithScoresAsync(double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) 222 | { 223 | var values = await this.Connection.Database.SortedSetRangeByScoreWithScoresAsync(this.Key, start, stop, exclude, order, skip, take, flags).ConfigureAwait(false); 224 | return values 225 | .Select(this.Connection.Converter, static (x, c) => x.ToGenerics(c)) 226 | .ToArray(); 227 | } 228 | 229 | 230 | /// 231 | /// ZRANGEBYLEX :
232 | /// ZREVRANGEBYLEX : 233 | ///
234 | public async Task RangeByValueAsync(T min, T max, Exclude exclude, long skip, long take = -1, CommandFlags flags = CommandFlags.None) 235 | { 236 | var minValue = this.Connection.Converter.Serialize(min); 237 | var maxValue = this.Connection.Converter.Serialize(max); 238 | var values = await this.Connection.Database.SortedSetRangeByValueAsync(this.Key, minValue, maxValue, exclude, skip, take, flags).ConfigureAwait(false); 239 | return values 240 | .Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)) 241 | .ToArray(); 242 | } 243 | 244 | 245 | /// 246 | /// ZRANGEBYLEX :
247 | /// ZREVRANGEBYLEX : 248 | ///
249 | public async Task RangeByValueAsync(T? min = default, T? max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) 250 | { 251 | var minValue = this.Connection.Converter.Serialize(min); 252 | var maxValue = this.Connection.Converter.Serialize(max); 253 | var values = await this.Connection.Database.SortedSetRangeByValueAsync(this.Key, minValue, maxValue, exclude, order, skip, take, flags).ConfigureAwait(false); 254 | return values 255 | .Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)) 256 | .ToArray(); 257 | } 258 | 259 | 260 | /// 261 | /// ZRANK : 262 | /// 263 | public Task RankAsync(T member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) 264 | { 265 | var serialized = this.Connection.Converter.Serialize(member); 266 | return this.Connection.Database.SortedSetRankAsync(this.Key, serialized, order, flags); 267 | } 268 | 269 | 270 | /// 271 | /// ZREM : 272 | /// 273 | public Task RemoveAsync(T member, CommandFlags flags = CommandFlags.None) 274 | { 275 | var serialized = this.Connection.Converter.Serialize(member); 276 | return this.Connection.Database.SortedSetRemoveAsync(this.Key, serialized, flags); 277 | } 278 | 279 | 280 | /// 281 | /// ZREM : 282 | /// 283 | public Task RemoveAsync(IEnumerable members, CommandFlags flags = CommandFlags.None) 284 | { 285 | var serialized = members.Select(this.Connection.Converter.Serialize).ToArray(); 286 | return this.Connection.Database.SortedSetRemoveAsync(this.Key, serialized, flags); 287 | } 288 | 289 | 290 | /// 291 | /// ZREMRANGEBYRANK : 292 | /// 293 | public Task RemoveRangeByRankAsync(long start, long stop, CommandFlags flags = CommandFlags.None) 294 | => this.Connection.Database.SortedSetRemoveRangeByRankAsync(this.Key, start, stop, flags); 295 | 296 | 297 | /// 298 | /// ZREMRANGEBYSCORE : 299 | /// 300 | public Task RemoveRangeByScoreAsync(double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) 301 | => this.Connection.Database.SortedSetRemoveRangeByScoreAsync(this.Key, start, stop, exclude, flags); 302 | 303 | 304 | /// 305 | /// ZREMRANGEBYLEX : 306 | /// 307 | public Task RemoveRangeByValueAsync(T min, T max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) 308 | { 309 | var minValue = this.Connection.Converter.Serialize(min); 310 | var maxValue = this.Connection.Converter.Serialize(max); 311 | return this.Connection.Database.SortedSetRemoveRangeByValueAsync(this.Key, minValue, maxValue, exclude, flags); 312 | } 313 | 314 | 315 | /// 316 | /// ZSCORE : 317 | /// 318 | public Task ScoreAsync(T member, CommandFlags flags = CommandFlags.None) 319 | { 320 | var serialized = this.Connection.Converter.Serialize(member); 321 | return this.Connection.Database.SortedSetScoreAsync(this.Key, serialized, flags); 322 | } 323 | 324 | 325 | /// 326 | /// SORT : 327 | /// 328 | public Task SortAndStoreAsync(RedisSortedSet destination, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, /*RedisValue by = default, RedisValue[] get = null,*/ CommandFlags flags = CommandFlags.None) 329 | { 330 | //--- I don't know if serialization is necessary or not, so I will fix the default value. 331 | RedisValue by = default; 332 | RedisValue[]? get = default; 333 | return this.Connection.Database.SortAndStoreAsync(destination.Key, this.Key, skip, take, order, sortType, by, get, flags); 334 | } 335 | 336 | 337 | /// 338 | /// SORT : 339 | /// 340 | public async Task SortAsync(long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, /*RedisValue by = default, RedisValue[] get = null,*/ CommandFlags flags = CommandFlags.None) 341 | { 342 | //--- I don't know if serialization is necessary or not, so I will fix the default value. 343 | RedisValue by = default; 344 | RedisValue[]? get = default; 345 | var values = await this.Connection.Database.SortAsync(this.Key, skip, take, order, sortType, by, get, flags).ConfigureAwait(false); 346 | return values.Select(this.Connection.Converter, static (x, c) => c.Deserialize(x)).ToArray(); 347 | } 348 | #endregion 349 | 350 | 351 | #region Custom Commands 352 | /// 353 | /// LUA Script including zincrby, zadd 354 | /// 355 | public async Task IncrementLimitByMinAsync(T member, double value, double min, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 356 | { 357 | expiry ??= this.DefaultExpiry; 358 | var script = 359 | @"local mem = ARGV[1] 360 | local inc = tonumber(ARGV[2]) 361 | local min = tonumber(ARGV[3]) 362 | local x = tonumber(redis.call('zincrby', KEYS[1], inc, mem)) 363 | if(x < min) then 364 | redis.call('zadd', KEYS[1], min, mem) 365 | x = min 366 | end 367 | return tostring(x)"; 368 | var keys = new[] { this.Key }; 369 | var serialized = this.Connection.Converter.Serialize(member); 370 | var values = new RedisValue[] { serialized, value, min }; 371 | var result 372 | = await this.ExecuteWithExpiryAsync 373 | ( 374 | static (db, state) => db.ScriptEvaluateAsync(state.script, state.keys, state.values, state.flags), 375 | state: (script, keys, values, flags), 376 | expiry, 377 | flags 378 | ) 379 | .ConfigureAwait(false); 380 | return (double)result; 381 | } 382 | 383 | 384 | /// 385 | /// LUA Script including zincrby, zadd 386 | /// 387 | public async Task IncrementLimitByMaxAsync(T member, double value, double max, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 388 | { 389 | expiry ??= this.DefaultExpiry; 390 | var script = 391 | @"local mem = ARGV[1] 392 | local inc = tonumber(ARGV[2]) 393 | local max = tonumber(ARGV[3]) 394 | local x = tonumber(redis.call('zincrby', KEYS[1], inc, mem)) 395 | if(x > max) then 396 | redis.call('zadd', KEYS[1], max, mem) 397 | x = max 398 | end 399 | return tostring(x)"; 400 | var keys = new[] { this.Key }; 401 | var serialized = this.Connection.Converter.Serialize(member); 402 | var values = new RedisValue[] { serialized, value, max }; 403 | var result 404 | = await this.ExecuteWithExpiryAsync 405 | ( 406 | static (db, state) => db.ScriptEvaluateAsync(state.script, state.keys, state.values, state.flags), 407 | state: (script, keys, values, flags), 408 | expiry, 409 | flags 410 | ) 411 | .ConfigureAwait(false); 412 | return (double)result; 413 | } 414 | #endregion 415 | } 416 | 417 | 418 | 419 | /// 420 | /// Represents element. 421 | /// 422 | /// Data type 423 | public readonly struct RedisSortedSetEntry(T value, double score) 424 | { 425 | #region Properties 426 | /// 427 | /// Gets value. 428 | /// 429 | public T Value { get; } = value; 430 | 431 | 432 | /// 433 | /// Gets score. 434 | /// 435 | public double Score { get; } = score; 436 | #endregion 437 | } 438 | 439 | 440 | 441 | /// 442 | /// Provides extension methods for . 443 | /// 444 | internal static class RedisSortedSetEntryExtensions 445 | { 446 | /// 447 | /// Converts to . 448 | /// 449 | /// Data type 450 | /// 451 | /// 452 | /// 453 | public static SortedSetEntry ToNonGenerics(this in RedisSortedSetEntry entry, ValueConverter converter) 454 | { 455 | var value = converter.Serialize(entry.Value); 456 | return new(value, entry.Score); 457 | } 458 | 459 | 460 | /// 461 | /// Converts to . 462 | /// 463 | /// Data type 464 | /// 465 | /// 466 | /// 467 | public static RedisSortedSetEntry ToGenerics(this in SortedSetEntry entry, ValueConverter converter) 468 | { 469 | var value = converter.Deserialize(entry.Element); 470 | return new(value, entry.Score); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/CloudStructures/Structures/RedisString.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CloudStructures.Internals; 4 | using StackExchange.Redis; 5 | 6 | namespace CloudStructures.Structures; 7 | 8 | 9 | 10 | /// 11 | /// Provides string related commands. 12 | /// 13 | /// Data type 14 | public readonly struct RedisString(RedisConnection connection, RedisKey key, TimeSpan? defaultExpiry) : IRedisStructureWithExpiry 15 | { 16 | #region IRedisStructureWithExpiry implementations 17 | /// 18 | /// Gets connection. 19 | /// 20 | public RedisConnection Connection { get; } = connection; 21 | 22 | 23 | /// 24 | /// Gets key. 25 | /// 26 | public RedisKey Key { get; } = key; 27 | 28 | 29 | /// 30 | /// Gets default expiration time. 31 | /// 32 | public TimeSpan? DefaultExpiry { get; } = defaultExpiry; 33 | #endregion 34 | 35 | 36 | #region Commands 37 | //- [] StringAppendAsync 38 | //- [x] StringDecrementAsync 39 | //- [x] StringGetAsync 40 | //- [x] StringGetDeleteAsync 41 | //- [] StringGetRangeAsync 42 | //- [x] StringGetSetAsync 43 | //- [x] StringGetWithExpiryAsync 44 | //- [x] StringIncrementAsync 45 | //- [x] StringLengthAsync 46 | //- [x] StringSetAsync 47 | //- [] StringSetRangeAsync 48 | 49 | 50 | /// 51 | /// DECRBY : 52 | /// 53 | public Task DecrementAsync(long value = 1, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 54 | { 55 | expiry ??= this.DefaultExpiry; 56 | return this.ExecuteWithExpiryAsync 57 | ( 58 | static (db, state) => db.StringDecrementAsync(state.key, state.value, state.flags), 59 | state: (key: this.Key, value, flags), 60 | expiry, 61 | flags 62 | ); 63 | } 64 | 65 | 66 | /// 67 | /// INCRBYFLOAT : 68 | /// 69 | public Task DecrementAsync(double value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 70 | { 71 | expiry ??= this.DefaultExpiry; 72 | return this.ExecuteWithExpiryAsync 73 | ( 74 | static (db, state) => db.StringDecrementAsync(state.key, state.value, state.flags), 75 | state: (key: this.Key, value, flags), 76 | expiry, 77 | flags 78 | ); 79 | } 80 | 81 | 82 | /// 83 | /// GET : 84 | /// 85 | public async Task> GetAsync(CommandFlags flags = CommandFlags.None) 86 | { 87 | var value = await this.Connection.Database.StringGetAsync(this.Key, flags).ConfigureAwait(false); 88 | return value.ToResult(this.Connection.Converter); 89 | } 90 | 91 | 92 | /// 93 | /// GETDEL : 94 | /// 95 | public async Task> GetDeleteAsync(CommandFlags flags = CommandFlags.None) 96 | { 97 | var value = await this.Connection.Database.StringGetDeleteAsync(this.Key, flags).ConfigureAwait(false); 98 | return value.ToResult(this.Connection.Converter); 99 | } 100 | 101 | 102 | /// 103 | /// GETSET : 104 | /// 105 | public async Task> GetSetAsync(T value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 106 | { 107 | expiry ??= this.DefaultExpiry; 108 | var serialized = this.Connection.Converter.Serialize(value); 109 | var result 110 | = await this.ExecuteWithExpiryAsync 111 | ( 112 | static (db, state) => db.StringGetSetAsync(state.key, state.serialized, state.flags), 113 | state: (key: this.Key, serialized, flags), 114 | expiry, 115 | flags 116 | ) 117 | .ConfigureAwait(false); 118 | return result.ToResult(this.Connection.Converter); 119 | } 120 | 121 | 122 | /// 123 | /// GET : 124 | /// 125 | public async Task> GetWithExpiryAsync(CommandFlags flags = CommandFlags.None) 126 | { 127 | var value = await this.Connection.Database.StringGetWithExpiryAsync(this.Key, flags).ConfigureAwait(false); 128 | return value.ToResult(this.Connection.Converter); 129 | } 130 | 131 | 132 | /// 133 | /// INCRBY : 134 | /// 135 | public Task IncrementAsync(long value = 1, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 136 | { 137 | expiry ??= this.DefaultExpiry; 138 | return this.ExecuteWithExpiryAsync 139 | ( 140 | static (db, state) => db.StringIncrementAsync(state.key, state.value, state.flags), 141 | state: (key: this.Key, value, flags), 142 | expiry, 143 | flags 144 | ); 145 | } 146 | 147 | 148 | /// 149 | /// INCRBYFLOAT : 150 | /// 151 | public Task IncrementAsync(double value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 152 | { 153 | expiry ??= this.DefaultExpiry; 154 | return this.ExecuteWithExpiryAsync 155 | ( 156 | static (db, state) => db.StringIncrementAsync(state.key, state.value, state.flags), 157 | state: (key: this.Key, value, flags), 158 | expiry, 159 | flags 160 | ); 161 | } 162 | 163 | 164 | /// 165 | /// STRLEN : 166 | /// 167 | public Task LengthAsync(CommandFlags flags = CommandFlags.None) 168 | => this.Connection.Database.StringLengthAsync(this.Key, flags); 169 | 170 | 171 | /// 172 | /// SET : 173 | /// 174 | public Task SetAsync(T value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) 175 | { 176 | expiry ??= this.DefaultExpiry; 177 | var serialized = this.Connection.Converter.Serialize(value); 178 | return this.Connection.Database.StringSetAsync(this.Key, serialized, expiry, when, flags); 179 | } 180 | #endregion 181 | 182 | 183 | #region Custom Commands 184 | /// 185 | /// GET :
186 | /// SET : 187 | ///
188 | public async Task GetOrSetAsync(Func valueFactory, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 189 | { 190 | expiry ??= this.DefaultExpiry; 191 | var result = await this.GetAsync(flags).ConfigureAwait(false); 192 | if (result.HasValue) 193 | { 194 | return result.Value; 195 | } 196 | else 197 | { 198 | var value = valueFactory(); 199 | await this.SetAsync(value, expiry, When.Always, flags).ConfigureAwait(false); 200 | return value; 201 | } 202 | } 203 | 204 | 205 | /// 206 | /// GET :
207 | /// SET : 208 | ///
209 | public async Task GetOrSetAsync(Func> valueFactory, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 210 | { 211 | expiry ??= this.DefaultExpiry; 212 | var result = await this.GetAsync(flags).ConfigureAwait(false); 213 | if (result.HasValue) 214 | { 215 | return result.Value; 216 | } 217 | else 218 | { 219 | var value = await valueFactory().ConfigureAwait(false); 220 | await this.SetAsync(value, expiry, When.Always, flags).ConfigureAwait(false); 221 | return value; 222 | } 223 | } 224 | 225 | 226 | /// 227 | /// GET :
228 | /// DEL : 229 | ///
230 | /// 231 | /// 232 | public async Task> GetAndDeleteAsync(CommandFlags flags = CommandFlags.None) 233 | { 234 | var result = await this.GetAsync(flags).ConfigureAwait(false); 235 | if (result.HasValue) 236 | await this.DeleteAsync(flags).ConfigureAwait(false); 237 | return result; 238 | } 239 | 240 | 241 | /// 242 | /// LUA Script including incrby, set 243 | /// 244 | public async Task IncrementLimitByMaxAsync(long value, long max, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 245 | { 246 | expiry ??= this.DefaultExpiry; 247 | var script = 248 | @"local inc = tonumber(ARGV[1]) 249 | local max = tonumber(ARGV[2]) 250 | local x = redis.call('incrby', KEYS[1], inc) 251 | if(x > max) then 252 | redis.call('set', KEYS[1], max) 253 | x = max 254 | end 255 | return x"; 256 | var keys = new[] { this.Key }; 257 | var values = new RedisValue[] { value, max }; 258 | var result 259 | = await this.ExecuteWithExpiryAsync 260 | ( 261 | static (db, state) => db.ScriptEvaluateAsync(state.script, state.keys, state.values, state.flags), 262 | state: (script, keys, values, flags), 263 | expiry, 264 | flags 265 | ) 266 | .ConfigureAwait(false); 267 | return (long)result; 268 | } 269 | 270 | 271 | /// 272 | /// LUA Script including incrbyfloat, set 273 | /// 274 | public async Task IncrementLimitByMaxAsync(double value, double max, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 275 | { 276 | expiry ??= this.DefaultExpiry; 277 | var script = 278 | @"local inc = tonumber(ARGV[1]) 279 | local max = tonumber(ARGV[2]) 280 | local x = tonumber(redis.call('incrbyfloat', KEYS[1], inc)) 281 | if(x > max) then 282 | redis.call('set', KEYS[1], max) 283 | x = max 284 | end 285 | return tostring(x)"; 286 | var keys = new[] { this.Key }; 287 | var values = new RedisValue[] { value, max }; 288 | var result 289 | = await this.ExecuteWithExpiryAsync 290 | ( 291 | static (db, state) => db.ScriptEvaluateAsync(state.script, state.keys, state.values, state.flags), 292 | state: (script, keys, values, flags), 293 | expiry, 294 | flags 295 | ) 296 | .ConfigureAwait(false); 297 | return (double)result; 298 | } 299 | 300 | 301 | /// 302 | /// LUA Script including incrby, set 303 | /// 304 | public async Task IncrementLimitByMinAsync(long value, long min, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 305 | { 306 | expiry ??= this.DefaultExpiry; 307 | var script = 308 | @"local inc = tonumber(ARGV[1]) 309 | local min = tonumber(ARGV[2]) 310 | local x = redis.call('incrby', KEYS[1], inc) 311 | if(x < min) then 312 | redis.call('set', KEYS[1], min) 313 | x = min 314 | end 315 | return x"; 316 | var keys = new[] { this.Key }; 317 | var values = new RedisValue[] { value, min }; 318 | var result 319 | = await this.ExecuteWithExpiryAsync 320 | ( 321 | static (db, state) => db.ScriptEvaluateAsync(state.script, state.keys, state.values, state.flags), 322 | state: (script, keys, values, flags), 323 | expiry, 324 | flags 325 | ) 326 | .ConfigureAwait(false); 327 | return (long)result; 328 | } 329 | 330 | 331 | /// 332 | /// LUA Script including incrbyfloat, set 333 | /// 334 | public async Task IncrementLimitByMinAsync(double value, double min, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) 335 | { 336 | expiry ??= this.DefaultExpiry; 337 | var script = 338 | @"local inc = tonumber(ARGV[1]) 339 | local min = tonumber(ARGV[2]) 340 | local x = tonumber(redis.call('incrbyfloat', KEYS[1], inc)) 341 | if(x < min) then 342 | redis.call('set', KEYS[1], min) 343 | x = min 344 | end 345 | return tostring(x)"; 346 | var keys = new[] { this.Key }; 347 | var values = new RedisValue[] { value, min }; 348 | var result 349 | = await this.ExecuteWithExpiryAsync 350 | ( 351 | static (db, state) => db.ScriptEvaluateAsync(state.script, state.keys, state.values, state.flags), 352 | state: (script, keys, values, flags), 353 | expiry, 354 | flags 355 | ) 356 | .ConfigureAwait(false); 357 | return (double)result; 358 | } 359 | #endregion 360 | } 361 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net462;net8;net9 5 | 13.0 6 | enable 7 | true 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------