-
-
Save cajuncoding/bf78bdcf790782090d231590cbc2438f to your computer and use it in GitHub Desktop.
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Text.Json; | |
| using System.Text.Json.Nodes; | |
| namespace CajunCoding | |
| { | |
| public static class SystemTextJsonMergeExtensions | |
| { | |
| /// <summary> | |
| /// Merges the specified Json Node into the base JsonNode for which this method is called. | |
| /// It is null safe and can be easily used with null-check & null coalesce operators for fluent calls. | |
| /// NOTE: JsonNodes are context aware and track their parent relationships therefore to merge the values both JsonNode objects | |
| /// specified are mutated. The Base is mutated with new data while the source is mutated to remove reverences to all | |
| /// fields so that they can be added to the base. | |
| /// | |
| /// Source taken directly from the open-source Gist here: | |
| /// https://gist.github.com/cajuncoding/bf78bdcf790782090d231590cbc2438f | |
| /// | |
| /// </summary> | |
| /// <param name="jsonBase"></param> | |
| /// <param name="jsonMerge"></param> | |
| /// <returns></returns> | |
| /// <exception cref="ArgumentException"></exception> | |
| public static JsonNode Merge(this JsonNode jsonBase, JsonNode jsonMerge) | |
| { | |
| if (jsonBase == null || jsonMerge == null) | |
| return jsonBase; | |
| switch (jsonBase) | |
| { | |
| case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj: | |
| { | |
| //NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array so the node can then be | |
| // re-assigned to the target/base Json; clearing the Object seems to be the most efficient approach... | |
| var mergeNodesArray = jsonMergeObj.ToArray(); | |
| jsonMergeObj.Clear(); | |
| foreach (var prop in mergeNodesArray) | |
| { | |
| jsonBaseObj[prop.Key] = jsonBaseObj[prop.Key] switch | |
| { | |
| JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj => jsonBaseChildObj.Merge(jsonMergeChildObj), | |
| JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray => jsonBaseChildArray.Merge(jsonMergeChildArray), | |
| _ => prop.Value | |
| }; | |
| } | |
| break; | |
| } | |
| case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray: | |
| { | |
| //NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array, | |
| // so they can then be re-assigned to the target/base Json... | |
| var mergeNodesArray = jsonMergeArray.ToArray(); | |
| jsonMergeArray.Clear(); | |
| foreach(var mergeNode in mergeNodesArray) jsonBaseArray.Add(mergeNode); | |
| break; | |
| } | |
| default: | |
| throw new ArgumentException($"The JsonNode type [{jsonBase.GetType().Name}] is incompatible for merging with the target/base " + | |
| $"type [{jsonMerge.GetType().Name}]; merge requires the types to be the same."); | |
| } | |
| return jsonBase; | |
| } | |
| /// <summary> | |
| /// Merges the specified Dictionary of values into the base JsonNode for which this method is called. | |
| /// </summary> | |
| /// <typeparam name="TKey"></typeparam> | |
| /// <typeparam name="TValue"></typeparam> | |
| /// <param name="jsonBase"></param> | |
| /// <param name="dictionary"></param> | |
| /// <param name="options"></param> | |
| /// <returns></returns> | |
| public static JsonNode MergeDictionary<TKey, TValue>(this JsonNode jsonBase, IDictionary<TKey, TValue> dictionary, JsonSerializerOptions options = null) | |
| => jsonBase.Merge(JsonSerializer.SerializeToNode(dictionary, options)); | |
| } | |
| } |
Yep!
I'm also working on an implementation right now that uses JsonElement, Span, and such to see if I can come up with a way that sticks to the stack more and outperforms the common shortcut of writing a string to a MemoryStream and adding that stream to the ConfigurationBuilder. I've got an implementation using strings and dictionaries and JsonNodes that has a few dozen bytes of extra memory overhead so far, but with equivalent performance for relatively small input, but scales better for larger (especially deeply nested) inputs than the MemoryStream way, and doesn't come with the issue of leaving an undisposed MemoryStream hanging around (and is parallelizable, too, but that's detrimental below a pretty large threshold in benchmarks I ran). But it's ugly since I was just kinda hacking away at the problem on a sleepless night. π
Hoping this new one I'm working on works out and benchmarks better than either of those solutions. If so, I'll share it for sure since merging JSON without resorting to large dependencies like NewtonSoft seems to be a pretty common problem.
Sounds interesting! π
@dodexahedron that's an interesting idea and I appreciate your use of
(ab)useπ as it's quite true...Assuming a key goal is performance (which was with this Gist π you can easily chain and/or call this in a loop to process an array of Jsons into one result... here's an extension that encapsulates something like that, in case it's helpful: