├── .github └── workflows │ ├── dotnet.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── NReJSON.IntegrationTests ├── BaseIntegrationTest.cs ├── DatabaseExtensionAsyncTests.cs ├── DatabaseExtensionTests.cs ├── Models │ ├── ExampleHelloWorld.cs │ └── ExamplePerson.cs ├── NReJSON.IntegrationTests.csproj └── TestJsonSerializer.cs ├── NReJSON.Tests ├── DatabaseExtensionsAsyncTests.cs ├── DatabaseExtensionsTests.cs ├── FakeDatabase.cs └── NReJSON.Tests.csproj ├── NReJSON.sln ├── NReJSON ├── DatabaseExtensions.cs ├── DatabaseExtensionsAsync.cs ├── DatabaseExtensionsUtilities.cs ├── ISerializerProxy.cs ├── IndexedCollection.cs ├── JsonCommands.cs ├── NReJSON.csproj ├── NReJSON.xml ├── NReJSONException.cs ├── NReJSONSerializer.cs ├── OperationResult.cs ├── PathedResult.cs ├── SetOption.cs └── license │ └── license.txt └── README.md /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | services: 17 | redis: 18 | image: redislabs/redismod:preview 19 | ports: 20 | - 6379:6379 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Setup .NET 25 | uses: actions/setup-dotnet@v1 26 | with: 27 | dotnet-version: 5.0.x 28 | - name: Restore dependencies 29 | run: dotnet restore 30 | - name: Build 31 | run: dotnet build --no-restore 32 | - name: Test 33 | run: dotnet test --no-build --verbosity normal 34 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Dotnet Package 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: nuget/setup-nuget@v1 15 | with: 16 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 17 | nuget-version: '5.x' 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: 5.0.x 22 | - name: Clean 23 | run: dotnet clean ./NReJSON.sln --configuration Release && dotnet nuget locals all --clear 24 | - name: Restore dependencies 25 | run: dotnet restore 26 | - name: Build 27 | run: dotnet build --no-restore --configuration Release 28 | - name: Publish NuGet Package 29 | env: 30 | api_key: ${{secrets.NUGET_API_KEY}} 31 | run: "nuget push NReJSON/bin/Release/*.nupkg -Source https://api.nuget.org/v3/index.json -ApiKey $api_key" -------------------------------------------------------------------------------- /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.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 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ 335 | 336 | .vscode 337 | .DS_Store 338 | 339 | 340 | global.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thomas Hanks 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 | 23 | Copyright 2018 Thomas Hanks 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. 36 | -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/BaseIntegrationTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StackExchange.Redis; 3 | 4 | namespace NReJSON.IntegrationTests 5 | { 6 | public abstract class BaseIntegrationTest : IDisposable 7 | { 8 | private static readonly ISerializerProxy _serializer = new TestJsonSerializer(); 9 | private readonly ConnectionMultiplexer _muxer; 10 | protected readonly IDatabase _db; 11 | 12 | protected BaseIntegrationTest() 13 | { 14 | _muxer = ConnectionMultiplexer.Connect("127.0.0.1"); 15 | _db = _muxer.GetDatabase(0); 16 | 17 | NReJSONSerializer.SerializerProxy = _serializer; 18 | } 19 | 20 | public void Dispose() 21 | { 22 | _muxer.Dispose(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/DatabaseExtensionAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using NReJSON.IntegrationTests.Models; 5 | using StackExchange.Redis; 6 | using Xunit; 7 | 8 | namespace NReJSON.IntegrationTests 9 | { 10 | public class DatabaseExtensionAsyncTests 11 | { 12 | public class JsonSetAsync : BaseIntegrationTest 13 | { 14 | [Fact] 15 | public async Task CanExecuteAsync() 16 | { 17 | var key = Guid.NewGuid().ToString("N"); 18 | 19 | var result = await _db.JsonSetAsync(key, "{}"); 20 | 21 | Assert.True(result); 22 | } 23 | 24 | [Fact] 25 | public async Task CanExecuteWithSerializer() 26 | { 27 | var key = Guid.NewGuid().ToString("N"); 28 | 29 | var obj = new ExampleHelloWorld 30 | { 31 | Hello = "World", 32 | 33 | GoodNight = new ExampleHelloWorld.InnerExample 34 | { 35 | Value = "Moon" 36 | } 37 | }; 38 | 39 | var result = await _db.JsonSetAsync(key, obj); 40 | 41 | Assert.True(result); 42 | } 43 | } 44 | 45 | public class JsonGetAsync : BaseIntegrationTest 46 | { 47 | [Fact] 48 | public async Task CanExecuteAsync() 49 | { 50 | var key = Guid.NewGuid().ToString("N"); 51 | 52 | await _db.JsonSetAsync(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 53 | 54 | var result = await _db.JsonGetAsync(key); 55 | 56 | Assert.False(result.IsNull); 57 | } 58 | 59 | [Fact] 60 | public async Task CanExecuteAsyncWithIdent() 61 | { 62 | var key = Guid.NewGuid().ToString("N"); 63 | 64 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 65 | 66 | var result = await _db.JsonGetAsync(key, indent: "&"); 67 | 68 | Assert.Contains("&", (string) result); 69 | } 70 | 71 | [Fact] 72 | public async Task CanExecuteAsyncWithNewline() 73 | { 74 | var key = Guid.NewGuid().ToString("N"); 75 | 76 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 77 | 78 | var result = await _db.JsonGetAsync(key, newline: "="); 79 | 80 | Assert.Contains("=", (string) result); 81 | } 82 | 83 | [Fact] 84 | public async Task CanExecuteAsyncWithSpace() 85 | { 86 | var key = Guid.NewGuid().ToString("N"); 87 | 88 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 89 | 90 | var result = await _db.JsonGetAsync(key, space: "+"); 91 | 92 | Assert.Contains("+", (string) result); 93 | } 94 | 95 | [Fact] 96 | public async Task CanExecuteWithSerializer() 97 | { 98 | var key = Guid.NewGuid().ToString("N"); 99 | 100 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 101 | 102 | ExampleHelloWorld result = await _db.JsonGetAsync(key); 103 | 104 | Assert.NotNull(result); 105 | Assert.Equal("world", result.Hello); 106 | Assert.Equal("moon", result.GoodNight.Value); 107 | } 108 | 109 | [Fact] 110 | public async Task CanExecuteWithMultiplePathMatchesWithSerializer() 111 | { 112 | var key = Guid.NewGuid().ToString("N"); 113 | 114 | _db.JsonSet(key, 115 | "{\r\n\"object\":{\r\n\"hello\":\"world\",\r\n\"goodnight\":{\r\n\"value\":\"moon\"\r\n}\r\n},\r\n\"nested\":{\r\n\"object\":{\r\n\"hello\": \"guy\",\r\n\"goodnight\": {\r\n\"value\": \"stuff\"\r\n}\r\n}\r\n}\r\n}"); 116 | 117 | var result = (await _db.JsonGetAsync(key, paths: "$..object")).ToList(); 118 | 119 | var firstResult = result[0]; 120 | var secondResult = result[1]; 121 | 122 | Assert.Equal("world", firstResult.Hello); 123 | Assert.Equal("guy", secondResult.Hello); 124 | } 125 | } 126 | 127 | public class JsonDeleteAsync : BaseIntegrationTest 128 | { 129 | [Fact] 130 | public async Task CanExecuteAsync() 131 | { 132 | var key = Guid.NewGuid().ToString("N"); 133 | 134 | await _db.JsonSetAsync(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 135 | 136 | var result = await _db.JsonDeleteAsync(key, ".goodnight"); 137 | var jsonResult = await _db.JsonGetAsync(key); 138 | 139 | Assert.Equal(1, result); 140 | Assert.DoesNotContain("goodnight", jsonResult.ToString()); 141 | } 142 | } 143 | 144 | public class JsonMultiGetAsync : BaseIntegrationTest 145 | { 146 | [Fact] 147 | public async Task CanExecuteAsync() 148 | { 149 | var key1 = Guid.NewGuid().ToString("N"); 150 | var key2 = Guid.NewGuid().ToString("N"); 151 | 152 | await _db.JsonSetAsync(key1, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 153 | await _db.JsonSetAsync(key2, "{\"hello\": \"tom\", \"goodnight\": {\"value\": \"tom\"}}"); 154 | 155 | var result = await _db.JsonMultiGetAsync(new RedisKey[] {key1, key2}); 156 | 157 | Assert.Equal(2, result.Length); 158 | Assert.Contains("world", result[0].ToString()); 159 | Assert.Contains("tom", result[1].ToString()); 160 | } 161 | 162 | [Fact] 163 | public async Task CanExecuteWithSerializer() 164 | { 165 | var key1 = Guid.NewGuid().ToString("N"); 166 | var key2 = Guid.NewGuid().ToString("N"); 167 | 168 | await _db.JsonSetAsync(key1, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 169 | await _db.JsonSetAsync(key2, "{\"hello\": \"tom\", \"goodnight\": {\"value\": \"tom\"}}"); 170 | 171 | var result = (await _db.JsonMultiGetAsync(new RedisKey[] {key1, "say what?", key2})) 172 | .ToList(); 173 | 174 | Assert.Equal(3, result.Count); 175 | Assert.Null(result[1]); 176 | Assert.Contains("world", result[0].Hello); 177 | Assert.Contains("tom", result[2].Hello); 178 | } 179 | } 180 | 181 | public class JsonTypeAsync : BaseIntegrationTest 182 | { 183 | [Theory] 184 | [InlineData(".string", "string")] 185 | [InlineData(".integer", "integer")] 186 | [InlineData(".boolean", "boolean")] 187 | [InlineData(".number", "number")] 188 | public async Task CanExecuteAsync(string path, string value) 189 | { 190 | var key = Guid.NewGuid().ToString("N"); 191 | 192 | await _db.JsonSetAsync(key, 193 | "{\"string\":\"hello world\", \"integer\":5, \"boolean\": true, \"number\":4.7}"); 194 | 195 | var typeResult = await _db.JsonTypeAsync(key, path); 196 | 197 | Assert.Equal(value, typeResult.ToString()); 198 | } 199 | } 200 | 201 | public class JsonIncrementNumberAsync : BaseIntegrationTest 202 | { 203 | [Theory] 204 | [InlineData(".integer", 1, 2)] 205 | [InlineData(".number", .9, 2)] 206 | public async Task CanExecuteAsync(string path, double number, double expectedResult) 207 | { 208 | var key = Guid.NewGuid().ToString("N"); 209 | 210 | await _db.JsonSetAsync(key, "{\"integer\":1,\"number\":1.1}"); 211 | 212 | var result = await _db.JsonIncrementNumberAsync(key, path, number); 213 | 214 | Assert.Equal(expectedResult, (double) result, 2); 215 | } 216 | 217 | [Fact] 218 | public async Task CanExecuteOnMultiplePaths() 219 | { 220 | var key = Guid.NewGuid().ToString("N"); 221 | 222 | _db.JsonSet(key, "{\"a\":1,\"number\":{\"a\":2}}"); 223 | 224 | var result = (await _db.JsonIncrementNumberAsync(key, "$..a", 2)).ToList(); 225 | 226 | Assert.Equal(3, result.First()); 227 | Assert.Equal(4, result[1]); 228 | } 229 | } 230 | 231 | public class JsonMultiplyNumberAsync : BaseIntegrationTest 232 | { 233 | [Theory] 234 | [InlineData(".integer", 10, 10)] 235 | [InlineData(".number", .9, .99)] 236 | public async Task CanExecuteAsync(string path, double number, double expectedResult) 237 | { 238 | var key = Guid.NewGuid().ToString("N"); 239 | 240 | await _db.JsonSetAsync(key, "{\"integer\":1,\"number\":1.1}"); 241 | 242 | var result = await _db.JsonMultiplyNumberAsync(key, path, number); 243 | 244 | Assert.Equal(expectedResult, (double) result, 2); 245 | } 246 | 247 | [Fact] 248 | public async Task CanExecuteOnMultiplePaths() 249 | { 250 | var key = Guid.NewGuid().ToString("N"); 251 | 252 | _db.JsonSet(key, "{\"a\":1,\"number\":{\"a\":2}}"); 253 | 254 | var result = (await _db.JsonMultiplyNumberAsync(key, "$..a", 2)).ToList(); 255 | 256 | Assert.Equal(2, result.First()); 257 | Assert.Equal(4, result[1]); 258 | } 259 | } 260 | 261 | public class JsonAppendJsonStringAsync : BaseIntegrationTest 262 | { 263 | [Fact] 264 | public async Task CanExecuteAsync() 265 | { 266 | var key = Guid.NewGuid().ToString("N"); 267 | 268 | await _db.JsonSetAsync(key, "{\"hello\":\"world\"}"); 269 | 270 | var result = await _db.JsonAppendJsonStringAsync(key, ".hello", "\"!\""); 271 | 272 | Assert.Equal(6, result[0]); 273 | } 274 | 275 | [Fact] 276 | public async Task WillAppendProvidedJsonStringIntoExistingJsonString() 277 | { 278 | var key = Guid.NewGuid().ToString("N"); 279 | 280 | await _db.JsonSetAsync(key, "{\"hello\":\"world\"}"); 281 | 282 | await _db.JsonAppendJsonStringAsync(key, ".hello", "\"!\""); 283 | 284 | var helloValue = await _db.JsonGetAsync(key, ".hello"); 285 | 286 | Assert.Equal("world!", helloValue); 287 | } 288 | 289 | [Fact] 290 | public async Task WillAppendProvidedJsonStringIntoRootIfNoPathProvided() 291 | { 292 | var key = Guid.NewGuid().ToString("N"); 293 | 294 | await _db.JsonSetAsync(key, "\"world\""); 295 | 296 | await _db.JsonAppendJsonStringAsync(key, jsonString: "\"!\""); 297 | 298 | var helloValue = await _db.JsonGetAsync(key); 299 | 300 | Assert.Equal("world!", helloValue); 301 | } 302 | 303 | [Fact] 304 | public async Task CanAppendOnMultiplePaths() 305 | { 306 | var key = Guid.NewGuid().ToString("N"); 307 | 308 | await _db.JsonSetAsync(key, 309 | "{\"a\":\"foo\", \"nested\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}"); 310 | 311 | var result = await _db.JsonAppendJsonStringAsync(key, "$..a", "\"baz\""); 312 | 313 | Assert.Equal(3, result.Length); 314 | Assert.Equal(6, result[0]); 315 | Assert.Equal(8, result[1]); 316 | Assert.Null(result[2]); 317 | } 318 | } 319 | 320 | public class JsonStringLengthAsync : BaseIntegrationTest 321 | { 322 | [Fact] 323 | public async Task CanExecuteAsync() 324 | { 325 | var key = Guid.NewGuid().ToString("N"); 326 | 327 | await _db.JsonSetAsync(key, "{\"hello\":\"world\"}"); 328 | 329 | var result = await _db.JsonStringLengthAsync(key, ".hello"); 330 | 331 | Assert.Equal(5, result[0]); 332 | } 333 | 334 | [Fact] 335 | public async Task WillReturnNullIfPathDoesntExist() 336 | { 337 | var key = Guid.NewGuid().ToString("N"); 338 | 339 | await _db.JsonSetAsync(key, "{\"hello\":\"world\"}"); 340 | 341 | var result = await _db.JsonStringLengthAsync("doesnt_exist", ".hello.doesnt.exist"); 342 | 343 | Assert.Null(result); 344 | } 345 | 346 | [Fact] 347 | public async Task CanExecuteOnMultiplePaths() 348 | { 349 | var key = Guid.NewGuid().ToString("N"); 350 | 351 | await _db.JsonSetAsync(key, "{\"a\":\"foo\", \"nested\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}"); 352 | 353 | var result = await _db.JsonStringLengthAsync(key, "$..a"); 354 | 355 | Assert.Equal(3, result.Length); 356 | Assert.Equal(3, result[0]); 357 | Assert.Equal(5, result[1]); 358 | Assert.Null(result[2]); 359 | } 360 | } 361 | 362 | public class JsonArrayAppendAsync : BaseIntegrationTest 363 | { 364 | [Fact] 365 | public async Task CanExecuteAsync() 366 | { 367 | var key = Guid.NewGuid().ToString("N"); 368 | 369 | await _db.JsonSetAsync(key, "{\"array\": []}"); 370 | 371 | var result = await _db.JsonArrayAppendAsync(key, ".array", "\"hello\"", "\"world\"", 23); 372 | 373 | Assert.Equal(3, result[0]); 374 | } 375 | 376 | [Fact] 377 | public async Task CanExecuteOnMultipleMatchingPaths() 378 | { 379 | var key = Guid.NewGuid().ToString("N"); 380 | 381 | await _db.JsonSetAsync(key, "{\"a\":[1], \"nested\": {\"a\": [1,2]}, \"nested2\": {\"a\": 42}}"); 382 | 383 | var result = await _db.JsonArrayAppendAsync(key, "$..a", 3, 4); 384 | 385 | Assert.Equal(3, result.Length); 386 | 387 | Assert.Equal(3, result[0]); 388 | Assert.Equal(4, result[1]); 389 | Assert.Null(result[2]); 390 | } 391 | } 392 | 393 | public class JsonArrayIndexOfAsync : BaseIntegrationTest 394 | { 395 | [Fact] 396 | public async Task CanExecuteAsync() 397 | { 398 | var key = Guid.NewGuid().ToString(); 399 | 400 | await _db.JsonSetAsync(key, "{\"array\": [\"hi\", \"world\", \"!\", 1]}"); 401 | 402 | var result = await _db.JsonArrayIndexOfAsync(key, ".array", "\"world\"", 0, 2); 403 | 404 | Assert.Equal(1, result[0]); 405 | 406 | result = await _db.JsonArrayIndexOfAsync(key, ".array", 1); 407 | 408 | Assert.Equal(3, result[0]); 409 | } 410 | 411 | [Fact] 412 | public async Task CanExecuteOnMultipleMatchingPaths() 413 | { 414 | var key = Guid.NewGuid().ToString(); 415 | 416 | await _db.JsonSetAsync(key, "{\"a\":[1,2,3,2], \"nested\": {\"a\": [3,4]}}"); 417 | 418 | var result = await _db.JsonArrayIndexOfAsync(key, "$..a", 2); 419 | 420 | Assert.Equal(2, result.Length); 421 | 422 | Assert.Equal(1, result[0]); 423 | Assert.Equal(-1, result[1]); 424 | } 425 | } 426 | 427 | public class JsonArrayInsertAsync : BaseIntegrationTest 428 | { 429 | [Fact] 430 | public async Task CanExecuteAsync() 431 | { 432 | var key = Guid.NewGuid().ToString(); 433 | 434 | await _db.JsonSetAsync(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 435 | 436 | var result = await _db.JsonArrayInsertAsync(key, ".array", 1, "\"there\"", 6); 437 | 438 | Assert.Equal(5, result[0]); 439 | } 440 | 441 | [Fact] 442 | public async Task CanExecuteOnMultipleMatchingPaths() 443 | { 444 | var key = Guid.NewGuid().ToString(); 445 | 446 | await _db.JsonSetAsync(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 447 | 448 | var result = await _db.JsonArrayInsertAsync(key, "$..a", 0, 1, 2); 449 | 450 | Assert.Equal(2, result.Length); 451 | 452 | Assert.Equal(3, result[0]); 453 | Assert.Equal(4, result[1]); 454 | } 455 | } 456 | 457 | public class JsonArrayLengthAsync : BaseIntegrationTest 458 | { 459 | [Fact] 460 | public async Task CanExecute() 461 | { 462 | var key = Guid.NewGuid().ToString(); 463 | 464 | await _db.JsonSetAsync(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 465 | 466 | var result = await _db.JsonArrayLengthAsync(key, ".array"); 467 | 468 | Assert.Equal(3, result[0]); 469 | } 470 | 471 | [Fact] 472 | public async Task CanExecuteOnMultipleMatchingPaths() 473 | { 474 | var key = Guid.NewGuid().ToString(); 475 | 476 | await _db.JsonSetAsync(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 477 | 478 | var result = await _db.JsonArrayLengthAsync(key, "$..a"); 479 | 480 | Assert.Equal(2, result.Length); 481 | 482 | Assert.Equal(1, result[0]); 483 | Assert.Equal(2, result[1]); 484 | } 485 | 486 | [Fact] 487 | public async Task CanExecuteOnMultipleMatchingPathsWithOneNonArrayMatch() 488 | { 489 | var key = Guid.NewGuid().ToString(); 490 | 491 | await _db.JsonSetAsync(key, "{\"a\":[1,2,3,2], \"nested\": {\"a\": false}}"); 492 | 493 | var result = await _db.JsonArrayLengthAsync(key, "$..a"); 494 | 495 | Assert.Equal(2, result.Length); 496 | 497 | Assert.Equal(4, result[0]); 498 | Assert.Null(result[1]); 499 | } 500 | } 501 | 502 | public class JsonArrayPopAsync : BaseIntegrationTest 503 | { 504 | [Fact] 505 | public async Task CanExecuteAsync() 506 | { 507 | var key = Guid.NewGuid().ToString(); 508 | 509 | await _db.JsonSetAsync(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 510 | 511 | var result = await _db.JsonArrayPopAsync(key, ".array", 1); 512 | 513 | Assert.Equal("\"world\"", result[0]); 514 | } 515 | 516 | [Fact] 517 | public async Task CanExecuteWithSerializerAsync() 518 | { 519 | var key = Guid.NewGuid().ToString(); 520 | 521 | await _db.JsonSetAsync(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 522 | 523 | var result = await _db.JsonArrayPopAsync(key, ".array", 1); 524 | 525 | Assert.Equal("world", result[0]); 526 | } 527 | 528 | [Fact] 529 | public async Task CanExecuteOnMultipleMatchingPaths() 530 | { 531 | var key = Guid.NewGuid().ToString(); 532 | 533 | await _db.JsonSetAsync(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 534 | 535 | var result = await _db.JsonArrayPopAsync(key, "$..a"); 536 | 537 | Assert.Equal(2, result.Length); 538 | 539 | Assert.Equal("3", result[0]); 540 | Assert.Equal("4", result[1]); 541 | } 542 | 543 | [Fact] 544 | public async Task CanExecuteOnMultipleMatchingPathsWithNulls() 545 | { 546 | var key = Guid.NewGuid().ToString(); 547 | 548 | await _db.JsonSetAsync(key, "{\"a\":[\"foo\", \"bar\"], \"nested\": {\"a\": false}, \"nested2\": {\"a\":[]}}"); 549 | 550 | var result = await _db.JsonArrayPopAsync(key, "$..a"); 551 | 552 | Assert.Equal(3, result.Length); 553 | 554 | Assert.Equal("\"bar\"", result[0]); 555 | Assert.Null(result[1]); 556 | Assert.Null(result[2]); 557 | } 558 | 559 | [Fact] 560 | public async Task CanExecuteOnMultipleMatchingPathsWithSerializer() 561 | { 562 | var key = Guid.NewGuid().ToString(); 563 | 564 | await _db.JsonSetAsync(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 565 | 566 | var result = await _db.JsonArrayPopAsync(key, "$..a"); 567 | 568 | Assert.Equal(2, result.Length); 569 | 570 | Assert.Equal(3, result[0]); 571 | Assert.Equal(4, result[1]); 572 | } 573 | 574 | [Fact] 575 | public async Task CanExecuteOnMultipleMatchingPathsWithSerializerWithNulls() 576 | { 577 | var key = Guid.NewGuid().ToString(); 578 | 579 | await _db.JsonSetAsync(key, "{\"a\":[\"foo\", \"bar\"], \"nested\": {\"a\": false}, \"nested2\": {\"a\":[]}}"); 580 | 581 | var result = await _db.JsonArrayPopAsync(key, "$..a"); 582 | 583 | Assert.Equal(3, result.Length); 584 | 585 | Assert.Equal("bar", result[0]); 586 | Assert.Null(result[1]); 587 | Assert.Null(result[2]); 588 | } 589 | } 590 | 591 | public class JsonArrayTrimAsync : BaseIntegrationTest 592 | { 593 | [Fact] 594 | public async Task CanExecuteAsync() 595 | { 596 | var key = Guid.NewGuid().ToString(); 597 | 598 | await _db.JsonSetAsync(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 599 | 600 | var result = await _db.JsonArrayTrimAsync(key, ".array", 0, 1); 601 | 602 | Assert.Equal(2, result[0]); 603 | } 604 | 605 | [Fact] 606 | public async Task CanExecuteForMultiplePaths() 607 | { 608 | var key = Guid.NewGuid().ToString(); 609 | 610 | await _db.JsonSetAsync(key, "{\"a\":[1,2,3,2], \"nested\": {\"a\": false}}"); 611 | 612 | var result = await _db.JsonArrayTrimAsync(key, "$..a", 1, 1); 613 | 614 | Assert.Equal(2, result.Length); 615 | 616 | Assert.Equal(1, result[0]); 617 | Assert.Null(result[1]); 618 | } 619 | } 620 | 621 | public class JsonObjectKeysAsync : BaseIntegrationTest 622 | { 623 | [Fact] 624 | public async Task CanExecuteAsync() 625 | { 626 | var key = Guid.NewGuid().ToString(); 627 | 628 | await _db.JsonSetAsync(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 629 | 630 | var result = await _db.JsonObjectKeysAsync(key); 631 | 632 | Assert.Equal(new[] {"hello", "goodnight"}, result.Select(x => x.ToString()).ToArray()); 633 | } 634 | } 635 | 636 | public class JsonObjectLengthAsync : BaseIntegrationTest 637 | { 638 | [Fact] 639 | public async Task CanExecuteAsync() 640 | { 641 | var key = Guid.NewGuid().ToString(); 642 | 643 | await _db.JsonSetAsync(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 644 | 645 | var result = await _db.JsonObjectLengthAsync(key, ".goodnight"); 646 | 647 | Assert.Equal(1, result); 648 | } 649 | } 650 | 651 | public class JsonDebugMemoryAsync : BaseIntegrationTest 652 | { 653 | [Fact] 654 | public async Task CanExecuteAsync() 655 | { 656 | var key = Guid.NewGuid().ToString(); 657 | 658 | await _db.JsonSetAsync(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 659 | 660 | var result = await _db.JsonDebugMemoryAsync(key, ".goodnight"); 661 | 662 | Assert.True(result > 50); 663 | } 664 | } 665 | 666 | public class JsonGetRespAsync : BaseIntegrationTest 667 | { 668 | [Fact] 669 | public async Task CanExecuteAsync() 670 | { 671 | var key = Guid.NewGuid().ToString(); 672 | 673 | await _db.JsonSetAsync(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 674 | 675 | var result = (await _db.JsonGetRespAsync(key))[2]; 676 | 677 | Assert.Equal("world", result.ToString()); 678 | } 679 | } 680 | 681 | public class JsonToggleAsync : BaseIntegrationTest 682 | { 683 | [Fact] 684 | public async Task CanExecute() 685 | { 686 | var key = Guid.NewGuid().ToString(); 687 | 688 | await _db.JsonSetAsync(key, "{\"foo\":true}"); 689 | 690 | 691 | Assert.False(await _db.JsonToggleAsync(key, ".foo")); 692 | Assert.True(await _db.JsonToggleAsync(key, ".foo")); 693 | } 694 | } 695 | 696 | public class JsonClearAsync : BaseIntegrationTest 697 | { 698 | [Fact] 699 | public async Task CanExecute() 700 | { 701 | var key = Guid.NewGuid().ToString(); 702 | 703 | await _db.JsonSetAsync(key, "{\"foo\":[1,2,3,4]}"); 704 | 705 | Assert.Equal(1, await _db.JsonClearAsync(key, ".foo")); 706 | } 707 | } 708 | } 709 | } -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/DatabaseExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using NReJSON.IntegrationTests.Models; 2 | using StackExchange.Redis; 3 | using System; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace NReJSON.IntegrationTests 8 | { 9 | public class DatabaseExtensionTests 10 | { 11 | public class JsonSet : BaseIntegrationTest 12 | { 13 | [Fact] 14 | public void CanExecute() 15 | { 16 | var key = Guid.NewGuid().ToString("N"); 17 | 18 | var result = _db.JsonSet(key, "{}"); 19 | 20 | Assert.True(result); 21 | } 22 | 23 | [Fact] 24 | public void CanExecuteWithSerializer() 25 | { 26 | var key = Guid.NewGuid().ToString("N"); 27 | 28 | var obj = new ExampleHelloWorld 29 | { 30 | Hello = "World", 31 | 32 | GoodNight = new ExampleHelloWorld.InnerExample 33 | { 34 | Value = "Moon" 35 | } 36 | }; 37 | 38 | var result = _db.JsonSet(key, obj); 39 | 40 | Assert.True(result); 41 | } 42 | } 43 | 44 | public class JsonGet : BaseIntegrationTest 45 | { 46 | [Fact] 47 | public void CanExecute() 48 | { 49 | var key = Guid.NewGuid().ToString("N"); 50 | 51 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 52 | 53 | var result = _db.JsonGet(key); 54 | 55 | Assert.False(result.IsNull); 56 | } 57 | 58 | [Fact] 59 | public void CanExecuteWithIdent() 60 | { 61 | var key = Guid.NewGuid().ToString("N"); 62 | 63 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 64 | 65 | var result = _db.JsonGet(key, indent: "&"); 66 | 67 | Assert.Contains("&", (string) result); 68 | } 69 | 70 | [Fact] 71 | public void CanExecuteWithNewline() 72 | { 73 | var key = Guid.NewGuid().ToString("N"); 74 | 75 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 76 | 77 | var result = _db.JsonGet(key, newline: "="); 78 | 79 | Assert.Contains("=", (string) result); 80 | } 81 | 82 | [Fact] 83 | public void CanExecuteWithSpace() 84 | { 85 | var key = Guid.NewGuid().ToString("N"); 86 | 87 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 88 | 89 | var result = _db.JsonGet(key, space: "+"); 90 | 91 | Assert.Contains("+", (string) result); 92 | } 93 | 94 | [Fact] 95 | public void CanExecuteWithSerializer() 96 | { 97 | var key = Guid.NewGuid().ToString("N"); 98 | 99 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 100 | 101 | ExampleHelloWorld result = _db.JsonGet(key); 102 | 103 | Assert.NotNull(result); 104 | Assert.Equal("world", result.Hello); 105 | Assert.Equal("moon", result.GoodNight.Value); 106 | } 107 | 108 | [Fact] 109 | public void CanExecuteWithMultiplePathMatches() 110 | { 111 | var key = Guid.NewGuid().ToString("N"); 112 | 113 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"hello\": \"moon\"}}"); 114 | 115 | var result = _db.JsonGet(key, paths: "$..hello"); 116 | 117 | Assert.Equal("[\"world\",\"moon\"]", result.ToString()); 118 | } 119 | 120 | [Fact] 121 | public void CanExecuteWithMultiplePathMatchesWithSerializer() 122 | { 123 | var key = Guid.NewGuid().ToString("N"); 124 | 125 | _db.JsonSet(key, "{\r\n\"object\":{\r\n\"hello\":\"world\",\r\n\"goodnight\":{\r\n\"value\":\"moon\"\r\n}\r\n},\r\n\"nested\":{\r\n\"object\":{\r\n\"hello\": \"guy\",\r\n\"goodnight\": {\r\n\"value\": \"stuff\"\r\n}\r\n}\r\n}\r\n}"); 126 | 127 | var result = _db.JsonGet(key, paths: "$..object").ToList(); 128 | 129 | var firstResult = result[0]; 130 | var secondResult = result[1]; 131 | 132 | Assert.Equal("world", firstResult.Hello); 133 | Assert.Equal("guy", secondResult.Hello); 134 | } 135 | } 136 | 137 | public class JsonDelete : BaseIntegrationTest 138 | { 139 | [Fact] 140 | public void CanExecute() 141 | { 142 | var key = Guid.NewGuid().ToString("N"); 143 | 144 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 145 | 146 | var result = _db.JsonDelete(key, ".goodnight"); 147 | var jsonResult = _db.JsonGet(key); 148 | 149 | Assert.Equal(1, result); 150 | Assert.DoesNotContain("goodnight", jsonResult.ToString()); 151 | } 152 | } 153 | 154 | public class JsonMultiGet : BaseIntegrationTest 155 | { 156 | [Fact] 157 | public void CanExecute() 158 | { 159 | var key1 = Guid.NewGuid().ToString("N"); 160 | var key2 = Guid.NewGuid().ToString("N"); 161 | 162 | _db.JsonSet(key1, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 163 | _db.JsonSet(key2, "{\"hello\": \"tom\", \"goodnight\": {\"value\": \"tom\"}}"); 164 | 165 | var result = _db.JsonMultiGet(new RedisKey[] {key1, key2}); 166 | 167 | Assert.Equal(2, result.Length); 168 | Assert.Contains("world", result[0].ToString()); 169 | Assert.Contains("tom", result[1].ToString()); 170 | } 171 | 172 | [Fact] 173 | public void CanExecuteWithSerializer() 174 | { 175 | var key1 = Guid.NewGuid().ToString("N"); 176 | var key2 = Guid.NewGuid().ToString("N"); 177 | 178 | _db.JsonSet(key1, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 179 | _db.JsonSet(key2, "{\"hello\": \"tom\", \"goodnight\": {\"value\": \"tom\"}}"); 180 | 181 | var result = _db.JsonMultiGet(new RedisKey[] {key1, "say what?", key2}).ToList(); 182 | 183 | Assert.Equal(3, result.Count); 184 | Assert.Null(result[1]); 185 | Assert.Contains("world", result[0].Hello); 186 | Assert.Contains("tom", result[2].Hello); 187 | } 188 | } 189 | 190 | public class JsonType : BaseIntegrationTest 191 | { 192 | [Theory] 193 | [InlineData(".string", "string")] 194 | [InlineData(".integer", "integer")] 195 | [InlineData(".boolean", "boolean")] 196 | [InlineData(".number", "number")] 197 | public void CanExecute(string path, string value) 198 | { 199 | var key = Guid.NewGuid().ToString("N"); 200 | 201 | _db.JsonSet(key, "{\"string\":\"hello world\", \"integer\":5, \"boolean\": true, \"number\":4.7}"); 202 | 203 | var typeResult = _db.JsonType(key, path); 204 | 205 | Assert.Equal(value, typeResult.ToString()); 206 | } 207 | } 208 | 209 | public class JsonIncrementNumber : BaseIntegrationTest 210 | { 211 | [Theory] 212 | [InlineData(".integer", 1, 2)] 213 | [InlineData(".number", .9, 2)] 214 | public void CanExecute(string path, double number, double expectedResult) 215 | { 216 | var key = Guid.NewGuid().ToString("N"); 217 | 218 | _db.JsonSet(key, "{\"integer\":1,\"number\":1.1}"); 219 | 220 | var result = _db.JsonIncrementNumber(key, path, number); 221 | 222 | Assert.Equal(expectedResult, (double) result, 2); 223 | } 224 | 225 | [Fact] 226 | public void CanExecuteOnMultiplePaths() 227 | { 228 | var key = Guid.NewGuid().ToString("N"); 229 | 230 | _db.JsonSet(key, "{\"a\":1,\"number\":{\"a\":2}}"); 231 | 232 | var result = _db.JsonIncrementNumber(key, "$..a", 2).ToList(); 233 | 234 | Assert.Equal(3, result.First()); 235 | Assert.Equal(4, result[1]); 236 | } 237 | } 238 | 239 | public class JsonMultiplyNumber : BaseIntegrationTest 240 | { 241 | [Theory] 242 | [InlineData(".integer", 10, 10)] 243 | [InlineData(".number", .9, .99)] 244 | public void CanExecute(string path, double number, double expectedResult) 245 | { 246 | var key = Guid.NewGuid().ToString("N"); 247 | 248 | _db.JsonSet(key, "{\"integer\":1,\"number\":1.1}"); 249 | 250 | var result = _db.JsonMultiplyNumber(key, path, number); 251 | 252 | Assert.Equal(expectedResult, (double) result, 2); 253 | } 254 | 255 | [Fact] 256 | public void CanExecuteOnMultiplePaths() 257 | { 258 | var key = Guid.NewGuid().ToString("N"); 259 | 260 | _db.JsonSet(key, "{\"a\":1,\"number\":{\"a\":2}}"); 261 | 262 | var result = _db.JsonMultiplyNumber(key, "$..a", 2).ToList(); 263 | 264 | Assert.Equal(2, result.First()); 265 | Assert.Equal(4, result[1]); 266 | } 267 | } 268 | 269 | public class JsonAppendJsonString : BaseIntegrationTest 270 | { 271 | [Fact] 272 | public void CanExecuteAsync() 273 | { 274 | var key = Guid.NewGuid().ToString("N"); 275 | 276 | _db.JsonSet(key, "{\"hello\":\"world\"}"); 277 | 278 | var result = _db.JsonAppendJsonString(key, ".hello", "\"!\""); 279 | 280 | Assert.Equal(6, result[0].Value); 281 | } 282 | 283 | [Fact] 284 | public void WillAppendProvidedJsonStringIntoExistingJsonString() 285 | { 286 | var key = Guid.NewGuid().ToString("N"); 287 | 288 | _db.JsonSet(key, "{\"hello\":\"world\"}"); 289 | 290 | _db.JsonAppendJsonString(key, ".hello", "\"!\""); 291 | 292 | var helloValue = _db.JsonGet(key, ".hello"); 293 | 294 | Assert.Equal("world!", helloValue); 295 | } 296 | 297 | [Fact] 298 | public void WillAppendProvidedJsonStringIntoRootIfNoPathProvided() 299 | { 300 | var key = Guid.NewGuid().ToString("N"); 301 | 302 | _db.JsonSet(key, "\"world\""); 303 | 304 | _db.JsonAppendJsonString(key, jsonString: "\"!\""); 305 | 306 | var helloValue = _db.JsonGet(key); 307 | 308 | Assert.Equal("world!", helloValue); 309 | } 310 | 311 | [Fact] 312 | public void CanAppendOnMultiplePaths() 313 | { 314 | var key = Guid.NewGuid().ToString("N"); 315 | 316 | _db.JsonSet(key, 317 | "{\"a\":\"foo\", \"nested\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}"); 318 | 319 | var result = _db.JsonAppendJsonString(key, "$..a", "\"baz\""); 320 | 321 | Assert.Equal(3, result.Length); 322 | Assert.Equal(6, result[0]); 323 | Assert.Equal(8, result[1]); 324 | Assert.Null(result[2]); 325 | } 326 | } 327 | 328 | public class JsonStringLength : BaseIntegrationTest 329 | { 330 | [Fact] 331 | public void CanExecute() 332 | { 333 | var key = Guid.NewGuid().ToString("N"); 334 | 335 | _db.JsonSet(key, "{\"hello\":\"world\"}"); 336 | 337 | var result = _db.JsonStringLength(key, ".hello"); 338 | 339 | Assert.Equal(5, result[0]); 340 | } 341 | 342 | [Fact] 343 | public void WillReturnNullIfPathDoesntExist() 344 | { 345 | var key = Guid.NewGuid().ToString("N"); 346 | 347 | _db.JsonSet(key, "{\"hello\":\"world\"}"); 348 | 349 | var result = _db.JsonStringLength("doesnt_exist", ".hello.doesnt.exist"); 350 | 351 | Assert.Null(result); 352 | } 353 | 354 | [Fact] 355 | public void CanExecuteOnMultiplePaths() 356 | { 357 | var key = Guid.NewGuid().ToString("N"); 358 | 359 | _db.JsonSet(key, "{\"a\":\"foo\", \"nested\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}"); 360 | 361 | var result = _db.JsonStringLength(key, "$..a"); 362 | 363 | Assert.Equal(3, result.Length); 364 | Assert.Equal(3, result[0]); 365 | Assert.Equal(5, result[1]); 366 | Assert.Null(result[2]); 367 | } 368 | } 369 | 370 | public class JsonArrayAppend : BaseIntegrationTest 371 | { 372 | [Fact] 373 | public void CanExecute() 374 | { 375 | var key = Guid.NewGuid().ToString("N"); 376 | 377 | _db.JsonSet(key, "{\"array\": []}"); 378 | 379 | var result = _db.JsonArrayAppend(key, ".array", "\"hello\"", "\"world\"", 2); 380 | 381 | Assert.Equal(3, result[0]); 382 | } 383 | 384 | [Fact] 385 | public void CanExecuteOnMultipleMatchingPaths() 386 | { 387 | var key = Guid.NewGuid().ToString("N"); 388 | 389 | _db.JsonSet(key, "{\"a\":[1], \"nested\": {\"a\": [1,2]}, \"nested2\": {\"a\": 42}}"); 390 | 391 | var result = _db.JsonArrayAppend(key, "$..a", 3, 4); 392 | 393 | Assert.Equal(3, result.Length); 394 | 395 | Assert.Equal(3, result[0]); 396 | Assert.Equal(4, result[1]); 397 | Assert.Null(result[2]); 398 | } 399 | } 400 | 401 | public class JsonArrayIndexOf : BaseIntegrationTest 402 | { 403 | [Fact] 404 | public void CanExecute() 405 | { 406 | var key = Guid.NewGuid().ToString(); 407 | 408 | _db.JsonSet(key, "{\"array\": [\"hi\", \"world\", \"!\", 2]}"); 409 | 410 | var result = _db.JsonArrayIndexOf(key, ".array", "\"world\"", 0, 2); 411 | 412 | Assert.Equal(1, result[0]); 413 | 414 | result = _db.JsonArrayIndexOf(key, ".array", 2, 0, 4); 415 | 416 | Assert.Equal(3, result[0]); 417 | } 418 | 419 | [Fact] 420 | public void CanExecuteOnMultipleMatchingPaths() 421 | { 422 | var key = Guid.NewGuid().ToString(); 423 | 424 | _db.JsonSet(key, "{\"a\":[1,2,3,2], \"nested\": {\"a\": [3,4]}}"); 425 | 426 | var result = _db.JsonArrayIndexOf(key, "$..a", 2); 427 | 428 | Assert.Equal(2, result.Length); 429 | 430 | Assert.Equal(1, result[0]); 431 | Assert.Equal(-1, result[1]); 432 | } 433 | } 434 | 435 | public class JsonArrayInsert : BaseIntegrationTest 436 | { 437 | [Fact] 438 | public void CanExecute() 439 | { 440 | var key = Guid.NewGuid().ToString(); 441 | 442 | _db.JsonSet(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 443 | 444 | var result = _db.JsonArrayInsert(key, ".array", 1, "\"there\"", 2); 445 | 446 | Assert.Equal(5, result[0]); 447 | } 448 | 449 | [Fact] 450 | public void CanExecuteOnMultipleMatchingPaths() 451 | { 452 | var key = Guid.NewGuid().ToString(); 453 | 454 | _db.JsonSet(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 455 | 456 | var result = _db.JsonArrayInsert(key, "$..a", 0, 1, 2); 457 | 458 | Assert.Equal(2, result.Length); 459 | 460 | Assert.Equal(3, result[0]); 461 | Assert.Equal(4, result[1]); 462 | } 463 | } 464 | 465 | public class JsonArrayLength : BaseIntegrationTest 466 | { 467 | [Fact] 468 | public void CanExecute() 469 | { 470 | var key = Guid.NewGuid().ToString(); 471 | 472 | _db.JsonSet(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 473 | 474 | var result = _db.JsonArrayLength(key, ".array"); 475 | 476 | Assert.Equal(3, result[0]); 477 | } 478 | 479 | [Fact] 480 | public void CanExecuteOnMultipleMatchingPaths() 481 | { 482 | var key = Guid.NewGuid().ToString(); 483 | 484 | _db.JsonSet(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 485 | 486 | var result = _db.JsonArrayLength(key, "$..a"); 487 | 488 | Assert.Equal(2, result.Length); 489 | 490 | Assert.Equal(1, result[0]); 491 | Assert.Equal(2, result[1]); 492 | } 493 | 494 | [Fact] 495 | public void CanExecuteOnMultipleMatchingPathsWithOneNonArrayMatch() 496 | { 497 | var key = Guid.NewGuid().ToString(); 498 | 499 | _db.JsonSet(key, "{\"a\":[1,2,3,2], \"nested\": {\"a\": false}}"); 500 | 501 | var result = _db.JsonArrayLength(key, "$..a"); 502 | 503 | Assert.Equal(2, result.Length); 504 | 505 | Assert.Equal(4, result[0]); 506 | Assert.Null(result[1]); 507 | } 508 | } 509 | 510 | public class JsonArrayPop : BaseIntegrationTest 511 | { 512 | [Fact] 513 | public void CanExecute() 514 | { 515 | var key = Guid.NewGuid().ToString(); 516 | 517 | _db.JsonSet(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 518 | 519 | var result = _db.JsonArrayPop(key, ".array", 1); 520 | 521 | Assert.Equal("\"world\"", result[0]); 522 | } 523 | 524 | [Fact] 525 | public void CanExecuteWithSerializer() 526 | { 527 | var key = Guid.NewGuid().ToString(); 528 | 529 | _db.JsonSet(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 530 | 531 | var result = _db.JsonArrayPop(key, ".array", 1); 532 | 533 | Assert.Equal("world", result[0]); 534 | } 535 | 536 | [Fact] 537 | public void CanExecuteOnMultipleMatchingPaths() 538 | { 539 | var key = Guid.NewGuid().ToString(); 540 | 541 | _db.JsonSet(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 542 | 543 | var result = _db.JsonArrayPop(key, "$..a"); 544 | 545 | Assert.Equal(2, result.Length); 546 | 547 | Assert.Equal("3", result[0]); 548 | Assert.Equal("4", result[1]); 549 | } 550 | 551 | [Fact] 552 | public void CanExecuteOnMultipleMatchingPathsWithNulls() 553 | { 554 | var key = Guid.NewGuid().ToString(); 555 | 556 | _db.JsonSet(key, "{\"a\":[\"foo\", \"bar\"], \"nested\": {\"a\": false}, \"nested2\": {\"a\":[]}}"); 557 | 558 | var result = _db.JsonArrayPop(key, "$..a"); 559 | 560 | Assert.Equal(3, result.Length); 561 | 562 | Assert.Equal("\"bar\"", result[0]); 563 | Assert.Null(result[1]); 564 | Assert.Null(result[2]); 565 | } 566 | 567 | [Fact] 568 | public void CanExecuteOnMultipleMatchingPathsWithSerializer() 569 | { 570 | var key = Guid.NewGuid().ToString(); 571 | 572 | _db.JsonSet(key, "{\"a\":[3], \"nested\": {\"a\": [3,4]}}"); 573 | 574 | var result = _db.JsonArrayPop(key, "$..a"); 575 | 576 | Assert.Equal(2, result.Length); 577 | 578 | Assert.Equal(3, result[0]); 579 | Assert.Equal(4, result[1]); 580 | } 581 | 582 | [Fact] 583 | public void CanExecuteOnMultipleMatchingPathsWithSerializerWithNulls() 584 | { 585 | var key = Guid.NewGuid().ToString(); 586 | 587 | _db.JsonSet(key, "{\"a\":[\"foo\", \"bar\"], \"nested\": {\"a\": false}, \"nested2\": {\"a\":[]}}"); 588 | 589 | var result = _db.JsonArrayPop(key, "$..a"); 590 | 591 | Assert.Equal(3, result.Length); 592 | 593 | Assert.Equal("bar", result[0]); 594 | Assert.Null(result[1]); 595 | Assert.Null(result[2]); 596 | } 597 | } 598 | 599 | public class JsonArrayTrim : BaseIntegrationTest 600 | { 601 | [Fact] 602 | public void CanExecute() 603 | { 604 | var key = Guid.NewGuid().ToString(); 605 | 606 | _db.JsonSet(key, "{\"array\": [\"hi\", \"world\", \"!\"]}"); 607 | 608 | var result = _db.JsonArrayTrim(key, ".array", 0, 1); 609 | 610 | Assert.Equal(2, result[0]); 611 | } 612 | 613 | [Fact] 614 | public void CanExecuteForMultiplePaths() 615 | { 616 | var key = Guid.NewGuid().ToString(); 617 | 618 | _db.JsonSet(key, "{\"a\":[1,2,3,2], \"nested\": {\"a\": false}}"); 619 | 620 | var result = _db.JsonArrayTrim(key, "$..a", 1, 1); 621 | 622 | Assert.Equal(2, result.Length); 623 | 624 | Assert.Equal(1, result[0]); 625 | Assert.Null(result[1]); 626 | } 627 | } 628 | 629 | public class JsonObjectKeys : BaseIntegrationTest 630 | { 631 | [Fact] 632 | public void CanExecute() 633 | { 634 | var key = Guid.NewGuid().ToString(); 635 | 636 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 637 | 638 | var result = _db.JsonObjectKeys(key); 639 | 640 | Assert.Equal(new[] {"hello", "goodnight"}, result.Select(x => x.ToString()).ToArray()); 641 | } 642 | } 643 | 644 | public class JsonObjectLength : BaseIntegrationTest 645 | { 646 | [Fact] 647 | public void CanExecute() 648 | { 649 | var key = Guid.NewGuid().ToString(); 650 | 651 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 652 | 653 | var result = _db.JsonObjectLength(key, ".goodnight"); 654 | 655 | Assert.Equal(1, result); 656 | } 657 | } 658 | 659 | public class JsonDebugMemory : BaseIntegrationTest 660 | { 661 | [Fact] 662 | public void CanExecute() 663 | { 664 | var key = Guid.NewGuid().ToString(); 665 | 666 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 667 | 668 | var result = _db.JsonDebugMemory(key, ".goodnight"); 669 | 670 | Assert.True(result > 50); 671 | } 672 | } 673 | 674 | public class JsonGetResp : BaseIntegrationTest 675 | { 676 | [Fact] 677 | public void CanExecute() 678 | { 679 | var key = Guid.NewGuid().ToString(); 680 | 681 | _db.JsonSet(key, "{\"hello\": \"world\", \"goodnight\": {\"value\": \"moon\"}}"); 682 | 683 | var result = _db.JsonGetResp(key)[2]; 684 | 685 | Assert.Equal("world", result.ToString()); 686 | } 687 | } 688 | 689 | public class JsonToggle : BaseIntegrationTest 690 | { 691 | [Fact] 692 | public void CanExecute() 693 | { 694 | var key = Guid.NewGuid().ToString(); 695 | 696 | _db.JsonSet(key, "{\"foo\":true}"); 697 | 698 | Assert.False(_db.JsonToggle(key, ".foo")); 699 | Assert.True(_db.JsonToggle(key, ".foo")); 700 | } 701 | } 702 | 703 | public class JsonClear : BaseIntegrationTest 704 | { 705 | [Fact] 706 | public void CanExecute() 707 | { 708 | var key = Guid.NewGuid().ToString(); 709 | 710 | _db.JsonSet(key, "{\"foo\":[1,2,3,4]}"); 711 | 712 | Assert.Equal(1, _db.JsonClear(key, ".foo")); 713 | } 714 | } 715 | } 716 | } -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/Models/ExampleHelloWorld.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NReJSON.IntegrationTests.Models 4 | { 5 | public class ExampleHelloWorld 6 | { 7 | public class InnerExample 8 | { 9 | [JsonPropertyName("value")] 10 | public string Value { get; set; } 11 | } 12 | 13 | [JsonPropertyName("hello")] 14 | public string Hello { get; set; } 15 | 16 | [JsonPropertyName("goodnight")] 17 | public InnerExample GoodNight { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/Models/ExamplePerson.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace NReJSON.IntegrationTests.Models 4 | { 5 | public class ExamplePerson 6 | { 7 | [JsonPropertyName("first")] 8 | public string FirstName { get; set; } 9 | 10 | [JsonPropertyName("last")] 11 | public string LastName { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/NReJSON.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp5 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /NReJSON.IntegrationTests/TestJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | using System.Text.Json; 3 | 4 | namespace NReJSON.IntegrationTests 5 | { 6 | public sealed class TestJsonSerializer : ISerializerProxy 7 | { 8 | public TResult Deserialize(RedisResult serializedValue) => 9 | JsonSerializer.Deserialize(serializedValue.ToString()); 10 | 11 | public string Serialize(TObjectType obj) => 12 | JsonSerializer.Serialize(obj); 13 | } 14 | } -------------------------------------------------------------------------------- /NReJSON.Tests/DatabaseExtensionsAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace NReJSON.Tests 5 | { 6 | public class DatabaseExtensionsAsyncTests 7 | { 8 | public class JsonDeleteAsync 9 | { 10 | [Fact] 11 | public async Task EmitsCorrectParameters() 12 | { 13 | var db = new FakeDatabase(); 14 | 15 | await db.JsonDeleteAsync("fake_key", ".fakeObject"); 16 | 17 | Assert.Equal(new[] { "JSON.DEL", "fake_key", ".fakeObject" }, db.PreviousCommand); 18 | } 19 | 20 | [Fact] 21 | public async Task HasRootAsDefaultPath() 22 | { 23 | var db = new FakeDatabase(); 24 | 25 | await db.JsonDeleteAsync("fake_key"); 26 | 27 | Assert.Equal(new[] { "JSON.DEL", "fake_key", "." }, db.PreviousCommand); 28 | } 29 | } 30 | 31 | public class JsonGetAsync 32 | { 33 | [Fact] 34 | public async Task EmitsCorrectParameters() 35 | { 36 | var db = new FakeDatabase(); 37 | 38 | await db.JsonGetAsync("fake_key", ".firstPath", ".firstPath.secondItem"); 39 | 40 | Assert.Equal(new[] { "JSON.GET", "fake_key", "NOESCAPE", ".firstPath", ".firstPath.secondItem" }, db.PreviousCommand); 41 | } 42 | 43 | [Fact] 44 | public async Task HasRootAsDefaultPath() 45 | { 46 | var db = new FakeDatabase(); 47 | 48 | await db.JsonGetAsync("fake_key"); 49 | 50 | Assert.Equal(new[] { "JSON.GET", "fake_key", "NOESCAPE", "." }, db.PreviousCommand); 51 | } 52 | 53 | [Fact] 54 | public async Task CanSpecifyEscaping() 55 | { 56 | var db = new FakeDatabase(); 57 | 58 | await db.JsonGetAsync("fake_key", false); 59 | 60 | Assert.Equal(new[] { "JSON.GET", "fake_key", "." }, db.PreviousCommand); 61 | } 62 | 63 | [Fact] 64 | public async Task CanSpecifyIndent() 65 | { 66 | var db = new FakeDatabase(); 67 | 68 | await db.JsonGetAsync("fake_key", indent: "\t\t"); 69 | 70 | Assert.Equal(new[] { "JSON.GET", "fake_key", "INDENT", "\t\t", "." }, db.PreviousCommand); 71 | } 72 | 73 | [Fact] 74 | public async Task CanSpecifySpace() 75 | { 76 | var db = new FakeDatabase(); 77 | 78 | await db.JsonGetAsync("fake_key", space: " "); 79 | 80 | Assert.Equal(new[] { "JSON.GET", "fake_key", "SPACE", " ", "." }, db.PreviousCommand); 81 | } 82 | 83 | [Fact] 84 | public async Task CanSpecifyNewLine() 85 | { 86 | var db = new FakeDatabase(); 87 | 88 | await db.JsonGetAsync("fake_key", newline: "\r\n"); 89 | 90 | Assert.Equal(new[] { "JSON.GET", "fake_key", "NEWLINE", "\r\n", "." }, db.PreviousCommand); 91 | } 92 | 93 | [Fact] 94 | public async Task CanSpecifyIndentSpaceAndNewLine() 95 | { 96 | var db = new FakeDatabase(); 97 | 98 | await db.JsonGetAsync("fake_key", indent: "\t", newline: "\r\n", space: " "); 99 | 100 | Assert.Equal(new[] { "JSON.GET", "fake_key", "INDENT", "\t", "NEWLINE", "\r\n", "SPACE", " ", "." }, db.PreviousCommand); 101 | } 102 | } 103 | 104 | public class JsonMultiGetAsync 105 | { 106 | [Fact] 107 | public async Task EmitsCorrectParameters() 108 | { 109 | var db = new FakeDatabase(true); 110 | 111 | await db.JsonMultiGetAsync(new[] { "fake_key::1", "fake_key::2" }, ".super.fake.path"); 112 | 113 | Assert.Equal(new[] { "JSON.MGET", "fake_key::1", "fake_key::2", ".super.fake.path" }, db.PreviousCommand); 114 | } 115 | 116 | [Fact] 117 | public async Task HasRootAsDefaultPath() 118 | { 119 | var db = new FakeDatabase(true); 120 | 121 | await db.JsonMultiGetAsync(new[] { "fake_key::1", "fake_key::2" }); 122 | 123 | Assert.Equal(new[] { "JSON.MGET", "fake_key::1", "fake_key::2", "." }, db.PreviousCommand); 124 | } 125 | } 126 | 127 | public class JsonSetAsync 128 | { 129 | [Fact] 130 | public async Task EmitsCorrectParameters() 131 | { 132 | var db = new FakeDatabase(); 133 | 134 | await db.JsonSetAsync("fake_key", "{\"hello\":\"world\"}", ".fake"); 135 | 136 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".fake", "{\"hello\":\"world\"}" }, db.PreviousCommand); 137 | } 138 | 139 | [Fact] 140 | public async Task HasRootAsDefaultPath() 141 | { 142 | var db = new FakeDatabase(); 143 | 144 | await db.JsonSetAsync("fake_key", "{\"hello\":\"world\"}"); 145 | 146 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".", "{\"hello\":\"world\"}" }, db.PreviousCommand); 147 | } 148 | 149 | [Fact] 150 | public async Task SetIfNotExistsIsProperlyEmitted() 151 | { 152 | var db = new FakeDatabase(); 153 | 154 | await db.JsonSetAsync("fake_key", "{\"hello\":\"world\"}", setOption: SetOption.SetIfNotExists); 155 | 156 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".", "{\"hello\":\"world\"}", "NX" }, db.PreviousCommand); 157 | } 158 | 159 | [Fact] 160 | public async Task SetOnlyIfExistsIsProperlyEmitted() 161 | { 162 | var db = new FakeDatabase(); 163 | 164 | await db.JsonSetAsync("fake_key", "{\"hello\":\"world\"}", setOption: SetOption.SetOnlyIfExists); 165 | 166 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".", "{\"hello\":\"world\"}", "XX" }, db.PreviousCommand); 167 | } 168 | } 169 | 170 | public class JsonTypeAsync 171 | { 172 | [Fact] 173 | public async Task EmitsCorrectParameters() 174 | { 175 | var db = new FakeDatabase(); 176 | 177 | await db.JsonTypeAsync("fake_key", ".fakePath"); 178 | 179 | Assert.Equal(new[] { "JSON.TYPE", "fake_key", ".fakePath" }, db.PreviousCommand); 180 | } 181 | 182 | [Fact] 183 | public async Task HasRootAsDefaultPath() 184 | { 185 | var db = new FakeDatabase(); 186 | 187 | await db.JsonTypeAsync("fake_key"); 188 | 189 | Assert.Equal(new[] { "JSON.TYPE", "fake_key", "." }, db.PreviousCommand); 190 | } 191 | } 192 | 193 | public class JsonIncrementNumberAsync 194 | { 195 | [Fact] 196 | public async Task EmitsCorrectParameters() 197 | { 198 | var db = new FakeDatabase(); 199 | 200 | await db.JsonIncrementNumberAsync("fake_key", ".fake.path", 2); 201 | 202 | Assert.Equal(new[] { "JSON.NUMINCRBY", "fake_key", ".fake.path", "2" }, db.PreviousCommand); 203 | } 204 | } 205 | 206 | public class JsonMultiplyNumberAsync 207 | { 208 | [Fact] 209 | public async Task EmitsCorrectParameters() 210 | { 211 | var db = new FakeDatabase(); 212 | 213 | await db.JsonMultiplyNumberAsync("fake_key", ".fake.path", 5); 214 | 215 | Assert.Equal(new[] { "JSON.NUMMULTBY", "fake_key", ".fake.path", "5" }, db.PreviousCommand); 216 | } 217 | } 218 | 219 | public class JsonAppendJsonStringAsync 220 | { 221 | [Fact] 222 | public async Task EmitsCorrectParameters() 223 | { 224 | var db = new FakeDatabase(); 225 | 226 | await db.JsonAppendJsonStringAsync("fake_key", ".fake.path", "\"fake_string\""); 227 | 228 | Assert.Equal(new[] { "JSON.STRAPPEND", "fake_key", ".fake.path", "\"fake_string\"" }, db.PreviousCommand); 229 | } 230 | 231 | [Fact] 232 | public async Task HasRootAsDefaultPath() 233 | { 234 | var db = new FakeDatabase(); 235 | 236 | await db.JsonAppendJsonStringAsync("fake_key", jsonString: "\"fake_string\""); 237 | 238 | Assert.Equal(new[] { "JSON.STRAPPEND", "fake_key", ".", "\"fake_string\"" }, db.PreviousCommand); 239 | } 240 | } 241 | 242 | public class JsonStringLengthAsync 243 | { 244 | [Fact] 245 | public async Task EmitsCorrectParameters() 246 | { 247 | var db = new FakeDatabase(); 248 | 249 | await db.JsonStringLengthAsync("fake_key", ".fake.path"); 250 | 251 | Assert.Equal(new[] { "JSON.STRLEN", "fake_key", ".fake.path" }, db.PreviousCommand); 252 | } 253 | 254 | [Fact] 255 | public async Task HasRootAsDefaultPath() 256 | { 257 | var db = new FakeDatabase(); 258 | 259 | await db.JsonStringLengthAsync("fake_key"); 260 | 261 | Assert.Equal(new[] { "JSON.STRLEN", "fake_key", "." }, db.PreviousCommand); 262 | } 263 | } 264 | 265 | public class JsonArrayAppendAsync 266 | { 267 | [Fact] 268 | public async Task EmitsCorrectParameters() 269 | { 270 | var db = new FakeDatabase(); 271 | 272 | await db.JsonArrayAppendAsync("fake_key", ".fake.path", "\"1\"", "\"2\""); 273 | 274 | Assert.Equal(new[] { "JSON.ARRAPPEND", "fake_key", ".fake.path", "\"1\"", "\"2\"" }, db.PreviousCommand); 275 | } 276 | } 277 | 278 | public class JsonArrayIndexOfAsync 279 | { 280 | [Fact] 281 | public async Task EmitsCorrectParameters() 282 | { 283 | var db = new FakeDatabase(); 284 | 285 | await db.JsonArrayIndexOfAsync("fake_key", ".fake.path", "\"hello world\"", 10, 20); 286 | 287 | Assert.Equal(new[] { "JSON.ARRINDEX", "fake_key", ".fake.path", "\"hello world\"", "10", "20" }, db.PreviousCommand); 288 | } 289 | 290 | [Fact] 291 | public async Task HasZeroAsDefaultForStartAndStop() 292 | { 293 | var db = new FakeDatabase(); 294 | 295 | await db.JsonArrayIndexOfAsync("fake_key", ".fake.path", "\"hello world\""); 296 | 297 | Assert.Equal(new[] { "JSON.ARRINDEX", "fake_key", ".fake.path", "\"hello world\"", "0", "0" }, db.PreviousCommand); 298 | } 299 | } 300 | 301 | public class JsonArrayInsertAsync 302 | { 303 | [Fact] 304 | public async Task EmitsCorrectParameters() 305 | { 306 | var db = new FakeDatabase(); 307 | 308 | await db.JsonArrayInsertAsync("fake_key", ".fake.path", 15, "\"hello\"", "\"world\""); 309 | 310 | Assert.Equal(new[] { "JSON.ARRINSERT", "fake_key", ".fake.path", "15", "\"hello\"", "\"world\"" }, db.PreviousCommand); 311 | } 312 | } 313 | 314 | public class JsonArrayLengthAsync 315 | { 316 | [Fact] 317 | public async Task EmitsCorrectParameters() 318 | { 319 | var db = new FakeDatabase(); 320 | 321 | await db.JsonArrayLengthAsync("fake_key", ".fake.array.path"); 322 | 323 | Assert.Equal(new[] { "JSON.ARRLEN", "fake_key", ".fake.array.path" }, db.PreviousCommand); 324 | } 325 | 326 | [Fact] 327 | public async Task HasRootAsDefaultPath() 328 | { 329 | var db = new FakeDatabase(); 330 | 331 | await db.JsonArrayLengthAsync("fake_key"); 332 | 333 | Assert.Equal(new[] { "JSON.ARRLEN", "fake_key", "." }, db.PreviousCommand); 334 | } 335 | } 336 | 337 | public class JsonArrayPopAsync 338 | { 339 | [Fact] 340 | public async Task EmitsCorrectParameters() 341 | { 342 | var db = new FakeDatabase(expectArrayResult: true); 343 | 344 | await db.JsonArrayPopAsync("fake_key", ".what.ever", 10); 345 | 346 | Assert.Equal(new[] { "JSON.ARRPOP", "fake_key", ".what.ever", "10" }, db.PreviousCommand); 347 | } 348 | 349 | [Fact] 350 | public async Task HasRootAsDefaultPath() 351 | { 352 | var db = new FakeDatabase(expectArrayResult: true); 353 | 354 | await db.JsonArrayPopAsync("fake_key", index: 10); 355 | 356 | Assert.Equal(new[] { "JSON.ARRPOP", "fake_key", ".", "10" }, db.PreviousCommand); 357 | } 358 | 359 | [Fact] 360 | public async Task HasNegativeOneAsDefaultIndex() 361 | { 362 | var db = new FakeDatabase(expectArrayResult: true); 363 | 364 | await db.JsonArrayPopAsync("fake_key", ".what.ever"); 365 | 366 | Assert.Equal(new[] { "JSON.ARRPOP", "fake_key", ".what.ever", "-1" }, db.PreviousCommand); 367 | } 368 | } 369 | 370 | public class JsonArrayTrimAsync 371 | { 372 | [Fact] 373 | public async Task EmitsCorrectParameters() 374 | { 375 | var db = new FakeDatabase(); 376 | 377 | await db.JsonArrayTrimAsync("fake_key", ".fake.path", 1, 10); 378 | 379 | Assert.Equal(new[] { "JSON.ARRTRIM", "fake_key", ".fake.path", "1", "10" }, db.PreviousCommand); 380 | } 381 | } 382 | 383 | public class JsonObjectKeysAsync 384 | { 385 | [Fact] 386 | public async Task EmitsCorrectParameters() 387 | { 388 | var db = new FakeDatabase(true); 389 | 390 | await db.JsonObjectKeysAsync("fake_key", ".fake.path"); 391 | 392 | Assert.Equal(new[] { "JSON.OBJKEYS", "fake_key", ".fake.path" }, db.PreviousCommand); 393 | } 394 | 395 | [Fact] 396 | public async Task HasRootAsDefaultPath() 397 | { 398 | var db = new FakeDatabase(true); 399 | 400 | await db.JsonObjectKeysAsync("fake_key"); 401 | 402 | Assert.Equal(new[] { "JSON.OBJKEYS", "fake_key", "." }, db.PreviousCommand); 403 | } 404 | } 405 | 406 | public class JsonObjectLengthAsync 407 | { 408 | [Fact] 409 | public async Task EmitsCorrectParameters() 410 | { 411 | var db = new FakeDatabase(); 412 | 413 | await db.JsonObjectLengthAsync("fake_key", ".fake.path"); 414 | 415 | Assert.Equal(new[] { "JSON.OBJLEN", "fake_key", ".fake.path" }, db.PreviousCommand); 416 | } 417 | 418 | [Fact] 419 | public async Task HasRootAsDefaultPath() 420 | { 421 | var db = new FakeDatabase(); 422 | 423 | await db.JsonObjectLengthAsync("fake_key"); 424 | 425 | Assert.Equal(new[] { "JSON.OBJLEN", "fake_key", "." }, db.PreviousCommand); 426 | } 427 | } 428 | 429 | public class JsonDebugMemoryAsync 430 | { 431 | [Fact] 432 | public async Task EmitsCorrectParameters() 433 | { 434 | var db = new FakeDatabase(); 435 | 436 | await db.JsonDebugMemoryAsync("fake_key", ".fake.path"); 437 | 438 | Assert.Equal(new[] { "JSON.DEBUG", "MEMORY", "fake_key", ".fake.path" }, db.PreviousCommand); 439 | } 440 | 441 | [Fact] 442 | public async Task HasRootAsDefaultPath() 443 | { 444 | var db = new FakeDatabase(); 445 | 446 | await db.JsonDebugMemoryAsync("fake_key"); 447 | 448 | Assert.Equal(new[] { "JSON.DEBUG", "MEMORY", "fake_key", "." }, db.PreviousCommand); 449 | } 450 | } 451 | 452 | public class JsonGetRespAsync 453 | { 454 | [Fact] 455 | public async Task EmitsCorrectParameters() 456 | { 457 | var db = new FakeDatabase(true); 458 | 459 | await db.JsonGetRespAsync("fake_key", ".hello.fake"); 460 | 461 | Assert.Equal(new[] { "JSON.RESP", "fake_key", ".hello.fake" }, db.PreviousCommand); 462 | } 463 | 464 | [Fact] 465 | public async Task HasRootAsDefaultPath() 466 | { 467 | var db = new FakeDatabase(true); 468 | 469 | await db.JsonGetRespAsync("fake_key"); 470 | 471 | Assert.Equal(new[] { "JSON.RESP", "fake_key", "." }, db.PreviousCommand); 472 | } 473 | } 474 | 475 | public class JsonToggleAsync 476 | { 477 | [Fact] 478 | public async Task EmitsCorrectParameters() 479 | { 480 | var db = new FakeDatabase(expectBoolResult: true); 481 | 482 | await db.JsonToggleAsync("whatever", ".path"); 483 | 484 | Assert.Equal(new[] { "JSON.TOGGLE", "whatever", ".path" }, db.PreviousCommand); 485 | } 486 | } 487 | 488 | public class JsonClearAsync 489 | { 490 | [Fact] 491 | public async Task EmitsCorrectParameters() 492 | { 493 | var db = new FakeDatabase(); 494 | 495 | await db.JsonClearAsync("whatever", ".path"); 496 | 497 | Assert.Equal(new[] { "JSON.CLEAR", "whatever", ".path" }, db.PreviousCommand); 498 | } 499 | } 500 | } 501 | } -------------------------------------------------------------------------------- /NReJSON.Tests/DatabaseExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace NReJSON.Tests 4 | { 5 | public class DatabaseExtensionsTests 6 | { 7 | public class JsonDelete 8 | { 9 | [Fact] 10 | public void EmitsCorrectParameters() 11 | { 12 | var db = new FakeDatabase(); 13 | 14 | db.JsonDelete("fake_key", ".fakeObject"); 15 | 16 | Assert.Equal(new[] { "JSON.DEL", "fake_key", ".fakeObject" }, db.PreviousCommand); 17 | } 18 | 19 | [Fact] 20 | public void HasRootAsDefaultPath() 21 | { 22 | var db = new FakeDatabase(); 23 | 24 | db.JsonDelete("fake_key"); 25 | 26 | Assert.Equal(new[] { "JSON.DEL", "fake_key", "." }, db.PreviousCommand); 27 | } 28 | } 29 | 30 | public class JsonGet 31 | { 32 | [Fact] 33 | public void EmitsCorrectParameters() 34 | { 35 | var db = new FakeDatabase(); 36 | 37 | db.JsonGet("fake_key", ".firstPath", ".firstPath.secondItem"); 38 | 39 | Assert.Equal(new[] { "JSON.GET", "fake_key", "NOESCAPE", ".firstPath", ".firstPath.secondItem" }, db.PreviousCommand); 40 | } 41 | 42 | [Fact] 43 | public void HasRootAsDefaultPath() 44 | { 45 | var db = new FakeDatabase(); 46 | 47 | db.JsonGet("fake_key"); 48 | 49 | Assert.Equal(new[] { "JSON.GET", "fake_key", "NOESCAPE", "." }, db.PreviousCommand); 50 | } 51 | 52 | [Fact] 53 | public void CanSpecifyEscaping() 54 | { 55 | var db = new FakeDatabase(); 56 | 57 | db.JsonGet("fake_key", false); 58 | 59 | Assert.Equal(new[] { "JSON.GET", "fake_key", "." }, db.PreviousCommand); 60 | } 61 | 62 | [Fact] 63 | public void CanSpecifyIndent() 64 | { 65 | var db = new FakeDatabase(); 66 | 67 | db.JsonGet("fake_key", indent: "\t\t"); 68 | 69 | Assert.Equal(new[] { "JSON.GET", "fake_key", "INDENT", "\t\t", "." }, db.PreviousCommand); 70 | } 71 | 72 | [Fact] 73 | public void CanSpecifySpace() 74 | { 75 | var db = new FakeDatabase(); 76 | 77 | db.JsonGet("fake_key", space: " "); 78 | 79 | Assert.Equal(new[] { "JSON.GET", "fake_key", "SPACE", " ", "." }, db.PreviousCommand); 80 | } 81 | 82 | [Fact] 83 | public void CanSpecifyNewLine() 84 | { 85 | var db = new FakeDatabase(); 86 | 87 | db.JsonGet("fake_key", newline: "\r\n"); 88 | 89 | Assert.Equal(new[] { "JSON.GET", "fake_key", "NEWLINE", "\r\n", "." }, db.PreviousCommand); 90 | } 91 | 92 | [Fact] 93 | public void CanSpecifyIndentSpaceAndNewLine() 94 | { 95 | var db = new FakeDatabase(); 96 | 97 | db.JsonGet("fake_key", indent: "\t", newline: "\r\n", space: " "); 98 | 99 | Assert.Equal(new[] { "JSON.GET", "fake_key", "INDENT", "\t", "NEWLINE", "\r\n", "SPACE", " ", "." }, db.PreviousCommand); 100 | } 101 | } 102 | 103 | public class JsonMultiGet 104 | { 105 | [Fact] 106 | public void EmitsCorrectParameters() 107 | { 108 | var db = new FakeDatabase(true); 109 | 110 | db.JsonMultiGet(new[] { "fake_key::1", "fake_key::2" }, ".super.fake.path"); 111 | 112 | Assert.Equal(new[] { "JSON.MGET", "fake_key::1", "fake_key::2", ".super.fake.path" }, db.PreviousCommand); 113 | } 114 | 115 | [Fact] 116 | public void HasRootAsDefaultPath() 117 | { 118 | var db = new FakeDatabase(true); 119 | 120 | db.JsonMultiGet(new[] { "fake_key::1", "fake_key::2" }); 121 | 122 | Assert.Equal(new[] { "JSON.MGET", "fake_key::1", "fake_key::2", "." }, db.PreviousCommand); 123 | } 124 | } 125 | 126 | public class JsonSet 127 | { 128 | [Fact] 129 | public void EmitsCorrectParameters() 130 | { 131 | var db = new FakeDatabase(); 132 | 133 | db.JsonSet("fake_key", "{\"hello\":\"world\"}", ".fake"); 134 | 135 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".fake", "{\"hello\":\"world\"}" }, db.PreviousCommand); 136 | } 137 | 138 | [Fact] 139 | public void HasRootAsDefaultPath() 140 | { 141 | var db = new FakeDatabase(); 142 | 143 | db.JsonSet("fake_key", "{\"hello\":\"world\"}"); 144 | 145 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".", "{\"hello\":\"world\"}" }, db.PreviousCommand); 146 | } 147 | 148 | [Fact] 149 | public void SetIfNotExistsIsProperlyEmitted() 150 | { 151 | var db = new FakeDatabase(); 152 | 153 | db.JsonSet("fake_key", "{\"hello\":\"world\"}", setOption: SetOption.SetIfNotExists); 154 | 155 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".", "{\"hello\":\"world\"}", "NX" }, db.PreviousCommand); 156 | } 157 | 158 | [Fact] 159 | public void SetOnlyIfExistsIsProperlyEmitted() 160 | { 161 | var db = new FakeDatabase(); 162 | 163 | db.JsonSet("fake_key", "{\"hello\":\"world\"}", setOption: SetOption.SetOnlyIfExists); 164 | 165 | Assert.Equal(new[] { "JSON.SET", "fake_key", ".", "{\"hello\":\"world\"}", "XX" }, db.PreviousCommand); 166 | } 167 | } 168 | 169 | public class JsonType 170 | { 171 | [Fact] 172 | public void EmitsCorrectParameters() 173 | { 174 | var db = new FakeDatabase(); 175 | 176 | db.JsonType("fake_key", ".fakePath"); 177 | 178 | Assert.Equal(new[] { "JSON.TYPE", "fake_key", ".fakePath" }, db.PreviousCommand); 179 | } 180 | 181 | [Fact] 182 | public void HasRootAsDefaultPath() 183 | { 184 | var db = new FakeDatabase(); 185 | 186 | db.JsonType("fake_key"); 187 | 188 | Assert.Equal(new[] { "JSON.TYPE", "fake_key", "." }, db.PreviousCommand); 189 | } 190 | } 191 | 192 | public class JsonIncrementNumber 193 | { 194 | [Fact] 195 | public void EmitsCorrectParameters() 196 | { 197 | var db = new FakeDatabase(); 198 | 199 | db.JsonIncrementNumber("fake_key", ".fake.path", 2); 200 | 201 | Assert.Equal(new[] { "JSON.NUMINCRBY", "fake_key", ".fake.path", "2" }, db.PreviousCommand); 202 | } 203 | } 204 | 205 | public class JsonMultiplyNumber 206 | { 207 | [Fact] 208 | public void EmitsCorrectParameters() 209 | { 210 | var db = new FakeDatabase(); 211 | 212 | db.JsonMultiplyNumber("fake_key", ".fake.path", 5); 213 | 214 | Assert.Equal(new[] { "JSON.NUMMULTBY", "fake_key", ".fake.path", "5" }, db.PreviousCommand); 215 | } 216 | } 217 | 218 | public class JsonAppendJsonString 219 | { 220 | [Fact] 221 | public void EmitsCorrectParameters() 222 | { 223 | var db = new FakeDatabase(); 224 | 225 | db.JsonAppendJsonString("fake_key", ".fake.path", "\"fake_string\""); 226 | 227 | Assert.Equal(new[] { "JSON.STRAPPEND", "fake_key", ".fake.path", "\"fake_string\"" }, db.PreviousCommand); 228 | } 229 | 230 | [Fact] 231 | public void HasRootAsDefaultPath() 232 | { 233 | var db = new FakeDatabase(); 234 | 235 | db.JsonAppendJsonString("fake_key", jsonString: "\"fake_string\""); 236 | 237 | Assert.Equal(new[] { "JSON.STRAPPEND", "fake_key", ".", "\"fake_string\"" }, db.PreviousCommand); 238 | } 239 | } 240 | 241 | public class JsonStringLength 242 | { 243 | [Fact] 244 | public void EmitsCorrectParameters() 245 | { 246 | var db = new FakeDatabase(); 247 | 248 | db.JsonStringLength("fake_key", ".fake.path"); 249 | 250 | Assert.Equal(new[] { "JSON.STRLEN", "fake_key", ".fake.path" }, db.PreviousCommand); 251 | } 252 | 253 | [Fact] 254 | public void HasRootAsDefaultPath() 255 | { 256 | var db = new FakeDatabase(); 257 | 258 | db.JsonStringLength("fake_key"); 259 | 260 | Assert.Equal(new[] { "JSON.STRLEN", "fake_key", "." }, db.PreviousCommand); 261 | } 262 | } 263 | 264 | public class JsonArrayAppend 265 | { 266 | [Fact] 267 | public void EmitsCorrectParameters() 268 | { 269 | var db = new FakeDatabase(); 270 | 271 | db.JsonArrayAppend("fake_key", ".fake.path", "\"1\"", "\"2\""); 272 | 273 | Assert.Equal(new[] { "JSON.ARRAPPEND", "fake_key", ".fake.path", "\"1\"", "\"2\"" }, db.PreviousCommand); 274 | } 275 | } 276 | 277 | public class JsonArrayIndexOf 278 | { 279 | [Fact] 280 | public void EmitsCorrectParameters() 281 | { 282 | var db = new FakeDatabase(); 283 | 284 | db.JsonArrayIndexOf("fake_key", ".fake.path", "\"hello world\"", 10, 20); 285 | 286 | Assert.Equal(new[] { "JSON.ARRINDEX", "fake_key", ".fake.path", "\"hello world\"", "10", "20" }, db.PreviousCommand); 287 | } 288 | 289 | [Fact] 290 | public void HasZeroAsDefaultForStartAndStop() 291 | { 292 | var db = new FakeDatabase(); 293 | 294 | db.JsonArrayIndexOf("fake_key", ".fake.path", "\"hello world\""); 295 | 296 | Assert.Equal(new[] { "JSON.ARRINDEX", "fake_key", ".fake.path", "\"hello world\"", "0", "0" }, db.PreviousCommand); 297 | } 298 | } 299 | 300 | public class JsonArrayInsert 301 | { 302 | [Fact] 303 | public void EmitsCorrectParameters() 304 | { 305 | var db = new FakeDatabase(); 306 | 307 | db.JsonArrayInsert("fake_key", ".fake.path", 15, "\"hello\"", "\"world\""); 308 | 309 | Assert.Equal(new[] { "JSON.ARRINSERT", "fake_key", ".fake.path", "15", "\"hello\"", "\"world\"" }, db.PreviousCommand); 310 | } 311 | } 312 | 313 | public class JsonArrayLength 314 | { 315 | [Fact] 316 | public void EmitsCorrectParameters() 317 | { 318 | var db = new FakeDatabase(); 319 | 320 | db.JsonArrayLength("fake_key", ".fake.array.path"); 321 | 322 | Assert.Equal(new[] { "JSON.ARRLEN", "fake_key", ".fake.array.path" }, db.PreviousCommand); 323 | } 324 | 325 | [Fact] 326 | public void HasRootAsDefaultPath() 327 | { 328 | var db = new FakeDatabase(); 329 | 330 | db.JsonArrayLength("fake_key"); 331 | 332 | Assert.Equal(new[] { "JSON.ARRLEN", "fake_key", "." }, db.PreviousCommand); 333 | } 334 | } 335 | 336 | public class JsonArrayPop 337 | { 338 | [Fact] 339 | public void EmitsCorrectParameters() 340 | { 341 | var db = new FakeDatabase(expectArrayResult: true); 342 | 343 | db.JsonArrayPop("fake_key", ".what.ever", 10); 344 | 345 | Assert.Equal(new[] { "JSON.ARRPOP", "fake_key", ".what.ever", "10" }, db.PreviousCommand); 346 | } 347 | 348 | [Fact] 349 | public void HasRootAsDefaultPath() 350 | { 351 | var db = new FakeDatabase(expectArrayResult: true); 352 | 353 | db.JsonArrayPop("fake_key", index: 10); 354 | 355 | Assert.Equal(new[] { "JSON.ARRPOP", "fake_key", ".", "10" }, db.PreviousCommand); 356 | } 357 | 358 | [Fact] 359 | public void HasNegativeOneAsDefaultIndex() 360 | { 361 | var db = new FakeDatabase(expectArrayResult: true); 362 | 363 | db.JsonArrayPop("fake_key", ".what.ever"); 364 | 365 | Assert.Equal(new[] { "JSON.ARRPOP", "fake_key", ".what.ever", "-1" }, db.PreviousCommand); 366 | } 367 | } 368 | 369 | public class JsonArrayTrim 370 | { 371 | [Fact] 372 | public void EmitsCorrectParameters() 373 | { 374 | var db = new FakeDatabase(); 375 | 376 | db.JsonArrayTrim("fake_key", ".fake.path", 1, 10); 377 | 378 | Assert.Equal(new[] { "JSON.ARRTRIM", "fake_key", ".fake.path", "1", "10" }, db.PreviousCommand); 379 | } 380 | } 381 | 382 | public class JsonObjectKeys 383 | { 384 | [Fact] 385 | public void EmitsCorrectParameters() 386 | { 387 | var db = new FakeDatabase(true); 388 | 389 | db.JsonObjectKeys("fake_key", ".fake.path"); 390 | 391 | Assert.Equal(new[] { "JSON.OBJKEYS", "fake_key", ".fake.path" }, db.PreviousCommand); 392 | } 393 | 394 | [Fact] 395 | public void HasRootAsDefaultPath() 396 | { 397 | var db = new FakeDatabase(true); 398 | 399 | db.JsonObjectKeys("fake_key"); 400 | 401 | Assert.Equal(new[] { "JSON.OBJKEYS", "fake_key", "." }, db.PreviousCommand); 402 | } 403 | } 404 | 405 | public class JsonObjectLength 406 | { 407 | [Fact] 408 | public void EmitsCorrectParameters() 409 | { 410 | var db = new FakeDatabase(); 411 | 412 | db.JsonObjectLength("fake_key", ".fake.path"); 413 | 414 | Assert.Equal(new[] { "JSON.OBJLEN", "fake_key", ".fake.path" }, db.PreviousCommand); 415 | } 416 | 417 | [Fact] 418 | public void HasRootAsDefaultPath() 419 | { 420 | var db = new FakeDatabase(); 421 | 422 | db.JsonObjectLength("fake_key"); 423 | 424 | Assert.Equal(new[] { "JSON.OBJLEN", "fake_key", "." }, db.PreviousCommand); 425 | } 426 | } 427 | 428 | public class JsonDebugMemory 429 | { 430 | [Fact] 431 | public void EmitsCorrectParameters() 432 | { 433 | var db = new FakeDatabase(); 434 | 435 | db.JsonDebugMemory("fake_key", ".fake.path"); 436 | 437 | Assert.Equal(new[] { "JSON.DEBUG", "MEMORY", "fake_key", ".fake.path" }, db.PreviousCommand); 438 | } 439 | 440 | [Fact] 441 | public void HasRootAsDefaultPath() 442 | { 443 | var db = new FakeDatabase(); 444 | 445 | db.JsonDebugMemory("fake_key"); 446 | 447 | Assert.Equal(new[] { "JSON.DEBUG", "MEMORY", "fake_key", "." }, db.PreviousCommand); 448 | } 449 | } 450 | 451 | public class JsonGetResp 452 | { 453 | [Fact] 454 | public void EmitsCorrectParameters() 455 | { 456 | var db = new FakeDatabase(true); 457 | 458 | db.JsonGetResp("fake_key", ".hello.fake"); 459 | 460 | Assert.Equal(new[] { "JSON.RESP", "fake_key", ".hello.fake" }, db.PreviousCommand); 461 | } 462 | 463 | [Fact] 464 | public void HasRootAsDefaultPath() 465 | { 466 | var db = new FakeDatabase(true); 467 | 468 | db.JsonGetResp("fake_key"); 469 | 470 | Assert.Equal(new[] { "JSON.RESP", "fake_key", "." }, db.PreviousCommand); 471 | } 472 | } 473 | 474 | public class JsonToggle 475 | { 476 | [Fact] 477 | public void EmitsCorrectParameters() 478 | { 479 | var db = new FakeDatabase(expectBoolResult: true); 480 | 481 | db.JsonToggle("whatever", ".path"); 482 | 483 | Assert.Equal(new[] { "JSON.TOGGLE", "whatever", ".path" }, db.PreviousCommand); 484 | } 485 | } 486 | 487 | public class JsonClear 488 | { 489 | [Fact] 490 | public void EmitsCorrectParameters() 491 | { 492 | var db = new FakeDatabase(); 493 | 494 | db.JsonClear("whatever", ".path"); 495 | 496 | Assert.Equal(new[] { "JSON.CLEAR", "whatever", ".path" }, db.PreviousCommand); 497 | } 498 | } 499 | } 500 | } -------------------------------------------------------------------------------- /NReJSON.Tests/NReJSON.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp5 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /NReJSON.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NReJSON", "NReJSON\NReJSON.csproj", "{4C558F46-656C-4201-B355-E0CA096A1EAB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NReJSON.Tests", "NReJSON.Tests\NReJSON.Tests.csproj", "{33DE391E-34B1-4D17-8D11-4E4352B87518}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NReJSON.IntegrationTests", "NReJSON.IntegrationTests\NReJSON.IntegrationTests.csproj", "{5AE938E5-2383-4D1F-82F7-26F99D9AD2CC}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C00BF79D-B4E2-4C44-A2C4-8B4D70E4E9EA}" 13 | ProjectSection(SolutionItems) = preProject 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {4C558F46-656C-4201-B355-E0CA096A1EAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {4C558F46-656C-4201-B355-E0CA096A1EAB}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {4C558F46-656C-4201-B355-E0CA096A1EAB}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {4C558F46-656C-4201-B355-E0CA096A1EAB}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {33DE391E-34B1-4D17-8D11-4E4352B87518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {33DE391E-34B1-4D17-8D11-4E4352B87518}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {33DE391E-34B1-4D17-8D11-4E4352B87518}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {33DE391E-34B1-4D17-8D11-4E4352B87518}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {5AE938E5-2383-4D1F-82F7-26F99D9AD2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {5AE938E5-2383-4D1F-82F7-26F99D9AD2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {5AE938E5-2383-4D1F-82F7-26F99D9AD2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {5AE938E5-2383-4D1F-82F7-26F99D9AD2CC}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {4C2D369E-2A36-4586-984C-6F7096248C9E} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /NReJSON/DatabaseExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using StackExchange.Redis; 4 | using static NReJSON.NReJSONSerializer; 5 | 6 | namespace NReJSON 7 | { 8 | /// 9 | /// This class defines the extension methods for StackExchange.Redis that allow 10 | /// for the interaction with the RedisJson Redis module. 11 | /// 12 | public static partial class DatabaseExtensions 13 | { 14 | /// 15 | /// `JSON.DEL` 16 | /// 17 | /// Delete a value. 18 | /// 19 | /// Non-existing keys and paths are ignored. Deleting an object's root is equivalent to deleting the key from Redis. 20 | /// 21 | /// https://oss.redislabs.com/rejson/commands/#jsondel 22 | /// 23 | /// 24 | /// Key where JSON object is stored. 25 | /// Defaults to root if not provided. 26 | /// Optional command flags. 27 | /// Integer, specifically the number of paths deleted (0 or more). 28 | public static int JsonDelete(this IDatabase db, RedisKey key, string path = ".", 29 | CommandFlags commandFlags = CommandFlags.None) => 30 | (int) db.Execute(JsonCommands.DEL, CombineArguments(key, path), flags: commandFlags); 31 | 32 | /// 33 | /// `JSON.GET` 34 | /// 35 | /// Return the value at `path` in JSON serialized form. 36 | /// 37 | /// `NOESCAPE` is `true` by default. 38 | /// 39 | /// https://oss.redislabs.com/rejson/commands/#jsonget 40 | /// 41 | /// 42 | /// Key where JSON object is stored. 43 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 44 | /// Array of bulk strings. 45 | public static RedisResult JsonGet(this IDatabase db, RedisKey key, params string[] paths) => 46 | db.JsonGet(key, noEscape: true, paths: paths, commandFlags: CommandFlags.None); 47 | 48 | /// 49 | /// `JSON.GET` 50 | /// 51 | /// Return the value at `path` as a deserialized value. 52 | /// 53 | /// `NOESCAPE` is `true` by default. 54 | /// 55 | /// https://oss.redislabs.com/rejson/commands/#jsonget 56 | /// 57 | /// 58 | /// Key where JSON object is stored. 59 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 60 | /// The type to deserialize the value as. 61 | /// 62 | public static PathedResult JsonGet(this IDatabase db, RedisKey key, params string[] paths) => 63 | db.JsonGet(key, noEscape: true, paths: paths, commandFlags: CommandFlags.None); 64 | 65 | /// 66 | /// `JSON.GET` 67 | /// 68 | /// Return the value at `path` in JSON serialized form. 69 | /// 70 | /// https://oss.redislabs.com/rejson/commands/#jsonget 71 | /// 72 | /// 73 | /// Key where JSON object is stored. 74 | /// This option will disable the sending of \uXXXX escapes for non-ascii characters. This option should be used for efficiency if you deal mainly with such text. 75 | /// Sets the indentation string for nested levels 76 | /// Sets the string that's printed at the end of each line 77 | /// Sets the string that's put between a key and a value 78 | /// Optional command flags. 79 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 80 | /// 81 | public static RedisResult JsonGet(this IDatabase db, RedisKey key, bool noEscape = false, 82 | string indent = default, string newline = default, string space = default, 83 | CommandFlags commandFlags = CommandFlags.None, params string[] paths) 84 | { 85 | var args = new List {key}; 86 | 87 | if (noEscape) 88 | { 89 | args.Add("NOESCAPE"); 90 | } 91 | 92 | if (indent != default) 93 | { 94 | args.Add("INDENT"); 95 | args.Add(indent); 96 | } 97 | 98 | if (newline != default) 99 | { 100 | args.Add("NEWLINE"); 101 | args.Add(newline); 102 | } 103 | 104 | if (space != default) 105 | { 106 | args.Add("SPACE"); 107 | args.Add(space); 108 | } 109 | 110 | foreach (var path in PathsOrDefault(paths, RootPathStringArray)) 111 | { 112 | args.Add(path); 113 | } 114 | 115 | return db.Execute(JsonCommands.GET, args, flags: commandFlags); 116 | } 117 | 118 | /// 119 | /// `JSON.GET` 120 | /// 121 | /// Return the value at `path` as a deserialized value. 122 | /// 123 | /// https://oss.redislabs.com/rejson/commands/#jsonget 124 | /// 125 | /// 126 | /// Key where JSON object is stored. 127 | /// This option will disable the sending of \uXXXX escapes for non-ascii characters. This option should be used for efficiency if you deal mainly with such text. 128 | /// Sets the indentation string for nested levels 129 | /// Sets the string that's printed at the end of each line 130 | /// Sets the string that's put between a key and a value 131 | /// Optional command flags. 132 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 133 | /// The type to deserialize the value as. 134 | /// 135 | public static PathedResult JsonGet(this IDatabase db, RedisKey key, bool noEscape = false, 136 | string indent = default, string newline = default, string space = default, 137 | CommandFlags commandFlags = CommandFlags.None, params string[] paths) => 138 | PathedResult.Create(db.JsonGet(key, noEscape, indent, newline, space, commandFlags, paths)); 139 | 140 | /// 141 | /// `JSON.MGET` 142 | /// 143 | /// Returns the values at `path` from multiple `key`s. Non-existing keys and non-existing paths are reported as null. 144 | /// 145 | /// https://oss.redislabs.com/rejson/commands/#jsonmget 146 | /// 147 | /// 148 | /// Keys where JSON objects are stored. 149 | /// The path of the JSON property that you want to return for each key. This is "root" by default. 150 | /// Optional command flags. 151 | /// Array of Bulk Strings, specifically the JSON serialization of the value at each key's path. 152 | public static RedisResult[] JsonMultiGet(this IDatabase db, string[] keys, string path = ".", 153 | CommandFlags commandFlags = CommandFlags.None) => 154 | db.JsonMultiGet(keys.Select(k => (RedisKey) k).ToArray(), path, commandFlags: commandFlags); 155 | 156 | /// 157 | /// `JSON.MGET` 158 | /// 159 | /// Returns the values at `path` from multiple `key`s. Non-existing keys and non-existing paths are reported as null. 160 | /// 161 | /// https://oss.redislabs.com/rejson/commands/#jsonmget 162 | /// 163 | /// 164 | /// Keys where JSON objects are stored. 165 | /// The path of the JSON property that you want to return for each key. This is "root" by default. 166 | /// Optional command flags. 167 | /// Array of Bulk Strings, specifically the JSON serialization of the value at each key's path. 168 | public static RedisResult[] JsonMultiGet(this IDatabase db, RedisKey[] keys, string path = ".", 169 | CommandFlags commandFlags = CommandFlags.None) => 170 | (RedisResult[]) db.Execute(JsonCommands.MGET, CombineArguments(keys, path), flags: commandFlags); 171 | 172 | /// 173 | /// `JSON.MGET` 174 | /// 175 | /// Returns an IEnumerable of the specified result type. Non-existing keys and non-existent paths are returnd as type default. 176 | /// 177 | /// https://oss.redislabs.com/rejson/commands/#jsonmget 178 | /// 179 | /// 180 | /// Keys where JSON objects are stored. 181 | /// The path of the JSON property that you want to return for each key. This is "root" by default. 182 | /// Optional command flags. 183 | /// The type to deserialize the value as. 184 | /// IEnumerable of TResult, non-existent paths/keys are returned as default(TResult). 185 | public static IEnumerable JsonMultiGet(this IDatabase db, RedisKey[] keys, string path = ".", 186 | CommandFlags commandFlags = CommandFlags.None) 187 | { 188 | var serializedResults = db.JsonMultiGet(keys, path, commandFlags: commandFlags); 189 | 190 | foreach (var serializedResult in serializedResults) 191 | { 192 | if (serializedResult.IsNull) 193 | { 194 | yield return default(TResult); 195 | } 196 | else 197 | { 198 | yield return SerializerProxy.Deserialize(serializedResult); 199 | } 200 | } 201 | } 202 | 203 | /// 204 | /// `JSON.SET` 205 | /// 206 | /// Sets the JSON value at path in key 207 | /// 208 | /// For new Redis keys the path must be the root. 209 | /// 210 | /// For existing keys, when the entire path exists, the value that it contains is replaced with the json value. 211 | /// 212 | /// https://oss.redislabs.com/rejson/commands/#jsonset 213 | /// 214 | /// 215 | /// Key where JSON object is to be stored. 216 | /// The JSON object which you want to persist. 217 | /// The path which you want to persist the JSON object. For new objects this must be root. 218 | /// By default the object will be overwritten, but you can specify that the object be set only if it doesn't already exist or to set only IF it exists. 219 | /// Optional command flags. 220 | /// An `OperationResult` indicating success or failure. 221 | public static OperationResult JsonSet(this IDatabase db, RedisKey key, string json, string path = ".", 222 | SetOption setOption = SetOption.Default, CommandFlags commandFlags = CommandFlags.None) 223 | { 224 | var result = db.Execute(JsonCommands.SET, 225 | CombineArguments(key, path, json, GetSetOptionString(setOption)), 226 | flags: commandFlags) 227 | .ToString(); 228 | 229 | return new OperationResult(result == "OK", result); 230 | } 231 | 232 | /// 233 | /// `JSON.SET` 234 | /// 235 | /// Sets the JSON value at path in key 236 | /// 237 | /// For new Redis keys the path must be the root. 238 | /// 239 | /// For existing keys, when the entire path exists, the value that it contains is replaced with the json value. 240 | /// 241 | /// https://oss.redislabs.com/rejson/commands/#jsonset 242 | /// 243 | /// 244 | /// Key where JSON object is to be stored. 245 | /// The object to serialize and send. 246 | /// The path which you want to persist the JSON object. For new objects this must be root. 247 | /// By default the object will be overwritten, but you can specify that the object be set only if it doesn't already exist or to set only IF it exists. 248 | /// Optional command flags. 249 | /// Type of the object being serialized. 250 | /// An `OperationResult` indicating success or failure. 251 | public static OperationResult JsonSet(this IDatabase db, RedisKey key, TObjectType obj, 252 | string path = ".", SetOption setOption = SetOption.Default, CommandFlags commandFlags = CommandFlags.None) => 253 | db.JsonSet(key, SerializerProxy.Serialize(obj), path, setOption, commandFlags: commandFlags); 254 | 255 | /// 256 | /// `JSON.TYPE` 257 | /// 258 | /// Report the type of JSON value at `path`. 259 | /// 260 | /// `path` defaults to root if not provided. 261 | /// 262 | /// https://oss.redislabs.com/rejson/commands/#jsontype 263 | /// 264 | /// 265 | /// The key of the JSON object you need the type of. 266 | /// The path of the JSON object you want the type of. This defaults to root. 267 | /// Optional command flags. 268 | /// 269 | public static RedisResult JsonType(this IDatabase db, RedisKey key, string path = ".", 270 | CommandFlags commandFlags = CommandFlags.None) => 271 | db.Execute(JsonCommands.TYPE, CombineArguments(key, path), flags: commandFlags); 272 | 273 | /// 274 | /// `JSON.NUMINCRBY` 275 | /// 276 | /// Increments the number value stored at `path` by `number`. 277 | /// 278 | /// https://oss.redislabs.com/rejson/commands/#jsonnumincrby 279 | /// 280 | /// 281 | /// The key of the JSON object which contains the number value you want to increment. 282 | /// The path of the JSON value you want to increment. 283 | /// The value you want to increment by. 284 | /// Optional command flags. 285 | public static PathedResult JsonIncrementNumber(this IDatabase db, RedisKey key, string path, 286 | double number, 287 | CommandFlags commandFlags = CommandFlags.None) => 288 | PathedResult.Create(db.Execute(JsonCommands.NUMINCRBY, CombineArguments(key, path, number), 289 | flags: commandFlags)); 290 | 291 | /// 292 | /// `JSON.NUMMULTBY` 293 | /// 294 | /// Multiplies the number value stored at `path` by `number`. 295 | /// 296 | /// https://oss.redislabs.com/rejson/commands/#jsonnummultby 297 | /// 298 | /// 299 | /// They key of the JSON object which contains the number value you want to multiply. 300 | /// The path of the JSON value you want to multiply. 301 | /// The value you want to multiply by. 302 | /// Optional command flags. 303 | public static PathedResult JsonMultiplyNumber(this IDatabase db, RedisKey key, string path, double number, 304 | CommandFlags commandFlags = CommandFlags.None) => 305 | PathedResult.Create(db.Execute(JsonCommands.NUMMULTBY, CombineArguments(key, path, number), flags: commandFlags)); 306 | 307 | /// 308 | /// `JSON.STRAPPEND` 309 | /// 310 | /// Append the json-string value(s) the string at path. 311 | /// path defaults to root if not provided. 312 | /// 313 | /// https://oss.redislabs.com/rejson/commands/#jsonstrappend 314 | /// 315 | /// 316 | /// The key of the JSON object you need to append a string value. 317 | /// The path of the JSON string you want to append do. This defaults to root. 318 | /// JSON formatted string. 319 | /// Optional command flags. 320 | /// Length of the new JSON string. 321 | public static int?[] JsonAppendJsonString(this IDatabase db, RedisKey key, string path = ".", 322 | string jsonString = "\"\"", CommandFlags commandFlags = CommandFlags.None) => 323 | NullableIntArrayFrom(db.Execute(JsonCommands.STRAPPEND, CombineArguments(key, path, jsonString), flags: commandFlags)); 324 | 325 | /// 326 | /// `JSON.STRLEN` 327 | /// 328 | /// Report the length of the JSON String at `path` in `key`. 329 | /// 330 | /// `path` defaults to root if not provided. If the `key` or `path` do not exist, null is returned. 331 | /// 332 | /// https://oss.redislabs.com/rejson/commands/#jsonstrlen 333 | /// 334 | /// 335 | /// The key of the JSON object you need string length information about. 336 | /// The path of the JSON string you want the length of. This defaults to root. 337 | /// Optional command flags. 338 | /// Integer, specifically the string's length. 339 | public static int?[] JsonStringLength(this IDatabase db, RedisKey key, string path = ".", 340 | CommandFlags commandFlags = CommandFlags.None) => 341 | NullableIntArrayFrom(db.Execute(JsonCommands.STRLEN, CombineArguments(key, path), flags: commandFlags)); 342 | 343 | /// 344 | /// `JSON.ARRAPPEND` 345 | /// 346 | /// Append the `json` value(s) into the array at `path` after the last element in it. 347 | /// 348 | /// https://oss.redislabs.com/rejson/commands/#jsonarrappend 349 | /// 350 | /// 351 | /// The key of the JSON object that contains the array you want to append to. 352 | /// The path to the JSON array you want to append to. 353 | /// The JSON values that you want to append. 354 | /// Array of nullable integers indicating the array's new size at each matched path. 355 | public static int?[] JsonArrayAppend(this IDatabase db, RedisKey key, string path, params object[] json) => 356 | JsonArrayAppend(db, key, path, CommandFlags.None, json); 357 | 358 | /// 359 | /// `JSON.ARRAPPEND` 360 | /// 361 | /// Append the `json` value(s) into the array at `path` after the last element in it. 362 | /// 363 | /// https://oss.redislabs.com/rejson/commands/#jsonarrappend 364 | /// 365 | /// 366 | /// The key of the JSON object that contains the array you want to append to. 367 | /// The path to the JSON array you want to append to. 368 | /// Optional command flags. 369 | /// The JSON values that you want to append. 370 | /// Array of nullable integers indicating the array's new size at each matched path. 371 | public static int?[] JsonArrayAppend(this IDatabase db, RedisKey key, string path, 372 | CommandFlags commandFlags = CommandFlags.None, params object[] json) => 373 | NullableIntArrayFrom(db.Execute(JsonCommands.ARRAPPEND, CombineArguments(key, path, json), flags: commandFlags)); 374 | 375 | /// 376 | /// `JSON.ARRINDEX` 377 | /// 378 | /// Search for the first occurrence of a scalar JSON value in an array. 379 | /// 380 | /// The optional inclusive `start`(default 0) and exclusive `stop`(default 0, meaning that the last element is included) specify a slice of the array to search. 381 | /// 382 | /// Note: out of range errors are treated by rounding the index to the array's start and end. An inverse index range (e.g. from 1 to 0) will return unfound. 383 | /// 384 | /// https://oss.redislabs.com/rejson/commands/#jsonarrindex 385 | /// 386 | /// 387 | /// The key of the JSON object that contains the array you want to check for a scalar value in. 388 | /// The path to the JSON array that you want to check. 389 | /// The JSON object that you are looking for. 390 | /// Where to start searching, defaults to 0 (the beginning of the array). 391 | /// Where to stop searching, defaults to 0 (the end of the array). 392 | /// Optional command flags. 393 | /// Array of nullable integers, specifically, for each JSON value matching the path, the first position of the scalar value in the array, -1 if unfound in the array, or null if the matching JSON value is not an array. 394 | public static int?[] JsonArrayIndexOf(this IDatabase db, RedisKey key, string path, object jsonScalar, 395 | int start = 0, int stop = 0, CommandFlags commandFlags = CommandFlags.None) => 396 | NullableIntArrayFrom(db.Execute(JsonCommands.ARRINDEX, CombineArguments(key, path, jsonScalar, start, stop), 397 | flags: commandFlags)); 398 | 399 | /// 400 | /// `JSON.ARRINSERT` 401 | /// 402 | /// Insert the `json` value(s) into the array at `path` before the `index` (shifts to the right). 403 | /// 404 | /// The index must be in the array's range. Inserting at `index` 0 prepends to the array. Negative index values are interpreted as starting from the end. 405 | /// 406 | /// https://oss.redislabs.com/rejson/commands/#jsonarrinsert 407 | /// 408 | /// 409 | /// The key of the JSON object that contains the array you want to insert an object into. 410 | /// The path of the JSON array that you want to insert into. 411 | /// The index at which you want to insert, 0 prepends and negative values are interpreted as starting from the end. 412 | /// The object that you want to insert. 413 | /// Array of nullable integer, specifically, for each path, the array's new size, or null element if the matching JSON value is not an array. 414 | public static int?[] JsonArrayInsert(this IDatabase db, RedisKey key, string path, int index, 415 | params object[] json) => 416 | JsonArrayInsert(db, key, path, index, CommandFlags.None, json); 417 | 418 | /// 419 | /// `JSON.ARRINSERT` 420 | /// 421 | /// Insert the `json` value(s) into the array at `path` before the `index` (shifts to the right). 422 | /// 423 | /// The index must be in the array's range. Inserting at `index` 0 prepends to the array. Negative index values are interpreted as starting from the end. 424 | /// 425 | /// https://oss.redislabs.com/rejson/commands/#jsonarrinsert 426 | /// 427 | /// 428 | /// The key of the JSON object that contains the array you want to insert an object into. 429 | /// The path of the JSON array that you want to insert into. 430 | /// The index at which you want to insert, 0 prepends and negative values are interpreted as starting from the end. 431 | /// Optional command flags. 432 | /// The object that you want to insert. 433 | /// Array of nullable integer, specifically, for each path, the array's new size, or null element if the matching JSON value is not an array. 434 | public static int?[] JsonArrayInsert(this IDatabase db, RedisKey key, string path, int index, 435 | CommandFlags commandFlags = CommandFlags.None, 436 | params object[] json) => 437 | NullableIntArrayFrom(db.Execute(JsonCommands.ARRINSERT, CombineArguments(key, path, index, json), 438 | flags: commandFlags)); 439 | 440 | /// 441 | /// `JSON.ARRLEN` 442 | /// 443 | /// Report the length of the JSON Array at `path` in `key`. 444 | /// 445 | /// `path` defaults to root if not provided. If the `key` or `path` do not exist, null is returned. 446 | /// 447 | /// https://oss.redislabs.com/rejson/commands/#jsonarrlen 448 | /// 449 | /// 450 | /// The key of the JSON object that contains the array you want the length of. 451 | /// The path to the JSON array that you want the length of. 452 | /// Optional command flags. 453 | /// Array of nullable integer, specifically, for each path, the array's length, or null element if the matching JSON value is not an array. 454 | public static int?[] JsonArrayLength(this IDatabase db, RedisKey key, string path = ".", 455 | CommandFlags commandFlags = CommandFlags.None) 456 | { 457 | var result = db.Execute(JsonCommands.ARRLEN, CombineArguments(key, path), flags: commandFlags); 458 | 459 | if (result.IsNull) 460 | { 461 | return null; 462 | } 463 | else 464 | { 465 | return NullableIntArrayFrom(result); 466 | } 467 | } 468 | 469 | /// 470 | /// `JSON.ARRPOP` 471 | /// 472 | /// Remove and return element from the index in the array. 473 | /// 474 | /// Out of range indices are rounded to their respective array ends.Popping an empty array yields null. 475 | /// 476 | /// https://oss.redislabs.com/rejson/commands/#jsonarrpop 477 | /// 478 | /// 479 | /// The key of the JSON object that contains the array you want to pop an object off of. 480 | /// Defaults to root (".") if not provided. 481 | /// Is the position in the array to start popping from (defaults to -1, meaning the last element). 482 | /// Optional command flags. 483 | /// Array of strings, specifically, for each path, the popped JSON value, or null element if the matching JSON value is not an array. 484 | public static string[] JsonArrayPop(this IDatabase db, RedisKey key, string path = ".", int index = -1, 485 | CommandFlags commandFlags = CommandFlags.None) => 486 | StringArrayFrom(db.Execute(JsonCommands.ARRPOP, CombineArguments(key, path, index), flags: commandFlags)); 487 | 488 | /// 489 | /// `JSON.ARRPOP` 490 | /// 491 | /// Remove and return element from the index in the array. 492 | /// 493 | /// Out of range indices are rounded to their respective array ends.Popping an empty array yields null. 494 | /// 495 | /// https://oss.redislabs.com/rejson/commands/#jsonarrpop 496 | /// 497 | /// 498 | /// The key of the JSON object that contains the array you want to pop an object off of. 499 | /// Defaults to root (".") if not provided. 500 | /// Is the position in the array to start popping from (defaults to -1, meaning the last element). 501 | /// Optional command flags. 502 | /// The type to deserialize the value as. 503 | /// Array of `TResult`, specifically, for each path, the popped JSON value, or null element if the matching JSON value is not an array. 504 | public static TResult[] JsonArrayPop(this IDatabase db, RedisKey key, string path = ".", 505 | int index = -1, CommandFlags commandFlags = CommandFlags.None) => 506 | TypedArrayFrom(db.Execute(JsonCommands.ARRPOP, CombineArguments(key, path, index), flags: commandFlags)); 507 | 508 | /// 509 | /// `JSON.ARRTRIM` 510 | /// 511 | /// Trim an array so that it contains only the specified inclusive range of elements. 512 | /// 513 | /// This command is extremely forgiving and using it with out of range indexes will not produce an error. 514 | /// 515 | /// If start is larger than the array's size or start > stop, the result will be an empty array. 516 | /// 517 | /// If start is < 0 then it will be treated as 0. 518 | /// 519 | /// If stop is larger than the end of the array, it will be treated like the last element in it. 520 | /// 521 | /// https://oss.redislabs.com/rejson/commands/#jsonarrtrim 522 | /// 523 | /// 524 | /// The key of the JSON object that contains the array you want to trim. 525 | /// The path of the JSON array that you want to trim. 526 | /// The inclusive start index. 527 | /// The inclusive stop index. 528 | /// Optional command flags. 529 | /// Array of nullable integer, specifically for each path, the array's new size, or null element if the matching JSON value is not an array. 530 | public static int?[] JsonArrayTrim(this IDatabase db, RedisKey key, string path, int start, int stop, 531 | CommandFlags commandFlags = CommandFlags.None) => 532 | NullableIntArrayFrom(db.Execute(JsonCommands.ARRTRIM, CombineArguments(key, path, start, stop), 533 | flags: commandFlags)); 534 | 535 | /// 536 | /// `JSON.OBJKEYS` 537 | /// 538 | /// Return the keys in the object that's referenced by `path`. 539 | /// 540 | /// `path` defaults to root if not provided.If the object is empty, or either `key` or `path` do not exist, then null is returned. 541 | /// 542 | /// https://oss.redislabs.com/rejson/commands/#jsonobjkeys 543 | /// 544 | /// 545 | /// The key of the JSON object which you want to enumerate keys for. 546 | /// The path to the JSON object you want the keys for, this defaults to root. 547 | /// Optional command flags. 548 | /// Array, specifically the key names in the object as Bulk Strings. 549 | public static RedisResult[] JsonObjectKeys(this IDatabase db, RedisKey key, string path = ".", 550 | CommandFlags commandFlags = CommandFlags.None) => 551 | (RedisResult[]) db.Execute(JsonCommands.OBJKEYS, CombineArguments(key, path), flags: commandFlags); 552 | 553 | /// 554 | /// `JSON.OBJLEN` 555 | /// 556 | /// Report the number of keys in the JSON Object at `path` in `key`. 557 | /// 558 | /// `path` defaults to root if not provided. If the `key` or `path` do not exist, null is returned. 559 | /// 560 | /// https://oss.redislabs.com/rejson/commands/#jsonobjlen 561 | /// 562 | /// 563 | /// The key of the JSON object which you want the length of. 564 | /// The path to the JSON object which you want the length of, defaults to root. 565 | /// Optional command flags. 566 | /// Integer, specifically the number of keys in the object. 567 | public static int? JsonObjectLength(this IDatabase db, RedisKey key, string path = ".", 568 | CommandFlags commandFlags = CommandFlags.None) 569 | { 570 | var result = db.Execute(JsonCommands.OBJLEN, CombineArguments(key, path), flags: commandFlags); 571 | 572 | if (result.IsNull) 573 | { 574 | return null; 575 | } 576 | else 577 | { 578 | return (int) result; 579 | } 580 | } 581 | 582 | /// 583 | /// `JSON.DEBUG MEMORY` 584 | /// 585 | /// Report the memory usage in bytes of a value. `path` defaults to root if not provided. 586 | /// 587 | /// https://oss.redislabs.com/rejson/commands/#jsondebug 588 | /// 589 | /// 590 | /// The key of the JSON object that you want to determine the memory usage of. 591 | /// The path to JSON object you want to check, this defaults to root. 592 | /// Optional command flags. 593 | /// Integer, specifically the size in bytes of the value 594 | public static int JsonDebugMemory(this IDatabase db, RedisKey key, string path = ".", 595 | CommandFlags commandFlags = CommandFlags.None) => 596 | (int) db.Execute(JsonCommands.DEBUG, CombineArguments("MEMORY", key.ToString(), path), flags: commandFlags); 597 | 598 | /// 599 | /// `JSON.RESP` 600 | /// 601 | /// This command uses the following mapping from JSON to RESP: 602 | /// 603 | /// - JSON Null is mapped to the RESP Null Bulk String 604 | /// 605 | /// - JSON `false` and `true` values are mapped to the respective RESP Simple Strings 606 | /// 607 | /// - JSON Numbers are mapped to RESP Integers or RESP Bulk Strings, depending on type 608 | /// 609 | /// - JSON Strings are mapped to RESP Bulk Strings 610 | /// 611 | /// - JSON Arrays are represented as RESP Arrays in which the first element is the simple string `[` followed by the array's elements 612 | /// 613 | /// - JSON Objects are represented as RESP Arrays in which the first element is the simple string `{`. Each successive entry represents a key-value pair as a two-entries array of bulk strings. 614 | /// 615 | /// https://oss.redislabs.com/rejson/commands/#jsonresp 616 | /// 617 | /// 618 | /// The key of the JSON object that you want an RESP result for. 619 | /// Defaults to root if not provided. 620 | /// Optional command flags. 621 | /// Array, specifically the JSON's RESP form as detailed. 622 | public static RedisResult[] JsonGetResp(this IDatabase db, RedisKey key, string path = ".", 623 | CommandFlags commandFlags = CommandFlags.None) => 624 | (RedisResult[]) db.Execute(JsonCommands.RESP, new object[] 625 | { 626 | key, path 627 | }, flags: commandFlags); 628 | 629 | /// 630 | /// `JSON.TOGGLE` 631 | /// 632 | /// Toggle the boolean property of a JSON object. 633 | /// 634 | /// Official documentation forthcoming. 635 | /// 636 | /// 637 | /// The key of the JSON object that contains the property that you'd like to toggle. 638 | /// The path to the boolean property on JSON object that you'd like to toggle. 639 | /// Optional command flags. 640 | /// 641 | public static bool JsonToggle(this IDatabase db, RedisKey key, string path, 642 | CommandFlags commandFlags = CommandFlags.None) 643 | { 644 | var result = db.Execute(JsonCommands.TOGGLE, new object[] {key, path}, flags: commandFlags); 645 | 646 | return bool.Parse(result.ToString()); 647 | } 648 | 649 | /// 650 | /// `JSON.CLEAR` 651 | /// 652 | /// Clear/empty arrays and objects (to have zero slots/keys without deleting the array/object) returning the count 653 | /// of cleared paths (ignoring non-array and non-objects paths). 654 | /// 655 | /// Official documentation forthcoming. 656 | /// 657 | /// 658 | /// 659 | /// 660 | /// Optional command flags. 661 | /// 662 | public static int JsonClear(this IDatabase db, RedisKey key, string path, 663 | CommandFlags commandFlags = CommandFlags.None) 664 | { 665 | var result = db.Execute(JsonCommands.CLEAR, new object[] {key, path}, flags: commandFlags); 666 | 667 | return (int) result; 668 | } 669 | } 670 | } -------------------------------------------------------------------------------- /NReJSON/DatabaseExtensionsAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using StackExchange.Redis; 5 | using static NReJSON.NReJSONSerializer; 6 | 7 | namespace NReJSON 8 | { 9 | public static partial class DatabaseExtensions 10 | { 11 | /// 12 | /// `JSON.DEL` 13 | /// 14 | /// Delete a value. 15 | /// 16 | /// Non-existing keys and paths are ignored. Deleting an object's root is equivalent to deleting the key from Redis. 17 | /// 18 | /// https://oss.redislabs.com/rejson/commands/#jsondel 19 | /// 20 | /// 21 | /// Key where JSON object is stored. 22 | /// Defaults to root if not provided. 23 | /// Optional command flags. 24 | /// Integer, specifically the number of paths deleted (0 or more). 25 | public static async Task JsonDeleteAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 26 | CommandFlags commandFlags = CommandFlags.None) => 27 | (int) (await db.ExecuteAsync(JsonCommands.DEL, CombineArguments(key, path), flags: commandFlags) 28 | .ConfigureAwait(false)); 29 | 30 | /// 31 | /// `JSON.GET` 32 | /// 33 | /// Return the value at `path` in JSON serialized form. 34 | /// 35 | /// `NOESCAPE` is `true` by default. 36 | /// 37 | /// https://oss.redislabs.com/rejson/commands/#jsonget 38 | /// 39 | /// 40 | /// Key where JSON object is stored. 41 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 42 | /// 43 | public static Task JsonGetAsync(this IDatabaseAsync db, RedisKey key, params string[] paths) => 44 | db.JsonGetAsync(key, noEscape: true, paths: paths, commandFlags: CommandFlags.None); 45 | 46 | /// 47 | /// `JSON.GET` 48 | /// 49 | /// Return the value at `path` as a deserialized value. 50 | /// 51 | /// `NOESCAPE` is `true` by default. 52 | /// 53 | /// https://oss.redislabs.com/rejson/commands/#jsonget 54 | /// 55 | /// 56 | /// Key where JSON object is stored. 57 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 58 | /// The type to deserialize the value as. 59 | /// 60 | public static Task> JsonGetAsync(this IDatabaseAsync db, RedisKey key, 61 | params string[] paths) => 62 | db.JsonGetAsync(key, noEscape: true, paths: paths, commandFlags: CommandFlags.None); 63 | 64 | /// 65 | /// `JSON.GET` 66 | /// 67 | /// Return the value at `path` in JSON serialized form. 68 | /// 69 | /// https://oss.redislabs.com/rejson/commands/#jsonget 70 | /// 71 | /// 72 | /// Key where JSON object is stored. 73 | /// This option will disable the sending of \uXXXX escapes for non-ascii characters. This option should be used for efficiency if you deal mainly with such text. 74 | /// Sets the indentation string for nested levels 75 | /// Sets the string that's printed at the end of each line 76 | /// Sets the string that's put between a key and a value 77 | /// Optional command flags. 78 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 79 | /// 80 | public static Task JsonGetAsync(this IDatabaseAsync db, RedisKey key, bool noEscape = false, 81 | string indent = default, string newline = default, string space = default, 82 | CommandFlags commandFlags = CommandFlags.None, params string[] paths) 83 | { 84 | var args = new List {key}; 85 | 86 | if (noEscape) 87 | { 88 | args.Add("NOESCAPE"); 89 | } 90 | 91 | if (indent != default) 92 | { 93 | args.Add("INDENT"); 94 | args.Add(indent); 95 | } 96 | 97 | if (newline != default) 98 | { 99 | args.Add("NEWLINE"); 100 | args.Add(newline); 101 | } 102 | 103 | if (space != default) 104 | { 105 | args.Add("SPACE"); 106 | args.Add(space); 107 | } 108 | 109 | foreach (var path in PathsOrDefault(paths, new[] {"."})) 110 | { 111 | args.Add(path); 112 | } 113 | 114 | return db.ExecuteAsync(JsonCommands.GET, args); 115 | } 116 | 117 | /// 118 | /// `JSON.GET` 119 | /// 120 | /// Return the value at `path` as a deserialized value. 121 | /// 122 | /// https://oss.redislabs.com/rejson/commands/#jsonget 123 | /// 124 | /// 125 | /// Key where JSON object is stored. 126 | /// This option will disable the sending of \uXXXX escapes for non-ascii characters. This option should be used for efficiency if you deal mainly with such text. 127 | /// Sets the indentation string for nested levels 128 | /// Sets the string that's printed at the end of each line 129 | /// Sets the string that's put between a key and a value 130 | /// 131 | /// The path(s) of the JSON properties that you want to return. By default, the entire JSON object will be returned. 132 | /// The type to deserialize the value as. 133 | /// 134 | public static async Task> JsonGetAsync(this IDatabaseAsync db, RedisKey key, 135 | bool noEscape = false, 136 | string indent = default, string newline = default, string space = default, 137 | CommandFlags commandFlags = CommandFlags.None, params string[] paths) 138 | { 139 | var serializedResult = 140 | await db.JsonGetAsync(key, noEscape, indent, newline, space, commandFlags, paths).ConfigureAwait(false); 141 | 142 | return PathedResult.Create(serializedResult); 143 | } 144 | 145 | /// 146 | /// `JSON.MGET` 147 | /// 148 | /// Returns the values at `path` from multiple `key`s. Non-existing keys and non-existing paths are reported as null. 149 | /// 150 | /// https://oss.redislabs.com/rejson/commands/#jsonmget 151 | /// 152 | /// 153 | /// Keys where JSON objects are stored. 154 | /// The path of the JSON property that you want to return for each key. This is "root" by default. 155 | /// Optional command flags. 156 | /// Array of Bulk Strings, specifically the JSON serialization of the value at each key's path. 157 | public static Task JsonMultiGetAsync(this IDatabaseAsync db, string[] keys, string path = ".", 158 | CommandFlags commandFlags = CommandFlags.None) => 159 | db.JsonMultiGetAsync(keys.Select(k => (RedisKey) k).ToArray(), path, commandFlags); 160 | 161 | /// 162 | /// `JSON.MGET` 163 | /// 164 | /// Returns the values at `path` from multiple `key`s. Non-existing keys and non-existing paths are reported as null. 165 | /// 166 | /// https://oss.redislabs.com/rejson/commands/#jsonmget 167 | /// 168 | /// 169 | /// Keys where JSON objects are stored. 170 | /// The path of the JSON property that you want to return for each key. This is "root" by default. 171 | /// Optional command flags. 172 | /// Array of Bulk Strings, specifically the JSON serialization of the value at each key's path. 173 | public static async Task JsonMultiGetAsync(this IDatabaseAsync db, RedisKey[] keys, 174 | string path = ".", 175 | CommandFlags commandFlags = CommandFlags.None) => 176 | (RedisResult[]) (await db.ExecuteAsync(JsonCommands.MGET, CombineArguments(keys, path), flags: commandFlags) 177 | .ConfigureAwait(false)); 178 | 179 | /// 180 | /// `JSON.MGET` 181 | /// 182 | /// Returns an IEnumerable of the specified result type. Non-existing keys and non-existent paths are returnd as type default. 183 | /// 184 | /// https://oss.redislabs.com/rejson/commands/#jsonmget 185 | /// 186 | /// 187 | /// Keys where JSON objects are stored. 188 | /// The path of the JSON property that you want to return for each key. This is "root" by default. 189 | /// Optional command flags. 190 | /// The type to deserialize the value as. 191 | /// IEnumerable of TResult, non-existent paths/keys are returned as default(TResult). 192 | public static async Task> JsonMultiGetAsync(this IDatabaseAsync db, 193 | RedisKey[] keys, 194 | string path = ".", CommandFlags commandFlags = CommandFlags.None) 195 | { 196 | IEnumerable CreateResult(RedisResult[] srs) 197 | { 198 | foreach (var sr in srs) 199 | { 200 | if (sr.IsNull) 201 | { 202 | yield return default(TResult); 203 | } 204 | else 205 | { 206 | yield return SerializerProxy.Deserialize(sr); 207 | } 208 | } 209 | } 210 | 211 | return CreateResult(await db.JsonMultiGetAsync(keys, path).ConfigureAwait(false)); 212 | } 213 | 214 | /// 215 | /// `JSON.SET` 216 | /// 217 | /// Sets the JSON value at path in key 218 | /// 219 | /// For new Redis keys the path must be the root. 220 | /// 221 | /// For existing keys, when the entire path exists, the value that it contains is replaced with the json value. 222 | /// 223 | /// https://oss.redislabs.com/rejson/commands/#jsonset 224 | /// 225 | /// 226 | /// Key where JSON object is to be stored. 227 | /// The JSON object which you want to persist. 228 | /// The path which you want to persist the JSON object. For new objects this must be root. 229 | /// By default the object will be overwritten, but you can specify that the object be set only if it doesn't already exist or to set only IF it exists. 230 | /// Optional command flags. 231 | /// An `OperationResult` indicating success or failure. 232 | public static async Task JsonSetAsync(this IDatabaseAsync db, RedisKey key, string json, 233 | string path = ".", SetOption setOption = SetOption.Default, CommandFlags commandFlags = CommandFlags.None) 234 | { 235 | var result = 236 | (await db.ExecuteAsync( 237 | JsonCommands.SET, 238 | CombineArguments(key, path, json, GetSetOptionString(setOption)), 239 | flags: commandFlags) 240 | .ConfigureAwait(false)).ToString(); 241 | 242 | return new OperationResult(result == "OK", result); 243 | } 244 | 245 | /// 246 | /// `JSON.SET` 247 | /// 248 | /// Sets the JSON value at path in key 249 | /// 250 | /// For new Redis keys the path must be the root. 251 | /// 252 | /// For existing keys, when the entire path exists, the value that it contains is replaced with the json value. 253 | /// 254 | /// https://oss.redislabs.com/rejson/commands/#jsonset 255 | /// 256 | /// 257 | /// Key where JSON object is to be stored. 258 | /// The object to serialize and send. 259 | /// The path which you want to persist the JSON object. For new objects this must be root. 260 | /// By default the object will be overwritten, but you can specify that the object be set only if it doesn't already exist or to set only IF it exists. 261 | /// 262 | /// Type of the object being serialized. 263 | /// An `OperationResult` indicating success or failure. 264 | public static Task JsonSetAsync(this IDatabaseAsync db, RedisKey key, 265 | TObjectType obj, 266 | string path = ".", SetOption setOption = SetOption.Default, 267 | CommandFlags commandFlags = CommandFlags.None) => 268 | db.JsonSetAsync(key, SerializerProxy.Serialize(obj), path, setOption, commandFlags: commandFlags); 269 | 270 | /// 271 | /// `JSON.TYPE` 272 | /// 273 | /// Report the type of JSON value at `path`. 274 | /// 275 | /// `path` defaults to root if not provided. 276 | /// 277 | /// https://oss.redislabs.com/rejson/commands/#jsontype 278 | /// 279 | /// 280 | /// The key of the JSON object you need the type of. 281 | /// The path of the JSON object you want the type of. This defaults to root. 282 | /// Optional command flags. 283 | /// 284 | public static Task JsonTypeAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 285 | CommandFlags commandFlags = CommandFlags.None) => 286 | db.ExecuteAsync(JsonCommands.TYPE, CombineArguments(key, path), flags: commandFlags); 287 | 288 | /// 289 | /// `JSON.NUMINCRBY` 290 | /// 291 | /// Increments the number value stored at `path` by `number`. 292 | /// 293 | /// https://oss.redislabs.com/rejson/commands/#jsonnumincrby 294 | /// 295 | /// 296 | /// The key of the JSON object which contains the number value you want to increment. 297 | /// The path of the JSON value you want to increment. 298 | /// The value you want to increment by. 299 | /// Optional command flags. 300 | public static async Task> JsonIncrementNumberAsync(this IDatabaseAsync db, RedisKey key, 301 | string path, 302 | double number, CommandFlags commandFlags = CommandFlags.None) => 303 | PathedResult.Create(await db.ExecuteAsync(JsonCommands.NUMINCRBY, 304 | CombineArguments(key, path, number), flags: commandFlags)); 305 | 306 | /// 307 | /// `JSON.NUMMULTBY` 308 | /// 309 | /// Multiplies the number value stored at `path` by `number`. 310 | /// 311 | /// https://oss.redislabs.com/rejson/commands/#jsonnummultby 312 | /// 313 | /// 314 | /// They key of the JSON object which contains the number value you want to multiply. 315 | /// The path of the JSON value you want to multiply. 316 | /// The value you want to multiply by. 317 | /// Optional command flags. 318 | public static async Task> JsonMultiplyNumberAsync(this IDatabaseAsync db, RedisKey key, 319 | string path, 320 | double number, CommandFlags commandFlags = CommandFlags.None) => 321 | PathedResult.Create(await db.ExecuteAsync(JsonCommands.NUMMULTBY, 322 | CombineArguments(key, path, number), flags: CommandFlags.None)); 323 | 324 | /// 325 | /// `JSON.STRAPPEND` 326 | /// 327 | /// Append the json-string value(s) the string at path. 328 | /// path defaults to root if not provided. 329 | /// 330 | /// https://oss.redislabs.com/rejson/commands/#jsonstrappend 331 | /// 332 | /// 333 | /// The key of the JSON object you need to append a string value. 334 | /// The path of the JSON string you want to append do. This defaults to root. 335 | /// JSON formatted string. 336 | /// Optional command flags. 337 | /// Length of the new JSON string. 338 | public static async Task JsonAppendJsonStringAsync(this IDatabaseAsync db, 339 | RedisKey key, 340 | string path = ".", 341 | string jsonString = "\"\"", 342 | CommandFlags commandFlags = CommandFlags.None 343 | ) => 344 | NullableIntArrayFrom(await db.ExecuteAsync(JsonCommands.STRAPPEND, CombineArguments(key, path, jsonString), 345 | flags: commandFlags) 346 | .ConfigureAwait(false)); 347 | 348 | /// 349 | /// `JSON.STRLEN` 350 | /// 351 | /// Report the length of the JSON String at `path` in `key`. 352 | /// 353 | /// `path` defaults to root if not provided. If the `key` or `path` do not exist, null is returned. 354 | /// 355 | /// https://oss.redislabs.com/rejson/commands/#jsonstrlen 356 | /// 357 | /// 358 | /// The key of the JSON object you need string length information about. 359 | /// The path of the JSON string you want the length of. This defaults to root. 360 | /// Optional command flags. 361 | /// Integer, specifically the string's length. 362 | public static async Task JsonStringLengthAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 363 | CommandFlags commandFlags = CommandFlags.None) 364 | { 365 | var result = await db.ExecuteAsync(JsonCommands.STRLEN, CombineArguments(key, path), flags: commandFlags) 366 | .ConfigureAwait(false); 367 | 368 | return NullableIntArrayFrom(result); 369 | } 370 | 371 | /// 372 | /// `JSON.ARRAPPEND` 373 | /// 374 | /// Append the `json` value(s) into the array at `path` after the last element in it. 375 | /// 376 | /// https://oss.redislabs.com/rejson/commands/#jsonarrappend 377 | /// 378 | /// 379 | /// The key of the JSON object that contains the array you want to append to. 380 | /// The path to the JSON array you want to append to. 381 | /// The JSON values that you want to append. 382 | /// Array of nullable integers indicating the array's new size at each matched path. 383 | public static Task JsonArrayAppendAsync(this IDatabaseAsync db, RedisKey key, string path, 384 | params object[] json) => 385 | JsonArrayAppendAsync(db, key, path, CommandFlags.None, json); 386 | 387 | /// 388 | /// `JSON.ARRAPPEND` 389 | /// 390 | /// Append the `json` value(s) into the array at `path` after the last element in it. 391 | /// 392 | /// https://oss.redislabs.com/rejson/commands/#jsonarrappend 393 | /// 394 | /// 395 | /// The key of the JSON object that contains the array you want to append to. 396 | /// The path to the JSON array you want to append to. 397 | /// Optional command flags. 398 | /// The JSON values that you want to append. 399 | /// Array of nullable integers indicating the array's new size at each matched path. 400 | public static async Task JsonArrayAppendAsync(this IDatabaseAsync db, RedisKey key, string path, 401 | CommandFlags commandFlags = CommandFlags.None, params object[] json) => 402 | NullableIntArrayFrom((await db.ExecuteAsync(JsonCommands.ARRAPPEND, CombineArguments(key, path, json), flags: commandFlags) 403 | .ConfigureAwait(false))); 404 | 405 | /// 406 | /// `JSON.ARRINDEX` 407 | /// 408 | /// Search for the first occurrence of a scalar JSON value in an array. 409 | /// 410 | /// The optional inclusive `start`(default 0) and exclusive `stop`(default 0, meaning that the last element is included) specify a slice of the array to search. 411 | /// 412 | /// Note: out of range errors are treated by rounding the index to the array's start and end. An inverse index range (e.g. from 1 to 0) will return unfound. 413 | /// 414 | /// https://oss.redislabs.com/rejson/commands/#jsonarrindex 415 | /// 416 | /// 417 | /// The key of the JSON object that contains the array you want to check for a scalar value in. 418 | /// The path to the JSON array that you want to check. 419 | /// The JSON object that you are looking for. 420 | /// Where to start searching, defaults to 0 (the beginning of the array). 421 | /// Where to stop searching, defaults to 0 (the end of the array). 422 | /// Optional command flags. 423 | /// Array of nullable integers, specifically, for each JSON value matching the path, the first position of the scalar value in the array, -1 if unfound in the array, or null if the matching JSON value is not an array. 424 | public static async Task JsonArrayIndexOfAsync(this IDatabaseAsync db, RedisKey key, string path, 425 | object jsonScalar, int start = 0, int stop = 0, CommandFlags commandFlags = CommandFlags.None) 426 | { 427 | var result = await db.ExecuteAsync(JsonCommands.ARRINDEX, 428 | CombineArguments(key, path, jsonScalar, start, stop), 429 | flags: commandFlags) 430 | .ConfigureAwait(false); 431 | 432 | return NullableIntArrayFrom(result); 433 | } 434 | 435 | /// 436 | /// `JSON.ARRINSERT` 437 | /// 438 | /// Insert the `json` value(s) into the array at `path` before the `index` (shifts to the right). 439 | /// 440 | /// The index must be in the array's range. Inserting at `index` 0 prepends to the array. Negative index values are interpreted as starting from the end. 441 | /// 442 | /// https://oss.redislabs.com/rejson/commands/#jsonarrinsert 443 | /// 444 | /// 445 | /// The key of the JSON object that contains the array you want to insert an object into. 446 | /// The path of the JSON array that you want to insert into. 447 | /// The index at which you want to insert, 0 prepends and negative values are interpreted as starting from the end. 448 | /// The object that you want to insert. 449 | /// Array of nullable integer, specifically, for each path, the array's new size, or null element if the matching JSON value is not an array. 450 | public static Task JsonArrayInsertAsync(this IDatabaseAsync db, RedisKey key, string path, int index, 451 | params object[] json) => 452 | JsonArrayInsertAsync(db, key, path, index, CommandFlags.None, json); 453 | 454 | /// 455 | /// `JSON.ARRINSERT` 456 | /// 457 | /// Insert the `json` value(s) into the array at `path` before the `index` (shifts to the right). 458 | /// 459 | /// The index must be in the array's range. Inserting at `index` 0 prepends to the array. Negative index values are interpreted as starting from the end. 460 | /// 461 | /// https://oss.redislabs.com/rejson/commands/#jsonarrinsert 462 | /// 463 | /// 464 | /// The key of the JSON object that contains the array you want to insert an object into. 465 | /// The path of the JSON array that you want to insert into. 466 | /// The index at which you want to insert, 0 prepends and negative values are interpreted as starting from the end. 467 | /// Optional command flags. 468 | /// The object that you want to insert. 469 | /// Array of nullable integer, specifically, for each path, the array's new size, or null element if the matching JSON value is not an array. 470 | public static async Task JsonArrayInsertAsync(this IDatabaseAsync db, RedisKey key, string path, int index, 471 | CommandFlags commandFlags = CommandFlags.None, params object[] json) => 472 | NullableIntArrayFrom((await db.ExecuteAsync(JsonCommands.ARRINSERT, CombineArguments(key, path, index, json), 473 | flags: commandFlags) 474 | .ConfigureAwait(false))); 475 | 476 | /// 477 | /// `JSON.ARRLEN` 478 | /// 479 | /// Report the length of the JSON Array at `path` in `key`. 480 | /// 481 | /// `path` defaults to root if not provided. If the `key` or `path` do not exist, null is returned. 482 | /// 483 | /// https://oss.redislabs.com/rejson/commands/#jsonarrlen 484 | /// 485 | /// 486 | /// The key of the JSON object that contains the array you want the length of. 487 | /// The path to the JSON array that you want the length of. 488 | /// Optional command flags. 489 | /// Array of nullable integer, specifically, for each path, the array's length, or null element if the matching JSON value is not an array. 490 | public static async Task JsonArrayLengthAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 491 | CommandFlags commandFlags = CommandFlags.None) 492 | { 493 | var result = await db.ExecuteAsync(JsonCommands.ARRLEN, CombineArguments(key, path), flags: commandFlags) 494 | .ConfigureAwait(false); 495 | 496 | if (result.IsNull) 497 | { 498 | return null; 499 | } 500 | else 501 | { 502 | return NullableIntArrayFrom(result); 503 | } 504 | } 505 | 506 | /// 507 | /// `JSON.ARRPOP` 508 | /// 509 | /// Remove and return element from the index in the array. 510 | /// 511 | /// Out of range indices are rounded to their respective array ends.Popping an empty array yields null. 512 | /// 513 | /// https://oss.redislabs.com/rejson/commands/#jsonarrpop 514 | /// 515 | /// 516 | /// The key of the JSON object that contains the array you want to pop an object off of. 517 | /// Defaults to root (".") if not provided. 518 | /// Is the position in the array to start popping from (defaults to -1, meaning the last element). 519 | /// Optional command flags. 520 | /// Array of strings, specifically, for each path, the popped JSON value, or null element if the matching JSON value is not an array. 521 | public static async Task JsonArrayPopAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 522 | int index = -1, CommandFlags commandFlags = CommandFlags.None) => 523 | StringArrayFrom(await db.ExecuteAsync(JsonCommands.ARRPOP, CombineArguments(key, path, index), flags: commandFlags)); 524 | 525 | /// 526 | /// `JSON.ARRPOP` 527 | /// 528 | /// Remove and return element from the index in the array. 529 | /// 530 | /// Out of range indices are rounded to their respective array ends.Popping an empty array yields null. 531 | /// 532 | /// https://oss.redislabs.com/rejson/commands/#jsonarrpop 533 | /// 534 | /// 535 | /// The key of the JSON object that contains the array you want to pop an object off of. 536 | /// Defaults to root (".") if not provided. 537 | /// Is the position in the array to start popping from (defaults to -1, meaning the last element). 538 | /// Optional command flags. 539 | /// The type to deserialize the value as. 540 | /// Array of `TResult`, specifically, for each path, the popped JSON value, or null element if the matching JSON value is not an array. 541 | public static async Task JsonArrayPopAsync(this IDatabaseAsync db, RedisKey key, 542 | string path = ".", int index = -1, CommandFlags commandFlags = CommandFlags.None) => 543 | TypedArrayFrom(await db.ExecuteAsync(JsonCommands.ARRPOP, CombineArguments(key, path, index), flags: commandFlags)); 544 | 545 | /// 546 | /// `JSON.ARRTRIM` 547 | /// 548 | /// Trim an array so that it contains only the specified inclusive range of elements. 549 | /// 550 | /// This command is extremely forgiving and using it with out of range indexes will not produce an error. 551 | /// 552 | /// If start is larger than the array's size or start > stop, the result will be an empty array. 553 | /// 554 | /// If start is < 0 then it will be treated as 0. 555 | /// 556 | /// If stop is larger than the end of the array, it will be treated like the last element in it. 557 | /// 558 | /// https://oss.redislabs.com/rejson/commands/#jsonarrtrim 559 | /// 560 | /// 561 | /// The key of the JSON object that contains the array you want to trim. 562 | /// The path of the JSON array that you want to trim. 563 | /// The inclusive start index. 564 | /// The inclusive stop index. 565 | /// Optional command flags. 566 | /// Array of nullable integer, specifically for each path, the array's new size, or null element if the matching JSON value is not an array. 567 | public static async Task JsonArrayTrimAsync(this IDatabaseAsync db, RedisKey key, string path, int start, 568 | int stop, CommandFlags commandFlags = CommandFlags.None) => 569 | NullableIntArrayFrom((await db.ExecuteAsync(JsonCommands.ARRTRIM, CombineArguments(key, path, start, stop), 570 | flags: commandFlags) 571 | .ConfigureAwait(false))); 572 | 573 | /// 574 | /// `JSON.OBJKEYS` 575 | /// 576 | /// Return the keys in the object that's referenced by `path`. 577 | /// 578 | /// `path` defaults to root if not provided.If the object is empty, or either `key` or `path` do not exist, then null is returned. 579 | /// 580 | /// https://oss.redislabs.com/rejson/commands/#jsonobjkeys 581 | /// 582 | /// 583 | /// The key of the JSON object which you want to enumerate keys for. 584 | /// The path to the JSON object you want the keys for, this defaults to root. 585 | /// Optional command flags. 586 | /// Array, specifically the key names in the object as Bulk Strings. 587 | public static async Task JsonObjectKeysAsync(this IDatabaseAsync db, RedisKey key, 588 | string path = ".", 589 | CommandFlags commandFlags = CommandFlags.None) => 590 | (RedisResult[]) (await db 591 | .ExecuteAsync(JsonCommands.OBJKEYS, CombineArguments(key, path), flags: commandFlags) 592 | .ConfigureAwait(false)); 593 | 594 | /// 595 | /// `JSON.OBJLEN` 596 | /// 597 | /// Report the number of keys in the JSON Object at `path` in `key`. 598 | /// 599 | /// `path` defaults to root if not provided. If the `key` or `path` do not exist, null is returned. 600 | /// 601 | /// https://oss.redislabs.com/rejson/commands/#jsonobjlen 602 | /// 603 | /// 604 | /// The key of the JSON object which you want the length of. 605 | /// The path to the JSON object which you want the length of, defaults to root. 606 | /// Optional command flags. 607 | /// Integer, specifically the number of keys in the object. 608 | public static async Task JsonObjectLengthAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 609 | CommandFlags commandFlags = CommandFlags.None) 610 | { 611 | var result = await db.ExecuteAsync(JsonCommands.OBJLEN, CombineArguments(key, path), flags: commandFlags) 612 | .ConfigureAwait(false); 613 | 614 | if (result.IsNull) 615 | { 616 | return null; 617 | } 618 | else 619 | { 620 | return (int) result; 621 | } 622 | } 623 | 624 | /// 625 | /// `JSON.DEBUG MEMORY` 626 | /// 627 | /// Report the memory usage in bytes of a value. `path` defaults to root if not provided. 628 | /// 629 | /// https://oss.redislabs.com/rejson/commands/#jsondebug 630 | /// 631 | /// 632 | /// The key of the JSON object that you want to determine the memory usage of. 633 | /// The path to JSON object you want to check, this defaults to root. 634 | /// Optional command flags. 635 | /// Integer, specifically the size in bytes of the value 636 | public static async Task JsonDebugMemoryAsync(this IDatabaseAsync db, RedisKey key, string path = ".", 637 | CommandFlags commandFlags = CommandFlags.None) => 638 | (int) (await db.ExecuteAsync(JsonCommands.DEBUG, CombineArguments("MEMORY", key.ToString(), path), 639 | flags: commandFlags) 640 | .ConfigureAwait(false)); 641 | 642 | /// 643 | /// `JSON.RESP` 644 | /// 645 | /// This command uses the following mapping from JSON to RESP: 646 | /// 647 | /// - JSON Null is mapped to the RESP Null Bulk String 648 | /// 649 | /// - JSON `false` and `true` values are mapped to the respective RESP Simple Strings 650 | /// 651 | /// - JSON Numbers are mapped to RESP Integers or RESP Bulk Strings, depending on type 652 | /// 653 | /// - JSON Strings are mapped to RESP Bulk Strings 654 | /// 655 | /// - JSON Arrays are represented as RESP Arrays in which the first element is the simple string `[` followed by the array's elements 656 | /// 657 | /// - JSON Objects are represented as RESP Arrays in which the first element is the simple string `{`. Each successive entry represents a key-value pair as a two-entries array of bulk strings. 658 | /// 659 | /// https://oss.redislabs.com/rejson/commands/#jsonresp 660 | /// 661 | /// 662 | /// The key of the JSON object that you want an RESP result for. 663 | /// Defaults to root if not provided. 664 | /// Optional command flags. 665 | /// Array, specifically the JSON's RESP form as detailed. 666 | public static async Task JsonGetRespAsync(this IDatabaseAsync db, RedisKey key, 667 | string path = ".", 668 | CommandFlags commandFlags = CommandFlags.None) => 669 | (RedisResult[]) (await db.ExecuteAsync(JsonCommands.RESP, new object[] {key, path}, flags: commandFlags) 670 | .ConfigureAwait(false)); 671 | 672 | /// 673 | /// `JSON.TOGGLE` 674 | /// 675 | /// Toggle the boolean property of a JSON object. 676 | /// 677 | /// Official documentation forthcoming. 678 | /// 679 | /// 680 | /// The key of the JSON object that contains the property that you'd like to toggle. 681 | /// The path to the boolean property on JSON object that you'd like to toggle. 682 | /// Optional command flags. 683 | /// 684 | public static async Task JsonToggleAsync(this IDatabaseAsync db, RedisKey key, string path, 685 | CommandFlags commandFlags = CommandFlags.None) 686 | { 687 | var result = await db.ExecuteAsync(JsonCommands.TOGGLE, new object[] {key, path}, flags: commandFlags) 688 | .ConfigureAwait(false); 689 | 690 | return bool.Parse(result.ToString()); 691 | } 692 | 693 | /// 694 | /// `JSON.CLEAR` 695 | /// 696 | /// Clear/empty arrays and objects (to have zero slots/keys without deleting the array/object) returning the count 697 | /// of cleared paths (ignoring non-array and non-objects paths). 698 | /// 699 | /// Official documentation forthcoming. 700 | /// 701 | /// 702 | /// 703 | /// 704 | /// Optional command flags. 705 | /// 706 | public static async Task JsonClearAsync(this IDatabaseAsync db, RedisKey key, string path, 707 | CommandFlags commandFlags = CommandFlags.None) 708 | { 709 | var result = await db.ExecuteAsync(JsonCommands.CLEAR, new object[] {key, path}, flags: commandFlags) 710 | .ConfigureAwait(false); 711 | 712 | return (int) result; 713 | } 714 | } 715 | } -------------------------------------------------------------------------------- /NReJSON/DatabaseExtensionsUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StackExchange.Redis; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using static NReJSON.NReJSONSerializer; 6 | 7 | namespace NReJSON 8 | { 9 | public static partial class DatabaseExtensions 10 | { 11 | private static string[] CombineArguments(params object[] args) 12 | { 13 | IEnumerable _combineArguments(object[] _args) 14 | { 15 | if (args == null) 16 | { 17 | yield break; 18 | } 19 | 20 | foreach (var arg in args) 21 | { 22 | if (arg.GetType() == typeof(RedisKey[])) 23 | { 24 | foreach (var aa in (RedisKey[]) arg) 25 | { 26 | yield return aa.ToString(); 27 | } 28 | } 29 | else if (arg.GetType().IsArray) 30 | { 31 | foreach (var aa in (object[]) arg) 32 | { 33 | yield return aa.ToString(); 34 | } 35 | } 36 | else 37 | { 38 | yield return arg.ToString(); 39 | } 40 | } 41 | } 42 | 43 | return _combineArguments(args).Where(a => a.Length > 0).ToArray(); 44 | } 45 | 46 | private static string[] PathsOrDefault(string[] paths, string[] @default) => 47 | paths == null || paths.Length == 0 ? @default : paths; 48 | 49 | private static string GetSetOptionString(SetOption setOption) 50 | { 51 | switch (setOption) 52 | { 53 | case SetOption.Default: 54 | return string.Empty; 55 | case SetOption.SetIfNotExists: 56 | return "NX"; 57 | case SetOption.SetOnlyIfExists: 58 | return "XX"; 59 | default: 60 | return string.Empty; 61 | } 62 | } 63 | 64 | private static readonly string[] RootPathStringArray = {"."}; 65 | 66 | private static int?[] NullableIntArrayFrom(RedisResult result) 67 | { 68 | if (result.IsNull) 69 | { 70 | return null; 71 | } 72 | 73 | switch (result.Type) 74 | { 75 | case ResultType.Integer: 76 | return new int?[] {(int) result}; 77 | case ResultType.MultiBulk: 78 | var resultArray = (RedisResult[]) result; 79 | return resultArray.Select(x => (int?) x).ToArray(); 80 | default: 81 | throw new ArgumentException(nameof(result), "Not sure how to handle this result."); 82 | } 83 | } 84 | 85 | private static string[] StringArrayFrom(RedisResult result) 86 | { 87 | if (result.IsNull) 88 | { 89 | return null; 90 | } 91 | 92 | switch (result.Type) 93 | { 94 | case ResultType.BulkString: 95 | return new[] {(string) result}; 96 | case ResultType.MultiBulk: 97 | var resultArray = (RedisResult[]) result; 98 | return resultArray.Select(x => (string) x).ToArray(); 99 | default: 100 | throw new ArgumentException(nameof(result), "Not sure how to handle this result."); 101 | } 102 | } 103 | 104 | private static TResult[] TypedArrayFrom(RedisResult result) 105 | { 106 | if (result.IsNull) 107 | { 108 | return default; 109 | } 110 | 111 | switch (result.Type) 112 | { 113 | case ResultType.BulkString: 114 | return new TResult[] { SerializerProxy.Deserialize(result) }; 115 | case ResultType.MultiBulk: 116 | var resultArray = (RedisResult[]) result; 117 | var typedResult = new TResult[resultArray.Length]; 118 | 119 | for (var i = 0; i < typedResult.Length; i++) 120 | { 121 | var current = resultArray[i]; 122 | 123 | if (current.IsNull) 124 | { 125 | typedResult[i] = default; 126 | } 127 | else 128 | { 129 | typedResult[i] = SerializerProxy.Deserialize(current); 130 | } 131 | } 132 | 133 | return typedResult; 134 | default: 135 | throw new ArgumentException(nameof(result), "Not sure how to handle this result."); 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /NReJSON/ISerializerProxy.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace NReJSON 4 | { 5 | /// 6 | /// Interface used to define a proxy to a JSON serializer. 7 | /// 8 | public interface ISerializerProxy 9 | { 10 | /// 11 | /// Proxy method for accessing the deserialization functionality of your chosen JSON deserializer. 12 | /// 13 | /// The JSON formatted value that is returned by Redis. 14 | /// The desired result type. 15 | /// 16 | TResult Deserialize(RedisResult serializedValue); 17 | 18 | /// 19 | /// Proxy method for accessing the serialization functionality of your chosne JSON serializer. 20 | /// 21 | /// Object to serialize. 22 | /// Type of the object to serialize. 23 | /// Serialization result as a string. 24 | string Serialize(TObjectType obj); 25 | } 26 | } -------------------------------------------------------------------------------- /NReJSON/IndexedCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace NReJSON 6 | { 7 | /// 8 | /// This type was introduced specifically for the result of the JsonIndexGet(Async) methods. 9 | /// 10 | /// Since those methods return a dictionary of IEnumerables I figured it'd be nice if you 11 | /// had the option to iterate over all of the values without having to directly interact with 12 | /// the dictionary, or access the underlying values by specifying the keys. 13 | /// 14 | /// 15 | public class IndexedCollection : IEnumerable 16 | { 17 | private readonly IDictionary> _wrappedResult; 18 | 19 | internal IndexedCollection(IDictionary> wrappedResult) => 20 | _wrappedResult = wrappedResult; 21 | 22 | /// 23 | /// Provides a way to access the underlying values by key (just like a regular dictionary). 24 | /// 25 | public IEnumerable this[string key] => _wrappedResult[key]; 26 | 27 | /// 28 | /// Provides a way to access the underlying keys in the wrapped dictionary. 29 | /// 30 | public IEnumerable Keys => _wrappedResult.Keys; 31 | 32 | IEnumerator IEnumerable.GetEnumerator() => 33 | InnerIterator().GetEnumerator(); 34 | 35 | IEnumerator IEnumerable.GetEnumerator() => 36 | InnerIterator().GetEnumerator(); 37 | 38 | private IEnumerable InnerIterator() => 39 | _wrappedResult.Values.SelectMany(x => x); 40 | } 41 | } -------------------------------------------------------------------------------- /NReJSON/JsonCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NReJSON 4 | { 5 | internal static class JsonCommands 6 | { 7 | public const string DEL = "JSON.DEL"; 8 | public const string GET = "JSON.GET"; 9 | public const string MGET = "JSON.MGET"; 10 | public const string SET = "JSON.SET"; 11 | public const string TYPE = "JSON.TYPE"; 12 | public const string NUMINCRBY = "JSON.NUMINCRBY"; 13 | public const string NUMMULTBY = "JSON.NUMMULTBY"; 14 | public const string STRAPPEND = "JSON.STRAPPEND"; 15 | public const string STRLEN = "JSON.STRLEN"; 16 | public const string ARRAPPEND = "JSON.ARRAPPEND"; 17 | public const string ARRINDEX = "JSON.ARRINDEX"; 18 | public const string ARRINSERT = "JSON.ARRINSERT"; 19 | public const string ARRLEN = "JSON.ARRLEN"; 20 | public const string ARRPOP = "JSON.ARRPOP"; 21 | public const string ARRTRIM = "JSON.ARRTRIM"; 22 | public const string OBJKEYS = "JSON.OBJKEYS"; 23 | public const string DEBUG = "JSON.DEBUG"; 24 | public const string FORGET = "JSON.FORGET"; 25 | public const string RESP = "JSON.RESP"; 26 | public const string OBJLEN = "JSON.OBJLEN"; 27 | [Obsolete("This command has been deprecated and will be removed in a future version of RedisJson.")] 28 | public const string INDEX = "JSON.INDEX"; 29 | [Obsolete("This command has been deprecated and will be removed in a future version of RedisJson.")] 30 | public const string QGET = "JSON.QGET"; 31 | public const string TOGGLE = "JSON.TOGGLE"; 32 | public const string CLEAR = "JSON.CLEAR"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NReJSON/NReJSON.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net472;net461 5 | true 6 | Tom Hanks 7 | 8 | 3.3.0.0 9 | 3.3.0.0 10 | 3.3.0 11 | A series of extension methods for use with StackExchange.Redis 2.x and the RedisJson Redis module. 12 | false 13 | https://github.com/tombatron/NReJSON 14 | https://github.com/tombatron/NReJSON 15 | true 16 | 4.0.0 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | license.txt 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /NReJSON/NReJSONException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NReJSON 4 | { 5 | /// 6 | /// NReJSONException is used to describe exceptions that occur within the NReJSON 7 | /// library.null 8 | /// 9 | [Serializable] 10 | public class NReJSONException : Exception 11 | { 12 | /// 13 | /// Initialize an instance of NReJSONException with a simple message. 14 | /// 15 | /// The message that you want populated in the `Message` property of the exception. 16 | /// 17 | public NReJSONException(string message) : base(message) { } 18 | 19 | /// 20 | /// Initialize an instance of NReJSONException with a simple message and wrap another exception. 21 | /// 22 | /// The message that you want populated in the `Message` property of the exception. 23 | /// The exception that you want to wrap. 24 | /// 25 | public NReJSONException(string message, Exception inner) : base(message, inner) { } 26 | } 27 | } -------------------------------------------------------------------------------- /NReJSON/NReJSONSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace NReJSON 2 | { 3 | /// 4 | /// This static class serves as a container for storing a JSON serializer for use by NReJSON 5 | /// when returning typed results from the RedisJson command. 6 | /// 7 | public static class NReJSONSerializer 8 | { 9 | private static object _locker = new object(); 10 | private static bool _serializerSet = false; 11 | 12 | private static volatile ISerializerProxy _serializerProxy = default; 13 | 14 | /// 15 | /// This is where you assign your implementation of the `ISerializerProxy` interface. 16 | /// 17 | /// This property is "write-once" meaning, that once it is set it cannot be reset. 18 | /// 19 | /// This property will throw an `NReJSONException` if you try to access it before it 20 | /// is assigned. 21 | /// 22 | /// An implementation of the ISerializerProxy that is appropriate for your application. 23 | public static ISerializerProxy SerializerProxy 24 | { 25 | get 26 | { 27 | if (_serializerSet) 28 | { 29 | return _serializerProxy; 30 | } 31 | 32 | lock (_locker) 33 | { 34 | if (_serializerProxy is null) 35 | { 36 | throw new NReJSONException("Attempted to access the ISerializerProxy before it was set. Consider setting `NReJSONSerializer.SerializerProxy` with an implementation of `ISerializerProxy` before proceeding."); 37 | } 38 | else 39 | { 40 | return _serializerProxy; 41 | } 42 | } 43 | } 44 | 45 | set 46 | { 47 | if (_serializerSet) 48 | { 49 | return; 50 | } 51 | 52 | if (_serializerProxy is null) 53 | { 54 | lock (_locker) 55 | { 56 | if (_serializerProxy is null) 57 | { 58 | _serializerProxy = value; 59 | _serializerSet = true; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /NReJSON/OperationResult.cs: -------------------------------------------------------------------------------- 1 | namespace NReJSON 2 | { 3 | /// 4 | /// This struct captures the result of an operation that returns either 5 | /// `OK` or an error message. 6 | /// 7 | /// This struct is implicitly convertable to `bool`. 8 | /// 9 | public readonly struct OperationResult 10 | { 11 | /// 12 | /// A boolean value indicating whether or not an operation succeeded. 13 | /// 14 | /// 15 | public bool IsSuccess { get; } 16 | 17 | /// 18 | /// The raw result of the operation will be stored here. 19 | /// 20 | /// 21 | public string RawResult { get; } 22 | 23 | /// 24 | /// Construct the OperationResult. 25 | /// 26 | /// True, if the operation succeeded and false, if the operation didn't succeed. 27 | /// The result as handled by NReJSON. 28 | public OperationResult(bool isSuccess, string rawResult) 29 | { 30 | IsSuccess = isSuccess; 31 | RawResult = rawResult; 32 | } 33 | 34 | /// 35 | /// Provides the ability to implicitly convert the `OperationalResult` to a boolean. 36 | /// 37 | /// 38 | public static implicit operator bool(OperationResult operationResult) => 39 | operationResult.IsSuccess; 40 | } 41 | } -------------------------------------------------------------------------------- /NReJSON/PathedResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using StackExchange.Redis; 5 | using static NReJSON.NReJSONSerializer; 6 | 7 | namespace NReJSON 8 | { 9 | /// 10 | /// Encapsulates the result of a "pathed" operation such as JsonGet(Async) which can return multiple results based 11 | /// on whatever path was provided. 12 | /// 13 | /// 14 | public class PathedResult : IEnumerable 15 | { 16 | /// 17 | /// The original unparsed result that came back from Redis. 18 | /// 19 | public RedisResult InnerResult { get; } 20 | 21 | private PathedResult(RedisResult redisResult) => 22 | InnerResult = redisResult; 23 | 24 | internal static PathedResult Create(RedisResult redisResult) => 25 | new PathedResult(redisResult); 26 | 27 | /// 28 | /// Implicit conversion to a single instance of `TResult` for convenience. 29 | /// 30 | /// 31 | /// 32 | public static implicit operator TResult(PathedResult pathedResult) => 33 | pathedResult.Single(); 34 | 35 | /// 36 | /// Returns the enumerator that will attempt to parse the RedisResult into multiple results. 37 | /// 38 | /// 39 | public IEnumerator GetEnumerator() => ParseRedisResult().GetEnumerator(); 40 | 41 | IEnumerator IEnumerable.GetEnumerator() => ParseRedisResult().GetEnumerator(); 42 | 43 | private IEnumerable ParseRedisResult() 44 | { 45 | if (InnerResult.IsNull) 46 | { 47 | return Enumerable.Empty(); 48 | } 49 | 50 | if (IsJsonArray(InnerResult.ToString())) 51 | { 52 | return SerializerProxy.Deserialize>(InnerResult); 53 | } 54 | else 55 | { 56 | return EnumerableFrom(SerializerProxy.Deserialize(InnerResult)); 57 | } 58 | } 59 | 60 | private static bool IsJsonArray(string redisStringResult) => 61 | redisStringResult.StartsWith("[") && redisStringResult.EndsWith("]"); 62 | 63 | private static IEnumerable EnumerableFrom(TResult result) 64 | { 65 | yield return result; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /NReJSON/SetOption.cs: -------------------------------------------------------------------------------- 1 | namespace NReJSON 2 | { 3 | /// 4 | /// An enumeration that captures the available options for setting values. 5 | /// 6 | public enum SetOption 7 | { 8 | /// 9 | /// Set the value not matter what. 10 | /// 11 | Default, 12 | 13 | /// 14 | /// Set the value only if it doesn't already exist. 15 | /// 16 | SetIfNotExists, 17 | 18 | /// 19 | /// Set the value only if it already exists. 20 | /// 21 | SetOnlyIfExists 22 | } 23 | } -------------------------------------------------------------------------------- /NReJSON/license/license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thomas Hanks 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 | 23 | Copyright 2018 Thomas Hanks 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NReJSON 2 | 3 | [![Build Status](https://github.com/tombatron/NReJSON/actions/workflows/dotnet.yml/badge.svg)](https://github.com/tombatron/NReJSON/actions/workflows/dotnet.yml) 4 | 5 | ## Overview 6 | 7 | NReJSON is a series of extension methods for the [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) library that will enable you to interact with the [Redis](https://redis.io/) module [RedisJSON](https://github.com/RedisJSON/RedisJSON). This is made possible by the `Execute` and `ExecuteAsync` methods already present in the SE.Redis library. 8 | 9 | The following blog post by Marc Gravell was the inspiration behind this: [StackExchange.Redis and Redis 4.0 Modules](https://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html). He even has an example of how to call a command from the RedisJSON module! 10 | 11 | ## Installation 12 | 13 | `PM> Install-Package NReJSON -Version 4.0.0` 14 | 15 | ## Usage 16 | 17 | I'm assuming that you already have the [RedisJSON](https://github.com/RedisJSON/RedisJSON) module installed on your Redis server. 18 | 19 | You can verify that the module is installed by executing the following command: 20 | 21 | `MODULE LIST` 22 | 23 | If RedisJSON is installed you should see output similar to the following: 24 | 25 | ``` 26 | 1) 1) "name" 27 | 2) "ReJSON" 28 | 3) "ver" 29 | 4) (integer) 10001 30 | ``` 31 | 32 | (The version of the module installed on your server obviously may vary.) 33 | 34 | ## Major Changes in Version 4.0 35 | 36 | - Requires RedisJson 2.0. 37 | - All deprecated RedisJson commands have been removed. 38 | 39 | - Introduced `PathedResult` in order to handle commands which can return multiple results in the format of a JSON array based on a provided JSONPath specification. 40 | - **BREAKING CHANGE** : The generic overloads for `JsonGet` and `JsonGetAsync` now return an instance of `PathedResult`. 41 | - **BREAKING CHANGE** : `JsonIncrementNumber` and `JsonIncrementNumberAsync` now return an instance of `PathedResult`. 42 | - **BREAKING CHANGE** : `JsonMultiplyNumber` and `JsonMultiplyNumberAsync` now return an instance of `PathedResult`. 43 | - **BREAKING CHANGE** : `JsonAppendJsonString` and `JsonAppendJsonStringAsync` now return `int?[]` to support multiple JSONPath matches. 44 | - **BREAKING CHANGE** : `JsonStringLength` and `JsonStringLengthAsync` now return `int?[]` to support multiple JSONPath matches. 45 | - **BREAKING CHANGE** : `JsonArrayAppend` and `JsonArrayAppendAsync` now return `int?[]` to support multiple JSONPath matches. 46 | - **BREAKING CHANGE** : `JsonArrayIndexOf` and `JsonArrayIndexOfAsync` now return `int?[]` to support multiple JSONPath matches. 47 | - **BREAKING CHANGE** : `JsonArrayInsert` and `JsonArrayInsertAsync` now return `int?[]` to support multiple JSONPath matches. 48 | - **BREAKING CHANGE** : `JsonArrayLength` and `JsonArrayLengthAsync` now return `int?[]` to support multiple JSONPath matches. 49 | - **BREAKING CHANGE** : `JsonArrayPop` and `JsonArrayPopAsync` now return `string?[]` or `TResult[]` for the generic overloads to support multiple JSONPath matches. 50 | - **BREAKING CHANGE** : `JsonArrayTrim` and `JsonArrayTrimAsync` now return `int?[]` to support multiple JSONPath matches. 51 | - Removed ability to associate a JSON object with an index using the `JsonSet` and `JsonSetAsync` methods. 52 | - Changed param array in `JsonArrayAppend` and `JsonArrayAppendAsync` to be an array of type `object`. 53 | - Changed `jsonScalar` parameter in `JsonArrayIndexOf` and `JsonArrayIndexOfAsync` to be of type `object`. 54 | - Changed param array in `JsonArrayInsert` and `JsonArrayInsertAsync` to be an array of type `object`. 55 | 56 | ## Major Changes in Version 3.0 57 | 58 | In version 3.0 support for serialization and deserialization was added in the form of new generic overloads for the following extension methods: 59 | 60 | - JsonGet/JsonGetAsync 61 | - JsonMultiGet/JsonMultiGetAsync 62 | - JsonSet/JsonSetAsync 63 | - JsonArrayPop/JsonArrayPopAsync 64 | - JsonIndexGet/JsonIndexGetAsync 65 | 66 | In order to leverage the serialization/deserialization support you must create an implementation of the `ISerializerProxy` interface. The following is a sample implementation taken from the integration tests: 67 | 68 | ```csharp 69 | public sealed class TestJsonSerializer : ISerializerProxy 70 | { 71 | public TResult Deserialize(RedisResult serializedValue) => 72 | JsonSerializer.Deserialize(serializedValue.ToString()); 73 | 74 | public string Serialize(TObjectType obj) => 75 | JsonSerializer.Serialize(obj); 76 | } 77 | ``` 78 | 79 | Once that is implemented it can be assigned to the static property `SerializerProxy` found in the static class `NReJSONSerializer`. The following is an example of how to do this: 80 | 81 | ```csharp 82 | NReJSONSerializer.SerializerProxy = new TestJsonSerializer(); 83 | 84 | ``` 85 | 86 | If this isn't setup before leveraging the extension methods that make use of it, an `NReJSONException` will be thrown in order to remind you that it needs to be done. 87 | 88 | The result type for the following methods has change to `OperationResult`: 89 | 90 | - JsonSet/JsonSetAsync 91 | - JsonIndexAdd/JsonIndexAddAsync 92 | - JsonIndexDelete/JsonIndexDeleteAsync 93 | 94 | The `OperationResult` is a struct that will return and contain whether or not the operation was successful, and will also contain the raw response from Redis. This type is implicitly convertable to `bool` so it can be used in operations like: 95 | 96 | ```csharp 97 | var result = await db.JsonSetAsync(key, obj); 98 | 99 | if (result) 100 | { 101 | // Do something if there was success... 102 | } 103 | else 104 | { 105 | // Do something if there wasn't success... 106 | } 107 | ``` 108 | 109 | Last but not least, we have a new result type called `IndexedCollection` which is now returned by the overload which deserializes results on the following method: 110 | 111 | - JsonIndexGet/JsonIndexGetAsync 112 | 113 | This type is generic and allows for dealing with the result of the `JsonIndexGet` and `JsonIndexGetAsync` as a dictionary and as a collection. 114 | 115 | ### Examples 116 | 117 | Sam Dzirasa has authored a blog post full of practical examples of how to use NReJSON in an application: 118 | 119 | [Using RedisJson](https://blog.alumdb.org/using-redisjson/) 120 | 121 | Also, in this repository there are a suite of integration tests that should be sufficent to serve as examples on how to use all supported RedisJSON commands. 122 | 123 | [Integration Tests](https://github.com/tombatron/NReJSON/blob/master/NReJSON.IntegrationTests/DatabaseExtensionAsyncTests.cs) 124 | 125 | --------------------------------------------------------------------------------