Improve XmlSerializationWriter.WriteTypedPrimitive (#76436)
authorTrayan Zapryanov <lrt80@abv.bg>
Fri, 31 Mar 2023 23:59:59 +0000 (02:59 +0300)
committerGitHub <noreply@github.com>
Fri, 31 Mar 2023 23:59:59 +0000 (16:59 -0700)
* Introduce TryFormats for almost all primitive types

* Use primitive char buffer in XmlSerializationWriter

* Fix char cast

* Add tests for different types

* Add byte type

* Address feedback

* Fix tests

* remove using

* Increase duration char buffer size as it is not enough for TimeSpan.Max/Min

* Address feedback

* Added assert if we cannot format primitive value to the suppiled buffer

* Lazy create primitives buffer

* Address new feadback

* Resolve feedback

* Optimize float and double TryFormat

* Replace ArrayPool renting with Interlocked. Fix Debug.Assert

* Do not expect concurrency when using primitives buffer

---------

Co-authored-by: Traian Zaprianov <Traian.Zaprianov@docuware.com>
src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDateTime.cs
src/libraries/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Xmlcustomformatter.cs
src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs

index f829370..4f774a4 100644 (file)
@@ -132,6 +132,8 @@ namespace System.Xml.Schema
         private static ReadOnlySpan<int> DaysToMonth366 => new int[] {
             0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366};
 
+        private const int CharStackBufferSize = 64;
+
         /// <summary>
         /// Constructs an XsdDateTime from a string using specific format.
         /// </summary>
@@ -495,7 +497,17 @@ namespace System.Xml.Schema
         /// </summary>
         public override string ToString()
         {
-            var vsb = new ValueStringBuilder(stackalloc char[64]);
+            Span<char> destination = stackalloc char[CharStackBufferSize];
+            bool success = TryFormat(destination, out int charsWritten);
+            Debug.Assert(success);
+
+            return destination.Slice(0, charsWritten).ToString();
+        }
+
+        public bool TryFormat(Span<char> destination, out int charsWritten)
+        {
+            var vsb = new ValueStringBuilder(destination);
+
             switch (InternalTypeCode)
             {
                 case DateTimeTypeCode.DateTime:
@@ -534,7 +546,9 @@ namespace System.Xml.Schema
                     break;
             }
             PrintZone(ref vsb);
-            return vsb.ToString();
+
+            charsWritten = vsb.Length;
+            return destination.Length >= vsb.Length;
         }
 
         // Serialize year, month and day
index 817e017..f4612b2 100644 (file)
@@ -23,6 +23,7 @@ namespace System.Xml.Schema
         private uint _nanoseconds;       // High bit is used to indicate whether duration is negative
 
         private const uint NegativeBit = 0x80000000;
+        private const int CharStackBufferSize = 32;
 
         private enum Parts
         {
@@ -341,7 +342,16 @@ namespace System.Xml.Schema
         /// </summary>
         internal string ToString(DurationType durationType)
         {
-            var vsb = new ValueStringBuilder(stackalloc char[20]);
+            Span<char> destination = stackalloc char[CharStackBufferSize];
+            bool success = TryFormat(destination, out int charsWritten, durationType);
+            Debug.Assert(success);
+
+            return destination.Slice(0, charsWritten).ToString();
+        }
+
+        public bool TryFormat(Span<char> destination, out int charsWritten, DurationType durationType = DurationType.Duration)
+        {
+            var vsb = new ValueStringBuilder(destination);
             int nanoseconds, digit, zeroIdx, len;
 
             if (IsNegative)
@@ -411,7 +421,9 @@ namespace System.Xml.Schema
                             }
 
                             vsb.EnsureCapacity(zeroIdx + 1);
-                            vsb.Append(tmpSpan.Slice(0, zeroIdx - len + 1));
+                            int nanoSpanLength = zeroIdx - len + 1;
+                            bool successCopy = tmpSpan[..nanoSpanLength].TryCopyTo(vsb.AppendSpan(nanoSpanLength));
+                            Debug.Assert(successCopy);
                         }
                         vsb.Append('S');
                     }
@@ -428,7 +440,8 @@ namespace System.Xml.Schema
                     vsb.Append("0M");
             }
 
