From 355cc71d5287fbee967110e1105272405ed861b2 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 8 Jul 2024 12:25:42 +0100 Subject: [PATCH 1/7] Ensure only one property serializer per type --- .../Expressions/IncludeWorkflowBuilder.cs | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index 30f39f6d9..4bd3f36d7 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -24,6 +24,7 @@ namespace Bonsai.Expressions public sealed class IncludeWorkflowBuilder : VariableArgumentExpressionBuilder, IGroupWorkflowBuilder, INamedElement, IRequireBuildContext { const char AssemblySeparator = ':'; + const string DefaultSerializerPropertyName = "Property"; static readonly XElement[] EmptyProperties = new XElement[0]; static readonly XmlSerializerNamespaces DefaultSerializerNamespaces = GetXmlSerializerNamespaces(); @@ -240,9 +241,12 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) return; } - var serializer = PropertySerializer.GetXmlSerializer(property.Name, property.PropertyType); - using (var reader = element.CreateReader()) + var previousName = element.Name; + element.Name = XName.Get(DefaultSerializerPropertyName, element.Name.NamespaceName); + try { + var serializer = PropertySerializer.GetXmlSerializer(property.PropertyType); + using var reader = element.CreateReader(); var value = serializer.Deserialize(reader); if (property.IsReadOnly) { @@ -263,6 +267,10 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) } else property.SetValue(this, value); } + finally + { + element.Name = previousName; + } } XElement SerializeProperty(ExternalizedPropertyDescriptor property) @@ -271,12 +279,15 @@ XElement SerializeProperty(ExternalizedPropertyDescriptor property) if (!allEqual) return null; var document = new XDocument(); - var serializer = PropertySerializer.GetXmlSerializer(property.Name, property.PropertyType); + var serializer = PropertySerializer.GetXmlSerializer(property.PropertyType); using (var writer = document.CreateWriter()) { serializer.Serialize(writer, value, DefaultSerializerNamespaces); } - return document.Root; + + var element = document.Root; + element.Name = XName.Get(property.Name, element.Name.NamespaceName); + return element; } static string GetWorkflowPath(string path) @@ -421,20 +432,19 @@ public override Expression Build(IEnumerable arguments) static class PropertySerializer { - static readonly Dictionary, XmlSerializer> serializerCache = new Dictionary, XmlSerializer>(); - static readonly object cacheLock = new object(); + static readonly Dictionary serializerCache = new(); + static readonly object cacheLock = new(); - internal static XmlSerializer GetXmlSerializer(string name, Type type) + internal static XmlSerializer GetXmlSerializer(Type type) { XmlSerializer serializer; - var serializerKey = Tuple.Create(name, type); lock (cacheLock) { - if (!serializerCache.TryGetValue(serializerKey, out serializer)) + if (!serializerCache.TryGetValue(type, out serializer)) { - var xmlRoot = new XmlRootAttribute(name) { Namespace = Constants.XmlNamespace }; + var xmlRoot = new XmlRootAttribute(DefaultSerializerPropertyName) { Namespace = Constants.XmlNamespace }; serializer = new XmlSerializer(type, xmlRoot); - serializerCache.Add(serializerKey, serializer); + serializerCache.Add(type, serializer); } } From 288b382a509186ead9b927fcc7b34a0f41aa5a6f Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 8 Jul 2024 12:46:53 +0100 Subject: [PATCH 2/7] Keep file not found exception in stack trace --- Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index 4bd3f36d7..85f90ca22 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -344,12 +344,11 @@ static Stream GetWorkflowStream(string path, bool embeddedResource) } else { - if (!File.Exists(path)) + try { return File.OpenRead(path); } + catch (FileNotFoundException ex) { - throw new InvalidOperationException("The specified workflow could not be found."); + throw new InvalidOperationException("The specified workflow could not be found.", ex); } - - return File.OpenRead(path); } } From a8bccdea9c07f7b2586eb00ef4e968bd4b5a8c11 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 8 Jul 2024 12:55:18 +0100 Subject: [PATCH 3/7] Preload all extension types from include workflows --- .../Expressions/IncludeWorkflowBuilder.cs | 4 +-- Bonsai.Core/WorkflowBuilder.cs | 32 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index 85f90ca22..bcfeee730 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -300,7 +300,7 @@ static string GetWorkflowPath(string path) return path; } - static bool IsEmbeddedResourcePath(string path) + internal static bool IsEmbeddedResourcePath(string path) { var separatorIndex = path.IndexOf(AssemblySeparator); return separatorIndex >= 0 && !SystemPath.IsPathRooted(path); @@ -318,7 +318,7 @@ static string GetDisplayName(string path) return name; } - static Stream GetWorkflowStream(string path, bool embeddedResource) + internal static Stream GetWorkflowStream(string path, bool embeddedResource) { if (embeddedResource) { diff --git a/Bonsai.Core/WorkflowBuilder.cs b/Bonsai.Core/WorkflowBuilder.cs index 232dc164a..dfad4f6f0 100644 --- a/Bonsai.Core/WorkflowBuilder.cs +++ b/Bonsai.Core/WorkflowBuilder.cs @@ -100,6 +100,12 @@ public static WorkflowMetadata ReadMetadata(string inputUri) /// A instance containing the retrieved metadata. /// public static WorkflowMetadata ReadMetadata(XmlReader reader) + { + var visitedWorkflows = new HashSet(); + return ReadMetadata(reader, visitedWorkflows); + } + + static WorkflowMetadata ReadMetadata(XmlReader reader, HashSet visitedWorkflows) { var metadata = new WorkflowMetadata(); var serializerNamespaces = new SerializerNamespaces(); @@ -128,7 +134,7 @@ public static WorkflowMetadata ReadMetadata(XmlReader reader) { workflowMarkup = ConvertDescriptorMarkup(reader.ReadOuterXml()); } - else workflowMarkup = ReadXmlExtensions(reader, types, serializerNamespaces); + else workflowMarkup = ReadXmlExtensions(reader, types, visitedWorkflows, serializerNamespaces); } if (reader.ReadToNextSibling(ExtensionTypeNodeName)) @@ -824,7 +830,14 @@ static Type ResolveXmlExtension(XmlReader reader, string value, string typeArgum return null; } - static void WriteXmlAttributes(XmlReader reader, XmlWriter writer, bool lookupTypes, HashSet types, SerializerNamespaces namespaces, ref bool includeWorkflow) + static void WriteXmlAttributes( + XmlReader reader, + XmlWriter writer, + bool lookupTypes, + HashSet types, + HashSet visitedWorkflows, + SerializerNamespaces namespaces, + ref bool includeWorkflow) { do { @@ -888,6 +901,17 @@ static void WriteXmlAttributes(XmlReader reader, XmlWriter writer, bool lookupTy value = XmlConvert.EncodeName(typeName); } } + else if (includeWorkflow && + reader.GetAttribute(nameof(IncludeWorkflowBuilder.Path)) is string path && + visitedWorkflows.Add(path)) + { + var embeddedResource = IncludeWorkflowBuilder.IsEmbeddedResourcePath(path); + using var workflowStream = IncludeWorkflowBuilder.GetWorkflowStream(path, embeddedResource); + using var workflowReader = XmlReader.Create(workflowStream, null, path); + workflowReader.MoveToContent(); + var nestedMetadata = ReadMetadata(workflowReader, visitedWorkflows); + types.UnionWith(nestedMetadata.Types); + } } writer.WriteString(value); @@ -912,7 +936,7 @@ static void WriteXmlAttributes(XmlReader reader, XmlWriter writer, bool lookupTy while (reader.MoveToNextAttribute()); } - static string ReadXmlExtensions(XmlReader reader, HashSet types, SerializerNamespaces namespaces) + static string ReadXmlExtensions(XmlReader reader, HashSet types, HashSet visitedWorkflows, SerializerNamespaces namespaces) { const int ChunkBufferSize = 1024; char[] chunkBuffer = null; @@ -944,7 +968,7 @@ static string ReadXmlExtensions(XmlReader reader, HashSet types, Serialize { var includeWorkflow = includeDepth >= 0; var lookupTypes = elementNamespace == Constants.XmlNamespace; - WriteXmlAttributes(reader, writer, lookupTypes, types, serializerNamespaces, ref includeWorkflow); + WriteXmlAttributes(reader, writer, lookupTypes, types, visitedWorkflows, serializerNamespaces, ref includeWorkflow); reader.MoveToElement(); if (lookupTypes && includeDepth < 0 && includeWorkflow) { From 7fd1893a924883cc7cc8079e30d85481776dcd09 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 8 Jul 2024 13:01:28 +0100 Subject: [PATCH 4/7] Avoid throwing when reading nested metadata --- Bonsai.Core/WorkflowBuilder.cs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Bonsai.Core/WorkflowBuilder.cs b/Bonsai.Core/WorkflowBuilder.cs index dfad4f6f0..06dcd869d 100644 --- a/Bonsai.Core/WorkflowBuilder.cs +++ b/Bonsai.Core/WorkflowBuilder.cs @@ -905,12 +905,23 @@ static void WriteXmlAttributes( reader.GetAttribute(nameof(IncludeWorkflowBuilder.Path)) is string path && visitedWorkflows.Add(path)) { - var embeddedResource = IncludeWorkflowBuilder.IsEmbeddedResourcePath(path); - using var workflowStream = IncludeWorkflowBuilder.GetWorkflowStream(path, embeddedResource); - using var workflowReader = XmlReader.Create(workflowStream, null, path); - workflowReader.MoveToContent(); - var nestedMetadata = ReadMetadata(workflowReader, visitedWorkflows); - types.UnionWith(nestedMetadata.Types); + // we don't want to fail in most cases while reading nested metadata, as this + // is an optional performance optimization and we would lose the visual context + // as to where exactly in the workflow the failure is happening + try + { + var embeddedResource = IncludeWorkflowBuilder.IsEmbeddedResourcePath(path); + using var workflowStream = IncludeWorkflowBuilder.GetWorkflowStream(path, embeddedResource); + using var workflowReader = XmlReader.Create(workflowStream, null, path); + workflowReader.MoveToContent(); + var nestedMetadata = ReadMetadata(workflowReader, visitedWorkflows); + types.UnionWith(nestedMetadata.Types); + } + catch (IOException) { } + catch (XmlException) { } + catch (BadImageFormatException) { } + catch (InvalidOperationException) { } + catch (UnauthorizedAccessException) { } } } From 2d9db3cf9711097eca96e3782c36191076ec0f65 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 8 Jul 2024 15:19:31 +0100 Subject: [PATCH 5/7] Avoid preloading extension types during build --- Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs | 10 ++++++---- Bonsai.Core/WorkflowBuilder.cs | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index bcfeee730..b66cc87d0 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -24,6 +24,7 @@ namespace Bonsai.Expressions public sealed class IncludeWorkflowBuilder : VariableArgumentExpressionBuilder, IGroupWorkflowBuilder, INamedElement, IRequireBuildContext { const char AssemblySeparator = ':'; + internal const string BuildUriPrefix = "::build:"; const string DefaultSerializerPropertyName = "Property"; static readonly XElement[] EmptyProperties = new XElement[0]; static readonly XmlSerializerNamespaces DefaultSerializerNamespaces = GetXmlSerializerNamespaces(); @@ -104,7 +105,7 @@ public override Range ArgumentRange { if (workflow == null) { - try { EnsureWorkflow(); } + try { EnsureWorkflow(null); } catch (Exception) { } } return base.ArgumentRange; @@ -119,7 +120,7 @@ IBuildContext IRequireBuildContext.BuildContext buildContext = value; if (buildContext != null) { - EnsureWorkflow(); + EnsureWorkflow(buildContext); InternalXmlProperties = null; } } @@ -352,7 +353,7 @@ internal static Stream GetWorkflowStream(string path, bool embeddedResource) } } - void EnsureWorkflow() + void EnsureWorkflow(IBuildContext buildContext) { var context = buildContext; while (context != null) @@ -376,12 +377,13 @@ void EnsureWorkflow() else { var embeddedResource = IsEmbeddedResourcePath(path); + var baseUri = buildContext != null ? $"{BuildUriPrefix}{path}" : path; var lastWriteTime = embeddedResource ? DateTime.MaxValue : File.GetLastWriteTime(path); if (workflow == null || lastWriteTime > writeTime) { var properties = workflow != null ? GetXmlProperties() : InternalXmlProperties; using (var stream = GetWorkflowStream(path, embeddedResource)) - using (var reader = XmlReader.Create(stream)) + using (var reader = XmlReader.Create(stream, null, baseUri)) { reader.MoveToContent(); var serializer = new XmlSerializer(typeof(WorkflowBuilder), reader.NamespaceURI); diff --git a/Bonsai.Core/WorkflowBuilder.cs b/Bonsai.Core/WorkflowBuilder.cs index 06dcd869d..58a437594 100644 --- a/Bonsai.Core/WorkflowBuilder.cs +++ b/Bonsai.Core/WorkflowBuilder.cs @@ -903,6 +903,7 @@ static void WriteXmlAttributes( } else if (includeWorkflow && reader.GetAttribute(nameof(IncludeWorkflowBuilder.Path)) is string path && + !reader.BaseURI.StartsWith(IncludeWorkflowBuilder.BuildUriPrefix) && visitedWorkflows.Add(path)) { // we don't want to fail in most cases while reading nested metadata, as this From 3d92c04d649bab8a1d9113588f7a7a4f746c75f6 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 10 Jul 2024 09:24:24 +0100 Subject: [PATCH 6/7] Rename constant identifier and clarify its use --- Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index b66cc87d0..3456e0894 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -25,7 +25,7 @@ public sealed class IncludeWorkflowBuilder : VariableArgumentExpressionBuilder, { const char AssemblySeparator = ':'; internal const string BuildUriPrefix = "::build:"; - const string DefaultSerializerPropertyName = "Property"; + const string PlaceholderSerializerPropertyName = "__Property__"; static readonly XElement[] EmptyProperties = new XElement[0]; static readonly XmlSerializerNamespaces DefaultSerializerNamespaces = GetXmlSerializerNamespaces(); @@ -243,7 +243,7 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) } var previousName = element.Name; - element.Name = XName.Get(DefaultSerializerPropertyName, element.Name.NamespaceName); + element.Name = XName.Get(PlaceholderSerializerPropertyName, element.Name.NamespaceName); try { var serializer = PropertySerializer.GetXmlSerializer(property.PropertyType); @@ -431,6 +431,9 @@ public override Expression Build(IEnumerable arguments) return workflow.BuildNested(arguments, includeContext); } + // We serialize all properties using the same placeholder root name to allow us to reuse + // a single serializer for each different property type. This means we need to rename + // the actual XElement name before or after serialization and deserialization. static class PropertySerializer { static readonly Dictionary serializerCache = new(); @@ -443,7 +446,7 @@ internal static XmlSerializer GetXmlSerializer(Type type) { if (!serializerCache.TryGetValue(type, out serializer)) { - var xmlRoot = new XmlRootAttribute(DefaultSerializerPropertyName) { Namespace = Constants.XmlNamespace }; + var xmlRoot = new XmlRootAttribute(PlaceholderSerializerPropertyName) { Namespace = Constants.XmlNamespace }; serializer = new XmlSerializer(type, xmlRoot); serializerCache.Add(type, serializer); } From 76734cd6b00addf8df27609818964f1aa1b2d521 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 10 Jul 2024 11:14:45 +0100 Subject: [PATCH 7/7] Defer workflow assignment until loading is complete Significant groundwork was required to avoid dependencies on the instance field. Mostly this involved making all property serializer methods static. --- .../Expressions/IncludeWorkflowBuilder.cs | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs index 3456e0894..23c5a1b16 100644 --- a/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs +++ b/Bonsai.Core/Expressions/IncludeWorkflowBuilder.cs @@ -159,7 +159,7 @@ public XElement[] PropertiesXml if (InternalXmlProperties != null) return InternalXmlProperties; else if (workflow != null) { - return GetXmlProperties(); + return GetXmlProperties(workflow); } else return EmptyProperties; } @@ -175,18 +175,24 @@ static XmlSerializerNamespaces GetXmlSerializerNamespaces() return serializerNamespaces; } - XElement[] GetXmlProperties() + static XElement[] GetXmlProperties(ExpressionBuilderGraph workflow) { - var properties = TypeDescriptor.GetProperties(this); + if (workflow is null) + throw new ArgumentNullException(nameof(workflow)); + + var properties = TypeDescriptor.GetProperties(workflow); return GetXmlSerializableProperties(properties) - .Select(SerializeProperty) + .Select(property => SerializeProperty(workflow, property)) .Where(element => element != null) .ToArray(); } - void SetXmlProperties(XElement[] xmlProperties) + static void SetXmlProperties(ExpressionBuilderGraph workflow, XElement[] xmlProperties) { - var properties = TypeDescriptor.GetProperties(this); + if (workflow is null) + throw new ArgumentNullException(nameof(workflow)); + + var properties = TypeDescriptor.GetProperties(workflow); var serializableProperties = GetXmlSerializableProperties(properties).ToDictionary(property => property.Name); for (int i = 0; i < xmlProperties.Length; i++) { @@ -196,14 +202,14 @@ void SetXmlProperties(XElement[] xmlProperties) { var value = xmlProperties[i].Value; property = (ExternalizedPropertyDescriptor)properties[property.Name]; - property.SetValue(this, property.Converter.ConvertFromInvariantString(value)); + property.SetValue(workflow, property.Converter.ConvertFromInvariantString(value)); } - else DeserializeProperty(xmlProperties[i], property); + else DeserializeProperty(workflow, xmlProperties[i], property); } } } - IEnumerable GetXmlSerializableProperties(PropertyDescriptorCollection properties) + static IEnumerable GetXmlSerializableProperties(PropertyDescriptorCollection properties) { return from property in properties.Cast() let externalizedProperty = EnsureXmlSerializable(property as ExternalizedPropertyDescriptor) @@ -211,7 +217,7 @@ IEnumerable GetXmlSerializableProperties(Propert select externalizedProperty; } - ExternalizedPropertyDescriptor EnsureXmlSerializable(ExternalizedPropertyDescriptor descriptor) + static ExternalizedPropertyDescriptor EnsureXmlSerializable(ExternalizedPropertyDescriptor descriptor) { if (descriptor == null) return null; var xmlIgnore = descriptor.Attributes[typeof(XmlIgnoreAttribute)]; @@ -234,11 +240,11 @@ ExternalizedPropertyDescriptor EnsureXmlSerializable(ExternalizedPropertyDescrip return descriptor; } - void DeserializeProperty(XElement element, PropertyDescriptor property) + static void DeserializeProperty(ExpressionBuilderGraph workflow, XElement element, PropertyDescriptor property) { if (property.PropertyType == typeof(XElement)) { - property.SetValue(this, element); + property.SetValue(workflow, element); return; } @@ -251,7 +257,7 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) var value = serializer.Deserialize(reader); if (property.IsReadOnly) { - var collection = (IList)property.GetValue(this); + var collection = (IList)property.GetValue(workflow); if (collection == null) { throw new InvalidOperationException("Collection reference not set to an instance of an object."); @@ -266,7 +272,7 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) } } } - else property.SetValue(this, value); + else property.SetValue(workflow, value); } finally { @@ -274,9 +280,9 @@ void DeserializeProperty(XElement element, PropertyDescriptor property) } } - XElement SerializeProperty(ExternalizedPropertyDescriptor property) + static XElement SerializeProperty(ExpressionBuilderGraph workflow, ExternalizedPropertyDescriptor property) { - var value = property.GetValue(this, out bool allEqual); + var value = property.GetValue(workflow, out bool allEqual); if (!allEqual) return null; var document = new XDocument(); @@ -381,29 +387,26 @@ void EnsureWorkflow(IBuildContext buildContext) var lastWriteTime = embeddedResource ? DateTime.MaxValue : File.GetLastWriteTime(path); if (workflow == null || lastWriteTime > writeTime) { - var properties = workflow != null ? GetXmlProperties() : InternalXmlProperties; + WorkflowBuilder builder; + var properties = workflow != null ? GetXmlProperties(workflow) : InternalXmlProperties; using (var stream = GetWorkflowStream(path, embeddedResource)) using (var reader = XmlReader.Create(stream, null, baseUri)) { reader.MoveToContent(); var serializer = new XmlSerializer(typeof(WorkflowBuilder), reader.NamespaceURI); - var builder = (WorkflowBuilder)serializer.Deserialize(reader); - description = builder.Description; - workflow = builder.Workflow; - writeTime = lastWriteTime; - } - - var parameterCount = workflow.GetNestedParameters().Count(); - SetArgumentRange(0, parameterCount); - if (inspectWorkflow) - { - workflow = workflow.ToInspectableGraph(); + builder = (WorkflowBuilder)serializer.Deserialize(reader); } if (properties != null) { - SetXmlProperties(properties); + SetXmlProperties(builder.Workflow, properties); } + + var parameterCount = builder.Workflow.GetNestedParameters().Count(); + workflow = inspectWorkflow ? builder.Workflow.ToInspectableGraph() : builder.Workflow; + description = builder.Description; + SetArgumentRange(0, parameterCount); + writeTime = lastWriteTime; } } } @@ -555,15 +558,17 @@ public override bool CanResetValue(object component) public override object GetValue(object component) { - if (!(component is IncludeWorkflowBuilder includeWorkflow)) + var workflow = component switch { - throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)); - } + IncludeWorkflowBuilder includeWorkflow => includeWorkflow.Workflow, + ExpressionBuilderGraph workflowComponent => workflowComponent, + _ => throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)) + }; - var serializableProperty = includeWorkflow.EnsureXmlSerializable(property); + var serializableProperty = EnsureXmlSerializable(property); if (serializableProperty != null) { - return includeWorkflow.SerializeProperty(serializableProperty); + return SerializeProperty(workflow, serializableProperty); } return null; @@ -576,20 +581,22 @@ public override void ResetValue(object component) public override void SetValue(object component, object value) { - if (!(value is XElement element)) + if (value is not XElement element) { throw new ArgumentException("Incompatible types found in workflow property assignment.", nameof(value)); } - if (!(component is IncludeWorkflowBuilder includeWorkflow)) + var workflow = component switch { - throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)); - } + IncludeWorkflowBuilder includeWorkflow => includeWorkflow.Workflow, + ExpressionBuilderGraph workflowComponent => workflowComponent, + _ => throw new ArgumentException("Incompatible component type in workflow property assignment.", nameof(component)) + }; - var serializableProperty = includeWorkflow.EnsureXmlSerializable(property); + var serializableProperty = EnsureXmlSerializable(property); if (serializableProperty != null) { - includeWorkflow.DeserializeProperty(element, serializableProperty); + DeserializeProperty(workflow, element, serializableProperty); } }