Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active October 20, 2023 14:02
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnm2/929d194c87df8ad0438f6cab0139a0a6 to your computer and use it in GitHub Desktop.
Save jnm2/929d194c87df8ad0438f6cab0139a0a6 to your computer and use it in GitHub Desktop.
using System;
using System.Diagnostics;
using System.Numerics;
#if NETCOREAPP2_1_OR_GREATER
using System.Buffers.Binary;
#endif
#nullable enable
[DebuggerDisplay("{ToString(),nq}")]
public readonly struct Rowversion : IComparable, IEquatable<Rowversion>, IComparable<Rowversion>
#if NET7_0_OR_GREATER
, IComparisonOperators<Rowversion, Rowversion, bool>
#endif
{
public static Rowversion Zero => default;
private readonly ulong value;
public Rowversion(ulong value)
{
this.value = value;
}
#if !NETCOREAPP2_1_OR_GREATER
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];
}
#else
public Rowversion(ReadOnlySpan<byte> value)
{
this.value = BinaryPrimitives.ReadUInt64BigEndian(value);
}
#endif
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];
#if NETCOREAPP2_1_OR_GREATER
WriteTo(array);
#else
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;
#endif
return array;
}
#if NETCOREAPP2_1_OR_GREATER
public void WriteTo(Span<byte> destination)
{
BinaryPrimitives.WriteUInt64BigEndian(destination, value);
}
#endif
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");
}
@daiplusplus
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?

@jnm2
Copy link
Author

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 https://learn.microsoft.com/en-us/dotnet/api/system.object.equals#notes-for-inheritors. 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