SequentialGuid version 2, with IComparable and creation date and time

by Alex Siepman 20. June 2015 21:00

Why a new version of SequentialGuid?

In an earlier post I compared the performance of Int (with autonumber) and a (Sequential) guid in SQL server. I posted an implementation of a C# SequentialGuid that created Guid's that were sequential, but they where still regular Guids. Once you had a Guid, it was just a regular Guid with the last 6 bytes ordered. The new version is a wrapper around the Guid with some cool extra stuff.

What is new in this version?

The basic algorithm is the same (backwards compatible) but the new version adds the following (new) possibilities:

// Create some SequentialGuids
var sGuid1 = SequentialGuid.NewSequentialGuid(); // 475db041-7922-4ac2-b111-0a727ed760b4
System.Threading.Thread.Sleep(500);
var sGuid2 = SequentialGuid.NewSequentialGuid(); // f7b1fd91-964a-48ca-8c6c-0a727ed826e7
var sGuid3 = SequentialGuid.NewSequentialGuid(); // 92ff115d-d1bd-4374-af81-0a727ed826e8

// NEW: Comparing Sequential Guids is now easy

var isBigger = sGuid2 > sGuid3; // false
var isSmaller = sGuid2 < sGuid3; // true

var wrongOrder = new[] {sGuid2, sGuid3, sGuid1};
var goodOrder = wrongOrder.OrderBy(g => g); // sGuid1, sGuid2, sGuid3

// NEW (and cool): You can see the time creation date and time, 
//                 even after it was stored in the database

var sGuidFromDb1 = (SequentialGuid) new Guid("475db041-7922-4ac2-b111-0a727ed760b4");
var sGuidFromDb2 = (SequentialGuid) new Guid("f7b1fd91-964a-48ca-8c6c-0a727ed826e7");
var sGuidFromDb3 = (SequentialGuid) new Guid("92ff115d-d1bd-4374-af81-0a727ed826e8");

string s1 = sGuidFromDb1.ToString(); //475db041-7922-4ac2-b111-0a727ed760b4 (2015-06-20 14:51:21.634)
            
DateTime d2 = sGuidFromDb2.CreatedDateTime; // 2015-06-20 14:51:22.1349254
DateTime d3 = sGuidFromDb3.CreatedDateTime; // 2015-06-20 14:51:22.1349353

// Regular stuff

Guid id = SequentialGuid.NewSequentialGuid(); // Just like Guid.NewGuid(); 
// id = 30adce22-de76-4279-99ca-0a72884df151

 The ordering is the same as in SQL Server.

Choices

I decided to do the conversion from SequentialGuid to Guid implicit because each SequentialGuid is a Guid. The conversion from Guid to SequentialGuid needs to be explicit because not all Guids are SequentialGuids.

The SequentialGuid is a struct, because the Guid was already a struct. This makes the difference between the two as small as possible. The CreationDataTime is not lazy because a struct should not use much memory.

The period is no longer parameterized in the constructor, because comparing will not work with different periods.

Guids and Sequential Guids can also be compared.

Source

This is the source of the new version of SequentialGuid:

[Serializable]
public struct SequentialGuid : IComparable<SequentialGuid>, IComparable<Guid>, IComparable
{
    private const int NumberOfSequenceBytes = 6;
    private const int PermutationsOfAByte = 256;
    private static readonly long MaximumPermutations = 
            (long)Math.Pow(PermutationsOfAByte, NumberOfSequenceBytes);
    private static long _lastSequence;
        
   
    private static readonly DateTime SequencePeriodStart = 
        new DateTime(2011, 11, 15, 0, 0, 0, DateTimeKind.Utc); // Start = 000000

