Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

var builder = DistributedApplication.CreateBuilder(args);

var sql1 = builder.AddAzureSqlServer("sql1")
var sqlLocal = builder.AddAzureSqlServer("sqlLocal")
.RunAsContainer();

var db1 = sql1.AddDatabase("db1");
var dbLocal = sqlLocal.AddDatabase("dbLocal");

var sqlAzure = builder.AddAzureSqlServer("sqlAzure");
var dbAzure = sqlAzure.AddDatabase("dbAzure");

var sql2 = builder.AddAzureSqlServer("sql2");
var db2 = sql2.AddDatabase("db2");
var sqlNoTls = builder.AddSqlServer("sqlNoTls")
.WithoutHttpsCertificate();

var dbsetup = builder.AddProject<Projects.SqlServerEndToEnd_DbSetup>("dbsetup")
.WithReference(db1).WaitFor(sql1)
.WithReference(db2).WaitFor(sql2);
.WithReference(dbLocal).WaitFor(sqlLocal)
.WithReference(dbAzure).WaitFor(sqlAzure);

builder.AddProject<Projects.SqlServerEndToEnd_ApiService>("api")
.WithExternalHttpEndpoints()
.WithReference(db1).WaitFor(db1)
.WithReference(db2).WaitFor(db2)
.WithReference(dbLocal).WaitFor(dbLocal)
.WithReference(dbAzure).WaitFor(dbAzure)
.WaitForCompletion(dbsetup);

#if !SKIP_DASHBOARD_REFERENCE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<Compile Include="$(SharedDir)StringComparers.cs" Link="Utils\StringComparers.cs" />
<Compile Include="$(SharedDir)X509Certificate2Extensions.cs" Link="Utils\X509Certificate2Extensions.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
54 changes: 53 additions & 1 deletion src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Aspire.Hosting.Utils;

namespace Aspire.Hosting;

#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

/// <summary>
/// Provides extension methods for adding SQL Server resources to the application model.
/// </summary>
Expand Down Expand Up @@ -49,7 +52,7 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr
var healthCheckKey = $"{name}_check";
builder.Services.AddHealthChecks().AddSqlServer(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);

return builder.AddResource(sqlServer)
var sqlBuilder = builder.AddResource(sqlServer)
.WithEndpoint(port: port, targetPort: 1433, name: SqlServerServerResource.PrimaryEndpointName)
.WithImage(SqlServerContainerImageTags.Image, SqlServerContainerImageTags.Tag)
.WithImageRegistry(SqlServerContainerImageTags.Registry)
Expand Down Expand Up @@ -89,6 +92,55 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr
await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false);
}
});

return sqlBuilder
.SubscribeHttpsEndpointsUpdate(ctx =>
{
var executionContext = ctx.Services.GetRequiredService<DistributedApplicationExecutionContext>();

// Dev cert versions prior to 6 don't include "127.0.0.1" in the SAN, and so won't be trusted
// So only enable TLS if we have a custom cert, or a dev cert with version 6 or higher.
if (executionContext.IsRunMode)
{
ctx.Resource.TryGetLastAnnotation<HttpsCertificateAnnotation>(out var certAnnotation);

if (certAnnotation == null || certAnnotation.UseDeveloperCertificate == true)
{
var devCertService = ctx.Services.GetRequiredService<IDeveloperCertificateService>();
Comment on lines +106 to +109
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

In run mode, the dev-cert version gate can be skipped when an HttpsCertificateAnnotation exists with UseDeveloperCertificate == null (default behavior). In that case, TLS gets enabled even for older dev certs, and the connection string will omit TrustServerCertificate, which is what this gate is trying to avoid. Consider computing whether a dev cert is in use via UseDeveloperCertificate.GetValueOrDefault(devCertService.UseForHttps) (and Certificate is null) rather than checking only == true.

Suggested change
if (certAnnotation == null || certAnnotation.UseDeveloperCertificate == true)
{
var devCertService = ctx.Services.GetRequiredService<IDeveloperCertificateService>();
var devCertService = ctx.Services.GetRequiredService<IDeveloperCertificateService>();
bool useDeveloperCertificate;
if (certAnnotation is null)
{
// No annotation: fall back to the developer certificate service setting.
useDeveloperCertificate = devCertService.UseForHttps;
}
else if (certAnnotation.Certificate is not null)
{
// A custom certificate is specified; do not treat this as using the developer certificate.
useDeveloperCertificate = false;
}
else
{
// No explicit certificate: compute effective dev-cert usage from the annotation value,
// defaulting to the developer certificate service setting when null.
useDeveloperCertificate = certAnnotation.UseDeveloperCertificate.GetValueOrDefault(devCertService.UseForHttps);
}
if (useDeveloperCertificate)
{

Copilot uses AI. Check for mistakes.
var cert = devCertService.Certificates.First();
if (cert.GetCertificateVersion() < 6)
{
Comment on lines +109 to +112
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

devCertService.Certificates.First() will throw if there are no trusted dev certificates (the service can return an empty list). Guard against an empty certificate list and skip enabling TLS (or fall back to TrustServerCertificate) when no cert is available.

Copilot uses AI. Check for mistakes.
return;
}
}
}

sqlBuilder.WithEndpoint(SqlServerServerResource.PrimaryEndpointName, x => x.TlsEnabled = true);
})
.WithContainerFiles("/var/opt/mssql/", async (ctx, ct) =>
{
var certContext = ctx.HttpsCertificateContext;

if (certContext is null)
{
return [];
}

var config = $"""
[network]
tlscert = {await certContext.CertificatePath.GetValueAsync(ct).ConfigureAwait(false)}
tlskey = {await certContext.KeyPath.GetValueAsync(ct).ConfigureAwait(false)}
forceencryption = 1
""";

return [
new ContainerFile
{
Name = "mssql.conf",
Contents = config
}
];
});
}

/// <summary>
Expand Down
47 changes: 25 additions & 22 deletions src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,19 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n
/// </summary>
public ParameterResource PasswordParameter { get; private set; }

private ReferenceExpression ConnectionString =>
ReferenceExpression.Create(
$"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};User ID={DefaultUserName};Password={PasswordParameter};TrustServerCertificate=true");
private ReferenceExpression BuildConnectionString()
{
var builder = new ReferenceExpressionBuilder();

builder.Append($"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};");
builder.Append($";User ID={UserNameReference}");
builder.Append($";Password={PasswordParameter}");
builder.Append($"{PrimaryEndpoint.GetTlsValue(
enabledValue: ReferenceExpression.Empty,
disabledValue: ReferenceExpression.Create($";TrustServerCertificate=true"))}");

return builder.Build();
}

