Skip to content

Commit 35eb257

Browse files
author
Erik Larsson
committed
Fix JsonEx::Merge to handle chunked properties (#7489)
1 parent c640c33 commit 35eb257

3 files changed

Lines changed: 208 additions & 5 deletions

File tree

edge-agent/src/Microsoft.Azure.Devices.Edge.Agent.IoTHub/EdgeAgentConnection.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,9 @@ async Task<Twin> GetTwinFunc()
403403
async Task ApplyPatchAsync(TwinCollection desiredProperties, TwinCollection patch)
404404
{
405405
try
406+
406407
{
407-
string mergedJson = JsonEx.Merge(desiredProperties, patch, true);
408+
string mergedJson = JsonEx.Merge(desiredProperties, patch, true, "createOptions");
408409
desiredProperties = new TwinCollection(mergedJson);
409410
Events.LogDesiredPropertiesAfterPatch(desiredProperties);
410411
this.desiredProperties = Option.Some(desiredProperties);

edge-util/src/Microsoft.Azure.Devices.Edge.Util/JsonEx.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ public static T Get<T>(this JObject obj, string key)
3535
return token.Value<T>();
3636
}
3737

38-
public static string Merge(object baseline, object patch, bool treatNullAsDelete)
38+
public static string Merge(object baseline, object patch, bool treatNullAsDelete, string chunkedProperty = "")
3939
{
4040
JToken baselineToken = JToken.FromObject(baseline);
4141
JToken patchToken = JToken.FromObject(patch);
42-
JToken mergedToken = Merge(baselineToken, patchToken, treatNullAsDelete);
42+
JToken mergedToken = Merge(baselineToken, patchToken, treatNullAsDelete, chunkedProperty);
4343
return mergedToken.ToString();
4444
}
4545

46-
public static JToken Merge(JToken baselineToken, JToken patchToken, bool treatNullAsDelete)
46+
public static JToken Merge(JToken baselineToken, JToken patchToken, bool treatNullAsDelete, string chunkedProperty = "")
4747
{
4848
// Reached the leaf JValue
4949
if (patchToken.Type != JTokenType.Object || baselineToken.Type != JTokenType.Object)
@@ -55,14 +55,24 @@ public static JToken Merge(JToken baselineToken, JToken patchToken, bool treatNu
5555
var baseline = (JObject)baselineToken;
5656
var result = new JObject(baseline);
5757

58+
// Collect the chunked (for example createOptionsXX) keys that exist in the patch
59+
HashSet<string> patchChunkedNames = new HashSet<string>();
60+
61+
if (!string.IsNullOrEmpty(chunkedProperty)) {
62+
patchChunkedNames = patch.Properties()
63+
.Where(p => IsChunkedName(chunkedProperty, p.Name))
64+
.Select(p => p.Name)
65+
.ToHashSet(StringComparer.Ordinal);
66+
}
67+
5868
foreach (JProperty patchProp in patch.Properties())
5969
{
6070
if (IsValidToken(patchProp.Value))
6171
{
6272
JProperty baselineProp = baseline.Property(patchProp.Name);
6373
if (baselineProp != null && patchProp.Value.Type != JTokenType.Null)
6474
{
65-
JToken nestedResult = Merge(baselineProp.Value, patchProp.Value, treatNullAsDelete);
75+
JToken nestedResult = Merge(baselineProp.Value, patchProp.Value, treatNullAsDelete, chunkedProperty);
6676
result[patchProp.Name] = nestedResult;
6777
}
6878
else // decide whether to remove or add the patch key
@@ -83,9 +93,42 @@ public static JToken Merge(JToken baselineToken, JToken patchToken, bool treatNu
8393
}
8494
}
8595

96+
// Clean up result from non-existing chunked properties.
97+
if (!string.IsNullOrEmpty(chunkedProperty)) {
98+
var resultToRemove = result.Properties()
99+
.Where(p =>
100+
IsChunkedName(chunkedProperty, p.Name) &&
101+
(patchChunkedNames.Count == 0 ||
102+
!patchChunkedNames.Contains(p.Name)))
103+
.Select(p => p.Name)
104+
.ToList();
105+
106+
foreach (var name in resultToRemove)
107+
{
108+
result.Remove(name);
109+
}
110+
}
86111
return result;
87112
}
88113

114+
private static bool IsChunkedName(string chunkedName, string propertyName)
115+
{
116+
if (!propertyName.StartsWith(chunkedName, StringComparison.Ordinal))
117+
{
118+
return false;
119+
}
120+
121+
// Require exactly two digits after chunked property (for example "createOptionsXX")
122+
if (propertyName.Length != chunkedName.Length + 2)
123+
{
124+
return false;
125+
}
126+
127+
string suffix = propertyName.Substring(chunkedName.Length); // e.g. "01", "15"
128+
129+
return int.TryParse(suffix, out int n) && n >= 0 && n <= 99;
130+
}
131+
89132
public static bool IsValidToken(JToken token) => ValidDiffTypes.Any(t => t == token.Type);
90133

91134
public static string Diff(object from, object to)

edge-util/test/Microsoft.Azure.Devices.Edge.Util.Test/JsonExTest.cs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,165 @@ public void TestMergeAllCases()
258258
Assert.True(JToken.DeepEquals(resultCollection, JToken.FromObject(nestedEmptyBaseline)));
259259
}
260260

261+
[Fact]
262+
public void TestMergeCreateOptions_remove_unused()
263+
{
264+
// Arrange
265+
var baseline = new
266+
{
267+
module = new
268+
{
269+
level0 = "nochange",
270+
level1 = "value1",
271+
settings = new
272+
{
273+
createOptions = "some-create",
274+
createOptions01 = "-options"
275+
}
276+
},
277+
};
278+
279+
var patch = new
280+
{
281+
module = new
282+
{
283+
level0 = "nochange",
284+
level1 = "value2",
285+
settings = new
286+
{
287+
createOptions = "some-create-option-that-is-not-chucked"
288+
}
289+
},
290+
};
291+
292+
var merged = new
293+
{
294+
module = new
295+
{
296+
level0 = "nochange",
297+
level1 = "value2",
298+
settings = new
299+
{
300+
createOptions = "some-create-option-that-is-not-chucked"
301+
}
302+
},
303+
};
304+
// Assert
305+
JToken resultCollection = JsonEx.Merge(JToken.FromObject(baseline), JToken.FromObject(patch), true, "createOptions");
306+
307+
// Assert
308+
Assert.True(JToken.DeepEquals(JToken.FromObject(resultCollection), JToken.FromObject(merged)), resultCollection.ToString());
309+
}
310+
311+
[Fact]
312+
public void TestMergeCreateOptions_remove_02_kepp_01()
313+
{
314+
// Arrange
315+
var baseline = new
316+
{
317+
module = new
318+
{
319+
level0 = "nochange",
320+
level1 = "value1",
321+
settings = new
322+
{
323+
createOptions = "some-create",
324+
createOptions01 = "-options",
325+
createOptions02 = "-that-is-old"
326+
}
327+
},
328+
};
329+
330+
var patch = new
331+
{
332+
module = new
333+
{
334+
level0 = "nochange",
335+
level1 = "value2",
336+
settings = new
337+
{
338+
createOptions = "some-create",
339+
createOptions01 = "-options-that-is-new"
340+
}
341+
},
342+
};
343+
344+
var merged = new
345+
{
346+
module = new
347+
{
348+
level0 = "nochange",
349+
level1 = "value2",
350+
settings = new
351+
{
352+
createOptions = "some-create",
353+
createOptions01 = "-options-that-is-new"
354+
}
355+
},
356+
};
357+
// Assert
358+
JToken resultCollection = JsonEx.Merge(JToken.FromObject(baseline), JToken.FromObject(patch), true, "createOptions");
359+
360+
// Assert
361+
Assert.True(JToken.DeepEquals(JToken.FromObject(resultCollection), JToken.FromObject(merged)), resultCollection.ToString());
362+
}
363+
364+
[Fact]
365+
public void TestMergeCreateOptions3_do_not_remove_since_both_have_01_02()
366+
{
367+
// Arrange
368+
var baseline = new
369+
{
370+
module = new
371+
{
372+
level0 = "nochange",
373+
level1 = "value1",
374+
settings = new
375+
{
376+
createOptions = "some-create",
377+
createOptions01 = "-options",
378+
createOptions02 = "-that-is-old"
379+
}
380+
},
381+
};
382+
383+
var patch = new
384+
{
385+
module = new
386+
{
387+
level0 = "nochange",
388+
level1 = "value2",
389+
settings = new
390+
{
391+
createOptions = "some-create",
392+
createOptions01 = "-options-that-is-new",
393+
createOptions02 = "-and-even-longer"
394+
}
395+
},
396+
};
397+
398+
var merged = new
399+
{
400+
module = new
401+
{
402+
level0 = "nochange",
403+
level1 = "value2",
404+
settings = new
405+
{
406+
createOptions = "some-create",
407+
createOptions01 = "-options-that-is-new",
408+
createOptions02 = "-and-even-longer"
409+
}
410+
},
411+
};
412+
// Assert
413+
JToken resultCollection = JsonEx.Merge(JToken.FromObject(baseline), JToken.FromObject(patch), true, "createOptions");
414+
415+
// Assert
416+
Assert.True(JToken.DeepEquals(JToken.FromObject(resultCollection), JToken.FromObject(merged)), resultCollection.ToString());
417+
}
418+
419+
261420
[Fact]
262421
public void TestDiffAllCases()
263422
{

0 commit comments

Comments
 (0)