Skip to content

Commit ea4baf5

Browse files
authored
Handle KeyNotFoundException thrown from DictionaryPropertyProxy and DictionaryTypedPropertyProxy (#40)
* handle KeyNotFoundException thrown from DictionaryPropertyProxy and DictionaryTypedPropertyProxy * use TryGetValue in DictionaryTypedPropertyProxy * remove try-catch in DictionaryPropertyProxy * revert to stopping patch execution on patch error
1 parent b7cf008 commit ea4baf5

File tree

4 files changed

+212
-41
lines changed

4 files changed

+212
-41
lines changed

SystemTextJsonPatch.Tests/IntegrationTests/DictionaryIntegrationTest.cs

+152-34
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
using System.Collections.Generic;
1+
using System.Collections;
2+
using System.Collections.Generic;
23
using System.Text.Json.Serialization;
34
using System.Text.Json.Serialization.Metadata;
45
using SystemTextJsonPatch.Exceptions;
6+
using SystemTextJsonPatch.Operations;
57
using Xunit;
68

79
namespace SystemTextJsonPatch.IntegrationTests;
@@ -130,11 +132,13 @@ private class Address
130132
private class IntDictionary
131133
{
132134
public IDictionary<string, int> DictionaryOfStringToInteger { get; } = new Dictionary<string, int>();
135+
public IDictionary NonGenericDictionary { get; } = new NonGenericDictionary();
133136
}
134137

135138
private class CustomerDictionary
136139
{
137-
public IDictionary<int, Customer> DictionaryOfStringToCustomer { get; } = new Dictionary<int, Customer>();
140+
public IDictionary<int, Customer> DictionaryOfIntegerToCustomer { get; } = new Dictionary<int, Customer>();
141+
public IDictionary NonGenericDictionary { get; } = new NonGenericDictionary();
138142
}
139143

140144
#if NET7_0_OR_GREATER
@@ -165,9 +169,9 @@ public void TestPocoObjectSucceeds()
165169
var key1 = 100;
166170
var value1 = new Customer() { Name = "James" };
167171
var model = new CustomerDictionary();
168-
model.DictionaryOfStringToCustomer[key1] = value1;
172+
model.DictionaryOfIntegerToCustomer[key1] = value1;
169173
var patchDocument = new JsonPatchDocument();
170-
patchDocument.Test($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
174+
patchDocument.Test($"/DictionaryOfIntegerToCustomer/{key1}/Name", "James");
171175

172176
// Act & Assert
173177
patchDocument.ApplyTo(model);
@@ -180,9 +184,9 @@ public void TestPocoObjectFailsWhenTestValueIsNotEqualToObjectValue()
180184
var key1 = 100;
181185
var value1 = new Customer() { Name = "James" };
182186
var model = new CustomerDictionary();
183-
model.DictionaryOfStringToCustomer[key1] = value1;
187+
model.DictionaryOfIntegerToCustomer[key1] = value1;
184188
var patchDocument = new JsonPatchDocument();
185-
patchDocument.Test($"/DictionaryOfStringToCustomer/{key1}/Name", "Mike");
189+
patchDocument.Test($"/DictionaryOfIntegerToCustomer/{key1}/Name", "Mike");
186190

187191
// Act
188192
var exception = Assert.Throws<JsonPatchTestOperationException>(() => { patchDocument.ApplyTo(model); });
@@ -200,19 +204,19 @@ public void AddPocoObjectSucceeds()
200204
var key2 = 200;
201205
var value2 = new Customer() { Name = "Mike" };
202206
var model = new CustomerDictionary();
203-
model.DictionaryOfStringToCustomer[key1] = value1;
207+
model.DictionaryOfIntegerToCustomer[key1] = value1;
204208
var patchDocument = new JsonPatchDocument();
205-
patchDocument.Add($"/DictionaryOfStringToCustomer/{key2}", value2);
209+
patchDocument.Add($"/DictionaryOfIntegerToCustomer/{key2}", value2);
206210

207211
// Act
208212
patchDocument.ApplyTo(model);
209213

210214
// Assert
211-
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
212-
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
215+
Assert.Equal(2, model.DictionaryOfIntegerToCustomer.Count);
216+
var actualValue1 = model.DictionaryOfIntegerToCustomer[key1];
213217
Assert.NotNull(actualValue1);
214218
Assert.Equal("James", actualValue1.Name);
215-
var actualValue2 = model.DictionaryOfStringToCustomer[key2];
219+
var actualValue2 = model.DictionaryOfIntegerToCustomer[key2];
216220
Assert.NotNull(actualValue2);
217221
Assert.Equal("Mike", actualValue2.Name);
218222
}
@@ -226,17 +230,17 @@ public void AddReplacesPocoObjectSucceeds()
226230
var key2 = 200;
227231
var value2 = new Customer() { Name = "Mike" };
228232
var model = new CustomerDictionary();
229-
model.DictionaryOfStringToCustomer[key1] = value1;
230-
model.DictionaryOfStringToCustomer[key2] = value2;
233+
model.DictionaryOfIntegerToCustomer[key1] = value1;
234+
model.DictionaryOfIntegerToCustomer[key2] = value2;
231235
var patchDocument = new JsonPatchDocument();
232-
patchDocument.Add($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
236+
patchDocument.Add($"/DictionaryOfIntegerToCustomer/{key1}/Name", "James");
233237

234238
// Act
235239
patchDocument.ApplyTo(model);
236240

237241
// Assert
238-
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
239-
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
242+
Assert.Equal(2, model.DictionaryOfIntegerToCustomer.Count);
243+
var actualValue1 = model.DictionaryOfIntegerToCustomer[key1];
240244
Assert.NotNull(actualValue1);
241245
Assert.Equal("James", actualValue1.Name);
242246
}
@@ -271,16 +275,16 @@ public void RemovePocoObjectSucceeds()
271275
var key2 = 200;
272276
var value2 = new Customer() { Name = "Mike" };
273277
var model = new CustomerDictionary();
274-
model.DictionaryOfStringToCustomer[key1] = value1;
275-
model.DictionaryOfStringToCustomer[key2] = value2;
278+
model.DictionaryOfIntegerToCustomer[key1] = value1;
279+
model.DictionaryOfIntegerToCustomer[key2] = value2;
276280
var patchDocument = new JsonPatchDocument();
277-
patchDocument.Remove($"/DictionaryOfStringToCustomer/{key1}/Name");
281+
patchDocument.Remove($"/DictionaryOfIntegerToCustomer/{key1}/Name");
278282

279283
// Act
280284
patchDocument.ApplyTo(model);
281285

282286
// Assert
283-
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
287+
var actualValue1 = model.DictionaryOfIntegerToCustomer[key1];
284288
Assert.Null(actualValue1.Name);
285289
}
286290