/// <summary>
/// Gets a reference to the user name for the SQL Server.
Expand All @@ -69,19 +79,17 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n
internal ReferenceExpression BuildJdbcConnectionString(string? databaseName = null)
{
var builder = new ReferenceExpressionBuilder();
builder.AppendLiteral("jdbc:sqlserver://");
builder.Append($"{Host}");
builder.AppendLiteral(":");
builder.Append($"{Port}");

builder.Append($"jdbc:sqlserver://{Host}:{Port}");

if (!string.IsNullOrEmpty(databaseName))
{
var databaseNameReference = ReferenceExpression.Create($"{databaseName:uri}");
builder.AppendLiteral(";databaseName=");
builder.Append($"{databaseNameReference}");
builder.Append($";databaseName={databaseName:uri}");
}

builder.AppendLiteral(";trustServerCertificate=true");
builder.Append($"{PrimaryEndpoint.GetTlsValue(
enabledValue: ReferenceExpression.Empty,
disabledValue: ReferenceExpression.Create($";trustServerCertificate=true"))}");

return builder.Build();
}
Expand All @@ -90,9 +98,9 @@ internal ReferenceExpression BuildJdbcConnectionString(string? databaseName = nu
/// Gets the JDBC connection string for the SQL Server.
/// </summary>
/// <remarks>
/// <para>Format: <c>jdbc:sqlserver://{host}:{port};trustServerCertificate=true</c>.</para>
/// <para>User and password credentials are not included in the JDBC connection string. Use the <c>Username</c> and <c>Password</c> connection properties to access credentials.</para>
/// </remarks>
/// <para>Format: <c>jdbc:sqlserver://{Host}:{Port}[;databaseName={Database}][;trustServerCertificate=true]</c>.</para>
/// <para>User and password credentials are not included in the JDBC connection string.
/// Use the <c>Username</c> and <c>Password</c> connection properties to access credentials.</para> /// </remarks>
public ReferenceExpression JdbcConnectionString => BuildJdbcConnectionString();

/// <summary>
Expand All @@ -107,23 +115,18 @@ public ReferenceExpression ConnectionStringExpression
return connectionStringAnnotation.Resource.ConnectionStringExpression;
}

return ConnectionString;
return BuildConnectionString();
}
}

/// <summary>
/// Gets the connection string for the SQL Server.
/// </summary>
/// <param name="cancellationToken"> A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
/// <returns>A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password;TrustServerCertificate=true".</returns>
/// <returns>A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password", with "TrustServerCertificate=true" appended when a certificate is not configured.</returns>
public ValueTask<string?> GetConnectionStringAsync(CancellationToken cancellationToken = default)
{
if (this.TryGetLastAnnotation<ConnectionStringRedirectAnnotation>(out var connectionStringAnnotation))
{
return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken);
}

return ConnectionString.GetValueAsync(cancellationToken);
return ConnectionStringExpression.GetValueAsync(cancellationToken);
}

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