From 8e3894e9ad5383ce507fb7ddcea538021b56ec75 Mon Sep 17 00:00:00 2001
From: Lacyway <20912169+Lacyway@users.noreply.github.com>
Date: Tue, 1 Jul 2025 11:17:39 +0200
Subject: [PATCH] MongoID improvements (#437)
* MongoID improvements
- Added extension to check whether a MongoID is valid, 33% faster than old method
- Cut down generation speed by 2/3
* Fix method used
* Add test
---
.../Extensions/MongoIDExtensions.cs | 71 +++++++++++++++++++
.../Models/Common/MongoId.cs | 39 +++++-----
UnitTests/Tests/Utils/MongoIdTests.cs | 35 +++++++++
3 files changed, 123 insertions(+), 22 deletions(-)
create mode 100644 Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs
create mode 100644 UnitTests/Tests/Utils/MongoIdTests.cs
diff --git a/Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs b/Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs
new file mode 100644
index 00000000..6b742e0e
--- /dev/null
+++ b/Libraries/SPTarkov.Server.Core/Extensions/MongoIDExtensions.cs
@@ -0,0 +1,71 @@
+using SPTarkov.Server.Core.Models.Common;
+
+namespace SPTarkov.Server.Core.Extensions
+{
+ public static class MongoIDExtensions
+ {
+ ///
+ /// Determines whether the specified is a valid 24-character hexadecimal string,
+ /// which is the standard format for MongoDB ObjectIds.
+ ///
+ /// The to validate.
+ /// if the is a valid MongoDB ObjectId; otherwise, .
+ public static bool IsValidMongoId(this MongoId mongoId)
+ {
+ var span = mongoId.ToString().AsSpan();
+
+ if (span.Length != 24)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < 24; i++)
+ {
+ var c = span[i];
+ var isHex =
+ (c >= '0' && c <= '9') ||
+ (c >= 'a' && c <= 'f') ||
+ (c >= 'A' && c <= 'F');
+
+ if (!isHex)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Determines whether the specified string is a valid 24-character hexadecimal representation
+ /// of a MongoDB ObjectId.
+ ///
+ /// The string to validate as a MongoDB ObjectId.
+ /// if the is a valid MongoDB ObjectId; otherwise, .
+ public static bool IsValidMongoId(this string mongoId)
+ {
+ var span = mongoId.AsSpan();
+
+ if (span.Length != 24)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < 24; i++)
+ {
+ var c = span[i];
+ var isHex =
+ (c >= '0' && c <= '9') ||
+ (c >= 'a' && c <= 'f') ||
+ (c >= 'A' && c <= 'F');
+
+ if (!isHex)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
index 313e2514..48acc184 100644
--- a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
+++ b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
+using SPTarkov.Server.Core.Extensions;
namespace SPTarkov.Server.Core.Models.Common;
@@ -35,29 +36,26 @@ public readonly partial struct MongoId : IEquatable
/// 24 character objectId
private static string Generate()
{
- // Allocate a span directly onto the stack, will dispose whenever we finished running
- // Span is recommended to work with stackalloc + we use stackalloc here because we don't do anything with this afterwards
Span objectId = stackalloc byte[12];
- // Time stamp (4 bytes)
- var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- // Convert to big-endian
- objectId[0] = (byte)(timestamp >> 24);
- objectId[1] = (byte)(timestamp >> 16);
- objectId[2] = (byte)(timestamp >> 8);
- objectId[3] = (byte)timestamp;
+ // 4 bytes: current timestamp (big endian)
+ var timestamp = (int) DateTimeOffset.UtcNow
+ .ToUnixTimeSeconds();
+ objectId[0] = (byte) (timestamp >> 24);
+ objectId[1] = (byte) (timestamp >> 16);
+ objectId[2] = (byte) (timestamp >> 8);
+ objectId[3] = (byte) timestamp;
- // Random value (5 bytes)
- var rand = new Random();
- rand.NextBytes(objectId.Slice(4, 5));
+ // 5 bytes: random machine/process identifier
+ Random.Shared.NextBytes(objectId.Slice(4, 5));
- // Incrementing counter (3 bytes)
- // 24-bit counter
- var counter = rand.Next(0, 16777215);
- objectId[9] = (byte)(counter >> 16);
- objectId[10] = (byte)(counter >> 8);
- objectId[11] = (byte)counter;
+ // 3 bytes: random counter fallback (no static state)
+ var counter = Random.Shared.Next(0, 0xFFFFFF);
+ objectId[9] = (byte) (counter >> 16);
+ objectId[10] = (byte) (counter >> 8);
+ objectId[11] = (byte) counter;
+ // Convert to lowercase hex string (24 chars)
return Convert.ToHexStringLower(objectId);
}
@@ -88,7 +86,7 @@ public readonly partial struct MongoId : IEquatable
public static bool IsValidMongoId(string stringToCheck)
{
- return MongoIdRegex().IsMatch(stringToCheck);
+ return stringToCheck.IsValidMongoId();
}
public static implicit operator string(MongoId mongoId)
@@ -140,7 +138,4 @@ public readonly partial struct MongoId : IEquatable
{
return new MongoId("000000000000000000000000");
}
-
- [GeneratedRegex("^[a-fA-F0-9]{24}$")]
- private static partial Regex MongoIdRegex();
}
diff --git a/UnitTests/Tests/Utils/MongoIdTests.cs b/UnitTests/Tests/Utils/MongoIdTests.cs
new file mode 100644
index 00000000..f5e4072b
--- /dev/null
+++ b/UnitTests/Tests/Utils/MongoIdTests.cs
@@ -0,0 +1,35 @@
+using SPTarkov.Server.Core.Extensions;
+using SPTarkov.Server.Core.Models.Common;
+
+namespace UnitTests.Tests.Utils
+{
+ ///
+ /// Unit tests for the struct.
+ ///
+ [TestClass]
+ public class MongoIdTests
+ {
+ ///
+ /// Test that generates 1000 and ensures they are all valid.
+ /// Validity is checked by ensuring the ID is non-empty, exactly 24 characters, and matches the expected format.
+ ///
+ [TestMethod]
+ public void Generate_ShouldProduceValidMongoIds()
+ {
+ var invalidIds = new List();
+
+ for (var i = 0; i < 1000; i++)
+ {
+ var id = new MongoId();
+ var idStr = id.ToString();
+
+ if (string.IsNullOrWhiteSpace(idStr) || idStr.Length != 24 || !id.IsValidMongoId())
+ {
+ invalidIds.Add(idStr);
+ }
+ }
+
+ Assert.AreEqual(0, invalidIds.Count, $"Invalid MongoIds found: {string.Join(", ", invalidIds)}");
+ }
+ }
+}