dotnet 9 and sequential id
This commit is contained in:
@@ -4,23 +4,23 @@ public static class Base32
|
||||
{
|
||||
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
public const char Padding = '=';
|
||||
public const int MaxBytesStack = 250;
|
||||
|
||||
[Pure]
|
||||
public static string Encode(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (input.IsEmpty) return string.Empty;
|
||||
|
||||
int outLength = 8 * ((input.Length + 4) / 5);
|
||||
Span<char> output = stackalloc char[outLength];
|
||||
Span<char> output = input.Length <= MaxBytesStack
|
||||
? stackalloc char[outLength]
|
||||
: new char[outLength];
|
||||
|
||||
_ = Encode(input, output);
|
||||
|
||||
return new string(output);
|
||||
}
|
||||
|
||||
|
||||
[Pure]
|
||||
public static int Encode(ReadOnlySpan<byte> input, Span<char> output)
|
||||
{
|
||||
@@ -47,10 +47,14 @@ public static class Base32
|
||||
[Pure]
|
||||
public static byte[] Decode(ReadOnlySpan<char> input)
|
||||
{
|
||||
input = input.TrimEnd(Padding);
|
||||
if (input.IsEmpty) return [];
|
||||
|
||||
Span<byte> output = stackalloc byte[5 * input.Length / 8];
|
||||
|
||||
|
||||
var outputLength = 5 * ((input.Length + 7) / 8);
|
||||
Span<byte> output = outputLength <= MaxBytesStack
|
||||
? stackalloc byte[outputLength]
|
||||
: new byte[outputLength];
|
||||
|
||||
var size = Decode(input, output);
|
||||
|
||||
return output[..size].ToArray();
|
||||
@@ -60,7 +64,14 @@ public static class Base32
|
||||
public static int Decode(ReadOnlySpan<char> input, Span<byte> output)
|
||||
{
|
||||
input = input.TrimEnd(Padding);
|
||||
Span<char> inputspan = stackalloc char[input.Length];
|
||||
|
||||
var outputLength = 5 * ((input.Length + 7) / 8);
|
||||
output = output[..outputLength];
|
||||
output.Clear();
|
||||
|
||||
Span<char> inputspan = outputLength <= MaxBytesStack
|
||||
? stackalloc char[input.Length]
|
||||
: new char[input.Length];
|
||||
input.ToUpperInvariant(inputspan);
|
||||
|
||||
int bitIndex = 0;
|
||||
@@ -79,7 +90,7 @@ public static class Base32
|
||||
{
|
||||
throw new FormatException("Provided string contains invalid characters.");
|
||||
}
|
||||
|
||||
|
||||
bitPos = 5 - bitIndex;
|
||||
outBitPos = 8 - outputBits;
|
||||
bits = bitPos < outBitPos ? bitPos : outBitPos;
|
||||
@@ -106,7 +117,6 @@ public static class Base32
|
||||
return outputIndex + (outputBits + 7) / 8;
|
||||
}
|
||||
|
||||
|
||||
// returns the number of bytes that were output
|
||||
[Pure]
|
||||
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Just.Core</AssemblyName>
|
||||
@@ -10,7 +10,7 @@
|
||||
<Description>Small .Net library with useful helper classes, functions and extensions.</Description>
|
||||
<PackageTags>extensions;helpers;helper-functions</PackageTags>
|
||||
<Authors>JustFixMe</Authors>
|
||||
<Copyright>Copyright (c) 2023,2024 JustFixMe</Copyright>
|
||||
<Copyright>Copyright (c) 2023-2025 JustFixMe</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl>
|
||||
|
||||
@@ -1,44 +1,114 @@
|
||||
namespace Just.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="Stream"/> to fully populate buffers.
|
||||
/// </summary>
|
||||
public static class SystemIOStreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads data from the stream until the specified section of the buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <param name="offset">The starting offset in the buffer</param>
|
||||
/// <param name="length">The number of bytes to read</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static void Populate(this Stream stream, byte[] buffer, int offset, int length)
|
||||
=> stream.Populate(buffer.AsSpan(offset, length));
|
||||
/// <summary>
|
||||
/// Reads data from the stream until the entire buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static void Populate(this Stream stream, byte[] buffer)
|
||||
=> stream.Populate(buffer.AsSpan());
|
||||
/// <summary>
|
||||
/// Reads data from the stream until the specified span is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The span to populate</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static void Populate(this Stream stream, Span<byte> buffer)
|
||||
{
|
||||
while (buffer.Length > 0)
|
||||
{
|
||||
var readed = stream.Read(buffer);
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
if (readed == 0)
|
||||
while (!buffer.IsEmpty)
|
||||
{
|
||||
var bytesRead = stream.Read(buffer);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
buffer = buffer[readed..];
|
||||
buffer = buffer[bytesRead..];
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
|
||||
=> await stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
|
||||
public static async ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
|
||||
=> await stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
|
||||
/// <summary>
|
||||
/// Asynchronously reads data from the stream until the entire buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
|
||||
=> stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
|
||||
/// <summary>
|
||||
/// Asynchronously reads data from the stream until the specified section of the buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <param name="offset">The starting offset in the buffer</param>
|
||||
/// <param name="length">The number of bytes to read</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
|
||||
=> stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
|
||||
/// <summary>
|
||||
/// Asynchronously reads data from the stream until the specified memory region is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The memory region to populate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static async ValueTask PopulateAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
while (buffer.Length > 0)
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
while (!buffer.IsEmpty)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var readed = await stream.ReadAsync(buffer, cancellationToken);
|
||||
var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (readed == 0)
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
buffer = buffer[readed..];
|
||||
buffer = buffer[bytesRead..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,13 @@ using System.Security.Cryptography;
|
||||
|
||||
namespace Just.Core;
|
||||
|
||||
public enum GuidV8Entropy { Strong, Weak }
|
||||
|
||||
public static class GuidV8
|
||||
{
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Guid NewGuid(GuidV8Entropy entropy = GuidV8Entropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
||||
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
||||
|
||||
[Pure]
|
||||
public static Guid NewGuid(DateTime dateTime, GuidV8Entropy entropy = GuidV8Entropy.Strong)
|
||||
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
|
||||
{
|
||||
var epoch = dateTime.Subtract(DateTime.UnixEpoch);
|
||||
var timestamp = epoch.Ticks / (TimeSpan.TicksPerMillisecond / 10);
|
||||
@@ -24,7 +22,7 @@ public static class GuidV8
|
||||
ts[0..2].CopyTo(bytes[4..6]);
|
||||
ts[2..6].CopyTo(bytes[..4]);
|
||||
|
||||
if (entropy == GuidV8Entropy.Strong)
|
||||
if (entropy == RngEntropy.Strong)
|
||||
{
|
||||
RandomNumberGenerator.Fill(bytes[6..]);
|
||||
}
|
||||
|
||||
16
Core/RngEntropy.cs
Normal file
16
Core/RngEntropy.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Just.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the quality of random entropy used in ID generation
|
||||
/// </summary>
|
||||
public enum RngEntropy
|
||||
{
|
||||
/// <summary>
|
||||
/// Cryptographically secure random numbers (slower but collision-resistant)
|
||||
/// </summary>
|
||||
Strong,
|
||||
/// <summary>
|
||||
/// Standard pseudo-random numbers (faster but less collision-resistant)
|
||||
/// </summary>
|
||||
Weak
|
||||
}
|
||||
121
Core/SeqId.cs
Normal file
121
Core/SeqId.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Just.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generates time-based sequential IDs with entropy
|
||||
/// <para>ID Structure (64 bits):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>[1 bit] Always 0 (positive signed longs)</description></item>
|
||||
/// <item><description>[41 bits] Milliseconds since epoch (covers ~69 years)</description></item>
|
||||
/// <item><description>[8 bits] Sequence counter (0-255 per millisecond)</description></item>
|
||||
/// <item><description>[14 bits] Random entropy (0-16383)</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Important behaviors:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Guarantees monotonic ordering within same millisecond</description></item>
|
||||
/// <item><description>Throws <see cref="InvalidOperationException"/> on backward time jumps</description></item>
|
||||
/// <item><description>Sequence resets when timestamp advances</description></item>
|
||||
/// <item><description>Thread-safe through locking</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class SeqId(DateTime epoch)
|
||||
{
|
||||
private const int TimestampBits = 41;
|
||||
private const int TimestampShift = 63 - TimestampBits;
|
||||
private const long TimestampMask = 0x000001FF_FFFFFFFF;
|
||||
|
||||
private const int SeqBits = 8;
|
||||
private const int SeqShift = TimestampShift - SeqBits;
|
||||
private const long SeqMask = 0x00000000_000000FF;
|
||||
|
||||
private const int RandExclusiveUpper = 1 << SeqShift;
|
||||
|
||||
/// <summary>
|
||||
/// Default epoch (2025-01-01 UTC) used for <see cref="Default"/> instance
|
||||
/// </summary>
|
||||
public static DateTime DefaultEpoch { get; } = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
/// <summary>
|
||||
/// Default instance using <see cref="DefaultEpoch"/>
|
||||
/// </summary>
|
||||
public static SeqId Default { get; } = new(DefaultEpoch);
|
||||
|
||||
/// <summary>
|
||||
/// Generates ID using default instance and current UTC time
|
||||
/// </summary>
|
||||
/// <param name="entropy">Entropy quality (default: Strong)</param>
|
||||
/// <returns>64-bit sequential ID with random component</returns>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if more than 255 IDs generated in 1ms
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(DateTime.UtcNow, entropy);
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _lock = new();
|
||||
#else
|
||||
private readonly object _lock = new();
|
||||
#endif
|
||||
|
||||
private readonly DateTime _epoch = epoch;
|
||||
private int _seqId = 0;
|
||||
private long _lastTimestamp = -1L;
|
||||
|
||||
/// <summary>
|
||||
/// Generates next ID using current UTC time
|
||||
/// </summary>
|
||||
/// <param name="entropy">Entropy quality (default: Strong)</param>
|
||||
/// <returns>64-bit sequential ID with random component</returns>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if more than 255 IDs generated in 1ms
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(DateTime.UtcNow, entropy);
|
||||
|
||||
/// <summary>
|
||||
/// Generates next ID with explicit timestamp
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Timestamp basis for ID generation</param>
|
||||
/// <param name="entropy">Entropy quality (default: Strong)</param>
|
||||
/// <returns>64-bit sequential ID with random component</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if <paramref name="dateTime"/> is earlier than last used timestamp
|
||||
/// </exception>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if more than 255 IDs generated in 1ms
|
||||
/// </exception>
|
||||
public long Next(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
|
||||
{
|
||||
var epoch = dateTime.Subtract(_epoch);
|
||||
var timestamp = ((epoch.Ticks / TimeSpan.TicksPerMillisecond) & TimestampMask) << TimestampShift;
|
||||
|
||||
long currentSeq;
|
||||
lock (_lock)
|
||||
{
|
||||
if (timestamp > _lastTimestamp)
|
||||
{
|
||||
_lastTimestamp = timestamp;
|
||||
_seqId = 0;
|
||||
}
|
||||
else if (timestamp < _lastTimestamp)
|
||||
{
|
||||
throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future.");
|
||||
}
|
||||
|
||||
if (_seqId == SeqMask)
|
||||
{
|
||||
throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted.");
|
||||
}
|
||||
|
||||
currentSeq = ((_seqId++) & SeqMask) << SeqShift;
|
||||
}
|
||||
|
||||
long currentRand = entropy == RngEntropy.Strong
|
||||
? RandomNumberGenerator.GetInt32(RandExclusiveUpper)
|
||||
: Random.Shared.Next(RandExclusiveUpper);
|
||||
|
||||
return timestamp | currentSeq | currentRand;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user