Gnar.Enver.Binding
1.0.0
dotnet add package Gnar.Enver.Binding --version 1.0.0
NuGet\Install-Package Gnar.Enver.Binding -Version 1.0.0
<PackageReference Include="Gnar.Enver.Binding" Version="1.0.0" />
<PackageVersion Include="Gnar.Enver.Binding" Version="1.0.0" />
<PackageReference Include="Gnar.Enver.Binding" />
paket add Gnar.Enver.Binding --version 1.0.0
#r "nuget: Gnar.Enver.Binding, 1.0.0"
#:package Gnar.Enver.Binding@1.0.0
#addin nuget:?package=Gnar.Enver.Binding&version=1.0.0
#tool nuget:?package=Gnar.Enver.Binding&version=1.0.0
Enver.Binding
Attributes and a Roslyn source generator that bind .env values to
strongly-typed config classes.
Part of the Enver family. See the main project README for the broader ecosystem.
Quick start
dotnet add package Gnar.Enver.Binding
Mark a partial type with [EnvBindable] and the generator emits a
Bind family of static methods on it:
using Enver;
using Enver.Binding;
[EnvBindable]
public partial record DatabaseConfig(string Host, int Port)
{
public bool UseSsl { get; init; } = true;
}
// Pick one:
// 1. Bind by loading a single .env file. Missing files are silent.
var cfg = DatabaseConfig.Bind("/path/to/my/.env");
// 2. Bind by loading a path list. Later files override earlier ones, with
// shared ${VAR} interpolation across the sequence. Pair with DotEnvPaths
// to build the canonical ladder.
var cfg = DatabaseConfig.Bind(DotEnvPaths.AppDirectory()); // .env in app dir
var cfg = DatabaseConfig.Bind(DotEnvPaths.WorkingDirectory().Standard("dev")); // 4-tier ladder
// 3. Bind from an existing IEnvReader (EnvCollection, Environment.Variables,
// configuration.AsEnvReader(), ...)
var cfg = DatabaseConfig.Bind(values);
By default, property names map to UPPER_SNAKE_CASE keys
(Host → HOST, UseSsl → USE_SSL).
Generated surface
For a self-bindable type ([EnvBindable] on the type itself), the generator
emits three static factories plus a streaming Binder:
public partial record DatabaseConfig
{
public static DatabaseConfig Bind(IEnvReader reader);
public static DatabaseConfig Bind(
string path,
EnvParseOptions parseOptions = default);
public static DatabaseConfig Bind(
IEnumerable<string> paths,
EnvParseOptions parseOptions = default);
public sealed partial class Binder : EnvParser
{
public DatabaseConfig Build();
}
}
The IEnumerable<string> overload is the primitive. Pair it with
DotEnvPaths to compose the canonical ladder. Missing files are silently
skipped.
The same surface is generated on an external host ([EnvBindable<T>]),
with method names suffixed by the target's simple type name
(Configs.BindCacheConfig(...) / Configs.CacheConfigBinder). See
External host below.
Naming and prefix: [EnvConfig]
[EnvConfig] configures how member names map to keys. It does not
trigger generation on its own. Pair it with [EnvBindable] on the same
type:
[EnvBindable]
[EnvConfig("DB", KeyNaming = EnvKeyNamingConvention.UpperSnakeCase)]
public partial record DatabaseConfig(string Host, int Port);
// Reads DB_HOST and DB_PORT.
Naming conventions:
UpperSnakeCase(default):HostName→HOST_NAMESnakeCase:HostName→host_namePreserveOriginal:HostName→HostNameInherit: use the nearest enclosing[EnvConfig](falls back toUpperSnakeCase)
Prefixes and names set with [EnvKey] are not passed through
the KeyNaming convention.
Per-member overrides: [EnvKey]
[EnvBindable]
[EnvConfig("APP")]
public partial class AppConfig
{
// Map to APP_CUSTOM_NAME
[EnvKey("CUSTOM_NAME")]
public string Name { get; init; } = "";
// Map to GLOBAL_SETTING
[EnvKey(IgnorePrefix = true)]
public string GlobalSetting { get; init; } = "";
// Force optional
[EnvKey(Requirement = EnvRequirement.Optional)]
public int Port { get; init; }
// Force required
[EnvKey(Requirement = EnvRequirement.Required)]
public string? Tag { get; init; }
}
Records: when annotating a primary-constructor parameter, use the
[property: EnvKey(...)] target so the attribute lands on the generated
property rather than the parameter.
Subsections
A property whose type is itself a bindable config type is bound as a subsection: its members are read from the same flat key space, under a prefix derived from the outer property. A property is treated as a subsection when any of these hold:
- the property is annotated with
[EnvKey] - the property's type carries
[EnvConfig] - the property's type has a member annotated with
[EnvKey]
A subsection key is composed, in order, of:
- the outer type's prefix (its
[EnvConfig]prefix plus anything inherited from further-out subsections) - the subsection property's segment (its
[EnvKey]name, or the property name run through the naming convention) - the subsection type's own
[EnvConfig]prefix - the member's key
record Sub(string Val);
[EnvBindable]
partial class Base
{
[EnvKey]
public required Sub Sub { get; init; }
}
// Sub.Val -> SUB_VAL
| Attributes | Key for Sub.Val |
|---|---|
| (as above) | SUB_VAL |
Sub has [EnvConfig("K1")] |
SUB_K1_VAL |
Base has [EnvConfig("K2")] |
K2_SUB_VAL |
| both of the above | K2_SUB_K1_VAL |
Opt out of the property-name segment with an empty key:
[EnvBindable]
partial class Base
{
[EnvKey("")]
public required Sub Sub { get; init; }
}
// Sub.Val -> VAL
[EnvKey(IgnorePrefix = true)] on a subsection property drops the inherited
(ancestor) prefix but keeps the property's own segment and the subsection
type's [EnvConfig] prefix. Requiredness is controlled the same way as a
leaf member, with [EnvKey(Requirement = ...)].
Other attributes
| Attribute | Effect |
|---|---|
[EnvIgnore] |
Skip a member that would otherwise be bound. |
[EnvUri(UriKind)] |
Specify UriKind for Uri members (default Absolute). |
[EnvFormatProvider(type, memberName)] |
Point at a static IFormatProvider member used when parsing numbers, dates, etc. Applies type-wide when placed on the class/struct; per-member when placed on a field/property. |
Required vs. optional
Each member is classified as required, optional, or with-default. The generator infers from C# signals, in order:
requiredmodifier → required- Nullable value or reference type → optional
- Property initializer → optional with default
- Non-nullable type → required
- Reference type under
#nullable disable→ optional
Required members that are missing throw EnvMissingVariableException at Bind() time
with the failing key. Optional members fall back to default(T) or the
declared initializer.
Override the inferred classification with
[EnvKey(Requirement = EnvRequirement.Required | .Optional)].
Validation
Annotate members with System.ComponentModel.DataAnnotations attributes and they
are checked automatically after binding. Any failure throws
EnvValidationException at Bind() / Build() time, so an invalid environment
never reaches your app:
using System.ComponentModel.DataAnnotations;
[EnvBindable]
public partial class ServerConfig
{
[Range(1, 65535)]
public int Port { get; init; }
[StringLength(253)]
public required string Host { get; init; }
}
// Throws EnvValidationException if PORT is outside 1..65535 or HOST exceeds 253 chars.
var cfg = ServerConfig.Bind(DotEnvPaths.AppDirectory());
Validation is separate from required-ness. Whether a key must be present
is Enver's decision (the required keyword, nullability, or
[EnvKey(Requirement)]) and a missing key throws EnvMissingVariableException
before validation runs. DataAnnotations validate the value after binding and
throw EnvValidationException. The two compose: [Required] adds a non-empty
check on top of presence (so it catches HOST=), but an entirely absent key is
still reported earlier by Enver. Prefer Enver's mechanisms for presence and
DataAnnotations for value shape.
- All failures are aggregated.
EnvValidationException.Failureslists every validation message. - Custom and cross-field rules: implement
IValidatableObjecton the config type (or any subsection) for logic the attributes can't express. - Subsections validate too. Each nested config validates itself, so a
[Range]on a subsection member is enforced when the parent binds. - Discovered at compile time. The generator reads your attributes during
generation and invokes them directly, rather than scanning your type with
Validator.ValidateObject.[Display(Name = "...")](including resource-based names) feeds the error messages so they match standard DataAnnotations output.
Custom validators work the same way. Derive from ValidationAttribute:
public sealed class UppercaseAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
=> value is string s && s == s.ToUpperInvariant()
? ValidationResult.Success
: new ValidationResult($"{ctx.DisplayName} must be uppercase");
}
The typed [Range(Type, string, string)] form is not supported. It parses its
bounds reflectively at runtime, which Enver's reflection-free model can't honor. The
generator reports ENVR0020. Use the numeric [Range(min, max)] overload, a
[CustomValidation] method, or IValidatableObject instead.
Trimming and AOT
The generator never calls Validator.ValidateObject. It discovers your attributes
at compile time and either invokes them directly or synthesizes an equivalent
inline check, so validation is fully reflection-free.
However, a handful of built-in attributes carry [RequiresUnreferencedCode] on their
constructor ([MinLength], [MaxLength], [Length], [Compare]). Because of this,
you'll see an IL2026 warning at the attribute in your own source, even though
Enver's generated check for it is reflection-free:
[MinLength(3)] // warning IL2026 here, at the attribute
public string Name { get; init; } = "";
The warning isn't wrong in the general case, but it is safe to suppress if you only use the generated bindings to validate your class:
[UnconditionalSuppressMessage("Trimming", "IL2026",
Justification = "Validated only via Enver's reflection-free generated binder.")]
[MinLength(3)]
public string Name { get; init; } = "";
External host: [EnvBindable<T>]
Generate binders on a separate partial class:
public sealed record CacheConfig(int Ttl, string Region);
[EnvBindable<CacheConfig>]
public partial class Configs;
The static factories on the host are suffixed with the target's name, and the streaming binder lives alongside them:
var cfg = Configs.BindCacheConfig(reader);
var cfg = Configs.BindCacheConfig(DotEnvPaths.AppDirectory());
var binder = new Configs.CacheConfigBinder();
[EnvBindable<T>] is repeatable. One host can have binders for several targets.
Custom parsing
The generated Binder derives from EnvParser, so you can control parsing directly.
Binding directly to UTF-8 content:
var binder = new DatabaseConfig.Binder();
binder.Parse(
"""
HOST=db.internal
URL="postgres://${HOST}:5432"
"""u8
);
var cfg = binder.Build();
Supported types
string,bool,Guid,Uriint,long, and any numeric type implementingINumber<T>/IParsable<T>(including0x/0bprefix support)- Enums
- Any
IUtf8SpanParsable<T>,ISpanParsable<T>, orIParsable<T>(such asIPAddress,IPNetwork,Version)
See the main project README for the full list.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Gnar.Enver (>= 1.0.0)
-
net8.0
- Gnar.Enver (>= 1.0.0)
-
net9.0
- Gnar.Enver (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 105 | 5/30/2026 |
| 0.1.0-beta.4 | 59 | 5/23/2026 |