Created
April 13, 2019 15:07
-
-
Save MrSmoke/367599105d95fe9ac6342e424f143be6 to your computer and use it in GitHub Desktop.
Id
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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