diff --git a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
index 2b133f019..72d28b223 100644
--- a/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
+++ b/Libraries/SPTarkov.Server.Core/Models/Common/MongoId.cs
@@ -5,7 +5,7 @@
namespace SPTarkov.Server.Core.Models.Common;
///
-/// Represents a 12-byte MongoDB-style ObjectId, consisting of:
+/// Represents a 12- MongoDB-style ObjectId, consisting of:
///
/// - 4-byte timestamp (seconds since Unix epoch, big-endian)
/// - 3-byte machine identifier
@@ -39,7 +39,7 @@ namespace SPTarkov.Server.Core.Models.Common;
private readonly int _pidAndIncrement;
private static readonly int _machine = BitConverter.ToInt32(RandomNumberGenerator.GetBytes(4), 0) & 0xFFFFFF;
- private static readonly short _pid = (short)Environment.ProcessId;
+ private static readonly short _pid = (short) Environment.ProcessId;
private static int _increment = RandomNumberGenerator.GetInt32(0, 0xFFFFFF);
public bool IsEmpty
@@ -53,25 +53,25 @@ public bool IsEmpty
///
public MongoId()
{
- var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ var timestamp = (int) DateTimeOffset.UtcNow.ToUnixTimeSeconds();
Span bytes = stackalloc byte[12];
// timestamp (4 bytes, big-endian)
BinaryPrimitives.WriteInt32BigEndian(bytes, timestamp);
// machine ID (3 bytes)
- bytes[4] = (byte)(_machine >> 16);
- bytes[5] = (byte)(_machine >> 8);
- bytes[6] = (byte)_machine;
+ bytes[4] = (byte) (_machine >> 16);
+ bytes[5] = (byte) (_machine >> 8);
+ bytes[6] = (byte) _machine;
// PID (2 bytes)
BinaryPrimitives.WriteInt16BigEndian(bytes[7..9], _pid);
// increment (3 bytes, big-endian)
var inc = Interlocked.Increment(ref _increment) & 0xFFFFFF;
- bytes[9] = (byte)(inc >> 16);
- bytes[10] = (byte)(inc >> 8);
- bytes[11] = (byte)inc;
+ bytes[9] = (byte) (inc >> 16);
+ bytes[10] = (byte) (inc >> 8);
+ bytes[11] = (byte) inc;
// pack into fields (avoids array allocations later)
_timestampAndMachine = BitConverter.ToInt64(bytes);
@@ -82,8 +82,7 @@ public MongoId(string? hex)
{
if (string.IsNullOrEmpty(hex) || hex == "000000000000000000000000")
{
- _timestampAndMachine = 0;
- _pidAndIncrement = 0;
+ this = default;
return;
}
@@ -93,26 +92,28 @@ public MongoId(string? hex)
}
Span bytes = stackalloc byte[12];
- Span chars = stackalloc char[24];
- hex.AsSpan().CopyTo(chars);
-
for (var i = 0; i < 12; i++)
{
var hi = HexCharToValue(hex[2 * i]);
- var lo = HexCharToValue(hex[2 * i + 1]);
+ var lo = HexCharToValue(hex[(2 * i) + 1]);
if (hi == -1 || lo == -1)
{
throw new FormatException("ObjectId contains invalid hex characters.");
}
- bytes[i] = (byte)((hi << 4) | lo);
+ bytes[i] = (byte) ((hi << 4) | lo);
}
_timestampAndMachine = BitConverter.ToInt64(bytes);
_pidAndIncrement = BitConverter.ToInt32(bytes[8..]);
}
+ ///
+ /// Converts a hexadecimal character into its corresponding integer nibble value.
+ ///
+ /// The hex character to evaluate (0-9, a-f, A-F).
+ /// An integer value from 0 to 15 if the character is valid hex; otherwise, -1.
private static int HexCharToValue(char c)
{
return c >= '0' && c <= '9' ? c - '0'
@@ -121,30 +122,74 @@ private static int HexCharToValue(char c)
: -1;
}
+ ///
+ /// Converts an integer nibble value into its corresponding lowercase hexadecimal character representation.
+ ///
+ /// The nibble value to convert (0-15).
+ /// A lowercase hexadecimal character representing the specified value.
+ private static char HexValueToChar(int value)
+ {
+ return (char) (value < 10 ? value + '0' : value - 10 + 'a');
+ }
+
///
/// Returns the MongoId as a 24-character lowercase hexadecimal string.
///
public override string ToString()
{
- if (_timestampAndMachine == 0 && _pidAndIncrement == 0)
+ if (IsEmpty)
{
return string.Empty;
}
- Span bytes = stackalloc byte[12];
- BitConverter.TryWriteBytes(bytes, _timestampAndMachine);
- BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement);
- return Convert.ToHexString(bytes).ToLowerInvariant();
+ return string.Create(24, this, static (chars, state) =>
+ {
+ Span bytes = stackalloc byte[12];
+ BitConverter.TryWriteBytes(bytes, state._timestampAndMachine);
+ BitConverter.TryWriteBytes(bytes[8..], state._pidAndIncrement);
+
+ for (var i = 0; i < 12; i++)
+ {
+ var b = bytes[i];
+ chars[i * 2] = HexValueToChar(b >> 4);
+ chars[(i * 2) + 1] = HexValueToChar(b & 0x0F);
+ }
+ });
}
- public bool Equals(MongoId? other)
+ ///
+ /// Tries to format the current instance into the provided character span.
+ ///
+ /// The destination span. Must be at least 24 characters long.
+ /// When this method returns, contains the number of characters written.
+ /// if the formatting was successful; otherwise, .
+ public bool TryFormat(Span destination, out int charsWritten)
{
- if (other is null)
+ if (destination.Length < 24)
{
+ charsWritten = 0;
return false;
}
- return _timestampAndMachine == other.Value._timestampAndMachine && _pidAndIncrement == other.Value._pidAndIncrement;
+ if (IsEmpty)
+ {
+ charsWritten = 0;
+ return true;
+ }
+
+ Span bytes = stackalloc byte[12];
+ BitConverter.TryWriteBytes(bytes, _timestampAndMachine);
+ BitConverter.TryWriteBytes(bytes[8..], _pidAndIncrement);
+
+ for (var i = 0; i < 12; i++)
+ {
+ var b = bytes[i];
+ destination[i * 2] = HexValueToChar(b >> 4);
+ destination[(i * 2) + 1] = HexValueToChar(b & 0x0F);
+ }
+
+ charsWritten = 24;
+ return true;
}
///
@@ -170,7 +215,7 @@ public bool Equals(string? other)
return false;
}
- bytes[i] = (byte)((hi << 4) | lo);
+ bytes[i] = (byte) ((hi << 4) | lo);
}
var a = BitConverter.ToInt64(bytes);
@@ -179,6 +224,11 @@ public bool Equals(string? other)
return _timestampAndMachine == a && _pidAndIncrement == b;
}
+ ///
+ /// Validates whether the specified string represents a valid MongoDB ObjectId format.
+ ///
+ /// The string representation of the identifier to validate.
+ /// if the string satisfies format constraints; otherwise, .
public static bool IsValidMongoId(string stringToCheck)
{
return stringToCheck.IsValidMongoId();
@@ -232,8 +282,12 @@ public override int GetHashCode()
return HashCode.Combine(_timestampAndMachine, _pidAndIncrement);
}
+ ///
+ /// Returns an empty instance with all internal bits initialized to zero.
+ ///
+ /// A default initialized .
public static MongoId Empty()
{
- return new MongoId("000000000000000000000000");
+ return default;
}
}
diff --git a/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/StringToMongoIdConverter.cs b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/StringToMongoIdConverter.cs
index b1a674750..67bd9b8e9 100644
--- a/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/StringToMongoIdConverter.cs
+++ b/Libraries/SPTarkov.Server.Core/Utils/Json/Converters/StringToMongoIdConverter.cs
@@ -4,7 +4,7 @@
namespace SPTarkov.Server.Core.Utils.Json.Converters;
-public class StringToMongoIdConverter : JsonConverter
+public sealed class StringToMongoIdConverter : JsonConverter
{
public override MongoId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@@ -13,12 +13,20 @@ public override MongoId Read(ref Utf8JsonReader reader, Type typeToConvert, Json
return new MongoId(reader.GetString());
}
- throw new JsonException();
+ throw new JsonException($"The JsonTokenType was not of type string, it was: {reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, MongoId mongoId, JsonSerializerOptions options)
{
- JsonSerializer.Serialize(writer, mongoId.ToString(), options);
+ Span buffer = stackalloc char[24];
+ if (mongoId.TryFormat(buffer, out var charsWritten))
+ {
+ writer.WriteStringValue(buffer[..charsWritten]);
+ }
+ else
+ {
+ throw new JsonException("Failed to format MongoId to stack buffer.");
+ }
}
// Deserialize MongoId as a dictionary key
@@ -30,6 +38,21 @@ public override MongoId ReadAsPropertyName(ref Utf8JsonReader reader, Type typeT
// Serialize MongoId as a dictionary key
public override void WriteAsPropertyName(Utf8JsonWriter writer, MongoId value, JsonSerializerOptions options)
{
- writer.WritePropertyName(value.ToString());
+ Span buffer = stackalloc char[24];
+ if (value.TryFormat(buffer, out var charsWritten))
+ {
+ if (charsWritten == 0)
+ {
+ writer.WritePropertyName(string.Empty);
+ }
+ else
+ {
+ writer.WritePropertyName(buffer[..charsWritten]);
+ }
+ }
+ else
+ {
+ throw new JsonException("Failed to format MongoId to stack buffer.");
+ }
}
}
diff --git a/Testing/UnitTests/Tests/MongoIDTests.cs b/Testing/UnitTests/Tests/MongoIDTests.cs
index f08e0b716..4804b8344 100644
--- a/Testing/UnitTests/Tests/MongoIDTests.cs
+++ b/Testing/UnitTests/Tests/MongoIDTests.cs
@@ -1,16 +1,40 @@
using System.Collections.Concurrent;
using System.Diagnostics;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using NUnit.Framework;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Common;
+using SPTarkov.Server.Core.Utils.Json.Converters;
namespace UnitTests.Tests;
[TestFixture]
public class MongoIDTests
{
+ private const string ValidHex = "507f1f77bcf86cd799439011";
+ private const string ZeroHexZeroHex = "000000000000000000000000";
+
+ private const string TestHex = "507f1f77bcf86cd799439011";
+ private MongoId _testId;
+ private JsonSerializerOptions? _options;
+
[OneTimeSetUp]
- public void Initialize() { }
+ public void Initialize()
+ {
+ _testId = new MongoId(TestHex);
+ _options = new JsonSerializerOptions()
+ {
+ // This is required for JSONC support
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ NewLine = "\n",
+ };
+ }
[Test]
public void GenerateTest()
@@ -28,6 +52,237 @@ public void GenerateTest()
}
}
+ [Test]
+ public void Constructor_EmptyAndNull_ReturnsDefaultInstance()
+ {
+ // Arrange & Act
+ var fromNull = new MongoId(null);
+ var fromEmpty = new MongoId(string.Empty);
+ var fromZeroes = new MongoId(ZeroHexZeroHex);
+ var defaultInstance = default(MongoId);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(fromNull.IsEmpty, Is.True);
+ Assert.That(fromEmpty.IsEmpty, Is.True);
+ Assert.That(fromZeroes.IsEmpty, Is.True);
+ Assert.That(fromNull, Is.EqualTo(defaultInstance));
+ Assert.That(fromEmpty, Is.EqualTo(defaultInstance));
+ Assert.That(fromZeroes, Is.EqualTo(defaultInstance));
+ });
+ }
+
+ [Test]
+ public void Constructor_ValidHex_ParsesCorrectly()
+ {
+ // Arrange & Act
+ var id = new MongoId(ValidHex);
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(id.IsEmpty, Is.False);
+ Assert.That(id.ToString(), Is.EqualTo(ValidHex));
+ });
+ }
+
+ [TestCase("507f1f77bcf86cd79943901")] // 23 chars
+ [TestCase("507f1f77bcf86cd7994390112")] // 25 chars
+ public void Constructor_InvalidLength_ThrowsArgumentException(string invalidLengthHex)
+ {
+ // Act & Assert
+ Assert.Throws(() => _ = new MongoId(invalidLengthHex));
+ }
+
+ [TestCase("507f1f77bcf86cd79943901g")] // 'g' is out of range
+ [TestCase("507f1f77bcf86cd79943901X")] // 'X' is out of range
+ [TestCase("507f1f77bcf86cd79943901-")] // Special character
+ public void Constructor_InvalidHexCharacters_ThrowsFormatException(string invalidCharHex)
+ {
+ // Act & Assert
+ Assert.Throws(() => _ = new MongoId(invalidCharHex));
+ }
+
+ [Test]
+ public void ParameterlessConstructor_GeneratesUniqueSequentialIds()
+ {
+ // Arrange & Act
+ var id1 = new MongoId();
+ var id2 = new MongoId();
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(id1.IsEmpty, Is.False);
+ Assert.That(id2.IsEmpty, Is.False);
+ Assert.That(id1, Is.Not.EqualTo(id2));
+ });
+ }
+
+ [Test]
+ public void Empty_ReturnsDefaultStruct()
+ {
+ // Arrange & Act
+ var empty = MongoId.Empty();
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(empty.IsEmpty, Is.True);
+ Assert.That(empty, Is.EqualTo(default(MongoId)));
+ Assert.That(empty.ToString(), Is.EqualTo(string.Empty));
+ });
+ }
+
+ [Test]
+ public void Equals_StateEquality_ReturnsTrueForMatchingData()
+ {
+ // Arrange
+ var id1 = new MongoId(ValidHex);
+ var id2 = new MongoId(ValidHex);
+ var diff = new MongoId();
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(id1.Equals(id2), Is.True);
+ Assert.That(id1 == id2, Is.True);
+ Assert.That(id1 != id2, Is.False);
+
+ Assert.That(id1.Equals(diff), Is.False);
+ Assert.That(id1 == diff, Is.False);
+ Assert.That(id1 != diff, Is.True);
+ });
+ }
+
+ [Test]
+ public void Equals_StringComparison_ValidatesCorrectly()
+ {
+ // Arrange
+ var id = new MongoId(ValidHex);
+ var mixedCaseHex = "507F1F77BCF86CD799439011";
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(id.Equals(ValidHex), Is.True);
+ Assert.That(id.Equals(mixedCaseHex), Is.True); // Hex parsing handles uppercase
+ Assert.That(id.Equals("invalidstringlen"), Is.False);
+ Assert.That(id.Equals((string?) null), Is.False);
+ });
+ }
+
+ [Test]
+ public void GetHashCode_IdenticalObjects_YieldIdenticalHash()
+ {
+ // Arrange
+ var id1 = new MongoId(ValidHex);
+ var id2 = new MongoId(ValidHex);
+
+ // Act & Assert
+ Assert.That(id1.GetHashCode(), Is.EqualTo(id2.GetHashCode()));
+ }
+
+ [Test]
+ public void ImplicitOperators_ConvertCorrectlyBetweenStringAndMongoId()
+ {
+ // Arrange & Act
+ MongoId id = ValidHex; // Implicit conversion from string
+ string hexStr = id; // Implicit conversion to string
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ Assert.That(id.IsEmpty, Is.False);
+ Assert.That(hexStr, Is.EqualTo(ValidHex));
+ });
+ }
+
+ [Test]
+ public void ToString_OutputScenarios_BehavesCorrectly()
+ {
+ // Arrange
+ var upperCaseInput = "507F1F77BCF86CD799439011";
+ var expectedLower = "507f1f77bcf86cd799439011";
+
+ var idFromUpper = new MongoId(upperCaseInput);
+ var emptyId = MongoId.Empty();
+
+ // Act
+ var outputFromParsed = idFromUpper.ToString();
+ var outputFromEmpty = emptyId.ToString();
+
+ // Assert
+ Assert.Multiple(() =>
+ {
+ // Verifies zero-allocation path transforms uppercase inputs to lowercase outputs
+ Assert.That(outputFromParsed, Is.EqualTo(expectedLower));
+
+ // Verifies empty instance strictly yields string.Empty
+ Assert.That(outputFromEmpty, Is.EqualTo(string.Empty));
+ });
+ }
+
+ [Test]
+ public void Write_ValidMongoId_WritesCorrectJsonString()
+ {
+ // Arrange
+ using var stream = new MemoryStream();
+ using var writer = new Utf8JsonWriter(stream);
+ var converter = new StringToMongoIdConverter();
+
+ // Act
+ writer.WriteStartObject();
+ writer.WritePropertyName("id");
+ converter.Write(writer, _testId, _options!);
+ writer.WriteEndObject();
+ writer.Flush();
+
+ // Assert
+ var jsonOutput = Encoding.UTF8.GetString(stream.ToArray());
+ Assert.That(jsonOutput, Is.EqualTo($"{{\"id\":\"{TestHex}\"}}"));
+ }
+
+ [Test]
+ public void WriteAsPropertyName_ValidMongoId_WritesCorrectPropertyName()
+ {
+ // Arrange
+ using var stream = new MemoryStream();
+ using var writer = new Utf8JsonWriter(stream);
+ var converter = new StringToMongoIdConverter();
+
+ // Act
+ writer.WriteStartObject();
+ converter.WriteAsPropertyName(writer, _testId, _options!);
+ writer.WriteBooleanValue(true);
+ writer.WriteEndObject();
+ writer.Flush();
+
+ // Assert
+ var jsonOutput = Encoding.UTF8.GetString(stream.ToArray());
+ Assert.That(jsonOutput, Is.EqualTo($"{{\"{TestHex}\":true}}"));
+ }
+
+ [Test]
+ public void Write_EmptyMongoId_WritesZeroHexString()
+ {
+ using var stream = new MemoryStream();
+ using var writer = new Utf8JsonWriter(stream);
+ var converter = new StringToMongoIdConverter();
+ var emptyId = MongoId.Empty();
+
+ writer.WriteStartObject();
+ writer.WritePropertyName("id");
+ converter.Write(writer, emptyId, _options!);
+ writer.WriteEndObject();
+ writer.Flush();
+
+ var jsonOutput = Encoding.UTF8.GetString(stream.ToArray());
+ Assert.That(jsonOutput, Is.EqualTo("{\"id\":\"\"}"));
+ Console.WriteLine($"Output: {jsonOutput}");
+ }
+
[TestCase("677ddb67406e9918a0264bbz", false, "677ddb67406e9918a0264bbz contains invalid char `z`, but result was true")]
[TestCase("677ddb67406e9918a0264bbcc", false, "677ddb67406e9918a0264bbcc is 25 characters, but result was true")]
[TestCase("677ddb67406e9918a0264bbc", true, "IsValidMongoId() `677ddb67406e9918a0264bbc` is a valid mongoId, but result was false")]
@@ -40,30 +295,39 @@ public void IsValidMongoIdTest(string mongoId, bool passes, string failMessage)
[Test]
public void MultiThreadedMongoIDGenerationTest()
{
- var concurrentBag = new ConcurrentBag();
- var random = new Random();
- var stopwatch = new Stopwatch();
- stopwatch.Start();
+ const int targetCount = 1_000_000;
+
+ // Store raw structs to eliminate heap allocations during generation
+ var concurrentBag = new ConcurrentBag();
+
+ var stopwatch = Stopwatch.StartNew();
Parallel.For(
0,
- 1000,
- i =>
+ targetCount,
+ _ =>
{
- Thread.Sleep(random.Next(0, 10));
var mongoId = new MongoId();
concurrentBag.Add(mongoId);
}
);
stopwatch.Stop();
- Console.WriteLine($"Elapsed time: {stopwatch.ElapsedMilliseconds} ms");
- var uniqueCount = concurrentBag.Distinct().Count();
+ Console.WriteLine($"Generated {targetCount:N0} IDs in: {stopwatch.ElapsedMilliseconds} ms");
+
+ // Verify uniqueness with minimum allocations
var totalCount = concurrentBag.Count;
- Assert.AreEqual(
- totalCount,
- uniqueCount,
- $"Expected all generated MongoId's to be unique, but found: {totalCount - uniqueCount} duplicates."
- );
+
+ // Explicitly size the HashSet capacity to prevent resizing overhead
+ var uniqueSet = new HashSet(totalCount);
+ foreach (var id in concurrentBag)
+ {
+ uniqueSet.Add(id);
+ }
+
+ var uniqueCount = uniqueSet.Count;
+
+ Assert.That(uniqueCount, Is.EqualTo(totalCount),
+ $"Expected all generated MongoId's to be unique, but found: {totalCount - uniqueCount} duplicates.");
}
}