From 23313b4fe1073621d2566196ec55d0b8db9cb1f0 Mon Sep 17 00:00:00 2001 From: Kloizdena Date: Sat, 20 Jun 2026 00:55:07 +0200 Subject: [PATCH] CSHARP-5611: Avoid buffer allocations in ObjectId.Parse --- src/MongoDB.Bson/BsonUtils.cs | 58 +++++++++++++++++-- src/MongoDB.Bson/ObjectModel/ObjectId.cs | 28 +++++++-- .../ObjectModel/ObjectIdTests.cs | 2 +- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/MongoDB.Bson/BsonUtils.cs b/src/MongoDB.Bson/BsonUtils.cs index 4b324286c10..1cbbe07a156 100644 --- a/src/MongoDB.Bson/BsonUtils.cs +++ b/src/MongoDB.Bson/BsonUtils.cs @@ -72,6 +72,24 @@ public static byte[] ParseHexString(string s) return bytes; } + /// + /// Parses a hex char span into its equivalent byte array. + /// + /// The hex char span to parse. + /// The result span to fill with the byte equivalent of the hex string. + public static void ParseHexString(ReadOnlySpan s, Span bytes) + { + int expectedLength = GetByteLength(s.Length); + if (bytes.Length != expectedLength) + { + throw new FormatException($"Target should be {expectedLength} bytes long"); + } + if (!TryParseHexString(s, bytes)) + { + throw new FormatException("String should contain only hexadecimal digits."); + } + } + /// /// Converts from number of milliseconds since Unix epoch to DateTime. /// @@ -212,14 +230,43 @@ public static DateTime ToUniversalTime(DateTime dateTime) /// True if the hex string was successfully parsed. public static bool TryParseHexString(string s, out byte[] bytes) { - bytes = null; - if (s == null) { + bytes = null; return false; } - var buffer = new byte[(s.Length + 1) / 2]; + var buffer = new byte[GetByteLength(s.Length)]; + if (TryParseHexString(s.AsSpan(), buffer)) + { + bytes = buffer; + return true; + } + else + { + bytes = null; + return false; + } + } + + /// + /// Calculate the result byte length for the hex string length + /// + /// The length of the hex string + /// The required length to convert the hex string to bytes + internal static int GetByteLength(int hexStringLength) + => (hexStringLength + 1) / 2; + + /// + /// Tries to parse a hex char span to a byte span. + /// + /// The hex chars. + /// A byte span. + /// True if the hex char span was successfully parsed. + public static bool TryParseHexString(ReadOnlySpan s, Span bytes) + { + if (bytes.Length != GetByteLength(s.Length)) + return false; var i = 0; var j = 0; @@ -232,7 +279,7 @@ public static bool TryParseHexString(string s, out byte[] bytes) { return false; } - buffer[j++] = (byte)y; + bytes[j++] = (byte)y; } while (i < s.Length) @@ -246,10 +293,9 @@ public static bool TryParseHexString(string s, out byte[] bytes) { return false; } - buffer[j++] = (byte)((x << 4) | y); + bytes[j++] = (byte)((x << 4) | y); } - bytes = buffer; return true; } diff --git a/src/MongoDB.Bson/ObjectModel/ObjectId.cs b/src/MongoDB.Bson/ObjectModel/ObjectId.cs index bb76b29b3a0..6347a748b60 100644 --- a/src/MongoDB.Bson/ObjectModel/ObjectId.cs +++ b/src/MongoDB.Bson/ObjectModel/ObjectId.cs @@ -87,8 +87,8 @@ public ObjectId(string value) { throw new ArgumentNullException(nameof(value)); } - - var bytes = BsonUtils.ParseHexString(value); + Span bytes = stackalloc byte[12]; + BsonUtils.ParseHexString(value.AsSpan(), bytes); FromBytesSpan(bytes, out _a, out _b, out _c); } @@ -255,11 +255,27 @@ public static ObjectId Parse(string s) /// True if the string was parsed successfully. public static bool TryParse(string s, out ObjectId objectId) { - // don't throw ArgumentNullException if s is null - if (s != null && s.Length == 24) + if (s == null) + { + // don't throw ArgumentNullException if s is null + objectId = default(ObjectId); + return false; + } + return TryParse(s.AsSpan(), out objectId); + } + + /// + /// Tries to parse a span of chars and create a new ObjectId. + /// + /// The span value. + /// The new ObjectId. + /// True if the string was parsed successfully. + public static bool TryParse(ReadOnlySpan s, out ObjectId objectId) + { + if (s.Length == 24) { - byte[] bytes; - if (BsonUtils.TryParseHexString(s, out bytes)) + Span bytes = stackalloc byte[12]; + if (BsonUtils.TryParseHexString(s, bytes)) { objectId = new ObjectId(bytes); return true; diff --git a/tests/MongoDB.Bson.Tests/ObjectModel/ObjectIdTests.cs b/tests/MongoDB.Bson.Tests/ObjectModel/ObjectIdTests.cs index 45c03e93f97..b779f950388 100644 --- a/tests/MongoDB.Bson.Tests/ObjectModel/ObjectIdTests.cs +++ b/tests/MongoDB.Bson.Tests/ObjectModel/ObjectIdTests.cs @@ -427,7 +427,7 @@ public void TestTryParse() Assert.False(ObjectId.TryParse("102030405060708090a0b0c", out objectId1)); // too short Assert.False(ObjectId.TryParse("x102030405060708090a0b0c", out objectId1)); // invalid character Assert.False(ObjectId.TryParse("00102030405060708090a0b0c", out objectId1)); // too long - Assert.False(ObjectId.TryParse(null, out objectId1)); // should return false not throw ArgumentNullException + Assert.False(ObjectId.TryParse(default(string), out objectId1)); // should return false not throw ArgumentNullException } [Fact]