From a9e5db11fd99203e8374e349b5d8593336877dfa Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:42:03 +0100 Subject: [PATCH 1/9] Json: Type binder and property name replacement --- .../Workspaces/SerializationExtensions.cs | 113 +++++++++++++++--- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs b/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs index 90171db741e..c30869b6734 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs @@ -1,9 +1,12 @@ using System; -using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Text; using System.Text.RegularExpressions; using Dynamo.Engine; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; namespace Dynamo.Graph.Workspaces { @@ -12,6 +15,19 @@ namespace Dynamo.Graph.Workspaces /// public static class SerializationExtensions { + private const string NewtonsoftTypeName = "$type"; + private const string ServerTypeName = "ConcreteType"; + + private const string IntegerSlider32Bit = "CoreNodeModels.Input.IntegerSlider"; + private const string IntegerSlider64Bit = "CoreNodeModels.Input.IntegerSlider64Bit"; + + private const int DefaultDynamoFileSize = 4096; + + private static readonly Regex serverTypeRegex = new Regex(ServerTypeName, RegexOptions.Compiled); + private static readonly Regex newtonsoftTypeRegex = new Regex(Regex.Escape(NewtonsoftTypeName), RegexOptions.Compiled); + + internal static SerializationBinder Binder { get; } = new SerializationBinder(); + /// /// Save a Workspace to json. /// @@ -47,7 +63,83 @@ internal static string ToJson(this WorkspaceModel workspace, EngineController en result = SerializeIntegerSliderAs32BitType(result); - return result; + internal static void SerializeJObject(JObject workspaceJObject, TextWriter textWriter) + { + using (var writer = new TypeReplacerWriter(textWriter)) + { + workspaceJObject.WriteTo(writer); + } + } + + /// + /// The replaces outgoing property name tokens in the raw json string that + /// match with so that the json can be + /// serialized correctly. + /// + /// The text writer to write the serialized json to + private class TypeReplacerWriter(TextWriter textWriter) : JsonTextWriter(textWriter) + { + public override void WritePropertyName(string name, bool escape) + { + base.WritePropertyName(name == NewtonsoftTypeName ? ServerTypeName : name, escape); + } + + public override void WritePropertyName(string name) + { + base.WritePropertyName(name == NewtonsoftTypeName ? ServerTypeName : name); + } + } + + /// + /// The replaces incoming property name tokens in the raw json string that + /// match with so that the json can be + /// deserialized correctly. + /// + /// The text reader to read the json string from + internal class TypeReplacerReader(TextReader reader) : JsonTextReader(reader) + { + public override bool Read() + { + var hasToken = base.Read(); + + if (hasToken && base.TokenType == JsonToken.PropertyName && base.Value != null && base.Value.Equals(ServerTypeName)) + { + SetToken(JsonToken.PropertyName, NewtonsoftTypeName); + } + + return hasToken; + } + } + + /// + /// The is used to used to replace the given json type with another while + /// serializing or deserializing. In our case, it is used for the integer sliders (32 vs. 64) bit. + /// If other types need to be switched out in the future, this is more flexible than doing string + /// replacements on the raw json. + /// + internal class SerializationBinder : ISerializationBinder + { + private static readonly DefaultSerializationBinder _defaultBinder = new(); + + public void BindToName(Type serializedType, out string assemblyName, out string typeName) + { + assemblyName = serializedType.Assembly.FullName; + typeName = serializedType.FullName; + + // serialize the serializedType (64 bit) to 32 bit + if (typeName == IntegerSlider64Bit) + { + typeName = IntegerSlider32Bit; + } + } + + public Type BindToType(string assemblyName, string typeName) + { + // deserialize the json type string (32 bit) to 64 bit + return typeName == IntegerSlider32Bit + ? _defaultBinder.BindToType(assemblyName, IntegerSlider64Bit) + : _defaultBinder.BindToType(assemblyName, typeName); + } } /// @@ -60,20 +152,9 @@ internal static string ToJson(this WorkspaceModel workspace, EngineController en /// internal static string ReplaceTypeDeclarations(string json, bool fromServer = false) { - var result = json; - - if (fromServer) - { - var rgx2 = new Regex(@"ConcreteType"); - result = rgx2.Replace(result, "$type"); - } - else - { - var rgx2 = new Regex(@"\$type"); - result = rgx2.Replace(result, "ConcreteType"); - } - - return result; + return fromServer + ? serverTypeRegex.Replace(json, NewtonsoftTypeName) + : newtonsoftTypeRegex.Replace(json, ServerTypeName); } [Obsolete("Remove method after obsoleting IntegerSlider and replacing it with IntegerSlider64Bit")] From c5663f176411c68998fe3c8db0de8467dac82474 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:44:50 +0100 Subject: [PATCH 2/9] Json: Use json serializer instead of json settings to keep the intermediate JObject around --- .../Workspaces/SerializationExtensions.cs | 48 +++++++++++-------- .../Core/SerializationExtensions.cs | 38 +++++++++------ .../ViewModels/Core/WorkspaceViewModel.cs | 4 +- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs b/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs index c30869b6734..0b33536280a 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs @@ -33,35 +33,45 @@ public static class SerializationExtensions /// /// A string representing the serialized WorkspaceModel. internal static string ToJson(this WorkspaceModel workspace, EngineController engine) + { + var jobject = ToJsonJObject(workspace, engine); + + var sb = new StringBuilder(DefaultDynamoFileSize); + using var tw = new StringWriter(sb); + SerializeJObject(jobject, tw); + + return sb.ToString(); + } + + internal static JObject ToJsonJObject(this WorkspaceModel workspace, EngineController engine) { var logger = engine != null ? engine.AsLogger() : null; - var settings = new JsonSerializerSettings + var serializer = new JsonSerializer { - Error = (sender, args) => - { - args.ErrorContext.Handled = true; - Console.WriteLine(args.ErrorContext.Error); - }, + SerializationBinder = Binder, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, TypeNameHandling = TypeNameHandling.Auto, Formatting = Formatting.Indented, Culture = CultureInfo.InvariantCulture, - Converters = new List{ - new ConnectorConverter(logger), - new WorkspaceWriteConverter(engine), - new DummyNodeWriteConverter(), - new TypedParameterConverter(), - new NodeLibraryDependencyConverter(logger), - new LinterManagerConverter(logger) - }, - ReferenceResolverProvider = () => { return new IdReferenceResolver(); } + ReferenceResolver = new IdReferenceResolver(), + NullValueHandling = NullValueHandling.Include, + }; + serializer.Converters.Add(new ConnectorConverter(logger)); + serializer.Converters.Add(new WorkspaceWriteConverter(engine)); + serializer.Converters.Add(new DummyNodeWriteConverter()); + serializer.Converters.Add(new TypedParameterConverter()); + serializer.Converters.Add(new NodeLibraryDependencyConverter(logger)); + serializer.Converters.Add(new LinterManagerConverter(logger)); + + serializer.Error += (sender, args) => + { + args.ErrorContext.Handled = true; + Console.WriteLine(args.ErrorContext.Error); }; - var json = JsonConvert.SerializeObject(workspace, settings); - var result = ReplaceTypeDeclarations(json); - - result = SerializeIntegerSliderAs32BitType(result); + return JObject.FromObject(workspace, serializer); + } internal static void SerializeJObject(JObject workspaceJObject, TextWriter textWriter) { diff --git a/src/DynamoCoreWpf/ViewModels/Core/SerializationExtensions.cs b/src/DynamoCoreWpf/ViewModels/Core/SerializationExtensions.cs index ab09e86e3ea..a6b7bbd78e7 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/SerializationExtensions.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/SerializationExtensions.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; +using System; using System.Globalization; using Dynamo.ViewModels; using Dynamo.Wpf.ViewModels.Core.Converters; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Dynamo.Wpf.ViewModels.Core { @@ -19,24 +19,32 @@ public static class SerializationExtensions /// A JSON string representing the WorkspaceViewModel internal static string ToJson(this WorkspaceViewModel viewModel) { - var settings = new JsonSerializerSettings - { - Error = (sender, args) => - { - args.ErrorContext.Handled = true; - Console.WriteLine(args.ErrorContext.Error); - }, + return ToJsonJObject(viewModel).ToString(); + } + + /// + /// Convert the WorkspaceViewModel to a JObject. + /// + /// + /// A JObject representing the WorkspaceViewModel + internal static JObject ToJsonJObject(this WorkspaceViewModel viewModel) + { + var serializer = new JsonSerializer { + SerializationBinder = Graph.Workspaces.SerializationExtensions.Binder, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, TypeNameHandling = TypeNameHandling.Auto, Formatting = Formatting.Indented, - Culture = CultureInfo.InvariantCulture, - Converters = new List{ - new WorkspaceViewWriteConverter(), - new AnnotationViewModelConverter(), - } + Culture = CultureInfo.InvariantCulture }; + serializer.Converters.Add(new WorkspaceViewWriteConverter()); + serializer.Converters.Add(new AnnotationViewModelConverter()); + serializer.Error += (sender, args) => + { + args.ErrorContext.Handled = true; + Console.WriteLine(args.ErrorContext.Error); + }; - return JsonConvert.SerializeObject(viewModel, settings); + return JObject.FromObject(viewModel, serializer); } } } diff --git a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs index 209a89ea37b..5421d747d9b 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs @@ -846,9 +846,7 @@ internal bool Save(string filePath, bool isBackup = false, EngineController engi /// Workspace Model data in JSON format private JObject AddViewBlockToJSON(JObject modelData) { - var token = JToken.Parse(this.ToJson()); - modelData.Add("View", token); - + modelData.Add("View", this.ToJsonJObject()); return modelData; } From a5f211274f7880a97f02601dab65a048efd911a1 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:46:53 +0100 Subject: [PATCH 3/9] Json: Update NodeReadConverter to use Binder --- .../Workspaces/SerializationConverters.cs | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs index 10c0b985bbd..1cafcd5c674 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs @@ -117,28 +117,28 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { NodeModel node = null; - String typeName = String.Empty; - String functionName = String.Empty; - String assemblyName = String.Empty; + Type type = null; + var functionName = string.Empty; var obj = JObject.Load(reader); - Type type = null; + var rawTypeName = obj["$type"].Value(); + Debug.Assert(rawTypeName != null); + + var rawTypeParts = rawTypeName.Split(','); + var typeName = rawTypeParts[0]; + // we get the assembly name from the type string for the node model nodes. + var assemblyName = rawTypeParts.Length > 1 ? rawTypeParts[1].Trim() : string.Empty; try { - type = Type.GetType(obj["$type"].Value()); - typeName = obj["$type"].Value().Split(',').FirstOrDefault(); + // use the native Newtonsoft DefaultSerializationBinder to resolve types. + type = SerializationExtensions.Binder.BindToType(assemblyName, typeName); if (typeName.Equals("Dynamo.Graph.Nodes.ZeroTouch.DSFunction")) { // If it is a zero touch node, then get the whole function name including the namespace. functionName = obj["FunctionSignature"].Value().Split('@').FirstOrDefault().Trim(); } - // we get the assembly name from the type string for the node model nodes. - else - { - assemblyName = obj["$type"].Value().Split(',').Skip(1).FirstOrDefault().Trim(); - } } catch(Exception e) { @@ -151,16 +151,12 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist // not be an issue during normal dynamo use but if it is we can enable this code. if(type == null && this.isTestMode == true) { - List resultList; - // This assemblyName does not usually contain version information... - assemblyName = obj["$type"].Value().Split(',').Skip(1).FirstOrDefault().Trim(); - if (assemblyName != null) + if (!string.IsNullOrWhiteSpace(assemblyName)) { - if(this.loadedAssemblies.TryGetValue(assemblyName, out resultList)) + if (loadedAssemblies.TryGetValue(assemblyName, out var resultList)) { - var matchingTypes = resultList.Select(x => x.GetType(typeName)).ToList(); - type = matchingTypes.FirstOrDefault(); + type = resultList.Select(x => x.GetType(typeName)).FirstOrDefault(); } } } @@ -169,30 +165,20 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist if (type == null) { // Attempt to resolve the type using `AlsoKnownAs` - var unresolvedName = obj["$type"].Value().Split(',').FirstOrDefault(); - Type newType; - nodeFactory.ResolveType(unresolvedName, out newType); - - // If resolved update the type - if (newType != null) + if (nodeFactory.ResolveType(typeName, out var newType)) { + // If resolved update the type type = newType; } } // If the id is not a guid, makes a guid based on the id of the node var guid = GuidUtility.tryParseOrCreateGuid(obj["Id"].Value()); - - var replication = obj["Replication"].Value(); - var inPorts = obj["Inputs"].ToArray().Select(t => t.ToObject()).ToArray(); - var outPorts = obj["Outputs"].ToArray().Select(t => t.ToObject()).ToArray(); - - var resolver = (IdReferenceResolver)serializer.ReferenceResolver; - string assemblyLocation = objectType.Assembly.Location; + var inPorts = obj["Inputs"].Select(t => t.ToObject()).ToArray(); + var outPorts = obj["Outputs"].Select(t => t.ToObject()).ToArray(); bool remapPorts = true; - if (type == null) { // If type is still null at this point return a dummy node @@ -283,11 +269,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist if (remapPorts) { + var resolver = (IdReferenceResolver)serializer.ReferenceResolver; RemapPorts(node, inPorts, outPorts, resolver, manager.AsLogger()); } // Cannot set Lacing directly as property is protected + var replication = obj["Replication"].Value(); node.UpdateValue(new UpdateValueParams("ArgumentLacing", replication)); node.GUID = guid; From 312dab8c2fcdd8a026e3b33b61d6f060ed5c341d Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:49:56 +0100 Subject: [PATCH 4/9] Json: Update WorkspaceReadConverter to use node dict for lookups. Skip redundant list copies. --- .../Workspaces/SerializationConverters.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs index 1cafcd5c674..8450be9c943 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs @@ -492,30 +492,31 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var nrc = (NodeReadConverter)serializer.Converters.First(c => c is NodeReadConverter); nrc.ElementResolver = elementResolver; - var nodes = obj["Nodes"].ToObject>(serializer); + var nodes = obj["Nodes"].ToObject>(serializer).ToList(); + var nodeLookup = nodes.ToDictionary(n => n.GUID); // Setting Inputs // Required in headless mode by Dynamo Player that certain view properties are set back to NodeModel var inputsToken = obj["Inputs"]; if (inputsToken != null) { - var inputs = inputsToken.ToArray().Select(x => + var inputs = inputsToken.Select(x => { try - { return x.ToObject(); } + { + return x.ToObject(); + } catch (Exception ex) { engine?.AsLogger().Log(ex); return null; } - //dump nulls - }).Where(x => !(x is null)).ToList(); + }).Where(x => x != null); // Use the inputs to set the correct properties on the nodes. foreach (var inputData in inputs) { - var matchingNode = nodes.Where(x => x.GUID == inputData.Id).FirstOrDefault(); - if (matchingNode != null) + if (nodeLookup.TryGetValue(inputData.Id, out var matchingNode)) { matchingNode.IsSetAsInput = true; matchingNode.Name = inputData.Name; @@ -527,12 +528,11 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var outputsToken = obj["Outputs"]; if (outputsToken != null) { - var outputs = outputsToken.ToArray().Select(x => x.ToObject()).ToList(); + var outputs = outputsToken.Select(x => x.ToObject()); // Use the outputs to set the correct properties on the nodes. foreach (var outputData in outputs) { - var matchingNode = nodes.Where(x => x.GUID == outputData.Id).FirstOrDefault(); - if (matchingNode != null) + if (nodeLookup.TryGetValue(outputData.Id, out var matchingNode)) { matchingNode.IsSetAsOutput = true; matchingNode.Name = outputData.Name; @@ -547,15 +547,13 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var view = obj["View"]; if (view != null && view["NodeViews"] != null) { - var nodeViews = view["NodeViews"].ToList(); + var nodeViews = view["NodeViews"]; foreach (var nodeview in nodeViews) { - Guid nodeGuid; try { - nodeGuid = Guid.Parse(nodeview["Id"].Value()); - var matchingNode = nodes.Where(x => x.GUID == nodeGuid).FirstOrDefault(); - if (matchingNode != null) + var nodeGuid = Guid.Parse(nodeview["Id"].Value()); + if (nodeLookup.TryGetValue(nodeGuid, out var matchingNode)) { matchingNode.IsSetAsInput = nodeview["IsSetAsInput"].Value(); matchingNode.IsSetAsOutput = nodeview["IsSetAsOutput"].Value(); @@ -563,10 +561,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist matchingNode.Name = nodeview["Name"].Value(); } } - catch - { - continue; - } + catch { } } } #endregion From aa1e18ce92f95e959a6f39bdbe4eaf2aaa14618c Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:51:57 +0100 Subject: [PATCH 5/9] Json: Bugfix for WorkspaceWriteConverter when serializing to JObject. --- .../Graph/Workspaces/SerializationConverters.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs index 8450be9c943..c81dafec4b6 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs @@ -910,7 +910,16 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s // GraphDocumentaionLink writer.WritePropertyName(nameof(HomeWorkspaceModel.GraphDocumentationURL)); - writer.WriteValue(hws.GraphDocumentationURL); + if (hws.GraphDocumentationURL == null) + { + // The JValue json text writer throws when writing null URI values, the regular + // JsonConvert serializer does not. + writer.WriteNull(); + } + else + { + writer.WriteValue(hws.GraphDocumentationURL); + } // ExtensionData writer.WritePropertyName(WorkspaceReadConverter.EXTENSION_WORKSPACE_DATA); From 14115894579f3c3148505baaf5c3e7355cde8209 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:54:02 +0100 Subject: [PATCH 6/9] Json: Deserialize WorkspaceModel with serializer. --- .../Graph/Workspaces/WorkspaceModel.cs | 73 ++++++------------- 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs b/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs index 1ac1f17a7c8..bb5b79abead 100644 --- a/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs +++ b/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs @@ -2279,37 +2279,7 @@ public static WorkspaceModel FromJson(string json, LibraryServices libraryServic EngineController engineController, DynamoScheduler scheduler, NodeFactory factory, bool isTestMode, bool verboseLogging, CustomNodeManager manager) { - var logger = engineController != null ? engineController.AsLogger() : null; - - var settings = new JsonSerializerSettings - { - Error = (sender, args) => - { - args.ErrorContext.Handled = true; - Console.WriteLine(args.ErrorContext.Error); - }, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - TypeNameHandling = TypeNameHandling.None, - Formatting = Newtonsoft.Json.Formatting.Indented, - Culture = CultureInfo.InvariantCulture, - Converters = new List{ - new ConnectorConverter(logger), - new WorkspaceReadConverter(engineController, scheduler, factory, isTestMode, verboseLogging), - new NodeReadConverter(manager, libraryServices, factory, isTestMode), - new TypedParameterConverter(), - new NodeLibraryDependencyConverter(logger) - }, - ReferenceResolverProvider = () => { return new IdReferenceResolver(); } - }; - - var result = SerializationExtensions.ReplaceTypeDeclarations(json, true); - - // TODO: Remove after deprecating "IntegerSlider : SliderBase" - result = SerializationExtensions.DeserializeIntegerSliderTo64BitType(result); - - var ws = JsonConvert.DeserializeObject(result, settings); - - return ws; + return FromJson(json, libraryServices, engineController, scheduler, factory, isTestMode, verboseLogging, manager, linterManager: null); } public static WorkspaceModel FromJson(string json, LibraryServices libraryServices, @@ -2318,35 +2288,34 @@ public static WorkspaceModel FromJson(string json, LibraryServices libraryServic { var logger = engineController != null ? engineController.AsLogger() : null; - var settings = new JsonSerializerSettings + var workspaceReadConverter = linterManager == null + ? new WorkspaceReadConverter(engineController, scheduler, factory, isTestMode, verboseLogging) + : new WorkspaceReadConverter(engineController, scheduler, factory, isTestMode, verboseLogging, linterManager); + + var serializer = new JsonSerializer { - Error = (sender, args) => - { - args.ErrorContext.Handled = true; - Console.WriteLine(args.ErrorContext.Error); - }, + SerializationBinder = SerializationExtensions.Binder, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, TypeNameHandling = TypeNameHandling.None, - Formatting = Newtonsoft.Json.Formatting.Indented, Culture = CultureInfo.InvariantCulture, - Converters = new List{ - new ConnectorConverter(logger), - new WorkspaceReadConverter(engineController, scheduler, factory, isTestMode, verboseLogging, linterManager), - new NodeReadConverter(manager, libraryServices, factory, isTestMode), - new TypedParameterConverter(), - new NodeLibraryDependencyConverter(logger) - }, - ReferenceResolverProvider = () => { return new IdReferenceResolver(); } + ReferenceResolver = new IdReferenceResolver(), }; - var result = SerializationExtensions.ReplaceTypeDeclarations(json, true); + serializer.Converters.Add(new ConnectorConverter(logger)); + serializer.Converters.Add(workspaceReadConverter); + serializer.Converters.Add(new NodeReadConverter(manager, libraryServices, factory, isTestMode)); + serializer.Converters.Add(new TypedParameterConverter()); + serializer.Converters.Add(new NodeLibraryDependencyConverter(logger)); - // TODO: Remove after deprecating "IntegerSlider : SliderBase" - result = SerializationExtensions.DeserializeIntegerSliderTo64BitType(result); - - var ws = JsonConvert.DeserializeObject(result, settings); + serializer.Error += (sender, args) => + { + args.ErrorContext.Handled = true; + Console.WriteLine(args.ErrorContext.Error); + }; - return ws; + using var reader = new StringReader(json); + using var jsonReader = new SerializationExtensions.TypeReplacerReader(reader); + return serializer.Deserialize(jsonReader); } /// From e8ceae8988cbe14f9fc8e04888471862874f9892 Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Tue, 18 Feb 2025 12:55:58 +0100 Subject: [PATCH 7/9] Json: Use intermediate jobject in tests, rather than serializing then parsing --- test/DynamoCoreWpfTests/SerializationTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/DynamoCoreWpfTests/SerializationTests.cs b/test/DynamoCoreWpfTests/SerializationTests.cs index dc2ad4fca39..02469d93d6a 100644 --- a/test/DynamoCoreWpfTests/SerializationTests.cs +++ b/test/DynamoCoreWpfTests/SerializationTests.cs @@ -318,7 +318,7 @@ public void NodeUserDescriptionTest() // Act // Stage 1: Serialize the workspace view. - var jobject1 = JObject.Parse(ViewModel.CurrentSpaceViewModel.ToJson()); + var jobject1 = ViewModel.CurrentSpaceViewModel.ToJsonJObject(); var userDescriptionBefore = jobject1["NodeViews"].FirstOrDefault()["UserDescription"]; // Stage 2: set UserDescription @@ -326,7 +326,7 @@ public void NodeUserDescriptionTest() nodeViewModel.UserDescription = userDescription; // Stage 3: Serialize the workspace view again to make sure UserDescription is now serialized. - var jobject2 = JToken.Parse(ViewModel.CurrentSpaceViewModel.ToJson()); + var jobject2 = ViewModel.CurrentSpaceViewModel.ToJsonJObject(); var userDescriptionAfter = jobject2["NodeViews"].FirstOrDefault()["UserDescription"]; // Assert @@ -409,7 +409,7 @@ public void NodeViewSerializationPropertiesTest() var serializationNodeViewModelFile = Path.Combine(TestDirectory, @"core\serialization\serializationNodeViewModel.dyn"); OpenModel(serializationNodeViewModelFile); - var workSpaceJobject = JObject.Parse(ViewModel.CurrentSpaceViewModel.ToJson()); + var workSpaceJobject = ViewModel.CurrentSpaceViewModel.ToJsonJObject(); JObject nodeViewModelJobject = JObject.Parse(workSpaceJobject["NodeViews"][0].ToString()); Assert.AreEqual(8, nodeViewModelJobject.Properties().Count(), "The number of Serialized properties is not the expected"); @@ -497,7 +497,7 @@ public void JSONisSameBeforeAndAfterSaveWithDummyNodes() // Stage 2: Add the View. var jobject2 = JObject.Parse(jsonModel); - var token = JToken.Parse(ViewModel.CurrentSpaceViewModel.ToJson()); + var token = ViewModel.CurrentSpaceViewModel.ToJsonJObject(); jobject2.Add("View", token); // Re-saving the file will update the version number (which can be expected) @@ -761,7 +761,7 @@ private static string ConvertCurrentWorkspaceViewToJsonAndSave(DynamoViewModel v // Stage 2: Add the View. var jo = JObject.Parse(jsonModel); - var token = JToken.Parse(viewModel.CurrentSpaceViewModel.ToJson()); + var token = viewModel.CurrentSpaceViewModel.ToJsonJObject(); jo.Add("View", token); Assert.IsFalse(string.IsNullOrEmpty(jsonModel)); @@ -793,7 +793,7 @@ private string ConvertCurrentWorkspaceViewToNonGuidJsonAndSave(DynamoViewModel v var jsonModel = viewModel.Model.CurrentWorkspace.ToJson(viewModel.Model.EngineController); // Stage 2: Add the View. var jo = JObject.Parse(jsonModel); - var token = JToken.Parse(viewModel.CurrentSpaceViewModel.ToJson()); + var token = viewModel.CurrentSpaceViewModel.ToJsonJObject(); jo.Add("View", token); var json = jo.ToString(); var model = viewModel.Model; @@ -1100,7 +1100,7 @@ public void NotesSerializeAsAnnotations() var numXMLNotes = workspace.Notes.Count(); var numXMLAnnotations = workspace.Annotations.Count(); - var view = JToken.Parse(ViewModel.CurrentSpaceViewModel.ToJson()); + var view = ViewModel.CurrentSpaceViewModel.ToJsonJObject(); var numJsonAnnotations = view["Annotations"].Count(); Assert.AreEqual(numXMLNotes, 0); From 2e1bf56ebe6606e6c865ebf32ebb1e2f63e0345d Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Wed, 19 Feb 2025 09:50:43 +0100 Subject: [PATCH 8/9] Json: Remove SerializeJObject --- .../Graph/Workspaces/SerializationExtensions.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs b/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs index 0b33536280a..023234ec3a0 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationExtensions.cs @@ -38,7 +38,8 @@ internal static string ToJson(this WorkspaceModel workspace, EngineController en var sb = new StringBuilder(DefaultDynamoFileSize); using var tw = new StringWriter(sb); - SerializeJObject(jobject, tw); + using var writer = new TypeReplacerWriter(tw); + jobject.WriteTo(writer); return sb.ToString(); } @@ -73,14 +74,6 @@ internal static JObject ToJsonJObject(this WorkspaceModel workspace, EngineContr return JObject.FromObject(workspace, serializer); } - internal static void SerializeJObject(JObject workspaceJObject, TextWriter textWriter) - { - using (var writer = new TypeReplacerWriter(textWriter)) - { - workspaceJObject.WriteTo(writer); - } - } - /// /// The replaces outgoing property name tokens in the raw json string that /// match with so that the json can be From beb5ecb42f1934b39113aabc0b62942014f857cd Mon Sep 17 00:00:00 2001 From: Bendik Tobias Berg Date: Mon, 24 Feb 2025 16:04:07 +0100 Subject: [PATCH 9/9] Json: Make lookup dictionary lazy --- .../Graph/Workspaces/SerializationConverters.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs index c81dafec4b6..6d7b9aa2d2f 100644 --- a/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs +++ b/src/DynamoCore/Graph/Workspaces/SerializationConverters.cs @@ -493,13 +493,16 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist nrc.ElementResolver = elementResolver; var nodes = obj["Nodes"].ToObject>(serializer).ToList(); - var nodeLookup = nodes.ToDictionary(n => n.GUID); + // initialize this dictionary lazily if it needs to be queried + Dictionary nodeLookup = null; // Setting Inputs // Required in headless mode by Dynamo Player that certain view properties are set back to NodeModel var inputsToken = obj["Inputs"]; if (inputsToken != null) { + nodeLookup ??= nodes.ToDictionary(n => n.GUID); + var inputs = inputsToken.Select(x => { try @@ -528,6 +531,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var outputsToken = obj["Outputs"]; if (outputsToken != null) { + nodeLookup ??= nodes.ToDictionary(n => n.GUID); + var outputs = outputsToken.Select(x => x.ToObject()); // Use the outputs to set the correct properties on the nodes. foreach (var outputData in outputs) @@ -547,6 +552,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var view = obj["View"]; if (view != null && view["NodeViews"] != null) { + nodeLookup ??= nodes.ToDictionary(n => n.GUID); + var nodeViews = view["NodeViews"]; foreach (var nodeview in nodeViews) {