Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7c72c7b
Introduce IResourceWithProperties
sebastienros Oct 8, 2025
21972f0
Take 2
sebastienros Oct 8, 2025
03c37b5
Support formats in ReferenceExpression
sebastienros Oct 10, 2025
858f6d4
Add ReferenceEnvironmentInjectionAnnotation
sebastienros Oct 10, 2025
bccee3d
Add common connection properties
sebastienros Oct 10, 2025
700f0ec
Add JDBC connection strings
sebastienros Oct 10, 2025
4154e0d
More resource comments
sebastienros Oct 10, 2025
3e33c1e
More resources
sebastienros Oct 10, 2025
aa5706e
Merge remote-tracking branch 'origin/main' into sebros/properties
sebastienros Oct 10, 2025
6689f8a
Fix some tests
sebastienros Oct 10, 2025
1e191c5
Verify Garnet, GH Models, Milvus,
sebastienros Oct 10, 2025
7d9dbdb
Verify more resources
sebastienros Oct 11, 2025
e02272b
Fix test
sebastienros Oct 11, 2025
95311a0
Fix breaking changes
sebastienros Oct 11, 2025
ac0397f
Update Node sample
sebastienros Oct 11, 2025
84ecae4
Revert Postgres playground
sebastienros Oct 11, 2025
5111376
Add connection properties docs
sebastienros Oct 11, 2025
50a28e6
Fix markdown linter errors
sebastienros Oct 11, 2025
0b160f7
Add extension method to configure connection properties on resources
sebastienros Oct 12, 2025
aa102a2
Add more tests
sebastienros Oct 13, 2025
9bbbd77
Fix test
sebastienros Oct 13, 2025
fdd090e
Update src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs
sebastienros Oct 13, 2025
0139957
Fixes
sebastienros Oct 13, 2025
18198d7
Fix deployment
sebastienros Oct 14, 2025
51d00e5
Merge branch 'sebros/properties' of https://github.com/dotnet/aspire …
sebastienros Oct 14, 2025
462ba39
Merge remote-tracking branch 'origin/main' into sebros/properties
sebastienros Oct 14, 2025
e7a6d75
PR feedback
sebastienros Oct 14, 2025
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
75 changes: 75 additions & 0 deletions docs/specs/connection-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Connection Properties
> Audience - Aspire contributors and integration authors implementing or consuming `IResourceWithConnectionString` resources.

## Overview
- `IResourceWithConnectionString.GetConnectionProperties()` returns a stable set of well known keys mapped to `ReferenceExpression` values that are then injected as environment variables in select resources.
- Keys describe connection metadata beyond the raw connection string so dependent resources can access specific pieces (via `GetConnectionProperty` or environment splatting).
- Keys are emitted in PascalCase; lookups are case insensitive and duplicates should be avoided.
- `WithReference(..., ReferenceEnvironmentInjectionFlags.ConnectionProperties)` splats every key as an environment variable using the reference name prefix and an uppercased key (for example `POSTGRES_HOST`).

## Common Behavior
- Prefer short, technology neutral names when one value is broadly useful (`Host`, `Port`, `Uri`).
- Use explicit prefixes only when a resource exposes multiple endpoints (`GrpcHost`, `HttpHost`).
- Child resources (`*DatabaseResource`, `OpenAIModelResource`) typically return the parent set plus a small overlay using `Union` so downstream callers see both shared and resource specific keys.
- When adding a new resource, surface the minimal property set needed for common configurations.

## Property Catalog

### Network Identity

| Key | Description | Provided By | Notes |
| --- | --- | --- | --- |
| Host | Primary hostname or address for the resource. | PostgreSQL, MySQL, SQL Server, Oracle, Redis, Garnet, Valkey, RabbitMQ, NATS, Kafka, MongoDB, Milvus. | Derived from `EndpointProperty.Host` of the primary endpoint. |
| Port | Primary TCP port number. | Same set as `Host`. | Derived from `EndpointProperty.Port`. |

### URIs and URLs

