From d320fd036ed3cff59206f9a8f14956a91135385a Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Thu, 26 Feb 2026 15:44:14 -0500 Subject: [PATCH 1/2] AB#44 Refactor markup extension tokenization and add tests Introduce AttributesTokenizer for markup extension attribute parsing, replacing inline logic in WorkflowMarkupSerializer. Improve error handling and modularity. Add comprehensive unit tests for tokenization and serialization/deserialization edge cases. Update project structure and event handling for better maintainability and reliability. --- .../Serialization/AttributesTokenizer.cs | 277 ++++++++ .../Serialization/IAttributesTokenizer.cs | 9 + .../MarkupExtensionSerializer.cs | 207 +++--- .../WorkflowMarkupSerializationException.cs | 10 - .../WorkflowMarkupSerializationManager.cs | 130 ++-- .../Serialization/WorkflowMarkupSerializer.cs | 453 ++++-------- ...rkflow.ComponentModel.Serialization.csproj | 3 +- .../Serialization/AttributesTokenizerTest.cs | 632 +++++++++++++++++ .../IFromCompactFormatDeserializer.cs | 11 + .../MarkupExtensionSerializerTest.cs | 44 +- ...orkflowMarkupSerializationExceptionTest.cs | 4 - .../WorkflowMarkupSerializerTest.cs | 647 +++++++++++++++++- 12 files changed, 1895 insertions(+), 532 deletions(-) create mode 100644 LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs create mode 100644 LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs create mode 100644 Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs create mode 100644 Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs new file mode 100644 index 0000000..0f3b3fe --- /dev/null +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/AttributesTokenizer.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections; +using System.Text; + +namespace LogicBuilder.Workflow.ComponentModel.Serialization +{ + internal class AttributesTokenizer : IAttributesTokenizer + { + private ArrayList list = null; + private int length = 0; + private bool inQuotes = false; + private bool gotEscape = false; + private bool nonWhitespaceFound = false; + private char quoteChar = '\''; + private int leftCurlies = 0; + private bool collectionIndexer = false; + private StringBuilder stringBuilder = null; + private int i = 0; + private enum LoopControl + { + Continue, + Break, + Error + } + + // This function splits the argument string into an array of tokens. + // For example: ID=Workflow1, Path=error1} would become an array that contains the following elements + // {ID} {=} {Workflwo1} {,} {Path} {=} {error1} + // Note that the input string should start with the first argument and end with '}'. + public ArrayList TokenizeAttributes(string args) + { + length = args.Length; + + // Loop through the args, creating a list of arguments and known delimiters. + // This loop does limited syntax checking, and serves to tokenize the argument + // string into chunks that are validated in greater detail in the next phase. + for (; i < length; i++) + { + // Escape character is always in effect for everything inside + // a MarkupExtension. We have to remember that the next character is + // escaped, and is not treated as a quote or delimiter. + if (CanSetGotEscapeToTrue(args)) + { + gotEscape = true; + continue; + } + + if (CanSetNonWhitespaceFoundToTrue(args)) + nonWhitespaceFound = true; + + // Process all characters that are not whitespace or are between quotes + if (NotCharacterForProcessing()) + continue; + + // We have a non-whitespace character, so ensure we have + // a string builder to accumulate characters and a list to collect + // attributes and delimiters. These are lazily + // created so that simple cases that have no parameters do not + // create any extra objects. + if (stringBuilder == null) + { + stringBuilder = new StringBuilder(length); + list = new ArrayList(1); + } + + var loopControl = ProcessChar(args); + if (loopControl == LoopControl.Break) + break; + else if (loopControl == LoopControl.Error) + return []; + } + + ValidateTheStringBuilder(args); + + return list; + } + + private LoopControl ProcessChar(string args) + { + // If the character is escaped, then it is part of the attribute + // being collected, regardless of its value and is not treated as + // a delimiter or special character. Write back the escape + // character since downstream processing will need it to determine + // whether the value is a MarkupExtension or not, and to prevent + // multiple escapes from being lost by recursive processing. + if (gotEscape) + { + stringBuilder.Append('\\'); + stringBuilder.Append(args[i]); + gotEscape = false; + return LoopControl.Continue; + } + + // If this characters is not escaped, then look for quotes and + // delimiters. + if (InsideQuotesOrCurlies()) + { + ProcessInQuotesAndCurlyBraces(args); + return LoopControl.Continue; + } + + if (IsQuoteCharacter(args)) + { + if (!ProcessQuoteChars(args)) + return LoopControl.Error; + } + else if (IsDelimiter(args)) + { + if (!ProcessDelimiters(args)) + return LoopControl.Error; + } + else if (args[i] == '}') + { + if (!ProcessRightCurly()) + return LoopControl.Error; + + return LoopControl.Break; + } + else + { + ProcessPlainCharacter(args); + } + + return LoopControl.Continue; + } + + private bool CanSetNonWhitespaceFoundToTrue(string args) + { + return !nonWhitespaceFound && !char.IsWhiteSpace(args[i]); + } + + private bool CanSetGotEscapeToTrue(string args) + { + return !gotEscape && args[i] == '\\'; + } + + private bool IsDelimiter(string args) + { + return args[i] == ',' || args[i] == '='; + } + + private bool IsQuoteCharacter(string args) + { + return (args[i] == '"' || args[i] == '\''); + } + + private bool InsideQuotesOrCurlies() + { + return inQuotes || leftCurlies > 0; + } + + private bool NotCharacterForProcessing() + { + return !inQuotes && leftCurlies <= 0 && !nonWhitespaceFound; + } + + private void ValidateTheStringBuilder(string args) + { + // If we've accumulated content but haven't hit a terminating '}' then the + // format is bad, so complain. + if (stringBuilder != null && stringBuilder.Length > 0) + throw new InvalidOperationException(SR.GetString(SR.Error_MarkupExtensionMissingTerminatingCharacter)); + else if (i < length) + { + // If there is non-whitespace text left that we haven't processes yet, + // then there is junk after the closing '}', so complain + for (++i; i < length; i++) + { + if (!Char.IsWhiteSpace(args[i])) + throw new InvalidOperationException(SR.GetString(SR.Error_ExtraCharacterFoundAtEnd)); + } + } + } + + private void ProcessPlainCharacter(string args) + { + if (args[i] == '{') + { + leftCurlies++; + } + // Must just be a plain old character, so add it to the stringbuilder + stringBuilder.Append(args[i]); + } + + private bool ProcessRightCurly() + { + // If we hit the outside right curly brace, then end processing. If + // there is a delimiter on the top of the stack and we haven't + // hit another non-whitespace character, then its an error + if (stringBuilder != null) + { + if (stringBuilder.Length > 0) + { + list.Add(stringBuilder.ToString().Trim()); + stringBuilder.Length = 0; + } + else if (list.Count > 0 && (list[list.Count - 1] is char)) + return false; + } + + return true; + } + + private bool ProcessDelimiters(string args) + { + // If there is something in the stringbuilder, then store it + if (stringBuilder != null && stringBuilder.Length > 0) + { + list.Add(stringBuilder.ToString().Trim()); + stringBuilder.Length = 0; + } + else if (list.Count == 0 || list[list.Count - 1] is Char) + { + // Can't have two delimiters in a row, so check what is on + // the list and complain if the last item is a character, or if + // a delimiter is the first item. + return false; + } + + // Append known delimiters. + list.Add(args[i]); + nonWhitespaceFound = false; + return true; + } + + private bool ProcessQuoteChars(string args) + { + // If we're not inside quotes, then a start quote can only + // occur as the first non-whitespace character in a name or value. + if (collectionIndexer && i < args.Length - 1 && args[i + 1] == ']') + { + collectionIndexer = false; + stringBuilder.Append(args[i]); + } + else if (i > 0 && args[i - 1] == '[') + { + collectionIndexer = true; + stringBuilder.Append(args[i]); + } + else + { + if (stringBuilder.Length != 0) + return false; + + inQuotes = true; + quoteChar = args[i]; + } + + return true; + } + + private void ProcessInQuotesAndCurlyBraces(string args) + { + if (inQuotes && args[i] == quoteChar) + { + // If we're inside quotes, then only an end quote that is not + // escaped is special, and will act as a delimiter. + inQuotes = false; + list.Add(stringBuilder.ToString()); + stringBuilder.Length = 0; + nonWhitespaceFound = false; + } + else + { + if (leftCurlies > 0 && args[i] == '}') + { + leftCurlies--; + } + else if (args[i] == '{') + { + leftCurlies++; + } + stringBuilder.Append(args[i]); + } + } + } +} diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs new file mode 100644 index 0000000..fbcf53c --- /dev/null +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/IAttributesTokenizer.cs @@ -0,0 +1,9 @@ +using System.Collections; + +namespace LogicBuilder.Workflow.ComponentModel.Serialization +{ + internal interface IAttributesTokenizer + { + ArrayList TokenizeAttributes(string args); + } +} diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs index 123278e..896a96c 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/MarkupExtensionSerializer.cs @@ -31,10 +31,10 @@ protected internal sealed override bool CanSerializeToString(WorkflowMarkupSeria protected internal sealed override string SerializeToString(WorkflowMarkupSerializationManager serializationManager, object value) { if (serializationManager == null) - throw new ArgumentNullException("serializationManager"); - XmlWriter writer = serializationManager.WorkflowMarkupStack[typeof(XmlWriter)] as XmlWriter ?? throw new ArgumentNullException("writer"); + throw new ArgumentNullException(nameof(serializationManager)); + XmlWriter writer = serializationManager.WorkflowMarkupStack[typeof(XmlWriter)] as XmlWriter ?? throw new InvalidOperationException("Writer is null."); if (value == null) - throw new ArgumentNullException("value"); + throw new ArgumentNullException(nameof(value)); writer.WriteString(MarkupExtensionSerializer.CompactFormatStart); XmlQualifiedName qualifiedName = serializationManager.GetXmlQualifiedName(value.GetType(), out _); @@ -46,46 +46,7 @@ protected internal sealed override string SerializeToString(WorkflowMarkupSerial InstanceDescriptor instanceDescriptor = this.GetInstanceDescriptor(serializationManager, value); if (instanceDescriptor != null && instanceDescriptor.MemberInfo is ConstructorInfo ctorInfo) { - ParameterInfo[] parameters = ctorInfo.GetParameters(); - if (parameters != null && parameters.Length == instanceDescriptor.Arguments.Count) - { - int i = 0; - foreach (object argValue in instanceDescriptor.Arguments) - { - constructorArguments ??= []; - // - if (argValue == null) - continue; - constructorArguments.Add(parameters[i].Name, parameters[i++].Name); - if (index++ > 0) - writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); - else - writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); - if (argValue.GetType() == typeof(string)) - { - writer.WriteString(CreateEscapedValue(argValue as string)); - } - else if (argValue is System.Type) - { - Type argType = argValue as Type; - if (argType?.Assembly != null) - { - XmlQualifiedName typeQualifiedName = serializationManager.GetXmlQualifiedName(argType, out _); - writer.WriteQualifiedName(XmlConvert.EncodeName(typeQualifiedName.Name), typeQualifiedName.Namespace); - } - else - { - writer.WriteString(argType?.FullName ?? string.Empty); - } - } - else - { - string stringValue = base.SerializeToString(serializationManager, argValue); - if (stringValue != null) - writer.WriteString(stringValue); - } - } - } + constructorArguments = GetConstructorArguments(serializationManager, writer, ref index, instanceDescriptor, ctorInfo); } List properties = @@ -94,13 +55,11 @@ .. GetProperties(serializationManager, value), .. serializationManager.GetExtendedProperties(value), ]; - foreach + foreach ( PropertyInfo serializableProperty in properties.Where ( - p => Helpers.GetSerializationVisibility(p) != DesignerSerializationVisibility.Hidden - && p.CanRead - && p.GetValue(value, null) != null + IsValidPropertyForSerialization(value) ) ) { @@ -119,53 +78,129 @@ PropertyInfo serializableProperty in properties.Where } //Get the property serializer so that we can convert the bind object to string - serializationManager.Context.Push(serializableProperty); - try + if (!ExecutePropetySerialization(serializationManager, value, writer, ref index, serializableProperty, propSerializer)) + continue; + } + + writer.WriteString(MarkupExtensionSerializer.CompactFormatEnd); + return string.Empty; + + } + + private static Func IsValidPropertyForSerialization(object value) + { + return p => Helpers.GetSerializationVisibility(p) != DesignerSerializationVisibility.Hidden + && p.CanRead + && p.GetValue(value, null) != null; + } + + private static bool ExecutePropetySerialization(WorkflowMarkupSerializationManager serializationManager, object value, XmlWriter writer, ref int index, PropertyInfo serializableProperty, WorkflowMarkupSerializer propSerializer) + { + //Get the property serializer so that we can convert the bind object to string + serializationManager.Context.Push(serializableProperty); + try + { + index = SerializeProperty(serializationManager, value, writer, index, serializableProperty, propSerializer); + } + catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) + { + serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]), ex)); + return false; + } + finally + { + Debug.Assert((PropertyInfo)serializationManager.Context.Current == serializableProperty, "Serializer did not remove an object it pushed into stack."); + serializationManager.Context.Pop(); + } + + return true; + } + + private static int SerializeProperty(WorkflowMarkupSerializationManager serializationManager, object value, XmlWriter writer, int index, PropertyInfo serializableProperty, WorkflowMarkupSerializer propSerializer) + { + object propValue = serializableProperty.GetValue(value, null); + if (!propSerializer.ShouldSerializeValue(serializationManager, propValue)) + return index; + + //We do not allow nested bind syntax + if (propSerializer.CanSerializeToString(serializationManager, propValue)) + { + if (index++ > 0) + writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); + else + writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); + writer.WriteString(serializableProperty.Name); + writer.WriteString(MarkupExtensionSerializer.CompactFormatNameValueSeperator); + + if (propValue is string propValueString) { - object propValue = serializableProperty.GetValue(value, null); - if (propSerializer.ShouldSerializeValue(serializationManager, propValue)) - { - //We do not allow nested bind syntax - if (propSerializer.CanSerializeToString(serializationManager, propValue)) - { - if (index++ > 0) - writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); - else - writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); - writer.WriteString(serializableProperty.Name); - writer.WriteString(MarkupExtensionSerializer.CompactFormatNameValueSeperator); - - if (propValue.GetType() == typeof(string)) - { - writer.WriteString(CreateEscapedValue(propValue as string)); - } - else - { - string stringValue = propSerializer.SerializeToString(serializationManager, propValue); - if (stringValue != null) - writer.WriteString(stringValue); - } - } - else - { - serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]))); - } - } + writer.WriteString(CreateEscapedValue(propValueString)); } - catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) + else { - serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]), ex)); + string stringValue = propSerializer.SerializeToString(serializationManager, propValue); + if (stringValue != null) + writer.WriteString(stringValue); + } + } + else + { + serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNoSerializeLogic, [serializableProperty.Name, value.GetType().FullName]))); + } + + return index; + } + + private Dictionary GetConstructorArguments(WorkflowMarkupSerializationManager serializationManager, XmlWriter writer, ref int index, InstanceDescriptor instanceDescriptor, ConstructorInfo ctorInfo) + { + Dictionary constructorArguments = null; + ParameterInfo[] parameters = ctorInfo.GetParameters(); + if (parameters == null || parameters.Length != instanceDescriptor.Arguments.Count) + return constructorArguments; + + int i = 0; + foreach (object argValue in instanceDescriptor.Arguments) + { + constructorArguments ??= []; + if (argValue == null) continue; + constructorArguments.Add(parameters[i].Name, parameters[i++].Name); + if (index++ > 0) + writer.WriteString(MarkupExtensionSerializer.CompactFormatPropertySeperator); + else + writer.WriteString(MarkupExtensionSerializer.CompactFormatTypeSeperator); + + if (argValue is string argValueString) + { + writer.WriteString(CreateEscapedValue(argValueString)); + } + else if (argValue is System.Type) + { + WriteTypeString(serializationManager, writer, argValue); } - finally + else { - Debug.Assert((PropertyInfo)serializationManager.Context.Current == serializableProperty, "Serializer did not remove an object it pushed into stack."); - serializationManager.Context.Pop(); + string stringValue = base.SerializeToString(serializationManager, argValue); + if (stringValue != null) + writer.WriteString(stringValue); } } - writer.WriteString(MarkupExtensionSerializer.CompactFormatEnd); - return string.Empty; + return constructorArguments; + } + + private static void WriteTypeString(WorkflowMarkupSerializationManager serializationManager, XmlWriter writer, object argValue) + { + Type argType = argValue as Type; + if (argType?.Assembly != null) + { + XmlQualifiedName typeQualifiedName = serializationManager.GetXmlQualifiedName(argType, out _); + writer.WriteQualifiedName(XmlConvert.EncodeName(typeQualifiedName.Name), typeQualifiedName.Namespace); + } + else + { + writer.WriteString(argType?.FullName ?? string.Empty); + } } protected virtual InstanceDescriptor GetInstanceDescriptor(WorkflowMarkupSerializationManager serializationManager, object value) @@ -176,7 +211,7 @@ protected virtual InstanceDescriptor GetInstanceDescriptor(WorkflowMarkupSeriali } // more escaped characters can be consider here, hence a seperate fn instead of string.Replace - private string CreateEscapedValue(string value) + private static string CreateEscapedValue(string value) { if (value == null) throw new ArgumentNullException("value"); diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs index 218c347..3def63c 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs @@ -39,16 +39,6 @@ public WorkflowMarkupSerializationException() { } - protected WorkflowMarkupSerializationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - if (info == null) - throw new ArgumentNullException("info"); - - this.lineNumber = info.GetInt32("lineNumber"); - this.columnNumber = info.GetInt32("columnNumber"); - } - public int LineNumber { get diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs index 083dedd..76c7acb 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationManager.cs @@ -15,8 +15,6 @@ #region Class WorkflowMarkupSerializationManager public class WorkflowMarkupSerializationManager : IDesignerSerializationManager { - private Assembly localAssembly = null; - private int writerDepth = 0; private readonly ContextStack workflowMarkupStack = new(); // Stack to keep a list of objects being serialized, to avoid stack overflow private readonly Stack serializationStack = new(); @@ -97,17 +95,7 @@ public void RemoveSerializationProvider(IDesignerSerializationProvider provider) this.serializationManager.RemoveSerializationProvider(provider); } - public Assembly LocalAssembly - { - get - { - return this.localAssembly; - } - set - { - this.localAssembly = value; - } - } + public Assembly LocalAssembly { get; set; } = null; #region Public Methods public virtual XmlQualifiedName GetXmlQualifiedName(Type type, out string prefix) @@ -116,7 +104,7 @@ public virtual XmlQualifiedName GetXmlQualifiedName(Type type, out string prefix throw new ArgumentNullException("type"); string typeNamespace = type.Namespace ?? String.Empty; - string assemblyName = (type.Assembly != null && type.Assembly != this.localAssembly) ? type.Assembly.FullName : String.Empty; + string assemblyName = (type.Assembly != null && type.Assembly != LocalAssembly) ? type.Assembly.FullName : String.Empty; int key = typeNamespace.GetHashCode() ^ assemblyName.GetHashCode(); if (!this.clrNamespaceBasedMappings.TryGetValue(key, out WorkflowMarkupSerializerMapping mappingForType)) @@ -165,48 +153,7 @@ public virtual Type GetType(XmlQualifiedName xmlQualifiedName) foreach (WorkflowMarkupSerializerMapping xmlnsMapping in xmlnsMappings) { - string assemblyName = xmlnsMapping.AssemblyName; - string clrNamespace = xmlnsMapping.ClrNamespace; - - // append dot net namespace name - string fullTypeName = xmlQualifiedName.Name; - if (clrNamespace.Length > 0) - fullTypeName = clrNamespace + "." + xmlQualifiedName.Name; - - // Work around for component model assembly - if (assemblyName.Equals(Assembly.GetExecutingAssembly().FullName, StringComparison.Ordinal)) - { - resolvedType = Assembly.GetExecutingAssembly().GetType(fullTypeName); - } - else if (assemblyName.Length == 0) - { - if (this.localAssembly != null) - resolvedType = this.localAssembly.GetType(fullTypeName); - } - else - { - string assemblyQualifiedName = fullTypeName; - if (assemblyName.Length > 0) - assemblyQualifiedName += (", " + assemblyName); - - // now grab the actual type - try - { - resolvedType = GetType(assemblyQualifiedName); - } - catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) - { - // Intentionally ignore non-critical exceptions when resolving the type. - // Fallback logic below will handle the case where resolvedType remains null. - } - - if (resolvedType == null) - { - resolvedType = GetType(fullTypeName); - if (resolvedType != null && !resolvedType.AssemblyQualifiedName.Equals(assemblyQualifiedName, StringComparison.Ordinal)) - resolvedType = null; - } - } + resolvedType = ResolveType(xmlQualifiedName, resolvedType, xmlnsMapping); //We found the type if (resolvedType != null) @@ -219,6 +166,53 @@ public virtual Type GetType(XmlQualifiedName xmlQualifiedName) return resolvedType; } + + private Type ResolveType(XmlQualifiedName xmlQualifiedName, Type resolvedType, WorkflowMarkupSerializerMapping xmlnsMapping) + { + string assemblyName = xmlnsMapping.AssemblyName; + string clrNamespace = xmlnsMapping.ClrNamespace; + + // append dot net namespace name + string fullTypeName = clrNamespace.Length > 0 + ? clrNamespace + "." + xmlQualifiedName.Name + : xmlQualifiedName.Name; + + // Work around for component model assembly + if (assemblyName.Equals(Assembly.GetExecutingAssembly().FullName, StringComparison.Ordinal)) + { + resolvedType = Assembly.GetExecutingAssembly().GetType(fullTypeName); + } + else if (assemblyName.Length == 0 && LocalAssembly != null) + { + resolvedType = LocalAssembly.GetType(fullTypeName); + } + else + { + string assemblyQualifiedName = fullTypeName; + if (assemblyName.Length > 0) + assemblyQualifiedName += (", " + assemblyName); + + // now grab the actual type + try + { + resolvedType = GetType(assemblyQualifiedName); + } + catch (Exception ex) when (!ExceptionUtility.IsCriticalException(ex)) + { + // Intentionally ignore non-critical exceptions when resolving the type. + // Fallback logic below will handle the case where resolvedType remains null. + } + + if (resolvedType == null) + { + resolvedType = GetType(fullTypeName); + if (resolvedType != null && !resolvedType.AssemblyQualifiedName.Equals(assemblyQualifiedName, StringComparison.Ordinal)) + resolvedType = null; + } + } + + return resolvedType; + } #endregion #region WorkflowMarkupSerializationManager Overrides @@ -292,17 +286,7 @@ public virtual Type GetType(string typeName) #endregion #region Helpers - internal int WriterDepth - { - get - { - return this.writerDepth; - } - set - { - this.writerDepth = value; - } - } + internal int WriterDepth { get; set; } = 0; internal ContextStack WorkflowMarkupStack { @@ -403,9 +387,17 @@ PropertyDescriptorCollection IDesignerSerializationManager.Properties get { return this.serializationManager.Properties; } } - event ResolveNameEventHandler IDesignerSerializationManager.ResolveName { add { } remove { } } + event ResolveNameEventHandler IDesignerSerializationManager.ResolveName + { + add { this.serializationManager.ResolveName += value; } + remove { this.serializationManager.ResolveName -= value; } + } - event EventHandler IDesignerSerializationManager.SerializationComplete { add { } remove { } } + event EventHandler IDesignerSerializationManager.SerializationComplete + { + add { this.serializationManager.SerializationComplete += value; } + remove { this.serializationManager.SerializationComplete -= value; } + } void IDesignerSerializationManager.SetName(object instance, string name) { diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs index c7180da..e896cf4 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializer.cs @@ -1631,20 +1631,24 @@ internal object DeserializeFromCompactFormat(WorkflowMarkupSerializationManager ArrayList argTokens; try { - argTokens = TokenizeAttributes(arguments); + IAttributesTokenizer attributesTokenizer = new AttributesTokenizer(); + argTokens = attributesTokenizer.TokenizeAttributes(arguments); } catch (Exception error) when (!ExceptionUtility.IsCriticalException(error)) { serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_MarkupExtensionDeserializeFailed, attrValue, error.Message), reader)); return null; } - if (argTokens != null) + if (argTokens != null && argTokens.Count > 0) { // Process the positional arugments and find the correct constructor to call. ArrayList positionalArgs = []; bool firstEqual = true; for (int i = 0; i < argTokens.Count; i++) { + if (i > 0 && argTokens[i - 1] is char previousArgToken && previousArgToken == '=') + continue; + char token = (argTokens[i] is char argToken) ? argToken : '\0'; if (token == '=') { @@ -1652,7 +1656,6 @@ internal object DeserializeFromCompactFormat(WorkflowMarkupSerializationManager positionalArgs.RemoveAt(positionalArgs.Count - 1); firstEqual = false; namedArgs.Add(argTokens[i - 1] as string, argTokens[i + 1] as string); - i++; } if (token == ',') continue; @@ -1753,195 +1756,9 @@ internal object DeserializeFromCompactFormat(WorkflowMarkupSerializationManager return obj; } - // This function splits the argument string into an array of tokens. - // For example: ID=Workflow1, Path=error1} would become an array that contains the following elements - // {ID} {=} {Workflwo1} {,} {Path} {=} {error1} - // Note that the input string should start with the first argument and end with '}'. - private ArrayList TokenizeAttributes(string args) - { - ArrayList list = null; - int length = args.Length; - bool inQuotes = false; - bool gotEscape = false; - bool nonWhitespaceFound = false; - Char quoteChar = '\''; - int leftCurlies = 0; - bool collectionIndexer = false; - - StringBuilder stringBuilder = null; - int i = 0; - - // Loop through the args, creating a list of arguments and known delimiters. - // This loop does limited syntax checking, and serves to tokenize the argument - // string into chunks that are validated in greater detail in the next phase. - for (; i < length; i++) - { - // Escape character is always in effect for everything inside - // a MarkupExtension. We have to remember that the next character is - // escaped, and is not treated as a quote or delimiter. - if (!gotEscape && args[i] == '\\') - { - gotEscape = true; - continue; - } - - if (!nonWhitespaceFound && !Char.IsWhiteSpace(args[i])) - { - nonWhitespaceFound = true; - } - - // Process all characters that are not whitespace or are between quotes - if (inQuotes || leftCurlies > 0 || nonWhitespaceFound) - { - // We have a non-whitespace character, so ensure we have - // a string builder to accumulate characters and a list to collect - // attributes and delimiters. These are lazily - // created so that simple cases that have no parameters do not - // create any extra objects. - if (stringBuilder == null) - { - stringBuilder = new StringBuilder(length); - list = new ArrayList(1); - } - - // If the character is escaped, then it is part of the attribute - // being collected, regardless of its value and is not treated as - // a delimiter or special character. Write back the escape - // character since downstream processing will need it to determine - // whether the value is a MarkupExtension or not, and to prevent - // multiple escapes from being lost by recursive processing. - if (gotEscape) - { - stringBuilder.Append('\\'); - stringBuilder.Append(args[i]); - gotEscape = false; - continue; - } - - // If this characters is not escaped, then look for quotes and - // delimiters. - if (inQuotes || leftCurlies > 0) - { - if (inQuotes && args[i] == quoteChar) - { - // If we're inside quotes, then only an end quote that is not - // escaped is special, and will act as a delimiter. - inQuotes = false; - list.Add(stringBuilder.ToString()); - stringBuilder.Length = 0; - nonWhitespaceFound = false; - } - else - { - if (leftCurlies > 0 && args[i] == '}') - { - leftCurlies--; - } - else if (args[i] == '{') - { - leftCurlies++; - } - stringBuilder.Append(args[i]); - } - } - else - { - if (args[i] == '"' || args[i] == '\'') - { - // If we're not inside quotes, then a start quote can only - // occur as the first non-whitespace character in a name or value. - if (collectionIndexer && i < args.Length - 1 && args[i + 1] == ']') - { - collectionIndexer = false; - stringBuilder.Append(args[i]); - } - else if (i > 0 && args[i - 1] == '[') - { - collectionIndexer = true; - stringBuilder.Append(args[i]); - } - else - { - if (stringBuilder.Length != 0) - return null; - - inQuotes = true; - quoteChar = args[i]; - } - } - else if (args[i] == ',' || args[i] == '=') - { - // If there is something in the stringbuilder, then store it - if (stringBuilder != null && stringBuilder.Length > 0) - { - list.Add(stringBuilder.ToString().Trim()); - stringBuilder.Length = 0; - } - else if (list.Count == 0 || list[list.Count - 1] is Char) - { - // Can't have two delimiters in a row, so check what is on - // the list and complain if the last item is a character, or if - // a delimiter is the first item. - return null; - } - - // Append known delimiters. - list.Add(args[i]); - nonWhitespaceFound = false; - } - else if (args[i] == '}') - { - // If we hit the outside right curly brace, then end processing. If - // there is a delimiter on the top of the stack and we haven't - // hit another non-whitespace character, then its an error - if (stringBuilder != null) - { - if (stringBuilder.Length > 0) - { - list.Add(stringBuilder.ToString().Trim()); - stringBuilder.Length = 0; - } - else if (list.Count > 0 && (list[list.Count - 1] is Char)) - return null; - } - break; - } - else - { - if (args[i] == '{') - { - leftCurlies++; - } - // Must just be a plain old character, so add it to the stringbuilder - stringBuilder.Append(args[i]); - } - } - } - - } - - - // If we've accumulated content but haven't hit a terminating '}' then the - // format is bad, so complain. - if (stringBuilder != null && stringBuilder.Length > 0) - throw new Exception(SR.GetString(SR.Error_MarkupExtensionMissingTerminatingCharacter)); - else if (i < length) - { - // If there is non-whitespace text left that we haven't processes yet, - // then there is junk after the closing '}', so complain - for (++i; i < length; i++) - { - if (!Char.IsWhiteSpace(args[i])) - throw new Exception(SR.GetString(SR.Error_ExtraCharacterFoundAtEnd)); - } - } - - return list; - } - // Remove any '\' escape characters from the passed string. This does a simple // pass through the string and won't do anything if there are no '\' characters. - private void RemoveEscapes(ref string value) + private static void RemoveEscapes(ref string value) { StringBuilder builder = null; bool noEscape = true; @@ -1972,7 +1789,7 @@ private void RemoveEscapes(ref string value) #region ContentProperty Support [ExcludeFromCodeCoverage] - private class ContentProperty : IDisposable + private sealed class ContentProperty : IDisposable { private readonly WorkflowMarkupSerializationManager serializationManager; private readonly WorkflowMarkupSerializer parentObjectSerializer; @@ -1993,58 +1810,79 @@ public ContentProperty(WorkflowMarkupSerializationManager serializationManager, this.contentPropertySerializer = this.serializationManager.GetSerializer(this.contentProperty.PropertyType, typeof(WorkflowMarkupSerializer)) as WorkflowMarkupSerializer; if (this.contentPropertySerializer != null) { + InitializeContentPropertySerializer(serializationManager); + } + else + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNotAvailableForSerialize, this.contentProperty.PropertyType.FullName))); + } + } + } + + private void InitializeContentPropertySerializer(WorkflowMarkupSerializationManager serializationManager) + { + try + { + XmlReader reader = this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] as XmlReader; + object contentPropertyValue = null; + if (reader == null) + { + contentPropertyValue = this.contentProperty.GetValue(this.parentObject, null); + } + else if (!this.contentProperty.PropertyType.IsValueType && + !this.contentProperty.PropertyType.IsPrimitive && + this.contentProperty.PropertyType != typeof(string) && + !IsMarkupExtensionType(this.contentProperty.PropertyType) && + this.contentProperty.CanWrite) + { + if (serializationManager.GetSerializer(this.contentProperty.PropertyType, typeof(WorkflowMarkupSerializer)) is not WorkflowMarkupSerializer serializer) + { + serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerNotAvailable, this.contentProperty.PropertyType.FullName), reader)); + return; + } try { - XmlReader reader = this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] as XmlReader; - object contentPropertyValue = null; - if (reader == null) - { - contentPropertyValue = this.contentProperty.GetValue(this.parentObject, null); - } - else if (!this.contentProperty.PropertyType.IsValueType && - !this.contentProperty.PropertyType.IsPrimitive && - this.contentProperty.PropertyType != typeof(string) && - !IsMarkupExtension(this.contentProperty.PropertyType) && - this.contentProperty.CanWrite) - { - if (serializationManager.GetSerializer(this.contentProperty.PropertyType, typeof(WorkflowMarkupSerializer)) is not WorkflowMarkupSerializer serializer) - { - serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerNotAvailable, this.contentProperty.PropertyType.FullName), reader)); - return; - } - try - { - contentPropertyValue = serializer.CreateInstance(serializationManager, this.contentProperty.PropertyType); - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerCreateInstanceFailed, this.contentProperty.PropertyType.FullName, e.Message), reader)); - return; - } - this.contentProperty.SetValue(this.parentObject, contentPropertyValue, null); - } - - if (contentPropertyValue != null && reader != null) - { - this.contentPropertySerializer.OnBeforeDeserialize(this.serializationManager, contentPropertyValue); - this.contentPropertySerializer.OnBeforeDeserializeContents(this.serializationManager, contentPropertyValue); - } + contentPropertyValue = serializer.CreateInstance(serializationManager, this.contentProperty.PropertyType); } catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e)); + serializationManager.ReportError(CreateSerializationError(SR.GetString(SR.Error_SerializerCreateInstanceFailed, this.contentProperty.PropertyType.FullName, e.Message), reader)); + return; } + this.contentProperty.SetValue(this.parentObject, contentPropertyValue, null); } - else + + if (contentPropertyValue != null && reader != null) { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerNotAvailableForSerialize, this.contentProperty.PropertyType.FullName))); + this.contentPropertySerializer.OnBeforeDeserialize(this.serializationManager, contentPropertyValue); + this.contentPropertySerializer.OnBeforeDeserializeContents(this.serializationManager, contentPropertyValue); } } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e)); + } } - void IDisposable.Dispose() + private static bool IsMarkupExtensionType(Type type) + { + return (typeof(MarkupExtension).IsAssignableFrom(type) || + typeof(System.Type).IsAssignableFrom(type) || + typeof(System.Array).IsAssignableFrom(type)); + } + + public void Dispose() { - if (this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] is XmlReader && this.contentProperty != null && this.contentPropertySerializer != null) + Dispose(true); + } + + private bool disposed; + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing && this.serializationManager.WorkflowMarkupStack[typeof(XmlReader)] is XmlReader && this.contentProperty != null && this.contentPropertySerializer != null) { try { @@ -2057,6 +1895,8 @@ void IDisposable.Dispose() this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e)); } } + + this.disposed = true; } internal PropertyInfo Property @@ -2100,82 +1940,92 @@ internal void SetContents(IList contents) object propertyValue = this.contentProperty.GetValue(this.parentObject, null); if (CollectionMarkupSerializer.IsValidCollectionType(this.contentProperty.PropertyType)) { - if (propertyValue == null) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyCanNotBeNull, this.contentProperty.Name, this.parentObject.GetType().FullName))); - return; - } - - //Notify serializer about begining of deserialization process - int i = 0; - try - { - foreach (ContentInfo contentInfo in contents) - { - this.contentPropertySerializer.AddChild(this.serializationManager, propertyValue, contentInfo.Content); - i++; - } - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[i].LineNumber, contents[i].LinePosition)); - } + SetPropertyValueForCollection(contents, propertyValue); } else { - if (!this.contentProperty.CanWrite) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoSetter, this.contentProperty.Name, this.parentObject.GetType()), contents[0].LineNumber, contents[0].LinePosition)); - return; - } + SetPropertyValueForObject(contents); + } + } + } - if (contents.Count > 1) - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoMultipleContents, this.contentProperty.Name, this.parentObject.GetType()), contents[1].LineNumber, contents[1].LinePosition)); + private void SetPropertyValueForCollection(IList contents, object propertyValue) + { + if (propertyValue == null) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyCanNotBeNull, this.contentProperty.Name, this.parentObject.GetType().FullName))); + return; + } - object content = contents[0].Content; - if (!this.contentProperty.PropertyType.IsAssignableFrom(content.GetType()) && typeof(string).IsAssignableFrom(content.GetType())) - { - try - { - content = this.contentPropertySerializer.DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, content as string); - content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); - return; - } - } + //Notify serializer about begining of deserialization process + int i = 0; + try + { + foreach (ContentInfo contentInfo in contents) + { + this.contentPropertySerializer.AddChild(this.serializationManager, propertyValue, contentInfo.Content); + i++; + } + } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[i].LineNumber, contents[i].LinePosition)); + } + } - if (content == null) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentCanNotBeConverted, content as string, contentProperty.Name, this.parentObject.GetType().FullName, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); - } - else if (!contentProperty.PropertyType.IsAssignableFrom(content.GetType())) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyValueInvalid, content.GetType(), this.contentProperty.Name, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); - } - else + private void SetPropertyValueForObject(IList contents) + { + if (!this.contentProperty.CanWrite) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoSetter, this.contentProperty.Name, this.parentObject.GetType()), contents[0].LineNumber, contents[0].LinePosition)); + return; + } + + if (contents.Count > 1) + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyNoMultipleContents, this.contentProperty.Name, this.parentObject.GetType()), contents[1].LineNumber, contents[1].LinePosition)); + + object content = contents[0].Content; + if (!this.contentProperty.PropertyType.IsInstanceOfType(content) && content is string contentString) + { + try + { + content = this.contentPropertySerializer.DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, contentString); + content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); + } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); + return; + } + } + + if (content == null) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentCanNotBeConverted, content as string, contentProperty.Name, this.parentObject.GetType().FullName, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); + } + else if (!contentProperty.PropertyType.IsInstanceOfType(content)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_ContentPropertyValueInvalid, content.GetType(), this.contentProperty.Name, this.contentProperty.PropertyType.FullName), contents[0].LineNumber, contents[0].LinePosition)); + } + else + { + try + { + if (this.contentProperty.PropertyType == typeof(string)) { - try - { - if (this.contentProperty.PropertyType == typeof(string)) - { - content = new WorkflowMarkupSerializer().DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, content as string); - content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); - } - this.contentProperty.SetValue(this.parentObject, content, null); - } - catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) - { - this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); - } + content = new WorkflowMarkupSerializer().DeserializeFromString(this.serializationManager, this.contentProperty.PropertyType, content as string); + content = WorkflowMarkupSerializer.GetValueFromMarkupExtension(this.serializationManager, content); } + this.contentProperty.SetValue(this.parentObject, content, null); + } + catch (Exception e) when (!ExceptionUtility.IsCriticalException(e)) + { + this.serializationManager.ReportError(new WorkflowMarkupSerializationException(SR.GetString(SR.Error_SerializerThrewException, this.parentObject.GetType(), e.Message), e, contents[0].LineNumber, contents[0].LinePosition)); } } } - private PropertyInfo GetContentProperty(WorkflowMarkupSerializationManager serializationManagerLocal, object parentObjectLocal) + private static PropertyInfo GetContentProperty(WorkflowMarkupSerializationManager serializationManagerLocal, object parentObjectLocal) { PropertyInfo contentPropertyLocal = null; @@ -2224,13 +2074,6 @@ internal static string EnsureMarkupExtensionTypeName(XmlQualifiedName xmlQualifi return typeName; } - private static bool IsMarkupExtension(Type type) - { - return (typeof(MarkupExtension).IsAssignableFrom(type) || - typeof(System.Type).IsAssignableFrom(type) || - typeof(System.Array).IsAssignableFrom(type)); - } - private static bool IsMarkupExtension(XmlQualifiedName xmlQualifiedName) { bool markupExtension = false; @@ -2246,10 +2089,10 @@ private static object GetMarkupExtensionFromValue(object value) { if (value == null) return new NullExtension(); - if (value is System.Type) - return new TypeExtension(value as System.Type); - if (value is Array) - return new ArrayExtension(value as Array); + if (value is System.Type typeValue) + return new TypeExtension(typeValue); + if (value is Array arrayValue) + return new ArrayExtension(arrayValue); return value; } diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj b/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj index 69c81b2..580f458 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/LogicBuilder.Workflow.ComponentModel.Serialization.csproj @@ -31,7 +31,7 @@ - + @@ -70,7 +70,6 @@ - diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs new file mode 100644 index 0000000..9281878 --- /dev/null +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/AttributesTokenizerTest.cs @@ -0,0 +1,632 @@ +using LogicBuilder.Workflow.ComponentModel.Serialization; +using System; +using System.Collections; + +namespace LogicBuilder.Workflow.Tests.ComponentModel.Serialization +{ + public class AttributesTokenizerTest + { + private readonly AttributesTokenizer _tokenizer; + + public AttributesTokenizerTest() + { + _tokenizer = new AttributesTokenizer(); + } + + #region Basic Tokenization Tests + + [Fact] + public void TokenizeAttributes_ReturnsNull_WhenInputIsEmpty() + { + // Arrange + string input = ""; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.Null(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsNull_WhenInputIsWhitespaceOnly() + { + // Arrange + string input = " "; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.Null(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsSingleToken_WhenInputIsSingleValue() + { + // Arrange + string input = "value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value", result[0]); + } + + [Fact] + public void TokenizeAttributes_TokenizesMultipleValues_WithCommaDelimiter() + { + // Arrange + string input = "value1,value2,value3}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal("value1", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("value2", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("value3", result[4]); + } + + [Fact] + public void TokenizeAttributes_TokenizesKeyValuePair_WithEqualsDelimiter() + { + // Arrange + string input = "key=value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("key", result[0]); + Assert.Equal('=', result[1]); + Assert.Equal("value", result[2]); + } + + #endregion + + #region Quote Handling Tests + + [Fact] + public void TokenizeAttributes_HandlesDoubleQuotedString() + { + // Arrange + string input = "\"quoted value\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("quoted value", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesSingleQuotedString() + { + // Arrange + string input = "'quoted value'}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("quoted value", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesQuotedStringWithComma() + { + // Arrange + string input = "\"value,with,commas\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value,with,commas", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesQuotedStringWithEquals() + { + // Arrange + string input = "\"value=with=equals\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value=with=equals", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesMultipleQuotedStrings() + { + // Arrange + string input = "\"first\",\"second\",\"third\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal("first", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("second", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("third", result[4]); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenQuoteNotAtBeginning() + { + // Arrange + string input = "value\"quoted\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + + #region Escape Character Tests + + [Fact] + public void TokenizeAttributes_HandlesEscapedBackslash() + { + // Arrange + string input = "value\\\\test}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value\\\\test", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedComma() + { + // Arrange + string input = "value\\,test}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value\\,test", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedEquals() + { + // Arrange + string input = "value\\=test}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value\\=test", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedQuote() + { + // Arrange + string input = "\\\"value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("\\\"value", result[0]); + } + + #endregion + + #region Curly Brace Tests + + [Fact] + public void TokenizeAttributes_HandlesNestedCurlyBraces() + { + // Arrange + string input = "Stuff {nested {content}}}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("Stuff {nested {content}}", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesMultipleLevelsOfNesting() + { + // Arrange + string input = "Stuff {level1 {level2 {level3}}}}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("Stuff {level1 {level2 {level3}}}", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesNestedBracesWithCommas() + { + // Arrange + string input = "{nested,content},value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("{nested,content}", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("value", result[2]); + } + + #endregion + + #region Collection Indexer Tests + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithSingleQuotedIndex() + { + // Arrange + string input = "collection['someName']}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection['someName']", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithDoubleQuotedIndex() + { + // Arrange + string input = "collection[\"someName\"]}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection[\"someName\"]", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithSingleQuote() + { + // Arrange + string input = "collection[']']}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection[']']", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesCollectionIndexerWithDoubleQuote() + { + // Arrange + string input = "collection[\"]\"]}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("collection[\"]\"]", result[0]); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyCollectionUsingSingleQuote_WithMissingClosingSquareBracket() + { + // Arrange + string input = "collection['A'}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyCollectionUsingDoubleQuote_WithMissingClosingSquareBracket() + { + // Arrange + string input = "collection[\"A\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_HandlesQuoteAfterOpenBracket() + { + // Arrange + string input = "array['key']}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("array['key']", result[0]); + } + + #endregion + + #region Whitespace Handling Tests + + [Fact] + public void TokenizeAttributes_TrimsWhitespaceFromValues() + { + // Arrange + string input = " value1 , value2 }"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("value1", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("value2", result[2]); + } + + [Fact] + public void TokenizeAttributes_PreservesWhitespaceInQuotes() + { + // Arrange + string input = "\" value with spaces \"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(" value with spaces ", result[0]); + } + + #endregion + + #region Error Handling Tests + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenTwoDelimitersInRow() + { + // Arrange + string input = "value1,,value2}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenDelimiterIsFirst() + { + // Arrange + string input = ",value}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ReturnsEmptyList_WhenDelimiterBeforeClosingBrace() + { + // Arrange + string input = "value,}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void TokenizeAttributes_ThrowsException_WhenMissingTerminatingCharacter() + { + // Arrange + string input = "value"; + + // Act & Assert + Assert.Throws(() => _tokenizer.TokenizeAttributes(input)); + } + + [Fact] + public void TokenizeAttributes_ThrowsException_WhenExtraCharactersAfterClosingBrace() + { + // Arrange + string input = "value}extra"; + + // Act & Assert + Assert.Throws(() => _tokenizer.TokenizeAttributes(input)); + } + + [Fact] + public void TokenizeAttributes_DoesNotThrow_WhenWhitespaceAfterClosingBrace() + { + // Arrange + string input = "value} "; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("value", result[0]); + } + + #endregion + + #region Complex Scenario Tests + + [Fact] + public void TokenizeAttributes_HandlesComplexMarkupExtension() + { + // Arrange + string input = "PropertyName={StaticResource Key},PropertyValue=\"test\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(7, result.Count); + Assert.Equal("PropertyName", result[0]); + Assert.Equal('=', result[1]); + Assert.Equal("{StaticResource Key}", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("PropertyValue", result[4]); + Assert.Equal('=', result[5]); + Assert.Equal("test", result[6]); + } + + [Fact] + public void TokenizeAttributes_HandlesMixedQuotesAndBraces() + { + // Arrange + string input = "\"quoted\",{nested},unquoted}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Count); + Assert.Equal("quoted", result[0]); + Assert.Equal(',', result[1]); + Assert.Equal("{nested}", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("unquoted", result[4]); + } + + [Fact] + public void TokenizeAttributes_HandlesMultipleKeyValuePairs() + { + // Arrange + string input = "key1=value1,key2=value2,key3=value3}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Equal(11, result.Count); + Assert.Equal("key1", result[0]); + Assert.Equal('=', result[1]); + Assert.Equal("value1", result[2]); + Assert.Equal(',', result[3]); + Assert.Equal("key2", result[4]); + Assert.Equal('=', result[5]); + Assert.Equal("value2", result[6]); + Assert.Equal(',', result[7]); + Assert.Equal("key3", result[8]); + Assert.Equal('=', result[9]); + Assert.Equal("value3", result[10]); + } + + [Fact] + public void TokenizeAttributes_HandlesEscapedCharactersInQuotedString() + { + // Arrange + string input = "\"{nested\\}} content\"}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("{nested\\}} content", result[0]); + } + + [Fact] + public void TokenizeAttributes_HandlesNestedMarkupExtensionsWithMultipleParameters() + { + // Arrange + string input = "Stuff {Binding Path=Property,Source={StaticResource ResourceKey}}}"; + + // Act + ArrayList result = _tokenizer.TokenizeAttributes(input); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("Stuff {Binding Path=Property,Source={StaticResource ResourceKey}}", result[0]); + } + + #endregion + } +} \ No newline at end of file diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs new file mode 100644 index 0000000..621d054 --- /dev/null +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/IFromCompactFormatDeserializer.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LogicBuilder.Workflow.Tests.ComponentModel.Serialization +{ + internal interface IFromCompactFormatDeserializer + { + object Deserialize(object value); + } +} diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs index a3feb0d..9be5e89 100644 --- a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/MarkupExtensionSerializerTest.cs @@ -82,7 +82,7 @@ public void SerializeToString_ThrowsArgumentNullException_WhenXmlWriterIsNull() var markupExtension = new TestMarkupExtension(); // Act & Assert - Assert.Throws(() => _serializer.SerializeToString(_serializationManager, markupExtension)); + Assert.Throws(() => _serializer.SerializeToString(_serializationManager, markupExtension)); } [Fact] @@ -370,46 +370,6 @@ public void SerializeToString_HandlesTypeConstructorArguments() } } - [Fact] - public void SerializeToString_SkipsPropertiesWithConstructorArgumentAttribute() - { - // Arrange - var designerSerializationManager = new DesignerSerializationManager(); - var manager = new WorkflowMarkupSerializationManager(designerSerializationManager); - - var serializer = new TestableMarkupExtensionSerializerWithConstructor(); - var markupExtension = new TestMarkupExtensionWithConstructor("testValue"); - var sb = new StringBuilder(); - var settings = new XmlWriterSettings { OmitXmlDeclaration = true }; - using var writer = XmlWriter.Create(sb, settings); - manager.WorkflowMarkupStack.Push(writer); - - try - { - writer.WriteStartElement("", "Root", "http://schemas.microsoft.com/winfx/2006/xaml"); - writer.WriteAttributeString("xmlns", "ns0", null, "clr-namespace:LogicBuilder.Workflow.Tests.ComponentModel.Serialization;Assembly=LogicBuilder.Workflow.ComponentModel.Serialization.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=646893bec0268535"); - - using (designerSerializationManager.CreateSession()) - { - // Act - serializer.SerializeToString(manager, markupExtension); - } - - writer.WriteEndElement(); - writer.Flush(); - - // Assert - Value property should not appear as a property since it's a constructor argument - var result = sb.ToString(); - // The value should appear as constructor arg, not as property=value syntax - Assert.Contains("testValue", result); - } - finally - { - manager.WorkflowMarkupStack.Pop(); - writer.Dispose(); - } - } - [Fact] public void SerializeToString_HandlesIntConstructorArguments() { @@ -671,7 +631,7 @@ public string TestCreateEscapedValue(string value) { // Use reflection to access private method var method = typeof(MarkupExtensionSerializer).GetMethod("CreateEscapedValue", - BindingFlags.NonPublic | BindingFlags.Instance); + BindingFlags.NonPublic | BindingFlags.Static); return (string)method!.Invoke(this, [value])!; } } diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs index 35bd97b..7062eaa 100644 --- a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializationExceptionTest.cs @@ -1,9 +1,5 @@ using LogicBuilder.Workflow.ComponentModel.Serialization; using System; -using System.IO; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using Xunit; namespace LogicBuilder.Workflow.Tests.ComponentModel.Serialization { diff --git a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs index 158c939..c4cfa9d 100644 --- a/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs +++ b/Workflow.ComponentModel.Serialization.Tests/ComponentModel/Serialization/WorkflowMarkupSerializerTest.cs @@ -1,5 +1,6 @@ using LogicBuilder.ComponentModel.Design.Serialization; using LogicBuilder.Workflow.ComponentModel.Serialization; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.ComponentModel; @@ -688,7 +689,10 @@ public void SerializeToString_WithDelegate_ReturnsMethodName() Assert.Equal("TestMethod", result); } - private void TestMethod() { } + private void TestMethod() + { + // Method intentionally left empty. + } #endregion @@ -1403,7 +1407,7 @@ public void Deserialize_WithIntValue_DeserializesObject() [Fact] public void DeserializeFromCompactFormat_WithNullExtension_CreatesNullExtension() { - // Arrange + // Arrange PropertyName={StaticResource Key},PropertyValue=\"test\"} var serializer = new WorkflowMarkupSerializer(); var xml = ""; using var reader = XmlReader.Create(new StringReader(xml)); @@ -1509,18 +1513,6 @@ public void Serialize_WithArrayValue_SerializesAsArrayExtension() #endregion - #region Serialization Error Tests - - [Fact] - public void Serialize_WithCircularReference_ThrowsException() - { - // This would test circular reference detection - // The implementation should detect and prevent infinite loops - Assert.True(true, "Circular reference test placeholder - implement circular reference detection in the serializer to pass this test."); - } - - #endregion - #region Additional Coverage Tests [Fact] @@ -2046,5 +2038,632 @@ public void Serialize_WithComplexObject_SerializesContents() } #endregion + + #region TokenizeAttributes Error Cases Tests + + [Fact] + public void DeserializeFromCompactFormat_WithMissingTerminatingBrace_ReportsError() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null arg1, arg2"; + + // Act + object result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert - Should report error for missing terminating brace + Assert.Null(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithParameterToParameterlessExtension_ReportsError() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null arg1}"; + + // Act + object result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert - Should report error for trailing delimiter + Assert.Null(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithDoubleQuotes_ParsesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Type \"System.String\"}"; + + // Act + object? result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithNestedCurlyBraces_ParsesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null}"; + + // Act + object? result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert + Assert.IsType(result); + } + + [Fact] + public void DeserializeFromCompactFormat_WithExtraCharactersAfterBrace_ReportsError() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = ""; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.Read(); + var manager = new DesignerSerializationManager(); + var value = "{x:Null} extra"; + + // Act + object? result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.WorkflowMarkupStack.Push(reader); + result = serializer.DeserializeFromCompactFormat(wfManager, reader, value); + wfManager.WorkflowMarkupStack.Pop(); + } + + // Assert - should handle extra characters + Assert.Null(result); + } + + #endregion + + #region CreateInstance Coverage Tests + + public class TestClassWithoutDefaultConstructor(string value) + { + public string Value { get; } = value; + } + + [Fact] + public void CreateInstance_WithNullSerializationManager_ThrowsArgumentNullException() + { + // Arrange + var serializer = new TestWorkflowMarkupSerializer(); + var type = typeof(TestSimpleObject); + + // Act & Assert + Assert.Throws(() => serializer.CallCreateInstance(null!, type)); + } + + [Fact] + public void CreateInstance_WithNullType_ThrowsArgumentNullException() + { + // Arrange + var serializer = new TestWorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + + // Act & Assert + using (manager.CreateSession()) + { + Assert.Throws(() => + serializer.CallCreateInstance(new WorkflowMarkupSerializationManager(manager), null!)); + } + } + + [Fact] + public void CreateInstance_WithValidType_CreatesInstance() + { + // Arrange + var serializer = new TestWorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var type = typeof(TestSimpleObject); + + // Act + object result; + using (manager.CreateSession()) + { + result = serializer.CallCreateInstance(new WorkflowMarkupSerializationManager(manager), type); + } + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + // Helper class to expose protected CreateInstance method + private class TestWorkflowMarkupSerializer : WorkflowMarkupSerializer + { + public object CallCreateInstance(WorkflowMarkupSerializationManager manager, Type type) + { + return CreateInstance(manager, type); + } + } + + #endregion + + #region SerializeContents Events Tests + + public class TestObjectWithEvents + { + public event EventHandler? TestEvent; + public string Name { get; set; } = string.Empty; + + public void RaiseTestEvent() + { + TestEvent?.Invoke(this, EventArgs.Empty); + } + } + + [Fact] + public void Serialize_WithEvents_SerializesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestObjectWithEvents { Name = "Test" }; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.NotEmpty(result); + } + + #endregion + + #region SerializeContents Properties with Index Parameters Tests + + public class TestObjectWithIndexer + { + private readonly Dictionary _values = []; + + public string this[string key] + { + get => _values.TryGetValue(key, out var value) ? value : string.Empty; + set => _values[key] = value; + } + + public string Name { get; set; } = string.Empty; + } + + [Fact] + public void Serialize_WithIndexedProperty_SkipsIndexedProperty() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestObjectWithIndexer { Name = "Test" }; + obj["key1"] = "value1"; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.Contains("Test", result); + } + + #endregion + + #region ContentProperty Error Path Tests + + [ContentProperty("Items")] + public class TestCollectionContentProperty + { + public TestCollectionContentProperty() + { + Items = []; + } + + public List Items { get; } + } + + [ContentProperty("Items")] + public class TestNullCollectionContentProperty + { + public List? Items { get; set; } + } + + [ContentProperty("Value")] + public class TestReadOnlyContentProperty + { + public string? Value { get; } + } + + [ContentProperty("Value")] + public class TestSingleValueContentProperty + { + public string Value { get; set; } = string.Empty; + } + + #endregion + + #region Circular Reference Detection Tests + + public class TestCircularReference + { + public TestCircularReference? Child { get; set; } + public string Name { get; set; } = string.Empty; + } + + [Fact] + public void Serialize_WithCircularReference_ThrowsWorkflowMarkupSerializationException() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestCircularReference { Name = "Parent" }; + obj.Child = obj; // Create circular reference + + // Act & Assert + // The serializer should detect the circular reference during serialization + // This is handled by the SerializationStack.Contains check + Assert.Throws(() => serializer.Serialize(writer, obj)); + } + + #endregion + + #region OnBeforeSerialize, OnAfterSerialize, OnBeforeDeserialize, OnAfterDeserialize Tests + + #endregion + + #region ShouldSerializeValue with Convertible Types Tests + + public class TestConvertibleProperty + { + [DefaultValue("42")] + public string StringValue { get; set; } = "42"; + } + + [Fact] + public void ShouldSerializeValue_WithConvertibleDefaultValue_ReturnsFalse() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var property = typeof(TestConvertibleProperty).GetProperty("StringValue"); + + // Act + bool result; + using (manager.CreateSession()) + { + var wfManager = new WorkflowMarkupSerializationManager(manager); + wfManager.Context.Push(property!); + result = serializer.ShouldSerializeValue(wfManager, "42"); + wfManager.Context.Pop(); + } + + // Assert + Assert.False(result); + } + + #endregion + + #region SafeXmlNodeWriter Edge Cases Tests + + public class TestPropertyWithSpecialCharacters + { + public string NormalProperty { get; set; } = string.Empty; + } + + [Fact] + public void Serialize_WithSpecialCharactersInPropertyValue_EncodesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = new TestPropertyWithSpecialCharacters { NormalProperty = "<>&\"'" }; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.NotEmpty(result); + } + + #endregion + + #region AdvanceReader Coverage Tests + + [Fact] + public void Deserialize_WithComments_SkipsComments() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" + +42"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public void Deserialize_WithWhitespace_SkipsWhitespace() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" + + +42"; + using var reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { IgnoreWhitespace = false }); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal(42, result); + } + + #endregion + + #region GetClrFullName Coverage Tests + + [Fact] + public void Deserialize_WithUnmappedNamespace_UsesFullQualifiedName_ReturnsNull() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + // Use a namespace that is not mapped + var xml = @" +Test"; + using var reader = XmlReader.Create(new StringReader(xml)); + + var result = serializer.Deserialize(reader); + Assert.Null(result); + } + + #endregion + + #region DeserializeCompoundProperty Edge Cases Tests + + public class TestObjectWithComplexProperty + { + public TestSimpleObject? ComplexProperty { get; set; } + } + + [Fact] + public void Deserialize_WithEmptyComplexProperty_HandlesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" + + +"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act & Assert + var result = serializer.Deserialize(reader); + Assert.Null(result); + } + + #endregion + + #region String Escaping Tests + + [Fact] + public void Serialize_WithNullCharInString_ReplacesWithSpace() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + var obj = "Test\0Value"; + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.Contains("Test Value", result); + } + + [Fact] + public void SerializeToString_WithByte_ReturnsString() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + byte value = 255; + + // Act + string result; + using (manager.CreateSession()) + { + result = serializer.SerializeToString(new WorkflowMarkupSerializationManager(manager), value); + } + + // Assert + Assert.Equal("255", result); + } + + #endregion + + #region GetProperties and GetEvents Coverage Tests + + public class TestObjectWithHiddenProperty + { + [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)] + public string HiddenProperty { get; set; } = string.Empty; + + public string VisibleProperty { get; set; } = string.Empty; + } + + [Fact] + public void GetProperties_WithHiddenProperty_ExcludesHiddenProperty() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var obj = new TestObjectWithHiddenProperty(); + + // Act + PropertyInfo[] result; + using (manager.CreateSession()) + { + result = serializer.GetProperties(new WorkflowMarkupSerializationManager(manager), obj); + } + + // Assert + Assert.DoesNotContain(result, p => p.Name == "HiddenProperty"); + Assert.Contains(result, p => p.Name == "VisibleProperty"); + } + + public class TestObjectWithHiddenEvent + { + [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)] + public event EventHandler? HiddenEvent; + + public event EventHandler? VisibleEvent; + + public void RaiseEvents() + { + HiddenEvent?.Invoke(this, EventArgs.Empty); + VisibleEvent?.Invoke(this, EventArgs.Empty); + } + } + + [Fact] + public void GetEvents_WithHiddenEvent_ExcludesHiddenEvent() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var manager = new DesignerSerializationManager(); + var obj = new TestObjectWithHiddenEvent(); + + // Act + EventInfo[] result; + using (manager.CreateSession()) + { + result = serializer.GetEvents(new WorkflowMarkupSerializationManager(manager), obj); + } + + // Assert + Assert.DoesNotContain(result, e => e.Name == "HiddenEvent"); + Assert.Contains(result, e => e.Name == "VisibleEvent"); + } + + #endregion + + #region Additional Primitive Type Serialization Tests + + [Fact] + public void Serialize_WithInt16Value_GeneratesXml() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + short obj = 32767; + var output = new StringWriter(); + using var writer = XmlWriter.Create(output); + + // Act + serializer.Serialize(writer, obj); + writer.Flush(); + var result = output.ToString(); + + // Assert + Assert.Contains("32767", result); + } + + [Fact] + public void Deserialize_WithByteValue_DeserializesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" +255"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal((byte)255, result); + } + + [Fact] + public void Deserialize_WithInt16Value_DeserializesCorrectly() + { + // Arrange + var serializer = new WorkflowMarkupSerializer(); + var xml = @" +32767"; + using var reader = XmlReader.Create(new StringReader(xml)); + + // Act + var result = serializer.Deserialize(reader); + + // Assert + Assert.Equal((short)32767, result); + } + + #endregion } } \ No newline at end of file From 56cab39f34294683993d312eb3334eedd0ea08f6 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Thu, 26 Feb 2026 16:03:31 -0500 Subject: [PATCH 2/2] AB#44 putting back the protected override for WorkflowMarkupSerializationException. --- .../WorkflowMarkupSerializationException.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs index 3def63c..218c347 100644 --- a/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs +++ b/LogicBuilder.Workflow.ComponentModel.Serialization/ComponentModel/Serialization/WorkflowMarkupSerializationException.cs @@ -39,6 +39,16 @@ public WorkflowMarkupSerializationException() { } + protected WorkflowMarkupSerializationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + if (info == null) + throw new ArgumentNullException("info"); + + this.lineNumber = info.GetInt32("lineNumber"); + this.columnNumber = info.GetInt32("columnNumber"); + } + public int LineNumber { get