Core Concepts

Named-Constant Structs: An Enum That Also Takes Freehand Values

WaitTime and AIModel look like enums. GPAL.WithWaitFor(WaitTime.Forever) or .WithModel(AIModel.Claude35Sonnet). But they are actually small readonly structs with an implicit conversion from a plain value, so any string or number you type by hand works too. It is a pattern most C# developers have never needed, because most APIs do not face this exact problem.

The Problem: A Closed Set That Is Not Actually Closed

A real C# enum is a fixed, closed set of named values baked into the compiled assembly. That is perfect when the set of valid values is genuinely fixed -- but some GPAL settings are not. WaitFor needs named special cases like WaitTime.Forever, WaitTime.Never, and WaitTime.Immediate, but also any millisecond value a caller might supply. WithModel needs well-known model IDs like AIModel.Claude35Sonnet and AIModel.GPT4o, but AI providers release new models constantly and GPAL cannot ship an update every time they do. A real enum cannot represent 'one of these named values, or literally anything else' -- the underlying type is fixed, and there is no implicit conversion from an arbitrary int or string into an enum value.

The Shape of the Pattern

Both WaitTime and AIModel are public readonly structs with a single private field. An int or a string. The named options are static readonly fields pre-built with a particular value. The key piece is an implicit conversion operator: because it is implicit, a named constant, a string literal, and a runtime string variable all compile to the same parameter. ToString() unwraps the value when GPAL needs it.

public readonly struct AIModel

{

private readonly string _value;

internal AIModel(string value) => _value = value;

// Named constants - just pre-built instances of the struct

public static readonly AIModel GPT4o = new AIModel("gpt-4o");

public static readonly AIModel Claude35Sonnet = new AIModel("claude-3-5-sonnet-20241022");

public static readonly AIModel Grok2Latest = new AIModel("grok-2-latest");

// The escape hatch: any string is also a valid AIModel

public static implicit operator AIModel(string modelId) => new AIModel(modelId);

public override string ToString() => _value;

}

// All three of these compile and work:

GPAL.AI.WithModel(AIModel.Claude35Sonnet); // named constant

GPAL.AI.WithModel("claude-3-haiku-20240307"); // freehand string literal

GPAL.AI.WithModel(modelIdFromConfigFile); // freehand string variable

Why This Is Unusual - and Where GPAL Uses It

Most closed sets are genuinely closed. A day of the week, a log level. The pattern earns its keep when an external system (an AI provider's model catalog) or a special sentinel (wait forever vs. Wait specific milliseconds) means the set must stay open at one end. WaitTime.Forever, WaitTime.Never, and WaitTime.Immediate read like named timeouts, but GPAL.WithWaitFor(5_000) is equally valid. AIModel works the same: use a curated constant, or pass any new model ID string.

TIP

Typing AIModel. Or WaitTime. In the IDE still shows the curated named constants first. Discoverability is preserved, and the freehand path is only needed when a value is not yet on the list.