Skip to content

Add IGlobalOptionsAccessor for typed access to global options#15

Merged
carldebilly merged 10 commits intomainfrom
dev/cdb/global-options-accessor
Apr 3, 2026
Merged

Add IGlobalOptionsAccessor for typed access to global options#15
carldebilly merged 10 commits intomainfrom
dev/cdb/global-options-accessor

Conversation

@carldebilly
Copy link
Copy Markdown
Member

@carldebilly carldebilly commented Mar 30, 2026

Summary

  • IGlobalOptionsAccessor is now registered in DI automatically, giving middleware, DI service factories, and handlers typed access to parsed global option values.
  • UseGlobalOptions<T>() extension on ReplApp allows declaring a typed class whose public settable properties become global options — the class is available via DI, populated from parsed values. Property names are converted to kebab-case (MaxRetries--max-retries).
  • New non-generic AddGlobalOption(name, typeName) overload accepts constraint-style type names ("int", "bool", "guid", "uri", "date", "timespan", etc.), matching the route constraint naming convention.
  • GlobalOptionDefinition now preserves the declared Type from AddGlobalOption<T>() for help text generation and future validation.
  • Values are updated per-invocation: in CLI mode after initial parsing, in interactive mode after each command's re-parse. DI singleton factories see the correct values because they resolve lazily (after parsing completes).

Examples

Dictionary-style access (works with both CoreReplApp and ReplApp):

app.Options(o => o.Parsing.AddGlobalOption<string>("tenant"));

// In middleware
app.Use(async (ctx, next) =>
{
    var globals = ctx.Services.GetRequiredService<IGlobalOptionsAccessor>();
    var tenant = globals.GetValue<string>("tenant");
    await next();
});

// In DI factory
services.AddSingleton<ITenantClient>(sp =>
{
    var globals = sp.GetRequiredService<IGlobalOptionsAccessor>();
    return new TenantClient(globals.GetValue<string>("tenant", "default")!);
});

Typed class (requires ReplApp with MS DI):

public class MyGlobalOptions
{
    public string? Tenant { get; set; }
    public int Port { get; set; } = 8080;
}

app.UseGlobalOptions<MyGlobalOptions>();
app.Map("show", (MyGlobalOptions opts) => $"{opts.Tenant}:{opts.Port}");

Open with Devin

Global options registered via AddGlobalOption<T> are now accessible
outside handlers through IGlobalOptionsAccessor, registered in DI
automatically. Middleware and DI factories can resolve it to read
parsed values. Also adds UseGlobalOptions<T>() for typed class
registration and a non-generic AddGlobalOption(name, typeName)
overload accepting constraint-style type names.
Update commands, configuration reference, execution pipeline,
parameter system, and best practices docs with global options
accessor usage patterns and examples.
devin-ai-integration[bot]

This comment was marked as resolved.

The non-generic AddGlobalOption(name, typeName) overload now resolves
custom route constraints registered via AddRouteConstraint as string
type. Adds tests for all built-in type names, custom constraints,
enum conversion, ValueType preservation, and error cases.
devin-ai-integration[bot]

This comment was marked as resolved.

UseGlobalOptions<T>() was registering the typed class as a singleton,
causing stale values when global options changed between interactive
REPL invocations. Changed to transient registration so each resolution
reflects the latest parsed values.

Added integration test verifying typed options update across invocations.
CLI-level global options now persist as session defaults across all
interactive commands. Per-command overrides are temporary and revert
to the session baseline on the next command.
Create a prototype instance of T to capture property initializer values
and pass them as registration defaults to AddGlobalOptionCore. This
ensures IGlobalOptionsAccessor returns correct defaults (e.g. 8080 for
Port { get; set; } = 8080) even when accessed outside the typed class.
devin-ai-integration[bot]

This comment was marked as resolved.

- HasValue now returns false for baseline-inherited keys not explicitly
  provided in the current command
- Bool-typed global options no longer consume the next positional token
- Rename typeName parameter to constraintOrTypeName to reflect that
  custom route constraint names are also accepted
- Add volatile to GlobalOptionsSnapshot fields for hosted session safety
- PopulateInstance now uses configured NumericFormatProvider instead of
  hardcoded InvariantCulture
devin-ai-integration[bot]

This comment was marked as resolved.

ToKebabCase now handles acronyms correctly: XMLPort → xml-port instead
of x-m-l-port. Only inserts a hyphen at the start of an uppercase run
or at the transition from uppercase run to lowercase.

Added warning in best-practices.md that singleton DI factories capture
global option values once and won't update in interactive mode.
PopulateInstance used HasValue() which only checks explicitly provided
keys, causing session-baseline values to be skipped for typed options
classes. Now checks GetRawValues() directly so baseline values are
picked up correctly.

Added "email" to the built-in type name resolution (resolves to string)
to match its presence in ReservedConstraintNames.
devin-ai-integration[bot]

This comment was marked as resolved.

SetSessionBaseline now rebuilds from only explicitly parsed keys,
preventing stale values from leaking when the same app instance
is used for multiple Run() calls.
Comment thread src/Repl.Core/GlobalOptionsSnapshot.cs Dismissed
@carldebilly carldebilly merged commit b49974a into main Apr 3, 2026
12 checks passed
@carldebilly carldebilly deleted the dev/cdb/global-options-accessor branch April 3, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant