From 747b234da476a3948fa3c0c9153abfbcd110b806 Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Wed, 7 May 2025 22:39:28 +0330 Subject: [PATCH 1/2] Add Adminer support for SqlServer --- ...lkit.Aspire.Hosting.Adminer.AppHost.csproj | 1 + .../Program.cs | 10 +++ .../Program.cs | 6 +- ...Aspire.Hosting.SqlServer.Extensions.csproj | 5 ++ .../README.md | 5 +- .../SqlServerBuilderExtensions.cs | 82 +++++++++++++++++++ 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj index 625db718c..2ab3cad52 100644 --- a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj @@ -15,6 +15,7 @@ + diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs index 325c7d91c..c0f47d0d1 100644 --- a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs @@ -10,4 +10,14 @@ postgres2.AddDatabase("db3"); postgres2.AddDatabase("db4"); +var sqlserver1 = builder.AddSqlServer("sqlserver1") + .WithAdminer(); +sqlserver1.AddDatabase("db5"); +sqlserver1.AddDatabase("db6"); + +var sqlserver2 = builder.AddSqlServer("sqlserver2") + .WithAdminer(); +sqlserver2.AddDatabase("db7"); +sqlserver2.AddDatabase("db8"); + builder.Build().Run(); \ No newline at end of file diff --git a/examples/sqlserver-ext/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.AppHost/Program.cs b/examples/sqlserver-ext/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.AppHost/Program.cs index c2f41067e..eb697ec13 100644 --- a/examples/sqlserver-ext/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.AppHost/Program.cs +++ b/examples/sqlserver-ext/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.AppHost/Program.cs @@ -1,12 +1,14 @@ var builder = DistributedApplication.CreateBuilder(args); var sqlserver1 = builder.AddSqlServer("sqlserver1") - .WithDbGate(c => c.WithHostPort(8068)); + .WithDbGate(c => c.WithHostPort(8068)) + .WithAdminer(c => c.WithHostPort(8069)); sqlserver1.AddDatabase("db1"); sqlserver1.AddDatabase("db2"); var sqlserver2 = builder.AddSqlServer("sqlserver2") - .WithDbGate(); + .WithDbGate() + .WithAdminer(); sqlserver2.AddDatabase("db3"); sqlserver2.AddDatabase("db4"); diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj b/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj index 79488bab6..4d6914c83 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj @@ -11,9 +11,14 @@ + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/README.md index d20d1e8bc..73f1ac76c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/README.md @@ -2,7 +2,7 @@ This integration contains extensions for the [SqlServer hosting package](https://nuget.org/packages/Aspire.Hosting.SqlServer) for .NET Aspire. -The integration provides support for running [DbGate](https://github.com/dbgate/dbgate) to interact with the SqlServer database. +The integration provides support for running [DbGate](https://github.com/dbgate/dbgate) and [Adminer](https://github.com/vrana/adminer) to interact with the SqlServer database. ## Getting Started @@ -20,7 +20,8 @@ Then, in the _Program.cs_ file of `AppHost`, define an SqlServer resource, then ```csharp var sqlserver = builder.AddSqlServer("sqlserver") - .WithDbGate(); + .WithDbGate() + .WithAdminer(); ``` ## Additional Information diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/SqlServerBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/SqlServerBuilderExtensions.cs index e3f351777..56fd49adc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/SqlServerBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/SqlServerBuilderExtensions.cs @@ -1,5 +1,6 @@ using Aspire.Hosting.ApplicationModel; using System.Text; +using System.Text.Json; namespace Aspire.Hosting; @@ -49,6 +50,46 @@ public static IResourceBuilder WithDbGate(this IResourc return builder; } + /// + /// Adds an administration and development platform for SqlServer to the application model using Adminer. + /// + /// + /// This version of the package defaults to the tag of the container image. + /// The SqlServer server resource builder. + /// Configuration callback for Adminer container resource. + /// The name of the container (Optional). + /// + /// Use in application host with a SqlServer resource + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var sqlserver = builder.AddSqlServer("sqlserver") + /// .WithAdminer(); + /// var db = sqlserver.AddDatabase("db"); + /// + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(db); + /// + /// builder.Build().Run(); + /// + /// + /// + /// A reference to the . + public static IResourceBuilder WithAdminer(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + containerName ??= $"{builder.Resource.Name}-adminer"; + var adminerBuilder = AdminerBuilderExtensions.AddAdminer(builder.ApplicationBuilder, containerName); + + adminerBuilder + .WithEnvironment(context => ConfigureAdminerContainer(context, builder.ApplicationBuilder)); + + configureContainer?.Invoke(adminerBuilder); + + return builder; + } + private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, IDistributedApplicationBuilder applicationBuilder) { var sqlServerInstances = applicationBuilder.Resources.OfType(); @@ -93,4 +134,45 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, } } } + + private static void ConfigureAdminerContainer(EnvironmentCallbackContext context, IDistributedApplicationBuilder applicationBuilder) + { + var sqlServerInstances = applicationBuilder.Resources.OfType(); + + string ADMINER_SERVERS = context.EnvironmentVariables.GetValueOrDefault("ADMINER_SERVERS")?.ToString() ?? string.Empty; + + var new_servers = sqlServerInstances.ToDictionary( + sqlServerServerResource => sqlServerServerResource.Name, + sqlServerServerResource => + { + return new AdminerLoginServer + { + Server = sqlServerServerResource.Name, + UserName = "sa", + Password = sqlServerServerResource.PasswordParameter.Value, + Driver = "mssql" + }; + }); + + if (string.IsNullOrEmpty(ADMINER_SERVERS)) + { + string servers_json = JsonSerializer.Serialize(new_servers); + context.EnvironmentVariables["ADMINER_SERVERS"] = servers_json; + } + else + { + var servers = JsonSerializer.Deserialize>(ADMINER_SERVERS) ?? throw new InvalidOperationException("The servers should not be null. This should never happen."); + foreach (var server in new_servers) + { + if (!servers.ContainsKey(server.Key)) + { + servers!.Add(server.Key, server.Value); + } + } + string servers_json = JsonSerializer.Serialize(servers); + context.EnvironmentVariables["ADMINER_SERVERS"] = servers_json; + } + + } + } \ No newline at end of file From 0e2b38123fada755d42dab018bf8e8333be115eb Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Wed, 7 May 2025 22:55:49 +0330 Subject: [PATCH 2/2] Add tests --- .../AddAdminerTests.cs | 36 ++++ ....Hosting.SqlServer.Extensions.Tests.csproj | 3 + .../ResourceCreationTests.cs | 157 ++++++++++++++++++ 3 files changed, 196 insertions(+) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs index 5ac142d41..7937e4deb 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs @@ -146,6 +146,16 @@ public async Task WithAdminerShouldAddAnnotationsForMultipleDatabaseTypes() var postgresResource2 = postgresResourceBuilder2.Resource; + var sqlserverResourceBuilder1 = builder.AddSqlServer("sqlserver1") + .WithAdminer(); + + var sqlserverResource1 = sqlserverResourceBuilder1.Resource; + + var sqlserverResourceBuilder2 = builder.AddSqlServer("sqlserver2") + .WithAdminer(); + + var sqlserverResource2 = sqlserverResourceBuilder2.Resource; + using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); @@ -181,6 +191,26 @@ public async Task WithAdminerShouldAddAnnotationsForMultipleDatabaseTypes() Password = postgresResource2.PasswordParameter.Value, UserName = postgresResource2.UserNameParameter?.Value ?? "postgres" } + }, + { + "sqlserver1", + new AdminerLoginServer + { + Driver = "mssql", + Server = sqlserverResource1.Name, + Password = sqlserverResource1.PasswordParameter.Value, + UserName = "sa" + } + }, + { + "sqlserver2", + new AdminerLoginServer + { + Driver = "mssql", + Server = sqlserverResource2.Name, + Password = sqlserverResource2.PasswordParameter.Value, + UserName = "sa" + } } }; @@ -201,6 +231,12 @@ public void WithAdminerShouldShouldAddOneAdminerResourceForMultipleDatabaseTypes builder.AddPostgres("postgres2") .WithAdminer(); + builder.AddSqlServer("sqlserver1") + .WithAdminer(); + + builder.AddSqlServer("sqlserver2") + .WithAdminer(); + using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj index 287ebec74..a46f31d14 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj @@ -4,4 +4,7 @@ + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/ResourceCreationTests.cs index 89b2c3368..6ed2589af 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/ResourceCreationTests.cs @@ -1,4 +1,5 @@ using Aspire.Hosting; +using System.Text.Json; namespace CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests; @@ -212,4 +213,160 @@ public async Task WithDbGateAddsAnnotationsForMultipleSqlServerResource() Assert.Equal("sqlserver1,sqlserver2", item.Value); }); } + + [Fact] + public async Task WithAdminerAddsAnnotations() + { + var builder = DistributedApplication.CreateBuilder(); + + var sqlServerResourceBuilder = builder.AddSqlServer("sqlserver") + .WithAdminer(); + + var sqlserverResource = sqlServerResourceBuilder.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerResource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(adminerResource); + + Assert.Equal("sqlserver-adminer", adminerResource.Name); + + var envs = await adminerResource.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + + var servers = new Dictionary + { + { + "sqlserver", + new AdminerLoginServer + { + Driver = "mssql", + Server = sqlserverResource.Name, + Password = sqlserverResource.PasswordParameter.Value, + UserName = "sa" + } + }, + }; + + var envValue = JsonSerializer.Serialize(servers); + var item = Assert.Single(envs); + Assert.Equal("ADMINER_SERVERS", item.Key); + Assert.Equal(envValue, item.Value); + } + + [Fact] + public void MultipleWithAdminerCallsAddsOneDbGateResource() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddSqlServer("sqlserver1").WithAdminer(); + builder.AddSqlServer("sqlserver2").WithAdminer(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(adminerContainer); + + Assert.Equal("sqlserver1-adminer", adminerContainer.Name); + } + + [Fact] + public void WithAdminerShouldChangeAdminerHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + var sqlserverResourceBuilder = builder.AddSqlServer("sqlserver") + .WithAdminer(c => c.WithHostPort(8068)); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(adminerContainer); + + var primaryEndpoint = adminerContainer.Annotations.OfType().Single(); + Assert.Equal(8068, primaryEndpoint.Port); + } + + [Fact] + public void WithAdminerShouldChangeAdminerContainerImageTag() + { + var builder = DistributedApplication.CreateBuilder(); + var sqlserverResourceBuilder = builder.AddSqlServer("sqlserver") + .WithAdminer(c => c.WithImageTag("manualTag")); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(adminerContainer); + + var containerImageAnnotation = adminerContainer.Annotations.OfType().Single(); + Assert.Equal("manualTag", containerImageAnnotation.Tag); + } + + [Fact] + public async Task WithAdminerAddsAnnotationsForMultipleSqlServerResource() + { + var builder = DistributedApplication.CreateBuilder(); + + var sqlserverResourceBuilder1 = builder.AddSqlServer("sqlserver1") + .WithAdminer(); + + var sqlserverResource1 = sqlserverResourceBuilder1.Resource; + + var sqlserverResourceBuilder2 = builder.AddSqlServer("sqlserver2") + .WithDbGate(); + + var sqlserverResource2 = sqlserverResourceBuilder2.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(adminerContainer); + + Assert.Equal("sqlserver1-adminer", adminerContainer.Name); + + var envs = await adminerContainer.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + + var servers = new Dictionary + { + { + "sqlserver1", + new AdminerLoginServer + { + Driver = "mssql", + Server = sqlserverResource1.Name, + Password = sqlserverResource1.PasswordParameter.Value, + UserName = "sa" + } + }, + { + "sqlserver2", + new AdminerLoginServer + { + Driver = "mssql", + Server = sqlserverResource2.Name, + Password = sqlserverResource2.PasswordParameter.Value, + UserName = "sa" + } + } + }; + + var envValue = JsonSerializer.Serialize(servers); + var item = Assert.Single(envs); + Assert.Equal("ADMINER_SERVERS", item.Key); + Assert.Equal(envValue, item.Value); + } + }