-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Open
Labels
api-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementationarea-System.RuntimeuntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
Description
Background and motivation
Summary
This proposal introduces the Vnum
(Value-backed enumeration) concept to the .NET framework as a more powerful alternative to traditional enums. The Vnum
pattern provides strongly-typed enumeration-like constructs with additional metadata, better serialization support, and enhanced functionality while maintaining performance through intelligent caching.
Motivation
Traditional C# enums have several limitations:
- Limited Metadata: Enums can only store numeric values and string names, making it difficult to associate additional properties like descriptions, display names, or business logic.
- Serialization Challenges: Enum serialization often requires custom converters, especially when dealing with string-based APIs or databases.
- No Business Logic: Enums cannot contain methods or properties that encapsulate domain-specific behavior.
- Type Safety Issues: Enum values can be cast to integers, potentially leading to invalid states.
- Limited Extensibility: Adding new properties or methods to enums requires workarounds or separate classes.
The Vnum
pattern addresses these limitations by providing:
- Rich metadata support (descriptions, codes, custom properties)
- Built-in serialization support
- Encapsulated business logic
- Type safety through sealed instances
- Performance optimization through reflection caching
API Proposal
namespace System;
/// <summary>
/// Provides a base class for creating enumeration-like constructs
/// with strongly-typed values and display codes.
/// </summary>
public abstract class Vnum : IEquatable<Vnum>
{
/// <summary>
/// Gets the numeric value of the Vnum item.
/// </summary>
public int Value { get; }
/// <summary>
/// Gets the string code of the Vnum item.
/// </summary>
public string Code { get; }
protected Vnum() { } // Required for reflection
protected Vnum(int value, string code)
{
Value = value;
Code = code ?? throw new ArgumentNullException(nameof(code));
}
/// <summary>
/// Returns the display code of the Vnum item.
/// </summary>
public override string ToString() => Code;
/// <summary>
/// Retrieves all Vnum instances of a given type.
/// </summary>
public static IEnumerable<T> GetAll<T>(Func<T, bool>? predicate = null) where T : Vnum, new();
/// <summary>
/// Retrieves a Vnum instance by its numeric value.
/// </summary>
public static T FromValue<T>(int value) where T : Vnum, new();
/// <summary>
/// Attempts to retrieve a Vnum instance by its numeric value.
/// </summary>
public static bool TryFromValue<T>(int value, out T vnum) where T : Vnum, new();
/// <summary>
/// Retrieves a Vnum instance by its code.
/// </summary>
public static T FromCode<T>(string code, bool ignoreCase = false) where T : Vnum, new();
/// <summary>
/// Attempts to retrieve a Vnum instance by its code.
/// </summary>
public static bool TryFromCode<T>(string code, bool ignoreCase, out T vnum) where T : Vnum, new();
// Equality and hashing implementations
public override bool Equals(object? obj);
public bool Equals(Vnum? other);
public override int GetHashCode();
}
/// <summary>
/// Provides a base class for creating enumeration-like constructs
/// that are strongly-typed to a specific enum type.
/// </summary>
public abstract class Vnum<TEnum> : Vnum where TEnum : struct, Enum
{
/// <summary>
/// Gets the enumeration value of the Vnum item.
/// </summary>
public TEnum Id => (TEnum)Enum.ToObject(typeof(TEnum), Value);
protected Vnum() : base() { }
protected Vnum(int value, string code) : base(value, code) { }
protected Vnum(TEnum value, string code) : base(Convert.ToInt32(value), code) { }
/// <summary>
/// Retrieves a Vnum instance by its enum value.
/// </summary>
public static TVnum FromEnum<TVnum, TEnum>(TEnum value)
where TVnum : Vnum<TEnum>, new()
where TEnum : struct, Enum;
/// <summary>
/// Attempts to retrieve a Vnum instance by its enum value.
/// </summary>
public static bool TryFromEnum<TVnum, TEnum>(TEnum value, out TVnum vnum)
where TVnum : Vnum<TEnum>, new()
where TEnum : struct, Enum;
}
API Usage
// Basic Vnum Implementation:
public enum StatusId
{
Active = 10,
Inactive = 20,
Closed = 95,
Archived = 99
}
public class Status : Vnum<StatusId>
{
public string Description { get; }
public Status() { }
private Status(StatusId id, string code, string description) : base(id, code)
{
Description = description;
}
public static readonly Status Active =
new(StatusId.Active, "Active", "Currently active and available");
public static readonly Status Inactive =
new(StatusId.Inactive, "Inactive", "Not currently in use but can be reactivated");
public static readonly Status Closed =
new(StatusId.Closed, "Closed", "Permanently disabled and cannot be reactivated");
public static readonly Status Archived =
new(StatusId.Archived, "Archived", "Record retained for audit/history only");
}
//Advanced Vnum with Business Logic:
public enum JobStatusId
{
Active = 10,
Fulfilled = 11,
Closed = 12,
Blocked = 13
}
public class JobStatus : Vnum<JobStatusId>
{
public bool CanAcceptOffers { get; }
public bool IsClosed { get; }
public string Description { get; }
public JobStatus() { }
private JobStatus(JobStatusId id, string code, bool canAcceptOffers, bool isClosed, string description)
: base(id, code)
{
CanAcceptOffers = canAcceptOffers;
IsClosed = isClosed;
Description = description;
}
public static readonly JobStatus Active =
new(JobStatusId.Active, "Active", true, false, "Job is active and accepting offers");
public static readonly JobStatus Fulfilled =
new(JobStatusId.Fulfilled, "Fulfilled", false, true, "Job has been fulfilled");
public static readonly JobStatus Closed =
new(JobStatusId.Closed, "Closed", false, true, "Job has been closed");
public static readonly JobStatus Blocked =
new(JobStatusId.Blocked, "Blocked", false, true, "Job is blocked by authority");
}
//Usage in Code:
// Getting all instances
var allStatuses = Vnum.GetAll<Status>();
var closedStatuses = Vnum.GetAll<Status>(s => s.IsClosed);
// Finding by value
var activeStatus = Vnum.FromValue<Status>(10);
// Finding by code
var closedStatus = Vnum.FromCode<Status>("Closed");
// Finding by enum
var inactiveStatus = Vnum.FromEnum<Status, StatusId>(StatusId.Inactive);
// Safe parsing
if (Vnum.TryFromCode<Status>("Active", out var status))
{
Console.WriteLine($"Status: {status.Code}, Description: {status.Description}");
}
// Business logic usage
if (jobStatus.CanAcceptOffers)
{
// Process new offers
}
// Serialization (works with System.Text.Json)
var json = JsonSerializer.Serialize(activeStatus);
var deserialized = JsonSerializer.Deserialize<Status>(json);
Alternative Designs
using System.Collections.Concurrent;
using System.Reflection;
namespace System;
public abstract class Vnum<TEnum> : Vnum where TEnum : struct, Enum
{
/// <summary>
/// Gets the enumeration value of the Vnum item.
/// </summary>
public TEnum Id => (TEnum)Enum.ToObject(typeof(TEnum), Value);
protected Vnum() : base() { }
protected Vnum(int value, string code) : base(value, code) { }
protected Vnum(TEnum value, string code) : base(Convert.ToInt32(value), code) { }
}
/// <summary>
/// Provides a base class for creating enumeration-like constructs
/// with strongly-typed values and display codes.
/// </summary>
public abstract class Vnum : IEquatable<Vnum>
{
/// <summary>
/// A thread-safe cache that stores arrays of <see cref="Vnum"/> instances for each derived type.
/// This cache is used to improve performance by avoiding repeated reflection operations when retrieving
/// all instances of a specific type (e.g., via <see cref="GetAll(Type)"/>).
///
/// Once populated, subsequent calls for the same type will retrieve the cached array instead of
/// invoking reflection, which can be computationally expensive. The cache uses a
/// <see cref="ConcurrentDictionary{TKey, TValue}"/> to ensure thread safety in multi-threaded environments.
///
/// Note: This cache is static, meaning it's shared across all instances of <see cref="Vnum"/>.
/// </summary>
private static readonly ConcurrentDictionary<Type, object[]> _cache = new();
/// <summary>
/// Gets the numeric value of the Vnum item.
/// </summary>
public int Value { get; }
/// <summary>
/// Gets the string code of the Vnum item.
/// </summary>
public string Code { get; } = null!;
protected Vnum() { } // Required for reflection
protected Vnum(int value, string code)
{
Value = value;
Code = code ?? throw new ArgumentNullException(nameof(code));
}
/// <summary>
/// Returns the display code of the Vnum item.
/// </summary>
public override string ToString() => Code;
/// <summary>
/// Retrieves all Vnum instances of a given type <typeparamref name="T"/>.
/// </summary>
public static IEnumerable<T> GetAll<T>(Func<T, bool>? predicate = null) where T : Vnum, new()
{
var all = GetAll(typeof(T)).Cast<T>();
return predicate is null ? all : all.Where(predicate);
}
/// <summary>
/// Retrieves all Vnum instances of the specified <see cref="Vnum"/> type.
/// </summary>
private static object[] GetAll(Type type)
{
//1. Validate the type parameter.
if (type is null)
{
throw new ArgumentNullException(
paramName: nameof(type),
message: "Type cannot be null");
}
if (!typeof(Vnum).IsAssignableFrom(type))
{
throw new ArgumentException(
message: $"Type '{type.Name}' is not a valid {nameof(Vnum)} type",
paramName: nameof(type));
}
//2. Check if the requested Vnum instances for type are already in _cache.
//3. If not cached, use reflection to get all public, static fields on type that represent Vnum instances.
//4. Store and return the results as an array.
return _cache.GetOrAdd(
key: type,
t => t.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(f => t.IsAssignableFrom(f.FieldType))
.Select(f => f.GetValue(null)!)
.ToArray());
}
/// <summary>
/// Retrieves a Vnum instance by its numeric value.
/// </summary>
public static T FromValue<T>(int value) where T : Vnum, new() =>
Parse<T, int>(
value: value,
description: nameof(value),
predicate: item => item.Value == value);
/// <summary>
/// Attempts to retrieve a Vnum instance by its numeric value.
/// </summary>
public static bool TryFromValue<T>(int value, out T vnum) where T : Vnum, new() =>
TryParse(() => FromValue<T>(value), out vnum);
/// <summary>
/// Retrieves a Vnum instance by its enum value.
/// </summary>
public static TVnum FromEnum<TVnum, TEnum>(TEnum value)
where TVnum : Vnum<TEnum>, new()
where TEnum : struct, Enum
=> FromValue<TVnum>(Convert.ToInt32(value));
/// <summary>
/// Attempts to retrieve a Vnum instance by its enum value.
/// </summary>
public static bool TryFromEnum<TVnum, TEnum>(TEnum value, out TVnum vnum)
where TVnum : Vnum<TEnum>, new()
where TEnum : struct, Enum
=> TryParse(() => FromValue<TVnum>(Convert.ToInt32(value)), out vnum);
/// <summary>
/// Retrieves a Vnum instance by its code.
/// </summary>
public static T FromCode<T>(string code, bool ignoreCase) where T : Vnum, new()
{
Func<T, bool> predicate =
(item) =>
ignoreCase
? item.Code.Equals(code, StringComparison.OrdinalIgnoreCase)
: item.Code == code;
return Parse(
value: code,
description: nameof(code),
predicate: predicate);
}
public static T FromCode<T>(string code) where T : Vnum, new() =>
FromCode<T>(code, ignoreCase: false);
/// <summary>
/// Attempts to retrieve a Vnum instance by its code.
/// </summary>
public static bool TryFromCode<T>(string code, bool ignoreCase, out T vnum) where T : Vnum, new() =>
TryParse(() => FromCode<T>(code, ignoreCase), out vnum);
/// <summary>
/// Attempts to retrieve a Vnum instance by its code (case sensitive).
/// </summary>
public static bool TryFromCode<T>(string code, out T vnum) where T : Vnum, new() =>
TryFromCode(code, ignoreCase: false, out vnum);
/// <summary>
/// Parses a Vnum instance based on a specified predicate.
/// This method is used internally to find a matching Vnum item.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if no matching Vnum instance is found.</exception>
private static T Parse<T, K>(
K value,
string description,
Func<T, bool> predicate) where T : Vnum, new()
{
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
if (matchingItem is null)
{
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T).Name}");
}
return matchingItem;
}
private static bool TryParse<T>(Func<T> parseFunc, out T result) where T : Vnum, new()
{
try
{
result = parseFunc();
return true;
}
catch (ArgumentNullException) // Narrow catch. Avoid masking catastrophic exceptions
{
result = null!;
return false;
}
catch (InvalidOperationException)
{
result = null!;
return false;
}
}
/// <summary>
/// Determines whether the specified object is equal to the current Vnum instance.
/// </summary>
public override bool Equals(object? obj) =>
obj is Vnum other &&
Equals(other);
/// <summary>
/// Determines whether the specified Vnum is equal to the current Vnum instance.
/// </summary>
public bool Equals(Vnum? other) =>
other != null &&
GetType().Equals(other.GetType()) &&
Value == other.Value;
/// <summary>
/// Returns the hash code for the Vnum item, based on its value.
/// </summary>
public override int GetHashCode() => HashCode.Combine(GetType(), Value);
}
Risks
Implementation Details
- Performance Optimization: Uses
ConcurrentDictionary<Type, object[]>
for caching reflection results, ensuring thread-safe access and avoiding repeated reflection operations. - Type Safety: All instances are created as static readonly fields, preventing invalid states and ensuring immutability.
- Serialization Support: Natural support for JSON serialization through
ToString()
and parsing methods. - Memory Efficiency: Static instances are shared across the application, reducing memory footprint.
- Reflection-Based Discovery: Uses reflection to automatically discover all static instances, reducing boilerplate code.
Drawbacks
- Reflection Overhead: Initial discovery of instances uses reflection, though this is cached for performance.
- Memory Usage: Each Vnum type maintains a cache of all instances, though this is typically minimal.
- Learning Curve: Developers need to understand the pattern and conventions for proper implementation.
- Static Dependencies: All instances must be defined as static fields, which can make testing more complex.
- Assembly Loading: Reflection-based discovery may have implications for assembly loading in certain scenarios.
Nice to have
- User-friendly syntax for implementing Vnums.
- Compile-Time Safety: Validation to ensure enum-Vnum mappings are correct at development time.
Metadata
Metadata
Assignees
Labels
api-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementationarea-System.RuntimeuntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner