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
This commit is contained in:
Lacyway
2025-07-01 11:17:39 +02:00
committed by GitHub
parent 6ac747d18d
commit 8e3894e9ad
3 changed files with 123 additions and 22 deletions
@@ -0,0 +1,71 @@
using SPTarkov.Server.Core.Models.Common;
namespace SPTarkov.Server.Core.Extensions
{
public static class MongoIDExtensions
{
/// <summary>
/// Determines whether the specified <see cref="MongoId"/> is a valid 24-character hexadecimal string,
/// which is the standard format for MongoDB ObjectIds.
/// </summary>
/// <param name="mongoId">The <see cref="MongoId"/> to validate.</param>
/// <returns><see langword="true"/> if the <paramref name="mongoId"/> is a valid MongoDB ObjectId; otherwise, <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Determines whether the specified string is a valid 24-character hexadecimal representation
/// of a MongoDB ObjectId.
/// </summary>
/// <param name="mongoId">The string to validate as a MongoDB ObjectId.</param>
/// <returns><see langword="true"/> if the <paramref name="mongoId"/> is a valid MongoDB ObjectId; otherwise, <see langword="false"/>.</returns>
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;
}
}
}
@@ -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<MongoId>
/// <returns>24 character objectId</returns>
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<byte> 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<MongoId>
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<MongoId>
{
return new MongoId("000000000000000000000000");
}
[GeneratedRegex("^[a-fA-F0-9]{24}$")]
private static partial Regex MongoIdRegex();
}
+35
View File
@@ -0,0 +1,35 @@
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Common;
namespace UnitTests.Tests.Utils
{
/// <summary>
/// Unit tests for the <see cref="MongoId"/> struct.
/// </summary>
[TestClass]
public class MongoIdTests
{
/// <summary>
/// Test that generates 1000 <see cref="MongoId"/> and ensures they are all valid. <br/>
/// Validity is checked by ensuring the ID is non-empty, exactly 24 characters, and matches the expected format.
/// </summary>
[TestMethod]
public void Generate_ShouldProduceValidMongoIds()
{
var invalidIds = new List<string>();
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)}");
}
}
}