    private static readonly DateTime SequencePeriodeEnd = 
        new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc);   // End   = FFFFFF

    private readonly Guid _guidValue;

    public SequentialGuid(Guid guidValue)
    {
        _guidValue = guidValue;
    }

    public SequentialGuid(string guidValue)
        : this(new Guid(guidValue))
    {
    }

    [System.Security.SecuritySafeCritical]
    public static SequentialGuid NewSequentialGuid()
    {
        // You might want to inject DateTime.Now in production code
        return new SequentialGuid(GetGuidValue(DateTime.Now));
    }

    public static TimeSpan TimePerSequence
    {
        get
        {
            var ticksPerSequence = TotalPeriod.Ticks / MaximumPermutations;
            var result = new TimeSpan(ticksPerSequence);
            return result;
        }
    }

    public static TimeSpan TotalPeriod
    {
        get
        {
            var result = SequencePeriodeEnd - SequencePeriodStart;
            return result;
        }
    }

    #region FromDateTimeToGuid

    // Internal for testing
    internal static Guid GetGuidValue(DateTime now)
    {
        if (now < SequencePeriodStart || now >= SequencePeriodeEnd)
        {
            return Guid.NewGuid(); // Outside the range, use regular Guid
        }

        var sequence = GetCurrentSequence(now);
        return GetGuid(sequence);
    }

    private static long GetCurrentSequence(DateTime now)
    {
        var ticksUntilNow = now.Ticks - SequencePeriodStart.Ticks;
        var factor = (decimal)ticksUntilNow / TotalPeriod.Ticks;
        var resultDecimal = factor * MaximumPermutations;
        var resultLong = (long)resultDecimal;
        return resultLong;
    }

    private static readonly object SynchronizationObject = new object();
    private static Guid GetGuid(long sequence)
    {
        lock (SynchronizationObject)
        {
            if (sequence <= _lastSequence)
            {
                // Prevent double sequence on same server
                sequence = _lastSequence + 1;
            }
            _lastSequence = sequence;
        }

        var sequenceBytes = GetSequenceBytes(sequence);
        var guidBytes = GetGuidBytes();
        var totalBytes = guidBytes.Concat(sequenceBytes).ToArray();
        var result = new Guid(totalBytes);
        return result;
    }

    private static IEnumerable<byte> GetSequenceBytes(long sequence)
    {
        var sequenceBytes = BitConverter.GetBytes(sequence);
        var sequenceBytesLongEnough = sequenceBytes.Concat(new byte[NumberOfSequenceBytes]);
        var result = sequenceBytesLongEnough.Take(NumberOfSequenceBytes).Reverse();
        return result;
    }

    private static IEnumerable<byte> GetGuidBytes()
    {
        return Guid.NewGuid().ToByteArray().Take(10);
    }

    #endregion

    #region FromGuidToDateTime

    public DateTime CreatedDateTime
    {
        get
        {
            return GetCreatedDateTime(_guidValue);
        }
    }

    internal static DateTime GetCreatedDateTime(Guid value)
    {
        var sequenceBytes = GetSequenceLongBytes(value).ToArray();
        var sequenceLong = BitConverter.ToInt64(sequenceBytes, 0);
        var sequenceDecimal = (decimal)sequenceLong;
        var factor = sequenceDecimal / MaximumPermutations;
        var ticksUntilNow = factor * TotalPeriod.Ticks;
        var nowTicksDecimal = ticksUntilNow + SequencePeriodStart.Ticks;
        var nowTicks = (long)nowTicksDecimal;
    var result = new DateTime(nowTicks);
    return result;
    }

    private static IEnumerable<byte> GetSequenceLongBytes(Guid value)
    {
        const int numberOfBytesOfLong = 8;
        var sequenceBytes = value.ToByteArray().Skip(10).Reverse().ToArray();
        var additionalBytesCount = numberOfBytesOfLong - sequenceBytes.Length;
        return sequenceBytes.Concat(new byte[additionalBytesCount]);
    }

    #endregion

    #region Relational Operators

    public static bool operator <(SequentialGuid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) < 0;
    }

    public static bool operator >(SequentialGuid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) > 0;
    }

    public static bool operator <(Guid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) < 0;
    }

    public static bool operator >(Guid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) > 0;
    }

    public static bool operator <(SequentialGuid value1, Guid value2)
    {
        return value1.CompareTo(value2) < 0;
    }

    public static bool operator >(SequentialGuid value1, Guid value2)
    {
        return value1.CompareTo(value2) > 0;
    }

    public static bool operator <=(SequentialGuid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) <= 0;
    }

    public static bool operator >=(SequentialGuid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) >= 0;
    }

    public static bool operator <=(Guid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) <= 0;
    }

    public static bool operator >=(Guid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) >= 0;
    }

    public static bool operator <=(SequentialGuid value1, Guid value2)
    {
        return value1.CompareTo(value2) <= 0;
    }

    public static bool operator >=(SequentialGuid value1, Guid value2)
    {
        return value1.CompareTo(value2) >= 0;
    }

    #endregion

    #region Equality Operators

    public static bool operator ==(SequentialGuid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) == 0;
    }

    public static bool operator !=(SequentialGuid value1, SequentialGuid value2)
    {
        return !(value1 == value2);
    }

    public static bool operator ==(Guid value1, SequentialGuid value2)
    {
        return value1.CompareTo(value2) == 0;
    }

    public static bool operator !=(Guid value1, SequentialGuid value2)
    {
        return !(value1 == value2);
    }

    public static bool operator ==(SequentialGuid value1, Guid value2)
    {
        return value1.CompareTo(value2) == 0;
    }

    public static bool operator !=(SequentialGuid value1, Guid value2)
    {
        return !(value1 == value2);
    }

    #endregion

    #region CompareTo

    public int CompareTo(object obj)
    {
        if (obj is SequentialGuid)
        {
            return CompareTo((SequentialGuid)obj);
        }
        if (obj is Guid)
        {
            return CompareTo((Guid)obj);
        }
        throw new ArgumentException("Parameter is not of the rigt type");
    }

    public int CompareTo(SequentialGuid other)
    {
        return CompareTo(other._guidValue);
    }

    public int CompareTo(Guid other)
    {
        return CompareImplementation(_guidValue, other);
    }

    private static readonly int[] IndexOrderingHighLow =
                                  { 10, 11, 12, 13, 14, 15, 8, 9, 7, 6, 5, 4, 3, 2, 1, 0 };

    private static int CompareImplementation(Guid left, Guid right)
    {
        var leftBytes = left.ToByteArray();
        var rightBytes = right.ToByteArray();

        return IndexOrderingHighLow.Select(i => leftBytes[i].CompareTo(rightBytes[i]))
                                   .FirstOrDefault(r => r != 0);
    }

    #endregion

    #region Equals

    public override bool Equals(Object obj)
    {
        if (obj is SequentialGuid || obj is Guid)
        {
            return CompareTo(obj) == 0;
        }

        return false;
    }

    public bool Equals(SequentialGuid other)
    {
        return CompareTo(other) == 0;
    }

    public bool Equals(Guid other)
    {
        return CompareTo(other) == 0;
    }

    public override int GetHashCode()
    {
        return _guidValue.GetHashCode();
    }

    #endregion

    #region Conversion operators

    public static implicit operator Guid(SequentialGuid value)
    {
        return value._guidValue;
    }

    public static explicit operator SequentialGuid(Guid value)
    {
        return new SequentialGuid(value);
    }

    #endregion

    #region ToString

    public override string ToString()
    {
        var roundedCreatedDateTime = Round(CreatedDateTime, TimeSpan.FromMilliseconds(1));
        return string.Format("{0} ({1:yyyy-MM-dd HH:mm:ss.fff})", 
                             _guidValue, roundedCreatedDateTime);
    }

    private static DateTime Round(DateTime dateTime, TimeSpan interval)
    {
        var halfIntervalTicks = (interval.Ticks + 1) >> 1;

        return dateTime.AddTicks(halfIntervalTicks - 
               ((dateTime.Ticks + halfIntervalTicks) % interval.Ticks));
    }

    #endregion
}

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About the author

I am a software architect at Roxit and also a C# Developer. My main interests in the area of ​​C# are LINQ and generics

Visit my personal homepage (Dutch) for more info about me.

Month List

Page List