V2/Domain/Identifiers.cs

Definitions of typed identifier structs and a StableIdentifier validator. Provides AccountId, various Guid-based ids (CharacterId, InventoryId, ItemId, SceneEntityId, InteractionSessionId, ConnectionId), and string-based ids validated by StableIdentifier (FactionId, ClassId, DefinitionId, ActionId, PersistedTypeId). StableIdentifier.Require enforces lowercase ASCII, digits, dot, dash, underscore and max length.

Embedded Secret
#nullable enable

using System;
using System.Text.Json.Serialization;

namespace Hexagon.V2.Domain;

/// <summary>
/// Validation shared by every stable, user-authored identifier in Hexagon v2.
/// Stable identifiers are lowercase ASCII and may contain digits, '.', '-', and '_'.
/// </summary>
public static class StableIdentifier
{
	public const int MaxLength = 96;

	public static string Require( string value, string parameterName )
	{
		if ( string.IsNullOrWhiteSpace( value ) )
			throw new ArgumentException( "Identifier cannot be empty.", parameterName );

		value = value.Trim();
		if ( value.Length > MaxLength )
			throw new ArgumentOutOfRangeException( parameterName, $"Identifier cannot exceed {MaxLength} characters." );

		for ( var i = 0; i < value.Length; i++ )
		{
			var c = value[i];
			var valid = c is >= 'a' and <= 'z'
				or >= '0' and <= '9'
				or '.' or '-' or '_';

			if ( !valid )
				throw new ArgumentException( $"Identifier '{value}' contains invalid character '{c}'.", parameterName );
		}

		return value;
	}
}

public readonly record struct AccountId
{
	public ulong Value { get; }

	[JsonConstructor]
	public AccountId( ulong value )
	{
		if ( value == 0 ) throw new ArgumentOutOfRangeException( nameof(value), "Account ID cannot be zero." );
		Value = value;
	}

	public override string ToString() => Value.ToString();
}

public readonly record struct CharacterId
{
	public Guid Value { get; }
	[JsonConstructor]
	public CharacterId( Guid value ) => Value = RequireGuid( value, nameof(value) );
	public static CharacterId New() => new( Guid.NewGuid() );
	public override string ToString() => Value.ToString( "N" );
	private static Guid RequireGuid( Guid value, string name ) => value == Guid.Empty ? throw new ArgumentOutOfRangeException( name ) : value;
}

public readonly record struct InventoryId
{
	public Guid Value { get; }
	[JsonConstructor]
	public InventoryId( Guid value ) => Value = RequireGuid( value, nameof(value) );
	public static InventoryId New() => new( Guid.NewGuid() );
	public override string ToString() => Value.ToString( "N" );
	private static Guid RequireGuid( Guid value, string name ) => value == Guid.Empty ? throw new ArgumentOutOfRangeException( name ) : value;
}

public readonly record struct ItemId
{
	public Guid Value { get; }
	[JsonConstructor]
	public ItemId( Guid value ) => Value = RequireGuid( value, nameof(value) );
	public static ItemId New() => new( Guid.NewGuid() );
	public override string ToString() => Value.ToString( "N" );
	private static Guid RequireGuid( Guid value, string name ) => value == Guid.Empty ? throw new ArgumentOutOfRangeException( name ) : value;
}

public readonly record struct SceneEntityId
{
	public Guid Value { get; }
	[JsonConstructor]
	public SceneEntityId( Guid value ) => Value = RequireGuid( value, nameof(value) );
	public static SceneEntityId New() => new( Guid.NewGuid() );
	public override string ToString() => Value.ToString( "N" );
	private static Guid RequireGuid( Guid value, string name ) => value == Guid.Empty ? throw new ArgumentOutOfRangeException( name ) : value;
}

public readonly record struct InteractionSessionId
{
	public Guid Value { get; }
	[JsonConstructor]
	public InteractionSessionId( Guid value ) => Value = RequireGuid( value, nameof(value) );
	public static InteractionSessionId New() => new( Guid.NewGuid() );
	public override string ToString() => Value.ToString( "N" );
	private static Guid RequireGuid( Guid value, string name ) => value == Guid.Empty ? throw new ArgumentOutOfRangeException( name ) : value;
}

public readonly record struct ConnectionId
{
	public Guid Value { get; }
	[JsonConstructor]
	public ConnectionId( Guid value ) => Value = RequireGuid( value, nameof(value) );
	public static ConnectionId New() => new( Guid.NewGuid() );
	public override string ToString() => Value.ToString( "N" );
	private static Guid RequireGuid( Guid value, string name ) => value == Guid.Empty ? throw new ArgumentOutOfRangeException( name ) : value;
}

public readonly record struct FactionId
{
	public string Value { get; }
	[JsonConstructor]
	public FactionId( string value ) => Value = StableIdentifier.Require( value, nameof(value) );
	public override string ToString() => Value;
}

public readonly record struct ClassId
{
	public string Value { get; }
	[JsonConstructor]
	public ClassId( string value ) => Value = StableIdentifier.Require( value, nameof(value) );
	public override string ToString() => Value;
}

public readonly record struct DefinitionId
{
	public string Value { get; }
	[JsonConstructor]
	public DefinitionId( string value ) => Value = StableIdentifier.Require( value, nameof(value) );
	public override string ToString() => Value;
}

public readonly record struct ActionId
{
	public string Value { get; }
	[JsonConstructor]
	public ActionId( string value ) => Value = StableIdentifier.Require( value, nameof(value) );
	public override string ToString() => Value;
}

public readonly record struct PersistedTypeId
{
	public string Value { get; }
	[JsonConstructor]
	public PersistedTypeId( string value ) => Value = StableIdentifier.Require( value, nameof(value) );
	public override string ToString() => Value;
}