dotnet 9 and sequential id
All checks were successful
.NET Test / test (push) Successful in 49s
.NET Publish / publish (push) Successful in 41s

This commit is contained in:
2025-08-01 22:21:42 +04:00
parent 2b68ba982d
commit 3665abaab8
14 changed files with 428 additions and 68 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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..];
}
}
}

View File

@@ -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
View 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
View 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;
}
}