Skip to content

Instantly share code, notes, and snippets.

@MrSmoke
Created April 13, 2019 15:07
Show Gist options
  • Save MrSmoke/367599105d95fe9ac6342e424f143be6 to your computer and use it in GitHub Desktop.
Save MrSmoke/367599105d95fe9ac6342e424f143be6 to your computer and use it in GitHub Desktop.
Id
public struct Id
{
public static readonly DateTime Epoch = new DateTime(2000, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
private static int _staticIncrement = new Random().Next();
private static readonly int StaticMachine = (GetMachineHash() + GetAppDomainId()) & 0x00ffffff;
private static readonly short StaticPid = GetPid();
// private fields
private readonly int _a;
private readonly int _b;
private readonly int _c;
// constructors
/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="bytes">The bytes.</param>
public Id(IReadOnlyList<byte> bytes)
{
if (bytes == null)
throw new ArgumentNullException(nameof(bytes));
if (bytes.Count != 12)
throw new ArgumentException("Byte array must be 12 bytes long", nameof(bytes));
FromByteArray(bytes, 0, out _a, out _b, out _c);
}
public Id(int timestamp, int machine, short pid, int increment)
{
if ((machine & 0xff000000) != 0)
throw new ArgumentOutOfRangeException(nameof(machine),
"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
if ((increment & 0xff000000) != 0)
throw new ArgumentOutOfRangeException(nameof(increment),
"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
_a = timestamp;
_b = (machine << 8) | ((pid >> 8) & 0xff);
_c = (pid << 24) | increment;
}
/// <summary>
/// Gets the creation time (derived from the timestamp).
/// </summary>
public DateTime CreationTime => Epoch.AddSeconds(Timestamp);
// public properties
/// <summary>
/// Gets the timestamp.
/// </summary>
public int Timestamp => _a;
/// <summary>
/// Gets the machine.
/// </summary>
public int Machine => (_b >> 8) & 0xffffff;
/// <summary>
/// Gets the PID.
/// </summary>
public short Pid => (short)(((_b << 8) & 0xff00) | ((_c >> 24) & 0x00ff));
/// <summary>
/// Gets the increment.
/// </summary>
public int Increment => _c & 0xffffff;
/// <summary>
/// Converts the ObjectId to a byte array.
/// </summary>
/// <returns>A byte array.</returns>
public byte[] ToByteArray()
{
var bytes = new byte[12];
ToByteArray(bytes, 0);
return bytes;
}
/// <summary>
/// Converts the ObjectId to a byte array.
/// </summary>
/// <param name="destination">The destination.</param>
/// <param name="offset">The offset.</param>
public void ToByteArray(byte[] destination, int offset)
{
if (destination == null)
{
throw new ArgumentNullException(nameof(destination));
}
if (offset + 12 > destination.Length)
{
throw new ArgumentException("Not enough room in destination buffer.", nameof(offset));
}
destination[offset + 0] = (byte)(_a >> 24);
destination[offset + 1] = (byte)(_a >> 16);
destination[offset + 2] = (byte)(_a >> 8);
destination[offset + 3] = (byte)(_a);
destination[offset + 4] = (byte)(_b >> 24);
destination[offset + 5] = (byte)(_b >> 16);
destination[offset + 6] = (byte)(_b >> 8);
destination[offset + 7] = (byte)(_b);
destination[offset + 8] = (byte)(_c >> 24);
destination[offset + 9] = (byte)(_c >> 16);
destination[offset + 10] = (byte)(_c >> 8);
destination[offset + 11] = (byte)(_c);
}
public static bool TryParse(string s, out Id id)
{
if (s != null && s.Length == 24)
{
try
{
var bytes = new byte[12];
for (var i = 0; i < 12; i++)
bytes[i] = Convert.ToByte(s.Substring(2 * i, 2), 16);
id = new Id(bytes);
return true;
}
catch
{
// ignore
}
}
id = default(Id);
return false;
}
/// <summary>
/// Gets the hash code.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
var hash = 17;
hash = 37 * hash + _a.GetHashCode();
hash = 37 * hash + _b.GetHashCode();
hash = 37 * hash + _c.GetHashCode();
return hash;
}
public override string ToString()
{
return BitConverter.ToString(ToByteArray())
.Replace("-", string.Empty)
.ToLower();
}
public static Id NewId()
{
return NewId(GetTimestampFromDateTime(DateTime.UtcNow));
}
private static Id NewId(int timestamp)
{
var increment = Interlocked.Increment(ref _staticIncrement) & 0x00ffffff; // only use low order 3 bytes
return new Id(timestamp, StaticMachine, StaticPid, increment);
}
private static void FromByteArray(IReadOnlyList<byte> bytes, int offset, out int a, out int b, out int c)
{
a = (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3];
b = (bytes[offset + 4] << 24) | (bytes[offset + 5] << 16) | (bytes[offset + 6] << 8) | bytes[offset + 7];
c = (bytes[offset + 8] << 24) | (bytes[offset + 9] << 16) | (bytes[offset + 10] << 8) | bytes[offset + 11];
}
private static short GetPid()
{
try
{
return (short)GetCurrentProcessId(); // use low order two bytes only
}
catch (SecurityException)
{
return 0;
}
}
/// <summary>
/// Gets the current process id. This method exists because of how CAS operates on the call stack, checking
/// for permissions before executing the method. Hence, if we inlined this call, the calling method would not execute
/// before throwing an exception requiring the try/catch at an even higher level that we don't necessarily control.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static int GetCurrentProcessId()
{
return Process.GetCurrentProcess().Id;
}
private static int GetMachineHash()
{
// use instead of Dns.HostName so it will work offline
var machineName = GetMachineName();
return 0x00ffffff & machineName.GetHashCode(); // use first 3 bytes of hash
}
private static string GetMachineName()
{
return Environment.MachineName;
}
// private static methods
private static int GetAppDomainId()
{
return AppDomain.CurrentDomain.Id;
}
private static int GetTimestampFromDateTime(DateTime timestamp)
{
var secondsSinceEpoch = (long) Math.Floor((ToUniversalTime(timestamp) - Epoch).TotalSeconds);
if (secondsSinceEpoch < int.MinValue || secondsSinceEpoch > int.MaxValue)
throw new ArgumentOutOfRangeException(nameof(timestamp));
return (int) secondsSinceEpoch;
}
private static DateTime ToUniversalTime(DateTime dateTime)
{
if (dateTime == DateTime.MinValue)
return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
if (dateTime == DateTime.MaxValue)
return DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc);
return dateTime.ToUniversalTime();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment