Skip to content
Merged
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
Expand Up @@ -87,4 +87,26 @@ public static IResourceBuilder<DbGateContainerResource> AddDbGate(this IDistribu
return dbGateContainerBuilder;
}
}

/// <summary>
/// Sanitizes a resource name to be used as a connection ID in DbGate environment variables.
/// </summary>
/// <param name="resourceName">The resource name to sanitize.</param>
/// <returns>A sanitized connection ID safe for use in environment variable names.</returns>
/// <remarks>
/// <para>
/// This method performs basic sanitization by replacing hyphens with underscores, as hyphens are not valid
/// in Linux environment variable names.
/// </para>
/// <para>
/// Note: Linux environment variable names have additional constraints (must contain only letters, numbers, and underscores,
/// and cannot start with a number). This method does not validate or enforce these additional constraints.
/// Resource names should follow standard naming conventions to ensure compatibility.
/// </para>
/// </remarks>
public static string SanitizeConnectionId(string resourceName)
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

The new SanitizeConnectionId method lacks direct unit tests. While integration tests verify the sanitization works end-to-end in MySQL, there should be unit tests specifically for this method to cover edge cases like multiple hyphens, leading/trailing hyphens, empty strings (if allowed), and strings without hyphens. Consider adding tests to tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/DbGatePublicApiTests.cs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added comprehensive unit tests for SanitizeConnectionId to DbGatePublicApiTests.cs, covering edge cases including multiple hyphens, leading/trailing hyphens, empty strings, and strings without hyphens. All tests pass. (commit 46c1820)

{
ArgumentNullException.ThrowIfNull(resourceName);
return resourceName.Replace('-', '_');
}
Comment on lines +107 to +111
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

The SanitizeConnectionId method only replaces hyphens with underscores, but Linux environment variable names have additional constraints beyond just hyphens. Valid environment variable names can only contain letters, numbers, and underscores, and cannot start with a number. Consider validating or sanitizing other invalid characters as well (e.g., dots, spaces, special characters) to prevent future issues. At minimum, add a comment explaining that only hyphens are currently addressed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the documentation to explain that only hyphens are currently addressed and noted the limitations. The remarks section now clarifies that other invalid characters (dots, spaces, special characters) are not handled and resource names should follow standard naming conventions. (commit 46c1820)

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
var mongoDBServer = builder.Resource;

var name = mongoDBServer.Name;
var label = $"LABEL_{name}";
var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name);
var label = $"LABEL_{connectionId}";

// Multiple WithDbGate calls will be ignored
if (context.EnvironmentVariables.ContainsKey(label))
Expand All @@ -67,16 +68,16 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
// DbGate assumes MongoDB is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add(label, name);
context.EnvironmentVariables.Add($"URL_{name}", mongoDBServer.ConnectionStringExpression);
context.EnvironmentVariables.Add($"ENGINE_{name}", "mongo@dbgate-plugin-mongo");
context.EnvironmentVariables.Add($"URL_{connectionId}", mongoDBServer.ConnectionStringExpression);
context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "mongo@dbgate-plugin-mongo");

if (context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS") is string { Length: > 0 } connections)
{
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{name}";
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{connectionId}";
}
else
{
context.EnvironmentVariables["CONNECTIONS"] = name;
context.EnvironmentVariables["CONNECTIONS"] = connectionId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
var mySqlServer = builder.Resource;

var name = mySqlServer.Name;
var label = $"LABEL_{name}";
var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name);
var label = $"LABEL_{connectionId}";

// Multiple WithDbGate calls will be ignored
if (context.EnvironmentVariables.ContainsKey(label))
Expand All @@ -107,19 +108,19 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,

// DbGate assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address
context.EnvironmentVariables.Add(label, name);
context.EnvironmentVariables.Add($"SERVER_{name}", name);
context.EnvironmentVariables.Add($"USER_{name}", "root");
context.EnvironmentVariables.Add($"PASSWORD_{name}", mySqlServer.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_{name}", mySqlServer.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_{name}", "mysql@dbgate-plugin-mysql");
context.EnvironmentVariables.Add($"SERVER_{connectionId}", name);
context.EnvironmentVariables.Add($"USER_{connectionId}", "root");
context.EnvironmentVariables.Add($"PASSWORD_{connectionId}", mySqlServer.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_{connectionId}", mySqlServer.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "mysql@dbgate-plugin-mysql");

if (context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS") is string { Length: > 0 } connections)
{
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{name}";
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{connectionId}";
}
else
{
context.EnvironmentVariables["CONNECTIONS"] = name;
context.EnvironmentVariables["CONNECTIONS"] = connectionId;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
var postgresServer = builder.Resource;

var name = postgresServer.Name;
var label = $"LABEL_{name}";
var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name);
var label = $"LABEL_{connectionId}";

// Multiple WithDbGate calls will be ignored
if (context.EnvironmentVariables.ContainsKey(label))
Expand All @@ -111,20 +112,20 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,

// DbGate assumes Postgres is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add($"LABEL_{name}", postgresServer.Name);
context.EnvironmentVariables.Add($"SERVER_{name}", postgresServer.Name);
context.EnvironmentVariables.Add($"USER_{name}", userParameter);
context.EnvironmentVariables.Add($"PASSWORD_{name}", postgresServer.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_{name}", postgresServer.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_{name}", "postgres@dbgate-plugin-postgres");
context.EnvironmentVariables.Add($"LABEL_{connectionId}", postgresServer.Name);
context.EnvironmentVariables.Add($"SERVER_{connectionId}", postgresServer.Name);
context.EnvironmentVariables.Add($"USER_{connectionId}", userParameter);
context.EnvironmentVariables.Add($"PASSWORD_{connectionId}", postgresServer.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_{connectionId}", postgresServer.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "postgres@dbgate-plugin-postgres");

if (context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS") is string { Length: > 0 } connections)
{
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{name}";
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{connectionId}";
}
else
{
context.EnvironmentVariables["CONNECTIONS"] = name;
context.EnvironmentVariables["CONNECTIONS"] = connectionId;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,25 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
var redisResource = builder.Resource;

var name = redisResource.Name;
var lalbel = $"LABEL_{name}";
var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name);
var label = $"LABEL_{connectionId}";

// DbGate assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address
var redisUrl = redisResource.PasswordParameter is not null ?
ReferenceExpression.Create($"redis://:{redisResource.PasswordParameter}@{name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}") :
ReferenceExpression.Create($"redis://{name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}");

context.EnvironmentVariables.Add(lalbel, name);
context.EnvironmentVariables.Add($"URL_{name}", redisUrl);
context.EnvironmentVariables.Add($"ENGINE_{name}", "redis@dbgate-plugin-redis");
context.EnvironmentVariables.Add(label, name);
context.EnvironmentVariables.Add($"URL_{connectionId}", redisUrl);
context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "redis@dbgate-plugin-redis");

if (context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS") is string { Length: > 0 } connections)
{
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{name}";
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{connectionId}";
}
else
{
context.EnvironmentVariables["CONNECTIONS"] = name;
context.EnvironmentVariables["CONNECTIONS"] = connectionId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
var sqlServerResource = builder.Resource;

var name = sqlServerResource.Name;
var label = $"LABEL_{name}";
var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name);
var label = $"LABEL_{connectionId}";

// Multiple WithDbGate calls will be ignored
if (context.EnvironmentVariables.ContainsKey(label))
Expand All @@ -107,19 +108,19 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
// DbGate assumes SqlServer is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add(label, sqlServerResource.Name);
context.EnvironmentVariables.Add($"SERVER_{name}", sqlServerResource.Name);
context.EnvironmentVariables.Add($"USER_{name}", "sa");
context.EnvironmentVariables.Add($"PASSWORD_{name}", sqlServerResource.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_{name}", sqlServerResource.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_{name}", "mssql@dbgate-plugin-mssql");
context.EnvironmentVariables.Add($"SERVER_{connectionId}", sqlServerResource.Name);
context.EnvironmentVariables.Add($"USER_{connectionId}", "sa");
context.EnvironmentVariables.Add($"PASSWORD_{connectionId}", sqlServerResource.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_{connectionId}", sqlServerResource.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "mssql@dbgate-plugin-mssql");

if (context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS") is string { Length: > 0 } connections)
{
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{name}";
context.EnvironmentVariables["CONNECTIONS"] = $"{connections},{connectionId}";
}
else
{
context.EnvironmentVariables["CONNECTIONS"] = name;
context.EnvironmentVariables["CONNECTIONS"] = connectionId;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,31 @@ public void WithHostPortShouldThrowWhenBuilderIsNull()
var exception = Assert.Throws<ArgumentNullException>(action);
Assert.Equal(nameof(builder), exception.ParamName);
}

[Fact]
public void SanitizeConnectionIdShouldThrowWhenResourceNameIsNull()
{
string resourceName = null!;

var action = () => DbGateBuilderExtensions.SanitizeConnectionId(resourceName);

var exception = Assert.Throws<ArgumentNullException>(action);
Assert.Equal(nameof(resourceName), exception.ParamName);
}

[Theory]
[InlineData("mysql", "mysql")]
[InlineData("mysql-db", "mysql_db")]
[InlineData("my-sql-db", "my_sql_db")]
[InlineData("mysql_db", "mysql_db")]
[InlineData("mysql-", "mysql_")]
[InlineData("-mysql", "_mysql")]
[InlineData("--mysql--", "__mysql__")]
[InlineData("", "")]
public void SanitizeConnectionIdShouldReplaceHyphensWithUnderscores(string input, string expected)
{
var result = DbGateBuilderExtensions.SanitizeConnectionId(input);

Assert.Equal(expected, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,68 @@ public async Task WithDbGateAddsAnnotationsForMultipleMySqlResource()
Assert.Equal("mysql@dbgate-plugin-mysql", item.Value);
});
}

[Fact]
public async Task WithDbGateSanitizesResourceNameWithHyphens()
{
var builder = DistributedApplication.CreateBuilder();

var mysqlResourceBuilder = builder.AddMySql("mysql-db")
.WithDbGate();

var mysqlResource = mysqlResourceBuilder.Resource;

using var app = builder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var dbGateResource = appModel.Resources.OfType<DbGateContainerResource>().SingleOrDefault();

Assert.NotNull(dbGateResource);

Assert.Equal("dbgate", dbGateResource.Name);

var envs = await dbGateResource.GetEnvironmentVariablesAsync();

Assert.NotEmpty(envs);
Assert.Collection(envs,
item =>
{
// Connection ID should be sanitized (hyphens replaced with underscores)
Assert.Equal("LABEL_mysql_db", item.Key);
// But the label value should still be the original resource name
Assert.Equal(mysqlResource.Name, item.Value);
},
item =>
{
Assert.Equal("SERVER_mysql_db", item.Key);
Assert.Equal(mysqlResource.Name, item.Value);
},
item =>
{
Assert.Equal("USER_mysql_db", item.Key);
Assert.Equal("root", item.Value);
},
async item =>
{
Assert.Equal("PASSWORD_mysql_db", item.Key);
Assert.Equal(await mysqlResource.PasswordParameter.GetValueAsync(default), item.Value);
},
Comment thread
aaronpowell marked this conversation as resolved.
item =>
{
Assert.Equal("PORT_mysql_db", item.Key);
Assert.Equal(mysqlResource.PrimaryEndpoint.TargetPort.ToString(), item.Value);
},
item =>
{
Assert.Equal("ENGINE_mysql_db", item.Key);
Assert.Equal("mysql@dbgate-plugin-mysql", item.Value);
},
item =>
{
Assert.Equal("CONNECTIONS", item.Key);
// Connection ID in the CONNECTIONS value should also be sanitized
Assert.Equal("mysql_db", item.Value);
});
}
}
Loading