Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3fb3e9
feat(Async): Add IAsyncConfigurationReader + ParseAsync overload
dimension-zero May 23, 2026
65c81bd
test(ParseAsync): Coverage for IAsyncConfigurationReader + ParseAsync
dimension-zero May 23, 2026
657524d
Merge branch 'master' into pr/29-async-config-reader
dimension-zero May 25, 2026
0dd505c
chore: expand .gitignore with build and IDE artifacts
May 28, 2026
a796266
feat(Async): Add IAsyncConfigurationReader + ParseAsync overload
dimension-zero May 23, 2026
c2fc2ad
test(ParseAsync): Coverage for IAsyncConfigurationReader + ParseAsync
dimension-zero May 23, 2026
3acc425
chore: expand .gitignore with build and IDE artifacts
May 28, 2026
c2fc8db
chore: remove .recode tooling artifacts and ignore the directory
Jun 1, 2026
df5b6d8
fix(ParseAsync): treat faulted GetValueAsync as missing, matching syn…
Jun 1, 2026
4027d0f
Revert "chore: expand .gitignore with build and IDE artifacts"
bartelink Jun 3, 2026
66d67cd
Merge: resolved conflicts via git-sync
Jun 4, 2026
35786cd
Merge remote-tracking branch 'origin/master' into pr/29-async-config-…
bartelink Jun 5, 2026
c838e15
chore: add fsharp build state to gitignore
Jun 5, 2026
eba2d5e
chore: Remove .recode tooling artifacts and ignore the directory
dimension-zero Jun 7, 2026
0e83042
feat(Async): Redesign IAsyncConfigurationReader around batched reads
dimension-zero Jun 7, 2026
c6eda31
docs(Async): Add Azure Key Vault sample, tutorial section, failure mo…
dimension-zero Jun 7, 2026
cbfb71b
chore: Trim .gitignore to repo-specific entries
dimension-zero Jun 8, 2026
b4660fe
Merge upstream/master into pr/29-async-config-reader
dimension-zero Jun 8, 2026
69c261d
Merge branch 'master' into pr/29-async-config-reader
dimension-zero Jun 8, 2026
c901626
tidy
bartelink Jun 8, 2026
fb30fb0
feat(ConfigurationReader): FromEnvironmentVariables(prefix), FromMicr…
dimension-zero Jun 8, 2026
9890737
# This is a combination of 2 commits.
bartelink Jun 8, 2026
e0f8b92
feat(ConfigurationReader): FromEnvironmentVariables(prefix), FromMicr…
dimension-zero Jun 8, 2026
1da665b
Remove ParseConfig release-note reference (#329)
Copilot Jun 10, 2026
f0afaf3
Merge remote-tracking branch 'origin/master' into pr/29-async-config-…
bartelink Jun 10, 2026
a49a45d
chore: add bash, .NET, and IDE ignore patterns
Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 180 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,183 @@ launchSettings.json
/tools/

# Ionide
.ionide/
.ionide/

# Local Recode tooling artifacts (per-machine session data)
.recode/

# bash
.fuse_hidden*
.directory
.Trash-*
.nfs*
nohup.out

# fsharp
*.rsuser
*.userosscache
*.sln.docstates
*.env
mono_crash.*
[Dd]ebugPublic/
[Rr]eleases/
[Dd]ebug/x64/
[Dd]ebugPublic/x64/
[Rr]elease/x64/
[Rr]eleases/x64/
bin/x64/
obj/x64/
[Dd]ebug/x86/
[Dd]ebugPublic/x86/
[Rr]elease/x86/
[Rr]eleases/x86/
bin/x86/
obj/x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Oo]ut/
[Ll]og/
[Ll]ogs/
**/[Bb]in/*
Generated\ Files/
*.trx
*.VisualState.xml
TestResult.xml
nunit-*.xml
*.received.*
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
BenchmarkDotNet.Artifacts/
project.fragment.lock.json
artifacts/
.artifacts/
ScaffoldingReadMe.txt
StyleCopReport.xml
*_h.h
*.idb
*.iobj
*.ipdb
!Directory.Build.rsp
*_wpftmp.csproj
*.tlog
*.svclog
_Chutzpah*
*.opendb
*.VC.db
*.VC.VC.opendb
*.sap
*.e2e
$tf/
*.DotSettings.user
.axoCover/*
!.axoCover/settings.json
coverage*.json
coverage*.xml
coverage*.info
*.coverage
*.coveragexml
_NCrunch_*
.NCrunch_*
nCrunchTemp_*
*.mm.*
AutoTest.Net/
.sass-cache/
*.azurePubxml
*.pubxml
*.publishproj
PublishScripts/
*.nupkg
*.snupkg
**/[Pp]ackages/*
!**/[Pp]ackages/build/
*.nuget.props
*.nuget.targets
csx/
ecf/
rcf/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
*.[Cc]ache
!?*.[Cc]ache/
*.dbproj.schemaview
*.jfm
orleans.codegen.cs
ServiceFabricBackup/
*.rptproj.bak
*.mdf
*.ldf
*.ndf
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
FakesAssemblies/
*.GhostDoc.xml
.ntvs_analysis.dat
node_modules/
*.plg
*.opt
*.vbw
*.dsw
*.dsp
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
**/.paket/paket.exe
paket-files/
**/.fake/
**/.cr/personal
**/__pycache__/
*.pyc
*.tss
*.jmconfig
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
OpenCover/
ASALocalRun/
*.binlog
MSBuild_Logs/
.aws-sam
*.nvuser
**/.mfractor/
**/.localhistory/
.vshistory/
healthchecksdb
MigrationBackup/
**/.ionide/
FodyWeavers.xsd
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
*.cab
*.msi
*.msix
*.msm
*.msp

# OS/IDE
.idea/
*.swp
*.swo

1 change: 1 addition & 0 deletions Argu.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<Folder Name="/resource/">
<File Path="resource/logo.png" />
</Folder>
<Project Path="samples/Argu.Samples.AsyncConfig/Argu.Samples.AsyncConfig.fsproj" />
<Project Path="samples/Argu.Samples.Introspect/Argu.Samples.Introspect.fsproj" />
<Project Path="samples/Argu.Samples.LS/Argu.Samples.LS.fsproj" />
<Project Path="samples/Argu.Samples.SourceGenerated/Argu.Samples.SourceGenerated.fsproj" />
Expand Down
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@

<!-- Benchmark harness -->
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0"/>

<!-- Sample-only deps (Azure Key Vault async config reader) -->
<PackageVersion Include="Azure.Security.KeyVault.Secrets" Version="4.7.0"/>
<PackageVersion Include="Azure.Identity" Version="1.13.1"/>
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Add `SourceGenerator.ArguGenerate` marker [#318](https://github.com/fsprojects/Argu/pull/318) [@dimension-zero](https://github.com/dimension-zero)
* Add `ConfigurationReader.FromEnvironmentVariables(prefix : String)` [#308](https://github.com/fsprojects/Argu/pull/308) [@dimension-zero](https://github.com/dimension-zero)
* Add `ConfigurationReader.FromMicrosoftConfiguration(Microsoft.Extensions.Configuration.IConfiguration)` [#308](https://github.com/fsprojects/Argu/pull/308) [@dimension-zero](https://github.com/dimension-zero)
* Add `ConfigurationReader.FromFunctionAsync`, `ConfigurationReader.WithFallback`: Provides support and error handling for batched retrieval from remote configuration stores [#317](https://github.com/fsprojects/Argu/pull/317) [@dimension-zero](https://github.com/dimension-zero)
* Add `ArgumentParser.ParseAsync`: Handles parsing from an asynchronously loaded configuration (i.e. `ConfigurationReader.FromFunctionAsync`) [#317](https://github.com/fsprojects/Argu/pull/317) [@dimension-zero](https://github.com/dimension-zero)
* Obsolete `EqualsAssignmentAttribute`, `ColonAssignmentAttribute`, `CustomAssignmentAttribute`, `EqualsAssignmentOrSpacedAttribute`, `ColonAssignmentOrSpacedAttribute` and `CustomAssignmentOrSpacedAttribute` [#315](https://github.com/fsprojects/Argu/pull/315) [@dimension-zero](https://github.com/dimension-zero)
* Obsolete `PostProcessResult`, `PostProcessResults`, `TryPostProcessResult` [#296](https://github.com/fsprojects/Argu/pull/296) [@dimension-zero](https://github.com/dimension-zero)

Expand Down
84 changes: 84 additions & 0 deletions docs/tutorial.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,90 @@ which would yield the following:
</appSettings>
</configuration>

## Async Configuration Sources

When the configuration source is genuinely remote (a secrets vault, a
distributed key/value store, a hosted config service) the synchronous
`Parse` shape forces the caller to block on I/O at startup, and involves
a roundtrip per setting looked up.

`ParseAsync` takes an `IAsyncConfigurationReader` instead:

[lang=fsharp]
type Args =
| [<CustomAppSettings("db-host")>] DbHost of string
| [<CustomAppSettings("port")>] Port of int
interface IArgParserTemplate with
member _.Usage = "..."

let parser = ArgumentParser.Create<Args>()
// myVaultReader implements IAsyncConfigurationReader
let! results = parser.ParseAsync(argv, configurationReader = myVaultReader)

The interface is **batched by design**: `ParseAsync` collects every
`AppSettings`-mapped key in the top-level schema and issues a single
`GetValuesAsync` call. A remote source that supports batched retrieval
(e.g. AWS SSM `GetParameters`, Azure Key Vault list-and-filter) satisfies
the whole parse in one round-trip. Implementations should return only
keys that were found; absent keys are treated as missing values
(equivalent to `null` from the synchronous reader).

### When to use `ParseAsync` vs alternatives

`ParseAsync` is the right tool when (a) the configuration source is
async-native and (b) command-line arguments and remote configuration must
be merged through Argu's normal precedence rules (CLI overrides config).

Alternatives that may fit better:

* **`Microsoft.Extensions.Configuration`**: aggregate appsettings.json,
env vars, command-line, user secrets, and Key Vault behind one
`IConfiguration`, then bind to `Argu` via the
`Argu.Extensions.Configuration` adapter package. This is the right
choice for ASP.NET Core hosting and any app already invested in the
Microsoft.Extensions.* ecosystem.
* **Host-level resolution**: fetch async config in the host's startup
code, hand the resolved values to Argu's synchronous `Parse` via a
`DictionaryConfigurationReader`. Appropriate when the host already has an
async startup phase the CLI parse can hook into.

`ParseAsync` is most relevant when the config source is genuinely
per-app and the host has no other async startup phase to leverage.

### Failure modes

`ParseAsync` distinguishes three failure modes:

1. **Schema is malformed** &mdash; `ArgumentParser.Create` throws an
`ArguException` synchronously at construction time. `ParseAsync`
never reaches the reader in this case. This is programmer error;
the test suite, not the user at runtime, should be seeing such cases.
2. **Remote source fails the batch** &mdash; the `Task` returned by
`GetValuesAsync` faults (network error, auth failure, vault
unreachable). By default the fault propagates out of `ParseAsync`
as a fatal startup error. Wrap the reader with
`ConfigurationReader.WithFallback` for a fallback policy
that substitutes fallback values and/or logs to stderr etc:

[lang=fsharp]
let reader =
let handle (ex: exn) =
log.Warn("vault unavailable: {0}", ex)
readOnlyDict Seq.null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could do with adding some values here

ConfigurationReader.WithFallback(myVaultReader, handle)
let! results = parser.ParseAsync(configurationReader = reader)

3. **User input is bad** &mdash; missing mandatory parameters, type
coercion failures, conflicting subcommands. These surface as
`ArguParseException` from the synchronous parse phase Argu runs
after the batch resolves. `ArguParseException` carries a friendly
error message; the default error handler prints usage and exits.

Treat the three flavors separately when wiring host-level error
handling: `ArguException` is a build-the-app failure, `Task` faults
without a fallback wrapper are a deploy-the-app failure, and
`ArguParseException` is a user-call-the-app failure.

## More Examples

Check out the [samples](https://github.com/fsprojects/Argu/tree/master/samples)
Expand Down
18 changes: 18 additions & 0 deletions samples/Argu.Samples.AsyncConfig/Argu.Samples.AsyncConfig.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<AssemblyName>argu-async-config</AssemblyName>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Argu\Argu.fsproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Security.KeyVault.Secrets"/>
<PackageReference Include="Azure.Identity"/>
</ItemGroup>
</Project>
Loading
Loading