| Key | Description | Provided By | Notes |
| --- | --- | --- | --- |
| Uri | Service specific URI built from host, port, credentials, and scheme. | PostgreSQL, PostgresDatabase, MySQL, MySqlDatabase, Redis, Garnet, Valkey, RabbitMQ, NATS, Seq, MongoDBDatabase, Milvus, Qdrant (gRPC), GitHubModel, OpenAI. | Formatting rules per resource are listed in [URI Construction](#uri-construction). |

### Credentials and Secrets

| Key | Description | Provided By | Notes |
| --- | --- | --- | --- |
| Username | Login user for the primary endpoint. | PostgreSQL, MySQL, SQL Server, Oracle, RabbitMQ, NATS, MongoDB. | Defaults align with respective containers (`postgres`, `root`, `sa`, etc.). |
| Password | Login password. | PostgreSQL, MySQL, SQL Server, Oracle, Redis (when configured), Garnet (when configured), Valkey (when configured), RabbitMQ, NATS (when configured). | Omitted when the resource does not manage credentials. |
| Key | API key or token parameter. | OpenAI, GitHubModel, Qdrant. | For GitHub Models the key is a PAT or minted token. |

### Database Specific Metadata

| Key | Description | Provided By | Notes |
| --- | --- | --- | --- |
| Database | Logical database name associated with the resource. | PostgresDatabase, MySqlDatabase, SqlServerDatabase, OracleDatabase, MongoDBDatabase, MilvusDatabase. | Added on top of the parent server keys. |
| JdbcConnectionString | JDBC compatible connection string. | PostgreSQL, PostgresDatabase, MySQL, MySqlDatabase, SQL Server, SqlServerDatabase, Oracle, OracleDatabase. | Formats vary per vendor; see [JDBC Formats](#jdbc-formats). |

## URI Construction

URIs as convey connection information in a generic format that is commonly used by client SDKs. It follows the following pattern: `{SCHEME}://[[USERNAME]:[PASSWORD]@]{HOST}:{PORT}[/{RESOURCE}]`

URI components should be url-encoded to prevent parsing issues. This is important as components like the password may contain conflicting characters.

- PostgreSQL server: `postgresql://{Username}:{Password}@{Host}:{Port}` (database resources append `/{Database}`).
- MySQL server: `mysql://{Username}:{Password}@{Host}:{Port}` (database resources append `/{Database}`).
- Redis, Garnet, Valkey: `{scheme}://[:{Password}@]{Host}:{Port}` where the scheme matches the resource (`redis`, `valkey`).
- RabbitMQ: `amqp://{Username}:{Password}@{Host}:{Port}`; `ManagementUri` uses HTTP(S) per endpoint configuration.
- NATS: `nats://[Username:Password@]{Host}:{Port}` with credentials omitted when absent.
- MongoDB database: `mongodb://[Username:Password@]{Host}:{Port}/{Database}` with optional `?authSource` and `authMechanism` query string when credentials are present.
- Milvus: `http(s)://{Host}:{Port}` produced from the primary endpoint; credentials are exposed via the `Token` property.

All URIs are composed with `:uri` formatting to ensure values are escaped correctly when rendered at deploy time.

## JDBC Formats

- PostgreSQL: `jdbc:postgresql://{Host}:{Port}[/{Database}]` (database resources append the database segment).
- MySQL: `jdbc:mysql://{Host}:{Port}[/{Database}]?user={Username}&password={Password}`.
- SQL Server: `jdbc:sqlserver://{Host}:{Port};user={Username};password={Password};trustServerCertificate=true[;databaseName={Database}]`.
- Oracle: `jdbc:oracle:thin:{Username}/{Password}@//{Host}:{Port}[/{Database}]`.

## Implementation Guidance

- Reuse existing key names whenever possible; introduce new keys only for data that is unique or disambiguates multiple endpoints.
- When a resource inherits another resource's connection properties, merge the parent set first to preserve overrides and annotations.
- Prefer emitting a single URI per endpoint type; if multiple endpoints exist, use a suffix that clarifies the transport (`HttpUri`, `GrpcUri`).
- Avoid recomputing expensive values in `GetConnectionProperties`; build reusable `ReferenceExpression` instances or helper methods instead.
- Expose the values as reusable properties on the resource such that users can create their own expressions when necessary.
8 changes: 6 additions & 2 deletions playground/AspireWithNode/AspireWithNode.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting;

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");
var pass = builder.AddParameter("pass", "p@ssw0rd1");

var cache = builder
.AddRedis("cache")
.WithPassword(pass);

var weatherapi = builder.AddProject<Projects.AspireWithNode_AspNetCoreApi>("weatherapi");

Expand Down
18 changes: 2 additions & 16 deletions playground/AspireWithNode/NodeFrontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const config = {
httpsRedirectPort: process.env['HTTPS_REDIRECT_PORT'] ?? (process.env['HTTPS_PORT'] ?? 8443),
certFile: process.env['HTTPS_CERT_FILE'] ?? '',
certKeyFile: process.env['HTTPS_CERT_KEY_FILE'] ?? '',
cacheAddress: process.env['ConnectionStrings__cache'] ?? '',
cacheUrl: process.env['CACHE_URI'] ?? '',
apiServer: process.env['services__weatherapi__https__0'] ?? process.env['services__weatherapi__http__0']
Copy link
Member

Choose a reason for hiding this comment

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

We gotta work on this next 😄

};
console.log(`config: ${JSON.stringify(config)}`);
Expand All @@ -28,21 +28,7 @@ const httpsOptions = fs.existsSync(config.certFile) && fs.existsSync(config.cert
}
: { enabled: false };

// Setup connection to Redis cache
const passwordPrefix = ',password=';
let cacheConfig = {
url: `redis://${config.cacheAddress}`
};

const cachePasswordIndex = config.cacheAddress.indexOf(passwordPrefix);
if (cachePasswordIndex > 0) {
cacheConfig = {
url: `redis://${config.cacheAddress.substring(0, cachePasswordIndex)}`,
password: config.cacheAddress.substring(cachePasswordIndex + passwordPrefix.length)
}
}

const cache = createClient(cacheConfig);
const cache = createClient({ url: config.cacheUrl });
cache.on('error', err => console.error('Redis Client Error', err));
await cache.connect();

Expand Down
2 changes: 1 addition & 1 deletion playground/milvus/MilvusPlayground.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
.WithAttu();

builder.AddProject<Projects.MilvusPlayground_ApiService>("apiservice")
.WithReference(milvusdb);
.WithReference(milvusdb).WaitFor(milvusdb);

builder.Build().Run();
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,21 @@ BicepValue<string> GetHostValue(string? prefix = null, string? suffix = null)
return (AllocateParameter(output, secretType: secretType), secretType);
}

if (value is IUrlEncoderProvider encoder && encoder.ValueProvider is { } valueProvider)
{
// Evaluate the inner value to get the bicep expression
var (innerValue, secret) = ProcessValue(valueProvider, secretType: secretType, parent: parent);

var innerExpression = innerValue switch
{
ProvisioningParameter p => p.Value.Compile(),
IBicepValue b => b.Compile(),
_ => throw new ArgumentException($"Invalid expression type for url-encoding: {innerValue.GetType()}")
};

return (new FunctionCallExpression(new IdentifierExpression("uriComponent"), innerExpression), secret);
}

#pragma warning disable CS0618 // Type or member is obsolete
if (value is BicepSecretOutputReference)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ private void ProcessEndpoints()
return (AllocateParameter(output, secretType: secretType), secretType);
}

