Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

11. Generating Typed Environment Records

Environment variables can change tool behavior as substantially as CLI flags. BinaryWrapper represents them as version-aware typed records generated from a declarative C# catalogue.

The design preserves a distinction between leaving a variable inherited and explicitly assigning a value that disables, removes or overrides it.

One catalogue, two consumers

The Design project compiles *EnvironmentCatalog.cs and reads its declared variables from the process environment. The contracts project supplies the same file as AdditionalFiles to EnvironmentCatalogGenerator.

The generator calls the shared EnvironmentClassGenerator engine and emits Abstract<Name>Environment : ICliEnvironment, declared enums and a compatibility exception. The concrete consumer record inherits that base.

Diagram
Catalogue reuse keeps Design and runtime variable names aligned without adding a Design runtime dependency to Contracts.

Environment compatibility is defined by this catalogue. It is independent of the reviewed output-profile interval and the versions present in command help.

The catalogue language

The catalogue describes a namespace, environment name and variable definitions. Each variable has its environment name, C# property name/type and availability baseline, with optional conversion, removal and deprecation metadata.

The following is a declaration example using the existing catalogue model, not a new implemented Example wrapper.

public static class ExampleEnvironmentCatalog
{
    private const string Baseline = "1.0.0";

    public static EnvironmentDefinition Definition { get; } = new(
        "Example", "Example",
        Array.AsReadOnly<EnvironmentVariableDefinition>([
            new("EXAMPLE_HOST", "Host", "string?", Baseline),
            new("EXAMPLE_FLAG", "Flag", "NonEmptyEnvironmentSwitch", Baseline,
                Kind: EnvironmentValueKind.NonEmptySwitch),
        ]));
}

Roslyn reads a constrained declarative syntax: literals, null, local constants, arrays or collection expressions, supported constructors and named arguments. Helper calls, interpolation and arbitrary executable expressions are rejected.

The file is therefore data expressed in C#, rather than code the generator invokes. The generated record remains ordinary compiled C# without a Design dependency at runtime.

Records and assignment state

Generated environment properties record assignment through readonly flags such as VAGRANT_HOME_hasBeenSet. Assigning null still records that assignment, even if its conversion emits no dictionary entry.

A with copy preserves existing assignment flags and updates those for changed properties. Equality includes values and assignment flags. As with normal records, copies are shallow: lists and dictionaries referenced by properties are not automatically deep-cloned.

This complete offline example demonstrates copying and presence-boolean serialization.

using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.Vagrant;

var version = SemanticVersion.Parse("2.4.9");
var initial = new VagrantEnvironment
{
    Home = "initial",
    NoPlugins = false,
    BoxUpdateCheckDisable = NonEmptyEnvironmentSwitch.Disabled,
};
var updated = initial with { Home = "updated" };
var values = updated.ToEnvironmentVariables(version);
Console.WriteLine(values["VAGRANT_HOME"]);
Console.WriteLine(values["VAGRANT_NO_PLUGINS"] is null);
Console.WriteLine(values["VAGRANT_BOX_UPDATE_CHECK_DISABLE"] == "");
Console.WriteLine(new VagrantEnvironment() ==
    new VagrantEnvironment { Home = null });

The results distinguish changing a value, removing an inherited presence boolean, explicitly setting an empty switch and preserving assignment history. The final equality is false even though both Home getters return null.

Non-empty switches have three semantic states

Some tools enable a feature when a variable is nonempty. A boolean cannot clearly express inheritance, activation and neutralization without an additional convention.

NonEmptyEnvironmentSwitch state Generated override Effect at the merge boundary
Inherit No entry Keep binding/process value
Enabled "1" Override with a nonempty value
Disabled "" Override with an explicit empty value

Disabled neutralizes this variable. It does not prove that another variable or CLI flag cannot enable the feature.

Presence-based booleans have different semantics: true emits a present enabling value; false emits a null dictionary entry to remove the inherited variable; null emits no entry. A nullable boolean used for this known presence protocol is different from assigning ambiguous meaning to a generic switch.

Conversions and availability

String variables preserve configured text. Enums map named C# values to wire strings. Converted values use IEnvironmentValueConverter<T> for formats such as token lists, API versions, HTTP headers and tool-specific booleans.

A variable's Since/Until interval determines whether a configured value is available for the target version. Deprecation documents a still-supported variable and is distinct from removal. Strict serialization is the default; incompatible configured entries can raise the generated compatibility exception.

Do not assume that every boolean uses 1/0 or every list uses commas. Docker headers, Git path lists and Dotnet workload notification have their own encoding. The implementation studies identify representative conversions.

Command integration

EnvironmentType connects the environment record to generated commands and builders. ToEnvironment() returns an independent dictionary and invokes the environment's version-aware serializer using the command's target version.

An absent environment returns an empty dictionary. A configured version-aware environment with no target version throws. A null target is not automatically replaced by the executor's parser-version fallback during environment serialization.

This is why a manually created command with an environment should receive its version explicitly, or be constructed through a client whose binding version is populated.

Generation-time validation

The catalogue reader and engine validate names, uniqueness, enum declarations, conversion kinds and compatibility metadata. Generated member names must not collide with record members, serializer methods or assignment flags.

An environment name unsuitable for a C# identifier uses a safe property-derived assignment flag. Errors belong to declaration or generation, rather than to the first infrastructure execution.

Environment propagation follows the resulting dictionary into Design help processes and runtime processes, where null and empty values acquire their concrete operating-system effects.

Source snapshot

Source inspection: 2 October 2026. These implementation paths are relative to Net/FrenchExDev in the local ecosystem checkout. The v2 changes are largely uncommitted; these paths identify inspected code, without claiming that public branch links contain it.

  • BinaryWrapper/doc/ENVIRONMENT-GENERATION.md
  • BinaryWrapper/src/FrenchExDev.Net.BinaryWrapper.Design.Lib/EnvironmentDefinition.cs
  • BinaryWrapper/src/FrenchExDev.Net.BinaryWrapper.Design.Lib/EnvironmentCatalogReader.cs
  • BinaryWrapper/src/FrenchExDev.Net.BinaryWrapper.Design.Lib/EnvironmentClassGenerator.cs
  • BinaryWrapper/src/FrenchExDev.Net.BinaryWrapper.Contracts/NonEmptyEnvironmentSwitch.cs
  • Vagrant/src/FrenchExDev.Net.Vagrant.Design/VagrantEnvironmentCatalog.cs

Previous · Series index · Next

⬇ Download