-            return vsb.ToString();
+            charsWritten = vsb.Length;
+            return destination.Length >= vsb.Length;
         }
 
         internal static Exception? TryParse(string s, out XsdDuration result)
index cfa361f..98ec06f 100644 (file)
@@ -38,6 +38,9 @@ namespace System.Xml.Serialization
         private bool _soap12;
         private bool _escapeName = true;
 
+        //char buffer for serializing primitive values
+        private readonly char[] _primitivesBuffer = new char[64];
+
         // this method must be called before any generated serialization methods are called
         internal void Init(XmlWriter w, XmlSerializerNamespaces? namespaces, string? encodingStyle, string? idBase)
         {
@@ -120,6 +123,11 @@ namespace System.Xml.Serialization
             return XmlCustomFormatter.FromDateTime(value);
         }
 
+        internal static bool TryFormatDateTime(DateTime value, Span<char> destination, out int charsWritten)
+        {
+            return XmlCustomFormatter.TryFormatDateTime(value, destination, out charsWritten);
+        }
+
         protected static string FromDate(DateTime value)
         {
             return XmlCustomFormatter.FromDate(value);
@@ -246,13 +254,15 @@ namespace System.Xml.Serialization
         [RequiresUnreferencedCode(XmlSerializer.TrimSerializationWarning)]
         protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiType)
         {
-            string? value;
+            string? value = null;
             string type;
             string typeNs = XmlSchema.Namespace;
             bool writeRaw = true;
             bool writeDirect = false;
             Type t = o.GetType();
             bool wroteStartElement = false;
+            bool? tryFormatResult = null;
+            int charsWritten = -1;
 
             switch (Type.GetTypeCode(t))
             {
@@ -262,60 +272,60 @@ namespace System.Xml.Serialization
                     writeRaw = false;
                     break;
                 case TypeCode.Int32:
-                    value = XmlConvert.ToString((int)o);
+                    tryFormatResult = XmlConvert.TryFormat((int)o, _primitivesBuffer, out charsWritten);
                     type = "int";
                     break;
                 case TypeCode.Boolean:
-                    value = XmlConvert.ToString((bool)o);
+                    tryFormatResult = XmlConvert.TryFormat((bool)o, _primitivesBuffer, out charsWritten);
                     type = "boolean";
                     break;
                 case TypeCode.Int16:
-                    value = XmlConvert.ToString((short)o);
+                    tryFormatResult = XmlConvert.TryFormat((short)o, _primitivesBuffer, out charsWritten);
                     type = "short";
                     break;
                 case TypeCode.Int64:
-                    value = XmlConvert.ToString((long)o);
+                    tryFormatResult = XmlConvert.TryFormat((long)o, _primitivesBuffer, out charsWritten);
                     type = "long";
                     break;
                 case TypeCode.Single:
-                    value = XmlConvert.ToString((float)o);
+                    tryFormatResult = XmlConvert.TryFormat((float)o, _primitivesBuffer, out charsWritten);
                     type = "float";
                     break;
                 case TypeCode.Double:
-                    value = XmlConvert.ToString((double)o);
+                    tryFormatResult = XmlConvert.TryFormat((double)o, _primitivesBuffer, out charsWritten);
                     type = "double";
                     break;
                 case TypeCode.Decimal:
-                    value = XmlConvert.ToString((decimal)o);
+                    tryFormatResult = XmlConvert.TryFormat((decimal)o, _primitivesBuffer, out charsWritten);
                     type = "decimal";
                     break;
                 case TypeCode.DateTime:
-                    value = FromDateTime((DateTime)o);
+                    tryFormatResult = TryFormatDateTime((DateTime)o, _primitivesBuffer, out charsWritten);
                     type = "dateTime";
                     break;
                 case TypeCode.Char:
-                    value = FromChar((char)o);
+                    tryFormatResult = XmlConvert.TryFormat((ushort)(char)o, _primitivesBuffer, out charsWritten);
                     type = "char";
                     typeNs = UrtTypes.Namespace;
                     break;
                 case TypeCode.Byte:
-                    value = XmlConvert.ToString((byte)o);
+                    tryFormatResult = XmlConvert.TryFormat((byte)o, _primitivesBuffer, out charsWritten);
                     type = "unsignedByte";
                     break;
                 case TypeCode.SByte:
-                    value = XmlConvert.ToString((sbyte)o);
+                    tryFormatResult = XmlConvert.TryFormat((sbyte)o, _primitivesBuffer, out charsWritten);
                     type = "byte";
                     break;
                 case TypeCode.UInt16:
-                    value = XmlConvert.ToString((ushort)o);
+                    tryFormatResult = XmlConvert.TryFormat((ushort)o, _primitivesBuffer, out charsWritten);
                     type = "unsignedShort";
                     break;
                 case TypeCode.UInt32:
-                    value = XmlConvert.ToString((uint)o);
+                    tryFormatResult = XmlConvert.TryFormat((uint)o, _primitivesBuffer, out charsWritten);
                     type = "unsignedInt";
                     break;
                 case TypeCode.UInt64:
-                    value = XmlConvert.ToString((ulong)o);
+                    tryFormatResult = XmlConvert.TryFormat((ulong)o, _primitivesBuffer, out charsWritten);
                     type = "unsignedLong";
                     break;
 
@@ -340,19 +350,19 @@ namespace System.Xml.Serialization
                     }
                     else if (t == typeof(Guid))
                     {
-                        value = XmlConvert.ToString((Guid)o);
+                        tryFormatResult = XmlConvert.TryFormat((Guid)o, _primitivesBuffer, out charsWritten);
                         type = "guid";
                         typeNs = UrtTypes.Namespace;
                     }
                     else if (t == typeof(TimeSpan))
                     {
-                        value = XmlConvert.ToString((TimeSpan)o);
+                        tryFormatResult = XmlConvert.TryFormat((TimeSpan)o, _primitivesBuffer, out charsWritten);
                         type = "TimeSpan";
                         typeNs = UrtTypes.Namespace;
                     }
                     else if (t == typeof(DateTimeOffset))
                     {
-                        value = XmlConvert.ToString((DateTimeOffset)o);
+                        tryFormatResult = XmlConvert.TryFormat((DateTimeOffset)o, _primitivesBuffer, out charsWritten);
                         type = "dateTimeOffset";
                         typeNs = UrtTypes.Namespace;
                     }
@@ -374,7 +384,10 @@ namespace System.Xml.Serialization
                         return;
                     }
                     else