@@ -293,16 +297,16 @@ public void MovePocoObjectSucceeds()
293297
var key2 = 200;
294298
var value2 = new Customer() { Name = "Mike" };
295299
var model = new CustomerDictionary();
296-
model.DictionaryOfStringToCustomer[key1] = value1;
297-
model.DictionaryOfStringToCustomer[key2] = value2;
300+
model.DictionaryOfIntegerToCustomer[key1] = value1;
301+
model.DictionaryOfIntegerToCustomer[key2] = value2;
298302
var patchDocument = new JsonPatchDocument();
299-
patchDocument.Move($"/DictionaryOfStringToCustomer/{key1}/Name", $"/DictionaryOfStringToCustomer/{key2}/Name");
303+
patchDocument.Move($"/DictionaryOfIntegerToCustomer/{key1}/Name", $"/DictionaryOfIntegerToCustomer/{key2}/Name");
300304

301305
// Act
302306
patchDocument.ApplyTo(model);
303307

304308
// Assert
305-
var actualValue2 = model.DictionaryOfStringToCustomer[key2];
309+
var actualValue2 = model.DictionaryOfIntegerToCustomer[key2];
306310
Assert.NotNull(actualValue2);
307311
Assert.Equal("James", actualValue2.Name);
308312
}
@@ -316,17 +320,17 @@ public void CopyPocoObjectSucceeds()
316320
var key2 = 200;
317321
var value2 = new Customer() { Name = "Mike" };
318322
var model = new CustomerDictionary();
319-
model.DictionaryOfStringToCustomer[key1] = value1;
320-
model.DictionaryOfStringToCustomer[key2] = value2;
323+
model.DictionaryOfIntegerToCustomer[key1] = value1;
324+
model.DictionaryOfIntegerToCustomer[key2] = value2;
321325
var patchDocument = new JsonPatchDocument();
322-
patchDocument.Copy($"/DictionaryOfStringToCustomer/{key1}/Name", $"/DictionaryOfStringToCustomer/{key2}/Name");
326+
patchDocument.Copy($"/DictionaryOfIntegerToCustomer/{key1}/Name", $"/DictionaryOfIntegerToCustomer/{key2}/Name");
323327

324328
// Act
325329
patchDocument.ApplyTo(model);
326330

