Skip to content

feat: C# 15 union type support — Argh CLI binding + UnionMakeApp<TUnion>#59

Draft
Mpdreamz wants to merge 3 commits into
mainfrom
feature/dotnet-11-du
Draft

feat: C# 15 union type support — Argh CLI binding + UnionMakeApp<TUnion>#59
Mpdreamz wants to merge 3 commits into
mainfrom
feature/dotnet-11-du

Conversation

@Mpdreamz

@Mpdreamz Mpdreamz commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Note: This PR targets .NET 11 preview (11.0.100-preview.5.26302.115) and uses C# 15 language features (union types). It is opened in anticipation of .NET 11 GA. When .NET 11 ships, global.json and the test project TFMs can be pinned to the stable SDK.

Summary

Two complementary features built on C# 15 discriminated unions (lowered to IUnion / [Union]):

1. Argh: union types as CLI parameters

Union cases become selectable CLI values with case-specific flags — two binding modes:

Flag mode — a selector flag names the case, case properties become --<case>-<prop> flags:

// registration
app.Map("convert", (Format format) => Console.WriteLine(format));

// usage
$ myapp convert --format json --json-pretty
$ myapp convert --format table --table-pretty
$ myapp convert --format csv

Argument mode — case name is a positional, properties become bare flags:

internal static void Convert([Argument] Format format) { ... }

// usage
$ myapp convert json --pretty
$ myapp convert table --pretty
$ myapp convert csv

Union type definition (C# 15 syntax):

public record class Json(bool Pretty = false, int Indent = 2);
public record class Table(bool Pretty = false);
public record class Csv;

public union Format(Table, Json, Csv);
  • Generator detects IUnion / [Union] on parameter types via Roslyn
  • Flag names are prefixed with the case CLI name in flag mode to avoid cross-case collisions (--json-pretty vs --table-pretty)
  • In argument mode, shared prop names (e.g. --pretty on both Json and Table) are valid — the active case determines which is used
  • Unknown case name → exit 2 with a clear parse error
  • 12 new integration tests covering both modes

2. Argh.Make: UnionMakeApp<TUnion> C# DSL

A C#-native build target DSL that uses union types as the target identity, bringing the C# API much closer to the existing F# MakeApp<'TCase> API.

// Define your targets as a union
union Build(Clean, Restore, Compile, Test, Pack);

// Wire them up
var app = new UnionMakeApp<Build>();
app.Bind(b => b switch
{
    Clean   c => app.Target().Executes(() => Shell.Run("git clean -xdf")),
    Restore r => app.Target().DependsOn(new Clean()).Executes(() => Shell.Run("dotnet restore")),
    Compile c => app.Target().DependsOn(new Restore()).Executes(() => Shell.Run("dotnet build -c Release")),
    Test    t => app.Target().DependsOn(new Compile()).Executes(() => Shell.Run("dotnet test -c Release")),
    Pack    p => app.Target().DependsOn<Test>().Executes(() => Shell.Run("dotnet pack -c Release")),
});
return await app.RunAsync(args);

Generic DependsOn overloads for compile-time-safe deps without instantiating a union value:

app.Target()
   .DependsOn<Clean>()
   .DependsOn<Restore, Compile>()
   .Executes(...);

Global flags/options (named options shared across all targets):

var config = app.Option<string>("--config", defaultValue: "Release");
var dryRun = app.Flag("--dry-run");

app.Bind(b => b switch
{
    Compile c => app.Target().Executes(() => Shell.Run($"dotnet build -c {config.Value}")),
    ...
});

Nested unions for namespaced target groups:

union Db(Migrate, Seed, Drop);
union Build(Clean, Compile, Db);
// → routes: clean, compile, db/migrate, db/seed, db/drop

Key implementation files:

  • src/Nullean.Make/UnionMakeApp.cs — entry point, graph building, dep resolution
  • src/Nullean.Make/UnionGraph/UnionReflector.csIUnion detection, recursive case enumeration, default construction
  • src/Nullean.Make/UnionGraph/IUnionTargetBuilder.cs — fluent builder interface
  • src/Nullean.Make/UnionGraph/UnionTargetBuilderImpl.cs — internal implementation
  • src/Nullean.Make/UnionGraph/UnionOptionRef.cs — typed global flag/option

Open items (tracked separately)

  • Source generator for C# union type deferred registration
  • F# record option fields have no description text in help output
  • schema --help falls back to root help instead of schema-specific help
  • Packages not yet published to NuGet
  • UnionMakeApp integration tests (Make side untested, only Argh CLI tested)
  • Help output for union params (--format <table|json|csv> etc.)
  • AGH0035 diagnostic: flag name uniqueness validation across union cases

Test plan

  • All 200 integration tests pass (dotnet test -c Release)
  • 12 new union binding tests: flag mode (5) + argument mode (5) + error cases (2)
  • Help text snapshot tests updated for new commands
  • Manual smoke test of UnionMakeApp (no automated tests yet)

🤖 Generated with Claude Code

Mpdreamz and others added 3 commits June 22, 2026 09:55
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds CliScalarKind.Union and two binding modes:

- Flag mode:   --format <case> [--<case>-<prop> ...]
  e.g. union-format-flag --format union-json --union-json-pretty

- Argument mode: <case> [--<prop> ...]
  e.g. union-format-arg union-json --pretty

The generator detects IUnion/[Union] on parameter types, emits a
switch-based assembly block post flag-parse, and wires prop flags to
the matching case constructor. Case-prop flags are prefixed with the
case CLI name in flag mode to avoid collisions; in argument mode they
are bare (matching across cases that share prop names is fine).

Includes 12 new integration tests covering both modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a C#-native build target DSL that uses union types as the target
identity, bringing the C# API much closer to the existing F# MakeApp.

Consumer pattern:

  union Build(Clean, Test, Pack);

  var app = new UnionMakeApp<Build>();
  app.Bind(b => b switch {
      Clean c => app.Target().Executes(() => ...),
      Test  t => app.Target().DependsOn(new Clean()).Executes(() => ...),
      Pack  p => app.Target().DependsOn<Test>().Executes(() => ...),
  });
  await app.RunAsync(args);

Key additions:
- UnionMakeApp<TUnion>: discovers cases via UnionReflector, resolves
  DependsOn/Composes deps, delegates to existing DepGraphExecutor.
- UnionReflector: IUnion detection, recursive case enumeration (nested
  unions become namespaced routes), default-instance construction.
- IUnionTargetBuilder<TUnion> / UnionTargetBuilderImpl<TUnion>: fluent
  builder with DependsOn(TUnion), DependsOn<T1..T4>() generic overloads,
  and Composes() variants.
- UnionOptionRef<T>: typed global flag/option with implicit T conversion.
- TargetNode gains RouteRequires/RouteComposes for deferred dep resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mpdreamz Mpdreamz force-pushed the feature/dotnet-11-du branch from a06ced6 to 89a931e Compare June 22, 2026 08:01
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