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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user