327331
// Assert
328-
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
329-
var actualValue2 = model.DictionaryOfStringToCustomer[key2];
332+
Assert.Equal(2, model.DictionaryOfIntegerToCustomer.Count);
333+
var actualValue2 = model.DictionaryOfIntegerToCustomer[key2];
330334
Assert.NotNull(actualValue2);
331335
Assert.Equal("James", actualValue2.Name);
332336
}
@@ -340,17 +344,17 @@ public void ReplacePocoObjectSucceeds()
340344
var key2 = 200;
341345
var value2 = new Customer() { Name = "Mike" };
342346
var model = new CustomerDictionary();
343-
model.DictionaryOfStringToCustomer[key1] = value1;
344-
model.DictionaryOfStringToCustomer[key2] = value2;
347+
model.DictionaryOfIntegerToCustomer[key1] = value1;
348+
model.DictionaryOfIntegerToCustomer[key2] = value2;
345349
var patchDocument = new JsonPatchDocument();
346-
patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
350+
patchDocument.Replace($"/DictionaryOfIntegerToCustomer/{key1}/Name", "James");
347351

348352
// Act
349353
patchDocument.ApplyTo(model);
350354

351355
// Assert
352-
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
353-
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
356+
Assert.Equal(2, model.DictionaryOfIntegerToCustomer.Count);
357+
var actualValue1 = model.DictionaryOfIntegerToCustomer[key1];
354358
Assert.NotNull(actualValue1);
355359
Assert.Equal("James", actualValue1.Name);
356360
}
@@ -379,4 +383,118 @@ public void ReplacePocoObjectWithEscapingSucceeds()
379383
Assert.Equal(300, actualValue1);
380384
Assert.Equal(200, actualValue2);
381385
}
386+
387+
[Theory]
388+
[InlineData("test", "DictionaryOfStringToInteger")]
389+
[InlineData("move", "DictionaryOfStringToInteger")]
390+
[InlineData("copy", "DictionaryOfStringToInteger")]
391+
[InlineData("test", "NonGenericDictionary")]
392+
[InlineData("move", "NonGenericDictionary")]
393+
[InlineData("copy", "NonGenericDictionary")]
394+
public void ReadIntegerValueOfMissingKeyThrowsJsonPatchExceptionWithDefaultErrorReporter(string op, string dictionaryPropertyName)
395+
{
396+
// Arrange
397+
var model = new IntDictionary();
398+
var missingKey = "eight";
399+
var operation = new Operation<IntDictionary>(
400+
op,
401+
path: $"/{dictionaryPropertyName}/{missingKey}",
402+
from: $"/{dictionaryPropertyName}/{missingKey}",
403+
value: 8);
404+
405+
var patchDocument = new JsonPatchDocument<IntDictionary>();
406+
patchDocument.Operations.Add(operation);
407+
408+
// Act
409+
var exception = Assert.Throws<JsonPatchException>(() => { patchDocument.ApplyTo(model); });
410+
411+
// Assert
412+
Assert.Equal($"The target location specified by path segment '{missingKey}' was not found.", exception.Message);
413+
}
414+
415+
[Theory]
416+
[InlineData("test", "DictionaryOfStringToInteger")]
417+
[InlineData("move", "DictionaryOfStringToInteger")]
418+
[InlineData("copy", "DictionaryOfStringToInteger")]
419+
[InlineData("test", "NonGenericDictionary")]
420+
[InlineData("move", "NonGenericDictionary")]
421+
[InlineData("copy", "NonGenericDictionary")]
422+
public void ReadIntegerValueOfMissingKeyDoesNotThrowExceptionWithCustomErrorReporter(string op, string dictionaryPropertyName)
423+
{
424+
// Arrange
425+
var patchErrorLogger = new TestErrorLogger<DictionaryTest>();
426+
var model = new IntDictionary();
427+
var missingKey = "eight";
428+
var operation = new Operation<IntDictionary>(
429+
op,
430+
path: $"/{dictionaryPropertyName}/{missingKey}",
431+
from: $"/{dictionaryPropertyName}/{missingKey}",
432+
value: 8);
433+
434+
var patchDocument = new JsonPatchDocument<IntDictionary>();
435+
patchDocument.Operations.Add(operation);
436+
437+
// Act
438+
patchDocument.ApplyTo(model, patchErrorLogger.LogErrorMessage);
439+
440+
// Assert
441+
Assert.Equal($"The target location specified by path segment '{missingKey}' was not found.", patchErrorLogger.ErrorMessage);
442+
}
443+
444+
[Theory]
445+
[InlineData("test", "DictionaryOfIntegerToCustomer")]
446+
[InlineData("move", "DictionaryOfIntegerToCustomer")]
447+
[InlineData("copy", "DictionaryOfIntegerToCustomer")]
448+
[InlineData("test", "NonGenericDictionary")]
449+
[InlineData("move", "NonGenericDictionary")]
450+
[InlineData("copy", "NonGenericDictionary")]
451+
public void ReadPocoObjectValueOfMissingKeyThrowsJsonPatchExceptionWithDefaultErrorReporter(string op, string dictionaryPropertyName)
452+
{
453+
// Arrange
454+
var model = new CustomerDictionary();
455+
var missingKey = 8;
456+
var operation = new Operation<CustomerDictionary>(
457+
op,
458+
path: $"/{dictionaryPropertyName}/{missingKey}/Address/City",
459+
from: $"/{dictionaryPropertyName}/{missingKey}/Address/City",
460+
value: "Nowhere");
461+
462+
var patchDocument = new JsonPatchDocument<CustomerDictionary>();
463+
patchDocument.Operations.Add(operation);
464+
465+
// Act
466+
var exception = Assert.Throws<JsonPatchException>(() => { patchDocument.ApplyTo(model); });
467+
468+
// Assert
469+
Assert.Equal($"The target location specified by path segment '{missingKey}' was not found.", exception.Message);
470+
}
471+
472+
[Theory]
473+
[InlineData("test", "DictionaryOfIntegerToCustomer")]
474+
[InlineData("move", "DictionaryOfIntegerToCustomer")]
475+
[InlineData("copy", "DictionaryOfIntegerToCustomer")]
476+
[InlineData("test", "NonGenericDictionary")]
477+
[InlineData("move", "NonGenericDictionary")]
478+
[InlineData("copy", "NonGenericDictionary")]
479+
public void ReadPocoObjectValueOfMissingKeyDoesNotThrowExceptionWithCustomErrorReporter(string op, string dictionaryPropertyName)
480+
{
481+
// Arrange
482+
var patchErrorLogger = new TestErrorLogger<DictionaryTest>();
483+
var model = new CustomerDictionary();
484+
var missingKey = 8;
485+
var operation = new Operation<CustomerDictionary>(
486+
op,
487+
path: $"/{dictionaryPropertyName}/{missingKey}/Address/City",
488+
from: $"/{dictionaryPropertyName}/{missingKey}/Address/City",
489+
value: "Nowhere");
490+
491+
var patchDocument = new JsonPatchDocument<CustomerDictionary>();
492+
patchDocument.Operations.Add(operation);
493+
494+
// Act
495+
patchDocument.ApplyTo(model, patchErrorLogger.LogErrorMessage);
496+
497+
// Assert
498+
Assert.Equal($"The target location specified by path segment '{missingKey}' was not found.", patchErrorLogger.ErrorMessage);
499+
}
382500
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
5+
namespace SystemTextJsonPatch.IntegrationTests;
6+
7+
public class NonGenericDictionary : IDictionary
8+
{
9+
private readonly Dictionary<object, object> _dictionary = new();
10+
11+
public object this[object key] { get => _dictionary[key]; set => _dictionary[key] = value; }
12+
13+
public bool IsFixedSize => false;
14+
15+
public bool IsReadOnly => false;
16+
17+
public ICollection Keys => _dictionary.Keys;
18+
19+
public ICollection Values => _dictionary.Values;
20+
21+
public int Count => _dictionary.Count;
22+
23+
public bool IsSynchronized => false;
24+
25+
public object SyncRoot => null;
26+
27+
public void Add(object key, object value) => _dictionary.Add(key, value);
28+
29+
public void Clear() => _dictionary.Clear();
30+
31+
public bool Contains(object key) => _dictionary.ContainsKey(key);
32+
33+
public void CopyTo(Array array, int index) => throw new NotImplementedException();
34+
35+
public IDictionaryEnumerator GetEnumerator() => _dictionary.GetEnumerator();
36+
37+
public void Remove(object key) => _dictionary.Remove(key);
38+
39+
IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
40+
}
41+

SystemTextJsonPatch/Internal/Proxies/DictionaryPropertyProxy.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using SystemTextJsonPatch.Exceptions;
34

45
namespace SystemTextJsonPatch.Internal.Proxies
56
{
@@ -40,9 +41,14 @@ internal DictionaryPropertyProxy(IDictionary dictionary, string propertyName)
4041

4142
public object? GetValue(object target)
4243
{
43-
var value = _dictionary[_propertyName];
44-
45-
return value;
44+
if (_dictionary.Contains(_propertyName))
45+
{
46+
return _dictionary[_propertyName];
47+
}
48+
else
49+
{
50+
throw new JsonPatchException(Resources.FormatTargetLocationAtPathSegmentNotFound(_propertyName), null);
51+
}
4652
}
4753

4854
public void SetValue(object target, object? convertedValue)
@@ -69,7 +75,7 @@ public Type PropertyType
6975
{
7076
get
7177
{
72-
var val = _dictionary[_propertyName];
78+
var val = _dictionary.Contains(_propertyName) ? _dictionary[_propertyName] : null;
7379
if (val == null)
7480
{
7581
return typeof(object);

0 commit comments

Comments
 (0)