+                    {
                         throw CreateUnknownTypeException(t);
+                    }
+
                     break;
             }
             if (!wroteStartElement)
@@ -387,21 +400,34 @@ namespace System.Xml.Serialization
 
             if (xsiType) WriteXsiType(type, typeNs);
 
-            if (value == null)
-            {
-                _w.WriteAttributeString("nil", XmlSchema.InstanceNamespace, "true");
-            }
-            else if (writeDirect)
+            if (writeDirect)
             {
                 // only one type currently writes directly to XML stream
                 XmlCustomFormatter.WriteArrayBase64(_w, (byte[])o, 0, ((byte[])o).Length);
             }
-            else if (writeRaw)
+            else if (tryFormatResult != null)
             {
-                _w.WriteRaw(value);
+                Debug.Assert(tryFormatResult.Value, "Something goes wrong with formatting primitives to the buffer.");
+#if DEBUG
+                const string escapeChars = "<>\"'&";
+                ReadOnlySpan<char> span = _primitivesBuffer;
+                Debug.Assert(span.Slice(0, charsWritten).IndexOfAny(escapeChars) == -1, "Primitive value contains illegal xml char.");
+#endif
+                //all the primitive types except string and XmlQualifiedName writes to the buffer
+                _w.WriteRaw(_primitivesBuffer, 0, charsWritten);
             }
             else
-                _w.WriteString(value);
+            {
+                if (value == null)
+                    _w.WriteAttributeString("nil", XmlSchema.InstanceNamespace, "true");
+                else if (writeRaw)
+                {
+                    _w.WriteRaw(value);
+                }
+                else
+                    _w.WriteString(value);
+            }
+
             _w.WriteEndElement();
         }
 
index 2338d8d..77a2bb1 100644 (file)
@@ -105,6 +105,17 @@ namespace System.Xml.Serialization
             }
         }
 
