Core Concepts

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

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 and WaitTime.Immediate, but also any millisecond value a caller might supply. WithModel needs well-known model IDs like AIModel.Claude35Sonnet, but AI providers release new models constantly and GPAL cannot ship an update every time they do. WithProvider has the same problem: XAI, OpenAI, and Anthropic are named constants, but any provider you define in AIProvidersConfig.yaml is equally valid. A real enum cannot represent "one of these named values, or literally anything else."

// Named constants

GPAL.AI.WithProvider(AIProviderType.Anthropic);

GPAL.AI.WithProvider(AIProviderType.XAI);

GPAL.AI.WithModel(AIModel.Claude35Sonnet);


// Freehand

GPAL.AI.WithProvider("Mistral");

GPAL.AI.WithModel("claude-opus-4-8-20251101");

The Shape of the Pattern

WaitTime, AIModel, and AIProviderType are all 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 or a special sentinel 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 the provider just released. AIProviderType extends it one step further -- XAI, OpenAI, and Anthropic are the built-in constants, but any provider name you add to AIProvidersConfig.yaml is resolved the same way at runtime without recompiling.

NOTE

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.

💬 Ask GPAL