#region Copyright 2012 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion
using System;
using System.Collections.Generic;
namespace CSharpTest.Net.Data
{
///
/// A Guid struct for creating sequentially advancing unique identifiers
///
///
/// Bits - usage
/// 00-03 - Reserved, always 000 if generated with DbGuid.NewGuid()
/// 03-52 - Represent bits 3-52 of the DateTime.UtcNow.Ticks value
/// 52-80 - 25 bits of incrementing numeric value
/// 64-66 - Guid type constant of 111, or 001 if IsSqlGuid is true
/// 76-128- Randomly generated bytes
///
public struct DbGuid : IFormattable, IComparable, IEquatable, IComparable
{
/// Returns an empty DbGuid structure.
public static readonly DbGuid Empty;
const long SqlTypeMask = unchecked((0x20L << 56));
const long BinTypeMask = unchecked((0xE0L << 56));
const int BufferSize = 1024 * 6;
// _nextSequence is an AppDomain specific unique value who's 25 least significant bits are merged
// to ensure each guid in this domain is guarunteed to be unique given the assumption that not
// more than 2^25th (33,554,432) DBGuids are generated per millisecond.
static int _nextSequence;
#region RandomByte()
static readonly System.Security.Cryptography.RandomNumberGenerator _rnGenerator;
static readonly byte[] _randomBytes;
static int _nextRandomByte;
static DbGuid()
{
Empty = new DbGuid();
_rnGenerator = new System.Security.Cryptography.RNGCryptoServiceProvider();
_randomBytes = new byte[BufferSize];
_nextRandomByte = BufferSize;
_nextSequence = Guid.NewGuid().GetHashCode() & 0x00FFFFFF;
}
static byte RandomByte()
{
while(true)
{
int offset = System.Threading.Interlocked.Increment(ref _nextRandomByte);
if (offset < BufferSize)
return _randomBytes[offset];
lock (_randomBytes)
{
if (_nextRandomByte >= BufferSize)
{
_rnGenerator.GetBytes(_randomBytes);
System.Threading.Interlocked.Exchange(ref _nextRandomByte, 0);
return _randomBytes[0];
}
}
}
}
#endregion
///
/// Creates a (mostly) sequential DbGuid using 7 bytes of DateTime in UTC ticks,
/// 3 bytes of a sequentially incremented random value, and 6 bytes of random data.
///
///
/// I say this is *mostly* sequential, because sometime after 2^24 (16m) guids are generated, the 25-bit
/// incrementing number will 'roll over'. When this occurs and multiple guids are generated within the
/// 4ms time window during this event, then it is possible that some may be out of sequence until the 4ms
/// window passes. This anomaly in the sequencing should have no practical impact on performance.
///
/// The collision chance within AppDomain are essentially non-existent due to the use of the incremented
/// value. Across domains or processes the collision chance requires that Guids are generated within a
/// 4ms window, happen to have the same 25-bit incremented value, and generate the same random 6-byte
/// sequence while using cryptographic-strength PRNG.
///
/// It's important to know that sequential guids like this drastically reduce the entropy size of the
/// random value. Therefore DbGuid is more likely to produce duplicates than a regular fully-randomized
/// guid. If you want to do the math on the collision probability of the 73-bit random value see the
/// wikipedia article on the birthday paradox: http://en.wikipedia.org/wiki/Birthday_problem
/// Keep in mind you must factor in the 4ms time interval with which DbGuids are timestamped. I'm not
/// a math genius or anything, but generating a duplicate DbGuid is going to be more probable than
/// generating a duplicate random guid, and less probable than being eaten by a shark.
///
public static DbGuid NewGuid()
{
return new DbGuid(true);
}
private readonly long _h64;
private readonly long _l64;
private DbGuid(bool initialize)
{
unchecked
{
long sequenceNum = System.Threading.Interlocked.Increment(ref _nextSequence);
_h64 = (DateTime.UtcNow.Ticks & ~(BinTypeMask | 0x0FFF)) | ((sequenceNum >> 13) & 0x0FFFL);
_l64 = (sequenceNum << 48) | BinTypeMask
| (long)RandomByte() << 40
| (long)RandomByte() << 32
| (long)RandomByte() << 24
| (long)RandomByte() << 16
| (long)RandomByte() << 8
| RandomByte();
}
}
///
/// Constructs a DbGuid from two 64-bit values
///
public DbGuid(long a, long b)
{
_h64 = a;
_l64 = b;
}
///
/// Constructs a DbGuid from the typical Guid values
///
public DbGuid(int a, short b, short c, byte d, byte e, byte f, byte g, byte h, byte i, byte j, byte k)
{
unchecked
{
_h64 = ((long)a) << 32 | ((long)(uint)b) << 16 | ((long)(uint)c);
_l64 = ((long) d) << 56
| ((long) e) << 48
| ((long) f) << 40
| ((long) g) << 32
| ((long) h) << 24
| ((long) i) << 16
| ((long) j) << 8
| ((long) k) << 0;
}
}
/// Creates a DbGuid from a System.Guid instance.
///
/// Unexpected results may occur if using System.Guid.NewGuid() to initialize this instance.
/// Instead use DbGuid.NewGuid() to construct a new identifier. This is provided primarily
/// for conversion and use with other systems (i.e. SqlServer, etc).
///
public DbGuid(Guid guid) : this(true, guid.ToByteArray(), 0) { }
///
/// Constructs a DbGuid from an array of 16 (or more) bytes.
///
public DbGuid(byte[] bytes) : this(false, bytes, 0) { }
///
/// Constructs a DbGuid from an array of 16 (or more) bytes beginning at the offset supplied.
///
public DbGuid(byte[] bytes, int offset) : this(false, bytes, offset)
{ }
///
/// Constructs a DbGuid from an array of 16 (or more) bytes beginning at the offset supplied.
/// System.Guid.ToByteArray is little-endian, DbGuid.ToByteArray is in big-endian format.
///
public DbGuid(bool littleEndian, byte[] bytes, int offset)
{
if (bytes == null)
throw new ArgumentNullException("bytes");
if ((bytes.Length - offset) < 16)
throw new ArgumentOutOfRangeException("bytes");
if (littleEndian)
{
byte tmp = bytes[offset];
bytes[offset] = bytes[offset + 3];
bytes[offset + 3] = tmp;
tmp = bytes[offset + 1];
bytes[offset + 1] = bytes[offset + 2];
bytes[offset + 2] = tmp;
tmp = bytes[offset + 4];
bytes[offset + 4] = bytes[offset + 5];
bytes[offset + 5] = tmp;
tmp = bytes[offset + 6];
bytes[offset + 6] = bytes[offset + 7];
bytes[offset + 7] = tmp;
}
unchecked
{
_h64 = (long)bytes[offset++] << 56
| (long)bytes[offset++] << 48
| (long)bytes[offset++] << 40
| (long)bytes[offset++] << 32
| (long)bytes[offset++] << 24
| (long)bytes[offset++] << 16
| (long)bytes[offset++] << 8
| (long)bytes[offset++] << 0;
_l64 = (long)bytes[offset++] << 56
| (long)bytes[offset++] << 48
| (long)bytes[offset++] << 40
| (long)bytes[offset++] << 32
| (long)bytes[offset++] << 24
| (long)bytes[offset++] << 16
| (long)bytes[offset++] << 8
| (long)bytes[offset] << 0;
}
}
/// Returns the first/high 64 bits as a long
public long High64 { get { return _h64; } }
/// Returns the second/low 64 bits as a long
public long Low64 { get { return _l64; } }
///
/// Returns the value as a System.Guid type.
///
public Guid ToGuid()
{
return new Guid(
(int)(_h64 >> 32),
(short)(_h64 >> 16),
(short)(_h64 >> 0),
(byte)(_l64 >> 56),
(byte)(_l64 >> 48),
(byte)(_l64 >> 40),
(byte)(_l64 >> 32),
(byte)(_l64 >> 24),
(byte)(_l64 >> 16),
(byte)(_l64 >> 8),
(byte)(_l64 >> 0)
);
}
///
/// Returns the UTC DateTime the guid was created. Will not work with ToSqlGuid().
///
public DateTime ToDateTimeUtc()
{
return IsSqlGuid
? new DateTime(_l64 & ~BinTypeMask, DateTimeKind.Utc)
: new DateTime(_h64, DateTimeKind.Utc);
}
///
/// Used to swap the first 8 bytes with the last 8 bytes for SQL-Server optimization.
/// The inverse operation can be performed by calling ToSequenceGuid() on the result.
/// If IsSqlGuid is already True this call has no effect and returns the same value.
///
public DbGuid ToSqlGuid()
{
return IsSqlGuid ? this : new DbGuid(_l64 & ~BinTypeMask, (_h64 & ~BinTypeMask) | SqlTypeMask);
}
///
/// Used to reverse the effects of ToSqlGuid() and obtain a guid who's ToByteArray result
/// is sequential.
/// If IsSqlGuid is already False this call has no effect and returns the same value.
///
public DbGuid ToSequenceGuid()
{
return !IsSqlGuid ? this : new DbGuid(_l64 & ~BinTypeMask, (_h64 & ~BinTypeMask) | BinTypeMask);
}
///
/// Returns true if the DbGuid is the result of a call to ToSqlGuid.
///
public bool IsSqlGuid { get { return (_l64 & BinTypeMask) == SqlTypeMask; } }
///
/// Big-endian byte array, not compatible with System.Guid.ToByteArray.
///
public byte[] ToByteArray()
{
byte[] bytes = new byte[16];
ToByteArray(bytes, 0);
return bytes;
}
///
/// Copies the big-endian byte array to the offset supplied, not compatible
/// with System.Guid.ToByteArray.
///
public void ToByteArray(byte[] bytes, int offset)
{
if (bytes == null)
throw new ArgumentNullException();
if (bytes.Length - offset < 16)
throw new ArgumentOutOfRangeException("offset");
unchecked
{
bytes[offset++] = (byte)(_h64 >> 56);
bytes[offset++] = (byte)(_h64 >> 48);
bytes[offset++] = (byte)(_h64 >> 40);
bytes[offset++] = (byte)(_h64 >> 32);
bytes[offset++] = (byte)(_h64 >> 24);
bytes[offset++] = (byte)(_h64 >> 16);
bytes[offset++] = (byte)(_h64 >> 8);
bytes[offset++] = (byte)(_h64 >> 0);
bytes[offset++] = (byte)(_l64 >> 56);
bytes[offset++] = (byte)(_l64 >> 48);
bytes[offset++] = (byte)(_l64 >> 40);
bytes[offset++] = (byte)(_l64 >> 32);
bytes[offset++] = (byte)(_l64 >> 24);
bytes[offset++] = (byte)(_l64 >> 16);
bytes[offset++] = (byte)(_l64 >> 8);
bytes[offset] = (byte)(_l64 >> 0);
}
}
#region Object overloads
///
/// Returns the fully qualified type name of this instance.
///
public override string ToString()
{
return ToSequenceGuid().ToGuid().ToString();
}
///
/// Returns the fully qualified type name of this instance.
///
public string ToString(string format)
{
return ToSequenceGuid().ToGuid().ToString(format);
}
///
/// Indicates whether this instance and a specified object are equal.
///
public override bool Equals(object obj)
{
if (!(obj is DbGuid)) return false;
return Equals((DbGuid)obj);
}
///
/// Returns the hash code for this instance.
///
public override int GetHashCode()
{
long result = (_h64 ^ _l64) | BinTypeMask;
return unchecked((int)(result ^ (result >> 32)));
}
#endregion
#region IEquatable Members
///
/// Indicates whether the current object is equal to another object of the same type.
///
public bool Equals(DbGuid other)
{
return CompareTo(other) == 0;
}
#endregion
#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (obj is DbGuid)
return CompareTo((DbGuid)obj);
return 1;
}
#endregion
#region IComparable Members
///
/// Compares the current object with another object of the same type.
///
public int CompareTo(DbGuid other)
{
long ha, hb, la, lb;
if (IsSqlGuid)
{
ha = _l64 & ~BinTypeMask;
la = _h64 & ~BinTypeMask;
}
else
{
ha = _h64 & ~BinTypeMask;
la = _l64 & ~BinTypeMask;
}
if (other.IsSqlGuid)
{
hb = other._l64 & ~BinTypeMask;
lb = other._h64 & ~BinTypeMask;
}
else
{
hb = other._h64 & ~BinTypeMask;
lb = other._l64 & ~BinTypeMask;
}
if (ha == hb)
{
if (la == lb)
return 0;
if (la < lb)
return -1;
}
else if (ha < hb)
return -1;
return 1;
}
#endregion
#region IFormattable Members
///
/// Formats the value of the current instance using the specified format.
///
public string ToString(string format, IFormatProvider formatProvider)
{
return ToSequenceGuid().ToGuid().ToString(format, formatProvider);
}
#endregion
#region Equality Operators
/// Compares the two objects for non-reference equality
public static bool operator ==(DbGuid x, DbGuid y)
{
return x.Equals(y);
}
/// Compares the two objects for non-reference equality
public static bool operator !=(DbGuid x, DbGuid y)
{
return !x.Equals(y);
}
#endregion
#region Comparison Operators
/// Compares the two objects
public static bool operator >(DbGuid x, DbGuid y)
{
return x.CompareTo(y) > 0;
}
/// Compares the two objects
public static bool operator >=(DbGuid x, DbGuid y)
{
return x.CompareTo(y) >= 0;
}
/// Compares the two objects
public static bool operator <(DbGuid x, DbGuid y)
{
return x.CompareTo(y) < 0;
}
/// Compares the two objects
public static bool operator <=(DbGuid x, DbGuid y)
{
return x.CompareTo(y) <= 0;
}
#endregion
#region Implicit Guid Cast
/// Converts the DbGuid to a System.Guid
public static implicit operator Guid(DbGuid value) { return value.ToGuid(); }
/// Converts a System.Guid to a DbGuid
public static explicit operator DbGuid(Guid value) { return new DbGuid(value); }
#endregion
///
/// Implementation of a comparer and equality comparer for DbGuid
///
public sealed class DbGuidComparer : IComparer, IEqualityComparer
{
///
/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
///
public int Compare(DbGuid x, DbGuid y)
{
return x.CompareTo(y);
}
///
/// Determines whether the specified objects are equal.
///
public bool Equals(DbGuid x, DbGuid y)
{
return x.Equals(y);
}
///
/// Returns a hash code for the specified object.
///
public int GetHashCode(DbGuid obj)
{
return obj.GetHashCode();
}
}
///
/// Returns a comparer for comparing equality or sorting DbGuid instances
///
public static readonly DbGuidComparer Comparer = new DbGuidComparer();
class DbGuidSerializer : CSharpTest.Net.Serialization.ISerializer
{
void CSharpTest.Net.Serialization.ISerializer.WriteTo(DbGuid value, System.IO.Stream stream)
{
unchecked
{
CSharpTest.Net.Serialization.PrimitiveSerializer.UInt64.WriteTo((ulong)value._h64, stream);
CSharpTest.Net.Serialization.PrimitiveSerializer.UInt64.WriteTo((ulong)value._l64, stream);
}
}
DbGuid CSharpTest.Net.Serialization.ISerializer.ReadFrom(System.IO.Stream stream)
{
unchecked
{
return new DbGuid(
(long)CSharpTest.Net.Serialization.PrimitiveSerializer.UInt64.ReadFrom(stream),
(long)CSharpTest.Net.Serialization.PrimitiveSerializer.UInt64.ReadFrom(stream)
);
}
}
}
///
/// Returns a serializer that can read/write a DbGuid to and from a stream.
///
public static readonly CSharpTest.Net.Serialization.ISerializer Serializer = new DbGuidSerializer();
}
}