You can use BinaryPrimitives.ReadUInt64BigEndian
instead if you are on .NET Core 2.1 or later (or .NET Standard).
-
-
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 Hi!
-
Span<byte>
didn't exist when this was written, butReadOnlySpan<byte>
for the constructor would be an obvious improvement. -
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 asy.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.
-
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.
-
A better ArgumentException for length would be a good idea.
-
Now that nullable reference types exist, that sounds like a good idea.
-
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.
A few questions:
byte[]
instead ofSpan<byte>
?struct Rowversion
supports implicit conversions from a few types, shouldn't itsoverride Equals(Object)
method also support those types when passed in viaobject obj
?ulong
/long
check for Endianness? e.g. ifstruct Rowverison
is used on an already big-endian system then there's no need to byte-swap? perhaps add an endianness option to thebyte[]
ctor andToArray
method?byte[]
ctor should also validate/assert the length of the input array as it's a precondition (maybe add anoffset
parameter too - if not usingReadOnlySpan
, ofc)?explicit operator Rowversion?(byte[] value)
would benefit from being changed to[return: NotNullIfNotNull("value")] explicit operator Rowversion?(byte[]? value)
.struct Rowversion
allows for numeric comparisons between values, might it also be worth implementing the new .NET 7 generic math interfaces?