+        internal static bool TryFormatDateTime(DateTime value, Span<char> destination, out int charsWritten)
+        {
+            if (Mode == DateTimeSerializationSection.DateTimeSerializationMode.Local)
+            {
+                return XmlConvert.TryFormat(value, "yyyy-MM-ddTHH:mm:ss.fffffffzzzzzz", destination, out charsWritten);
+            }
+
+            // for mode DateTimeSerializationMode.Roundtrip and DateTimeSerializationMode.Default
+            return XmlConvert.TryFormat(value, XmlDateTimeSerializationMode.RoundtripKind, destination, out charsWritten);
+        }
+
         internal static string FromChar(char value)
         {
             return XmlConvert.ToString((ushort)value);
index c465eb9..07b6bb9 100644 (file)
@@ -1645,5 +1645,173 @@ namespace System.Xml
         {
             return CreateException(index == 0 ? SR.Xml_BadStartNameChar : SR.Xml_BadNameChar, XmlException.BuildCharExceptionArgs(name, index), exceptionType, 0, index + 1);
         }
+
+        internal static bool TryFormat(bool value, Span<char> destination, out int charsWritten)
+        {
+            string valueAsString = value ? "true" : "false";
+
+            charsWritten = valueAsString.Length;
+            return valueAsString.TryCopyTo(destination);
+        }
+
+        internal static bool TryFormat(char value, Span<char> destination, out int charsWritten)
+        {
+            charsWritten = 1;
+            if (destination.Length < 1) return false;
+
+            destination[0] = value;
+            return true;
+        }
+
+        internal static bool TryFormat(decimal value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, NumberFormatInfo.InvariantInfo);
+        }
+
+        internal static bool TryFormat(sbyte value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out  charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(short value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(int value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(long value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(byte value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(ushort value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(uint value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(ulong value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, default, CultureInfo.InvariantCulture);
+        }
+
+        internal static bool TryFormat(float value, Span<char> destination, out int charsWritten)
+        {
+            ReadOnlySpan<char> valueSpan;
+
+            if (!float.IsFinite(value))
+            {
+                if (float.IsNaN(value))
+                    valueSpan = "NaN";
+                else
+                    valueSpan = float.IsNegative(value) ? "-INF" : "INF";
+            }
+            else if (IsNegativeZero((double)value))
+            {
+                valueSpan = "-0";
+            }
+            else
+            {
+                return value.TryFormat(destination, out charsWritten, "R", NumberFormatInfo.InvariantInfo);
+            }
+
+            charsWritten = valueSpan.Length;
+            return valueSpan.TryCopyTo(destination);
+        }
+
+        internal static bool TryFormat(double value, Span<char> destination, out int charsWritten)
+        {
+            ReadOnlySpan<char> valueSpan;
+
+            if (!double.IsFinite(value))
+            {
+                if (double.IsNaN(value))
+                    valueSpan = "NaN";
+                else
+                    valueSpan = double.IsNegative(value) ? "-INF" : "INF";
+            }
+            else if (IsNegativeZero(value))
+            {
+                valueSpan = "-0";
+            }
+            else
+            {
+                return value.TryFormat(destination, out charsWritten, "R", NumberFormatInfo.InvariantInfo);
+            }
+
+            charsWritten = valueSpan.Length;
+            return valueSpan.TryCopyTo(destination);
+        }
+
+        internal static bool TryFormat(TimeSpan value, Span<char> destination, out int charsWritten)
+        {
+            return new XsdDuration(value).TryFormat(destination, out charsWritten);
+        }
+
+        internal static bool TryFormat(DateTime value, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, format, DateTimeFormatInfo.InvariantInfo);
+        }
+
+        internal static bool TryFormat(DateTime value, Span<char> destination, out int charsWritten)
+        {
+            return TryFormat(value, XmlDateTimeSerializationMode.RoundtripKind, destination, out charsWritten);
+        }
+
+        internal static bool TryFormat(DateTime value, XmlDateTimeSerializationMode dateTimeOption, Span<char> destination, out int charsWritten)
+        {
+            switch (dateTimeOption)
+            {
+                case XmlDateTimeSerializationMode.Local:
+                    value = SwitchToLocalTime(value);
+                    break;
+
+                case XmlDateTimeSerializationMode.Utc:
+                    value = SwitchToUtcTime(value);
+                    break;
+
+                case XmlDateTimeSerializationMode.Unspecified:
+                    value = new DateTime(value.Ticks, DateTimeKind.Unspecified);
+                    break;
+
+                case XmlDateTimeSerializationMode.RoundtripKind:
+                    break;
+
+                default:
+                    throw new ArgumentException(SR.Format(SR.Sch_InvalidDateTimeOption, dateTimeOption, nameof(dateTimeOption)));
+            }
+
+            XsdDateTime xsdDateTime = new XsdDateTime(value, XsdDateTimeFlags.DateTime);
+            return xsdDateTime.TryFormat(destination, out charsWritten);
+        }
+
+        internal static bool TryFormat(DateTimeOffset value, Span<char> destination, out int charsWritten)
+        {
+            XsdDateTime xsdDateTime = new XsdDateTime(value);
+            return xsdDateTime.TryFormat(destination, out charsWritten);
+        }
+
+        internal static bool TryFormat(DateTimeOffset value, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string format, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten, format, DateTimeFormatInfo.InvariantInfo);
+        }
+
+        internal static bool TryFormat(Guid value, Span<char> destination, out int charsWritten)
+        {
+            return value.TryFormat(destination, out charsWritten);
+        }
     }
 }
index 29a7880..9fa615c 100644 (file)
@@ -412,6 +412,51 @@ public static partial class XmlSerializerTests
     }
 
     [Fact]
+    public static void Xml_CollectionRoot_MorePrimitiveTypes()
+    {
+        DateTime now = new DateTime(2022, 9, 30, 9, 4, 15, DateTimeKind.Utc);
+        DateTimeOffset dtoNow = now.AddDays(1);
+        TimeSpan ts = new TimeSpan(1, 2, 3, 4, 5);
+        MyCollection x = new MyCollection(123.45m, now, ts, dtoNow, (short)55, 2345324L, (sbyte)11, (ushort)34, (uint)4564, (ulong)456734767,
+            new byte[] { 33, 44, 55 }, (byte)67);
+        MyCollection y = SerializeAndDeserialize<MyCollection>(x,
+@"<?xml version=""1.0""?>
+<ArrayOfAnyType xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
+ <anyType xsi:type=""xsd:decimal"">123.45</anyType>
+ <anyType xsi:type=""xsd:dateTime"">2022-09-30T09:04:15Z</anyType>
+ <anyType xmlns:q2=""http://microsoft.com/wsdl/types/"" xsi:type=""q2:TimeSpan"">P1DT2H3M4.005S</anyType>
+ <anyType xmlns:q3=""http://microsoft.com/wsdl/types/"" xsi:type=""q3:dateTimeOffset"">2022-10-01T09:04:15Z</anyType>
+ <anyType xsi:type=""xsd:short"">55</anyType>
+ <anyType xsi:type=""xsd:long"">2345324</anyType>
+ <anyType xsi:type=""xsd:byte"">11</anyType>
+ <anyType xsi:type=""xsd:unsignedShort"">34</anyType>
+ <anyType xsi:type=""xsd:unsignedInt"">4564</anyType>
+ <anyType xsi:type=""xsd:unsignedLong"">456734767</anyType>
+ <anyType xsi:type=""xsd:base64Binary"">ISw3</anyType>
+ <anyType xsi:type=""xsd:unsignedByte"">67</anyType>
+</ArrayOfAnyType>");
+
+        Assert.NotNull(y);
+        Assert.True(y.Count == 12);
+        Assert.True((decimal)y[0] == 123.45m);
+        Assert.True((DateTime)y[1] == now);
+        Assert.True((TimeSpan)y[2] == ts);
+        Assert.True((DateTimeOffset)y[3] == dtoNow);
+        Assert.True((short)y[4] == 55);
+        Assert.True((long)y[5] == 2345324L);
+        Assert.True((sbyte)y[6] == 11);
+        Assert.True((ushort)y[7] == 34);
+        Assert.True((uint)y[8] == 4564);
+        Assert.True((ulong)y[9] == 456734767);
+        Assert.True(y[10] is byte[]);
+        Assert.Equal(3, ((byte[])y[10]).Length);
+        Assert.Equal(33, ((byte[])y[10])[0]);
+        Assert.Equal(44, ((byte[])y[10])[1]);
+        Assert.Equal(55, ((byte[])y[10])[2]);
+        Assert.True((byte)y[11] == 67);
+    }
+
+    [Fact]
     public static void Xml_EnumerableRoot()
     {
         MyEnumerable x = new MyEnumerable("abc", 3);