From 73ec1fd03f9942a5d42a624d32971712c4f09edf Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Sat, 11 Jul 2026 19:29:29 +0100 Subject: [PATCH] Optimise XElementExtensions value conversion and hashing Two hot-path optimisations in XElementExtensions: 1. ValueOrDefault (XElement + XAttribute overloads) now fast-paths the value types actually used (int, Guid, bool, enum) with direct parsers before falling back to Umbraco's reflection-based TryConvertTo. These getters run for (almost) every attribute of every node during a report/import, so avoiding the reflection/exception overhead adds up. The typeof(TObject) checks are JIT-folded per generic instantiation. 2. MakePlatformSafeHashAsync streams the XML straight through a CryptoStream instead of buffering the entire serialised document into a MemoryStream before hashing. The hashed byte sequence is unchanged, so hashes stay stable across platforms. Co-Authored-By: Claude Opus 4.8 --- uSync.Core/Extensions/XElementExtensions.cs | 54 ++++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/uSync.Core/Extensions/XElementExtensions.cs b/uSync.Core/Extensions/XElementExtensions.cs index badef56e..5fca7d19 100644 --- a/uSync.Core/Extensions/XElementExtensions.cs +++ b/uSync.Core/Extensions/XElementExtensions.cs @@ -138,7 +138,37 @@ public static string ValueOrDefault([AllowNull] this XElement? node, string defa public static TObject ValueOrDefault([AllowNull] this XElement? node, TObject defaultValue) { var value = node.ValueOrDefault(string.Empty); - if (value == string.Empty) return defaultValue; + if (value.Length == 0) return defaultValue; + + return value.ConvertOrDefault(defaultValue); + } + + /// + /// Convert a non-empty string value to the requested type. + /// + /// + /// These getters are called for (almost) every attribute of every node during a + /// report/import. The handful of value types actually used have direct, allocation + /// free parsers that are much cheaper than routing through Umbraco's reflection based + /// TryConvertTo. The typeof(TObject) == typeof(...) comparisons are folded to + /// constants by the JIT per generic instantiation, so the branches have no runtime + /// cost. Anything not matched falls through to TryGetValueAs. + /// + private static TObject ConvertOrDefault(this string value, TObject defaultValue) + { + if (typeof(TObject) == typeof(int)) + return int.TryParse(value, out var i) ? (TObject)(object)i : defaultValue; + + if (typeof(TObject) == typeof(Guid)) + return Guid.TryParse(value, out var g) ? (TObject)(object)g : defaultValue; + + if (typeof(TObject) == typeof(bool)) + return bool.TryParse(value, out var b) ? (TObject)(object)b : defaultValue; + + if (typeof(TObject).IsEnum) + return Enum.TryParse(typeof(TObject), value, true, out var e) && e is TObject enumValue + ? enumValue + : defaultValue; return value.TryGetValueAs(out var result) ? result : defaultValue; } @@ -284,9 +314,9 @@ public static string ValueOrDefault([AllowNull] this XAttribute? attribute, stri public static TObject ValueOrDefault([AllowNull] this XAttribute attribute, TObject defaultValue) { var value = attribute.ValueOrDefault(string.Empty); - if (value == string.Empty) return defaultValue; + if (value.Length == 0) return defaultValue; - return value.TryGetValueAs(out var result) ? result : defaultValue; + return value.ConvertOrDefault(defaultValue); } #endregion @@ -307,17 +337,17 @@ public static TObject ValueOrDefault([AllowNull] this XAttribute attrib /// public static async Task MakePlatformSafeHashAsync(this XElement node) { - using (MemoryStream stream = new MemoryStream()) - { - await node.SaveAsync(stream, SaveOptions.None, CancellationToken.None); - stream.Seek(0, SeekOrigin.Begin); + using HashAlgorithm hashAlgorithm = CryptoConfig.AllowOnlyFipsAlgorithms ? SHA1.Create() : MD5.Create(); - using (HashAlgorithm hashAlgorithm = CryptoConfig.AllowOnlyFipsAlgorithms ? SHA1.Create() : MD5.Create()) - { - var hash = await hashAlgorithm.ComputeHashAsync(stream); - return Convert.ToHexStringLower(hash); - } + // stream the xml straight into the hash instead of buffering the whole + // serialized document into a MemoryStream first. CryptoStream feeds each + // written block to the algorithm as it arrives, so nothing is held in memory. + using (var cryptoStream = new CryptoStream(Stream.Null, hashAlgorithm, CryptoStreamMode.Write)) + { + await node.SaveAsync(cryptoStream, SaveOptions.None, CancellationToken.None); } + + return Convert.ToHexStringLower(hashAlgorithm.Hash!); } }