Add API to pass options objects to resources as environment variables
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
I’m opening this as an API proposal before implementation to confirm whether this API shape fits Aspire’s hosting model. Please provide suggestions as I am bad at choosing ergonomic APIs.
## Background and Motivation
Aspire already provides a good pattern for passing individual configuration values to resources:
```csharp
var apiKey = builder.AddParameter("ExternalApi__ApiKey", secret: true);
builder.AddProject("api")
.WithEnvironment("ExternalApi__ApiKey", apiKey);
```
This works well when the value should be represented as an Aspire parameter.
However, there is also a simpler case where the AppHost already has an options object and only needs to pass that existing object’s values to a resource using the normal .NET configuration environment variable naming convention.
For example, given this configuration shape:
```json
{
"A": {
"B": 1,
"C": false,
"D": {
"E": "aaa"
}
}
}
```
And these options types:
```csharp
public sealed class AOptions
{
public required int B { get; set; }
public required bool C { get; set; }
public required DConfiguration D { get; set; }
}
public sealed class DConfiguration
{
public required string E { get; set; }
}
```
The consuming application can bind the values using the normal options pattern:
```csharp
builder.Services.Configure(
builder.Configuration.GetSection("A"));
```
Today, the AppHost must manually map each scalar value into environment variables:
```csharp
var aOptions = new AOptions
{
B = 1,
C = false,
D = new DConfiguration
{
E = "aaa"
}
};
builder.AddProject("api")
.WithEnvironment("A__B", aOptions.B.ToString(CultureInfo.InvariantCulture))
.WithEnvironment("A__C", aOptions.C.ToString())
.WithEnvironment("A__D__E", aOptions.D.E);
```
This becomes repetitive for larger options objects and is easy to get out of sync when the options type changes.
This proposal adds a convenience API for passing an existing options object to a resource as environment variables.
This is not intended to change Aspire’s parameter model. It does not make `ParameterResource` support complex object values. It only flattens an existing object into scalar environment variables using the standard .NET configuration naming convention.
## Proposed API
```diff
namespace Aspire.Hosting;
+ public static class OptionsEnvironmentResourceBuilderExtensions
+ {
+ public static IResourceBuilder WithEnvironmentFromOptions(
+ this IResourceBuilder builder,
+ string sectionName,
+ TOptions options,
+ Action? configure = null)
+ where TResource : IResourceWithEnvironment
+ where TOptions : class;
+
+ public static IResourceBuilder WithEnvironmentFromOptions(
+ this IResourceBuilder builder,
+ string sectionName,
+ IOptions options,
+ Action? configure = null)
+ where TResource : IResourceWithEnvironment
+ where TOptions : class;
+ }
+
+ public sealed class EnvironmentFromOptionsOptions
+ {
+ public int MaxDepth { get; set; }
+
+ public bool IncludeNullValues { get; set; }
+
+ public bool IncludeCollections { get; set; }
+
+ public bool ThrowOnUnsupportedValue { get; set; }
+ }
```
The API would flatten scalar leaf values from the provided options object and call `WithEnvironment(...)` for each generated key/value pair.
The generated environment variable names should use the normal .NET configuration environment variable convention, where nested sections are separated using `__`.
For example:
```text
A:B -> A__B
A:C -> A__C
A:D:E -> A__D__E
```
## Usage Examples
Given these options types:
```csharp
public sealed class AOptions
{
public required int B { get; set; }
public required bool C { get; set; }
public required DConfiguration D { get; set; }
}
public sealed class DConfiguration
{
public required string E { get; set; }
}
```
And an existing options object in the AppHost:
```csharp
var aOptions = new AOptions
{
B = 1,
C = false,
D = new DConfiguration
{
E = "aaa"
}
};
```
A resource could receive the options as environment variables like this:
```csharp
builder.AddProject("api")
.WithEnvironmentFromOptions("A", aOptions);
```
This would be equivalent to:
```csharp
builder.AddProject("api")
.WithEnvironment("A__B", "1")
.WithEnvironment("A__C", "false")
.WithEnvironment("A__D__E", "aaa");
```
The consuming application can then bind the values normally:
```csharp
builder.Services.Configure(
builder.Configuration.GetSection("A"));
```
An overload accepting `IOptions` would allow this shape:
```csharp
IOptions aOptions = /* resolved or created by the AppHost */;
builder.AddProject("api")
.WithEnvironmentFromOptions("A", aOptions);
```
This would use `aOptions.Value` as the source object.
### Collections
If collection support is enabled, collection items should use the standard .NET configuration index convention.
For example:
```csharp
public sealed class AOptions
{
public required ItemOptions[] Items { get; set; }
}
public sealed class ItemOptions
{
public required string Name { get; set; }
}
```
Would generate:
```text
A__Items__0__Name
A__Items__1__Name
```
### Null values
By default, null values should be skipped.
For example:
```csharp
public sealed class AOptions
{
public string? OptionalValue { get; set; }
}
```
This should not emit `A__OptionalValue` unless `IncludeNullValues` is set to `true`.
## Alternative Designs
### Add a parameter-backed API
An alternative design would be to create an Aspire parameter for each scalar leaf value and then pass each parameter to the resource as an environment variable.
For example:
```csharp
builder.AddProject("api")
.WithParameterEnvironmentFromOptions("A", aOptions);
```
This would be equivalent to:
```csharp
var b = builder.AddParameter("A__B", "1");
var c = builder.AddParameter("A__C", "false");
var e = builder.AddParameter("A__D__E", "aaa");
builder.AddProject("api")
.WithEnvironment("A__B", b)
.WithEnvironment("A__C", c)
.WithEnvironment("A__D__E", e);
```
A possible API shape for that would be:
```csharp
public static IResourceBuilder WithParameterEnvironmentFromOptions(
this IResourceBuilder builder,
string sectionName,
TOptions options,
Action? configure = null)
where TResource : IResourceWithEnvironment
where TOptions : class;
```
However, this proposal intentionally does not make that the primary API.
Aspire parameters are useful when values should be externalized and supplied through configuration, environment variables, prompts, or deployment infrastructure. In this proposal, the AppHost already has the options object and just needs to give those values to a resource. Direct environment variables are the simpler behaviour for that use case.
### Add `AddParametersFromOptions(...)`
Another alternative would be to add an AppHost-level API that only creates parameters:
```csharp
public static IReadOnlyDictionary> AddParametersFromOptions(
this IDistributedApplicationBuilder builder,
string sectionName,
TOptions options,
Action? configure = null)
where TOptions : class;
```
Usage would look like this:
```csharp
var parameters = builder.AddParametersFromOptions("A", aOptions);
builder.AddProject("api")
.WithEnvironment("A__B", parameters["A__B"])
.WithEnvironment("A__C", parameters["A__C"])
.WithEnvironment("A__D__E", parameters["A__D__E"]);
```
This gives users more control, but it does not remove most of the boilerplate. The main user intent is to give a resource an options section, so a resource-level API seems more useful.
### Support only `IConfigurationSection`
Another option would be to support only configuration sections:
```csharp
builder.AddProject("api")
.WithEnvironmentFromConfigurationSection("A", builder.Configuration.GetSection("A"));
```
This avoids reflecting over arbitrary objects, but it does not cover the scenario where the AppHost already has a typed options object.
### Serialize the object into one environment variable
Another design would be to serialize the entire options object as JSON and pass one environment variable.
That is not the goal of this proposal.
The goal is to preserve normal .NET configuration binding by expanding the object into multiple scalar environment variables.
## Risks
### Object traversal complexity
Flattening an object requires traversing its properties. The implementation should not blindly walk arbitrary object graphs.
The API should:
* Use public readable properties only.
* Ignore fields.
* Ignore indexers.
* Emit only scalar leaf values.
* Track visited object references to detect cycles.
* Enforce `MaxDepth`.
* Throw by default for unsupported values.
* Skip nulls by default.
For example, this object graph should fail clearly if it contains a cycle:
```csharp
public sealed class RecursiveOptions
{
public string Name { get; set; } = "";
public RecursiveOptions? Child { get; set; }
}
```
The exception could say something like:
```text
Cannot flatten options object for section 'A'. A cycle was detected at path 'A__Child'.
```
### Unsupported value types
The API needs a clear definition of supported scalar types.
Supported scalar values could include:
```text
string
bool
byte
short
int
long
float
double
decimal
Guid
DateTime
DateTimeOffset
TimeSpan
enum
nullable versions of the above
```
Unsupported values should throw by default when `ThrowOnUnsupportedValue` is true.
### Collections
Collections need a clear convention. If supported, they should follow the normal .NET configuration index format:
```text
A__Items__0__Name
A__Items__1__Name
```
If collections are not supported initially, the API should throw a clear exception when one is encountered.
### Formatting and culture
Values should be converted using invariant formatting where applicable so generated environment variables are stable across cultures.
### Secret handling
This API does not create Aspire parameters and therefore does not model values as secret parameters.
If values need to be treated as Aspire secrets, users should continue to use `AddParameter(..., secret: true)` manually, or a separate parameter-backed API should be considered.
### API naming
The proposed name is:
```csharp
WithEnvironmentFromOptions(...)
```
This is intended to make clear that the method applies environment variables to a resource from an options object. If maintainers prefer a different name that better matches existing Aspire naming conventions, I am happy to adjust the proposal.
### Non-goals
This proposal does not aim to:
* Add complex object support to `ParameterResource`.
* Serialize an entire object into a single environment variable.
* Replace `AddParameter(...)`.
* Replace `AddParameterFromConfiguration(...)`.
* Prompt for every property in an options object.
* Infer secrets automatically from property names.
* Support arbitrary object graphs.
* Include fields or private properties.
* Replace normal `WithEnvironment(...)` usage for simple values.
Contributor guide
Assessment
This issue has not been assessed yet.