if (value is IUrlEncoderProvider encoder && encoder.ValueProvider is { } valueProvider)
{
// Evaluate the inner value to get the bicep expression
var (innerValue, secret) = ProcessValue(valueProvider, secretType: secretType, parent: parent);

var innerExpression = innerValue switch
{
ProvisioningParameter p => p.Value.Compile(),
IBicepValue b => b.Compile(),
_ => throw new ArgumentException($"Invalid expression type for url-encoding: {innerValue.GetType()}")
};

return (new FunctionCallExpression(new IdentifierExpression("uriComponent"), innerExpression), secret);
}

if (value is IAzureKeyVaultSecretReference vaultSecretReference)
{
if (parent is null)
Expand Down
49 changes: 49 additions & 0 deletions src/Aspire.Hosting.Garnet/GarnetResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public GarnetResource(string name, ParameterResource password) : this(name)
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// Gets the host endpoint reference for this resource.
/// </summary>
public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host);

/// <summary>
/// Gets the port endpoint reference for this resource.
/// </summary>
public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port);

/// <summary>
/// Gets the parameter that contains the Garnet server password.
/// </summary>
Expand Down Expand Up @@ -61,4 +71,43 @@ private ReferenceExpression BuildConnectionString()

return builder.Build();
}

/// <summary>
/// Gets the connection URI expression for the Garnet server.
/// </summary>
/// <remarks>
/// Format: <c>redis://[:{password}@]{host}:{port}</c>. The password segment is omitted when no password is configured.
/// </remarks>
public ReferenceExpression UriExpression
{
get
{
var builder = new ReferenceExpressionBuilder();
builder.AppendLiteral("redis://");

if (PasswordParameter is not null)
{
builder.Append($":{PasswordParameter:uri}@");
}

builder.Append($"{Host}");
builder.AppendLiteral(":");
builder.Append($"{Port}");

return builder.Build();
}
}

IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
{
yield return new("Host", ReferenceExpression.Create($"{Host}"));
yield return new("Port", ReferenceExpression.Create($"{Port}"));

if (PasswordParameter is not null)
{
yield return new("Password", ReferenceExpression.Create($"{PasswordParameter}"));
}

yield return new("Uri", UriExpression);
}
}
45 changes: 42 additions & 3 deletions src/Aspire.Hosting.GitHub.Models/GitHubModelResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,50 @@ public GitHubModelResource(string name, string model, ParameterResource? organiz
/// </remarks>
public ParameterResource Key { get; internal set; }

/// <summary>
/// Gets the connection string expression for the GitHub Models resource.
/// </summary>
private ReferenceExpression EndpointExpression
{
get
{
if (Organization is not null)
{
var builder = new ReferenceExpressionBuilder();
builder.AppendLiteral("https://models.github.ai/orgs/");
builder.Append($"{Organization:uri}");
builder.AppendLiteral("/inference");

return builder.Build();
}

return ReferenceExpression.Create($"https://models.github.ai/inference");
}
}

/// <summary>
/// Gets the connection string expression for the GitHub Models resource.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
Organization is not null
? ReferenceExpression.Create($"Endpoint=https://models.github.ai/orgs/{Organization}/inference;Key={Key};Model={Model}")
: ReferenceExpression.Create($"Endpoint=https://models.github.ai/inference;Key={Key};Model={Model}");
ReferenceExpression.Create($"Endpoint={EndpointExpression};Key={Key};Model={Model}");

/// <summary>
/// Gets the endpoint URI expression for the GitHub Models resource.
/// </summary>
/// <remarks>
/// Format matches the configured endpoint, for example <c>https://models.github.ai/inference</c> or <c>https://models.github.ai/orgs/{organization}/inference</c> when an organization is specified.
/// </remarks>
public ReferenceExpression UriExpression => EndpointExpression;

IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
{
yield return new("Uri", UriExpression);
yield return new("Key", ReferenceExpression.Create($"{Key}"));
yield return new("Model", ReferenceExpression.Create($"{Model}"));

if (Organization is not null)
{
yield return new("Organization", ReferenceExpression.Create($"{Organization}"));
}
}
}
16 changes: 16 additions & 0 deletions src/Aspire.Hosting.Kafka/KafkaServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public class KafkaServerResource(string name) : ContainerResource(name), IResour
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// Gets the host endpoint reference for the primary endpoint.
/// </summary>
public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host);

/// <summary>
/// Gets the port endpoint reference for the primary endpoint.
/// </summary>
public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port);

/// <summary>
/// Gets the internal endpoint for the Kafka broker. This endpoint is used for container to broker communication.
/// To connect to the Kafka broker from a host process, use <see cref="PrimaryEndpoint"/>.
Expand All @@ -36,4 +46,10 @@ public class KafkaServerResource(string name) : ContainerResource(name), IResour
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}");

IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
{
yield return new("Host", ReferenceExpression.Create($"{Host}"));
yield return new("Port", ReferenceExpression.Create($"{Port}"));
}
}
8 changes: 8 additions & 0 deletions src/Aspire.Hosting.Milvus/MilvusDatabaseResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class MilvusDatabaseResource(string name, string databaseName, MilvusServ
/// <summary>
/// Gets the connection string expression for the Milvus database.
/// </summary>
/// <remarks>
/// Format: <c>Endpoint={uri};Key={token};Database={DatabaseName}</c>.
/// </remarks>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{Parent};Database={DatabaseName}");

Expand All @@ -36,4 +39,9 @@ private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgu
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
return argument;
}

IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties() =>
Parent.CombineProperties([
new("Database", ReferenceExpression.Create($"{DatabaseName}"))
]);
}
36 changes: 35 additions & 1 deletion src/Aspire.Hosting.Milvus/MilvusServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,38 @@ public MilvusServerResource(string name, ParameterResource apiKey) : base(name)
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// Gets the host endpoint reference for this resource.
/// </summary>
public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host);

/// <summary>
/// Gets the port endpoint reference for this resource.
/// </summary>
public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port);

/// <summary>
/// Gets a valid access token to access the Milvus instance.
/// </summary>
public ReferenceExpression Token => ReferenceExpression.Create($"root:{ApiKeyParameter}");

/// <summary>
/// Gets the connection string expression for the Milvus gRPC endpoint.
/// </summary>
/// <remarks>
/// Format: <c>Endpoint={uri};Key={token}</c>.
/// </remarks>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Url)};Key=root:{ApiKeyParameter}");
$"Endpoint={UriExpression};Key={Token}");

/// <summary>
/// Gets URI expression for the Milvus instance.
/// </summary>
/// <remarks>
/// Format: <c>http://{host}:{port}</c>.
/// </remarks>
public ReferenceExpression UriExpression => ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.Url)}");

private readonly Dictionary<string, string> _databases = new Dictionary<string, string>(StringComparers.ResourceName);

Expand All @@ -53,4 +79,12 @@ internal void AddDatabase(string name, string databaseName)
{
_databases.TryAdd(name, databaseName);
}

IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
{
yield return new("Host", ReferenceExpression.Create($"{Host}"));
yield return new("Port", ReferenceExpression.Create($"{Port}"));
yield return new("Token", ReferenceExpression.Create($"{Token}"));
yield return new("Uri", UriExpression);
}
}
Loading