Skip to content

Instantly share code, notes, and snippets.

Last active January 27, 2023 03:18
What would you like to do?
using System;
using System.Diagnostics;
using System.Numerics;
using System.Buffers.Binary;
#nullable enable
public readonly struct Rowversion : IComparable, IEquatable<Rowversion>, IComparable<Rowversion>
, IComparisonOperators<Rowversion, Rowversion, bool>
public static Rowversion Zero => default;
private readonly ulong value;
public Rowversion(ulong value)
this.value = value;
public Rowversion(byte[] value)
if (value is null)
throw new ArgumentNullException(nameof(value));
if (value.Length != 8)
throw new ArgumentException("The array does not have the correct length (8 bytes) to represent a SQL Server rowversion.", nameof(value));
this.value =
((ulong)value[0] << 56)
| ((ulong)value[1] << 48)
| ((ulong)value[2] << 40)
| ((ulong)value[3] << 32)
| ((ulong)value[4] << 24)
| ((ulong)value[5] << 16)
| ((ulong)value[6] << 8)
| value[7];
public Rowversion(ReadOnlySpan<byte> value)
this.value = BinaryPrimitives.ReadUInt64BigEndian(value);
public static implicit operator Rowversion(ulong value) => new(value);
public static implicit operator Rowversion(long value) => new(unchecked((ulong)value));
public static explicit operator Rowversion(byte[] value) => new(value);
public static explicit operator Rowversion?(byte[]? value) => value is null ? null : new Rowversion(value);
public byte[] ToArray()
var array = new byte[8];
array[0] = (byte)(value >> 56);
array[1] = (byte)(value >> 48);
array[2] = (byte)(value >> 40);
array[3] = (byte)(value >> 32);
array[4] = (byte)(value >> 24);
array[5] = (byte)(value >> 16);
array[6] = (byte)(value >> 8);
array[7] = (byte)value;
return array;
public void WriteTo(Span<byte> destination)
BinaryPrimitives.WriteUInt64BigEndian(destination, value);
public override bool Equals(object? obj) => obj is Rowversion other && Equals(other);
public override int GetHashCode() => value.GetHashCode();
public bool Equals(Rowversion other) => other.value == value;
int IComparable.CompareTo(object? obj) => obj == null ? 1 : CompareTo((Rowversion)obj);
public int CompareTo(Rowversion other) => value.CompareTo(other.value);
public static bool operator ==(Rowversion left, Rowversion right) => left.Equals(right);
public static bool operator !=(Rowversion left, Rowversion right) => !left.Equals(right);
public static bool operator >(Rowversion left, Rowversion right) => left.CompareTo(right) > 0;
public static bool operator >=(Rowversion left, Rowversion right) => left.CompareTo(right) >= 0;
public static bool operator <(Rowversion left, Rowversion right) => left.CompareTo(right) < 0;
public static bool operator <=(Rowversion left, Rowversion right) => left.CompareTo(right) <= 0;
public override string ToString() => value.ToString("x16");
Copy link

daiplusplus commented Jan 24, 2023

A few questions:

  • Is there a reason you opted to use byte[] instead of Span<byte>?
  • Given that this struct Rowversion supports implicit conversions from a few types, shouldn't its override Equals(Object) method also support those types when passed in via object obj?
  • Shouldn't the ctors that accept ulong/long check for Endianness? e.g. if struct Rowverison is used on an already big-endian system then there's no need to byte-swap? perhaps add an endianness option to the byte[] ctor and ToArray method?
  • Methinks the byte[] ctor should also validate/assert the length of the input array as it's a precondition (maybe add an offset parameter too - if not using ReadOnlySpan, ofc)?
  • I think explicit operator Rowversion?(byte[] value) would benefit from being changed to [return: NotNullIfNotNull("value")] explicit operator Rowversion?(byte[]? value).
  • As struct Rowversion allows for numeric comparisons between values, might it also be worth implementing the new .NET 7 generic math interfaces?

Copy link

jnm2 commented Jan 27, 2023

@daiplusplus Hi!

  1. Span<byte> didn't exist when this was written, but ReadOnlySpan<byte> for the constructor would be an obvious improvement.

  2. I'd never heard that implicit compile-time conversions should affect runtime conversions via Equals(object). I'm pretty sure that would be nonstandard and possibly even not best practice. See For instance, the requirement that:

    x.Equals(y) returns the same value as y.Equals(x).

    This requirement can't possibly be satisfied unless equality checks require the exact concrete type to match. You can also see the importance borne out in the attention that the C# 9 records feature paid to this concern, which is why there's an EqualityContract property on every record so that equality works properly with inheritance.

  3. I don't think so. This is where you'd pass a ulong that you queried from SQL Server as such, and that scenario would break if it was changed from what it is now.

    The numeric value of the ulong should not be interpreted as having a byte order at all for the purpose of this struct. It's the same numeric value as the numeric value SQL Server would see. In other words, don't use the ulong constructor to secretly pass a byte sequence rather than a numeric value. Use the byte constructor for that.

  4. A better ArgumentException for length would be a good idea.

  5. Now that nullable reference types exist, that sounds like a good idea.

  6. That sounds cool. I haven't run into code in the wild where I could imagine the motivation for interop via the generic math interfaces, since IComparable already covers the comparison operators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment