Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/Orleans.Core.Abstractions/CodeGeneration/VersionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using Orleans.Metadata;
using Orleans.Runtime.Versions;

namespace Orleans.CodeGeneration
{
Expand All @@ -22,15 +23,25 @@ public VersionAttribute(ushort version)
Version = version;
}


/// <summary>
/// Initializes a new instance of the <see cref="VersionAttribute"/> class with a semantic version string.
/// </summary>
/// <param name="version">The semantic version string (e.g. "1.2.0", "2.0.0-beta.1").</param>
public VersionAttribute(string version)
{
Version = new GrainInterfaceVersion(SemanticVersion.Parse(version));
}

/// <summary>
/// Gets the version.
/// </summary>
public ushort Version { get; private set; }
public GrainInterfaceVersion Version { get; private set; }

/// <inheritdoc />
void IGrainInterfacePropertiesProviderAttribute.Populate(IServiceProvider services, Type type, Dictionary<string, string> properties)
{
properties[WellKnownGrainInterfaceProperties.Version] = this.Version.ToString(CultureInfo.InvariantCulture);
properties[WellKnownGrainInterfaceProperties.Version] = this.Version.ToString();
}
}
}
107 changes: 107 additions & 0 deletions src/Orleans.Core.Abstractions/Manifest/GrainInterfaceVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Orleans.Runtime.Versions;

namespace Orleans.Runtime.Versions
{
public readonly struct GrainInterfaceVersion : IEquatable<GrainInterfaceVersion>, IComparable<GrainInterfaceVersion>
{
public static readonly GrainInterfaceVersion Zero = new((ushort)0);

private enum VersionKind : byte { Numeric, Semantic }

private readonly VersionKind _kind;
private readonly ushort _numeric;
private readonly SemanticVersion _semantic;

public GrainInterfaceVersion(ushort version)
{
_kind = VersionKind.Numeric;
_numeric = version;
_semantic = default;
}

public GrainInterfaceVersion(SemanticVersion version)
{
_kind = VersionKind.Semantic;
_numeric = 0;
_semantic = version;
}

/// <summary>Whether this is a legacy numeric version.</summary>
public bool IsNumeric => _kind == VersionKind.Numeric;

/// <summary>Whether this is a semantic version.</summary>
public bool IsSemantic => _kind == VersionKind.Semantic;

/// <summary>Gets the numeric value. Throws if this is a semantic version.</summary>
public ushort NumericValue => IsNumeric ? _numeric : throw new InvalidOperationException("Not a numeric version.");

/// <summary>Gets the semantic version value. Throws if this is a numeric version.</summary>
public SemanticVersion SemanticValue => IsSemantic ? _semantic : throw new InvalidOperationException("Not a semantic version.");

/// <summary>Whether this represents the default/zero version.</summary>
public bool IsDefault => IsNumeric ? _numeric == 0 : _semantic.Equals(SemanticVersion.Zero);

/// <summary>
/// Parses a version string from grain interface properties.
/// If the string is a valid ushort, creates a numeric version (backward compatible).
/// Otherwise attempts to parse as SemVer.
/// Falls back to Zero on null/empty.
/// </summary>
public static GrainInterfaceVersion Parse(string? versionString)
{
if (string.IsNullOrEmpty(versionString))
return Zero;

if (ushort.TryParse(versionString, out var numeric))
return new GrainInterfaceVersion(numeric);

if (SemanticVersion.TryParse(versionString, out var semver))
return new GrainInterfaceVersion(semver);

return Zero;
}

public static implicit operator GrainInterfaceVersion(ushort v) => new(v);
public static implicit operator GrainInterfaceVersion(SemanticVersion v) => new(v);

public int CompareTo(GrainInterfaceVersion other)
{
if (_kind != other._kind)
{
throw new InvalidOperationException(
$"Cannot compare {_kind} version with {other._kind} version. "
+ "All versions for a grain interface must use the same versioning scheme.");
}


return _kind == VersionKind.Numeric
? _numeric.CompareTo(other._numeric)
: _semantic.CompareTo(other._semantic);
}

public bool Equals(GrainInterfaceVersion other)
{
if (_kind != other._kind) return false;
return _kind == VersionKind.Numeric
? _numeric == other._numeric
: _semantic.Equals(other._semantic);
}

public override bool Equals(object? obj) => obj is GrainInterfaceVersion other && Equals(other);

public override int GetHashCode() => _kind == VersionKind.Numeric
? HashCode.Combine(0, _numeric)
: HashCode.Combine(1, _semantic);

public static bool operator ==(GrainInterfaceVersion left, GrainInterfaceVersion right) => left.Equals(right);
public static bool operator !=(GrainInterfaceVersion left, GrainInterfaceVersion right) => !left.Equals(right);
public static bool operator <(GrainInterfaceVersion left, GrainInterfaceVersion right) => left.CompareTo(right) < 0;
public static bool operator >(GrainInterfaceVersion left, GrainInterfaceVersion right) => left.CompareTo(right) > 0;
public static bool operator <=(GrainInterfaceVersion left, GrainInterfaceVersion right) => left.CompareTo(right) <= 0;
public static bool operator >=(GrainInterfaceVersion left, GrainInterfaceVersion right) => left.CompareTo(right) >= 0;

public override string ToString() => _kind == VersionKind.Numeric ? _numeric.ToString() : _semantic.ToString();
}

}

124 changes: 124 additions & 0 deletions src/Orleans.Core.Abstractions/Manifest/SemanticVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Diagnostics.CodeAnalysis;

namespace Orleans.Runtime.Versions
{
public readonly struct SemanticVersion : IEquatable<SemanticVersion>, IComparable<SemanticVersion>
{
public static readonly SemanticVersion Zero = new(0, 0, 0);

private int Major { get; }
private int Minor { get; }
private int Patch { get; }
private string? PreRelease { get; }

private bool HasPreRelease => !string.IsNullOrEmpty(PreRelease);

public SemanticVersion(int major, int minor, int patch, string? preRelease = null)
{
ArgumentOutOfRangeException.ThrowIfNegative(major);
ArgumentOutOfRangeException.ThrowIfNegative(minor);
ArgumentOutOfRangeException.ThrowIfNegative(patch);

Major = major;
Minor = minor;
Patch = patch;
PreRelease = string.IsNullOrWhiteSpace(preRelease) ? null : preRelease;
}

public static SemanticVersion Parse(string value)
{
if (!TryParse(value, out var result))
throw new FormatException($"Invalid semantic version: '{value}'");
return result;
}

public static bool TryParse(string? value, [NotNullWhen(true)] out SemanticVersion result)
{
result = default;
if (string.IsNullOrWhiteSpace(value))
return false;

var span = value.AsSpan().Trim();

// Strip leading 'v' or 'V'
if (span.Length > 0 && (span[0] == 'v' || span[0] == 'V'))
span = span.Slice(1);

// Split off pre-release: everything after first '-'
string? preRelease = null;
var hyphenIdx = span.IndexOf('-');
if (hyphenIdx >= 0)
{
preRelease = span.Slice(hyphenIdx + 1).ToString();
span = span.Slice(0, hyphenIdx);
}

// Strip build metadata (+...)
var plusIdx = preRelease?.IndexOf('+') ?? span.IndexOf('+');
if (preRelease != null && plusIdx >= 0)
{
preRelease = preRelease.Substring(0, plusIdx);
}
else if (plusIdx >= 0)
{
span = span.Slice(0, plusIdx);
}

// Parse Major.Minor.Patch
var parts = span.ToString().Split('.');
if (parts.Length < 2 || parts.Length > 3)
return false;

if (!int.TryParse(parts[0], out var major) || major < 0)
return false;
if (!int.TryParse(parts[1], out var minor) || minor < 0)
return false;

var patch = 0;
if (parts.Length == 3 && (!int.TryParse(parts[2], out patch) || patch < 0))
return false;

result = new SemanticVersion(major, minor, patch, preRelease);
return true;
}

public int CompareTo(SemanticVersion other)
{
var cmp = Major.CompareTo(other.Major);
if (cmp != 0) return cmp;

cmp = Minor.CompareTo(other.Minor);
if (cmp != 0) return cmp;

cmp = Patch.CompareTo(other.Patch);
if (cmp != 0) return cmp;

// No pre-release > has pre-release (1.0.0 > 1.0.0-alpha)
if (!HasPreRelease && other.HasPreRelease) return 1;
if (HasPreRelease && !other.HasPreRelease) return -1;
if (!HasPreRelease && !other.HasPreRelease) return 0;

return string.Compare(PreRelease, other.PreRelease, StringComparison.Ordinal);
}

public bool Equals(SemanticVersion other)
=> Major == other.Major
&& Minor == other.Minor
&& Patch == other.Patch
&& string.Equals(PreRelease, other.PreRelease, StringComparison.Ordinal);

public override bool Equals(object? obj) => obj is SemanticVersion other && Equals(other);

public override int GetHashCode() => HashCode.Combine(Major, Minor, Patch, PreRelease);

public static bool operator ==(SemanticVersion left, SemanticVersion right) => left.Equals(right);
public static bool operator !=(SemanticVersion left, SemanticVersion right) => !left.Equals(right);
public static bool operator <(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) < 0;
public static bool operator >(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) > 0;
public static bool operator <=(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) <= 0;
public static bool operator >=(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) >= 0;

public override string ToString() => HasPreRelease ? $"{Major}.{Minor}.{Patch}-{PreRelease}" : $"{Major}.{Minor}.{Patch}";
}
}

7 changes: 4 additions & 3 deletions src/Orleans.Core.Abstractions/Runtime/GrainReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Diagnostics;
using System.Collections.Generic;
using System.Threading;
using Orleans.Runtime.Versions;

namespace Orleans.Runtime
{
Expand All @@ -23,7 +24,7 @@ public class GrainReferenceShared
public GrainReferenceShared(
GrainType grainType,
GrainInterfaceType grainInterfaceType,
ushort interfaceVersion,
GrainInterfaceVersion interfaceVersion,
IGrainReferenceRuntime runtime,
InvokeMethodOptions invokeMethodOptions,
CodecProvider codecProvider,
Expand Down Expand Up @@ -78,7 +79,7 @@ public GrainReferenceShared(
/// <summary>
/// Gets the interface version.
/// </summary>
public ushort InterfaceVersion { get; }
public GrainInterfaceVersion InterfaceVersion { get; }
}

/// <summary>
Expand Down Expand Up @@ -386,7 +387,7 @@ public uint GetUniformHashCode()
/// <summary>
/// Gets the interface version.
/// </summary>
public ushort InterfaceVersion => Shared.InterfaceVersion;
public GrainInterfaceVersion InterfaceVersion => Shared.InterfaceVersion;

/// <summary>
/// Gets the interface name.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Orleans.Runtime.Versions;

namespace Orleans.Versions.Compatibility
{
Expand All @@ -13,7 +14,7 @@ public interface ICompatibilityDirector
/// <param name="requestedVersion">The requested interface version.</param>
/// <param name="currentVersion">The currently available interface version.</param>
/// <returns><see langword="true"/> if the current version of an interface is compatible with the requested version, <see langword="false"/> otherwise.</returns>
bool IsCompatible(ushort requestedVersion, ushort currentVersion);
bool IsCompatible(GrainInterfaceVersion requestedVersion, GrainInterfaceVersion currentVersion);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Orleans.Runtime.Versions;
using Orleans.Versions.Compatibility;

namespace Orleans.Versions.Selector
Expand All @@ -15,7 +16,7 @@ public interface IVersionSelector
/// <param name="availableVersions">The collection of available interface versions.</param>
/// <param name="compatibilityDirector">The compatibility director.</param>
/// <returns>A collection of suitable interface versions for a given request.</returns>
ushort[] GetSuitableVersion(ushort requestedVersion, ushort[] availableVersions, ICompatibilityDirector compatibilityDirector);
GrainInterfaceVersion[] GetSuitableVersion(GrainInterfaceVersion requestedVersion, GrainInterfaceVersion[] availableVersions, ICompatibilityDirector compatibilityDirector);
}

/// <summary>
Expand Down
Loading
Loading