-
Notifications
You must be signed in to change notification settings - Fork 723
Create database for Sql Server resource #8022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
f50d0f6
93efeb2
8b88f42
4dda707
37a9720
ab9548d
fa47903
c3761c2
1d8456c
0a97670
8ef42a6
b9d281c
61875ef
c5e6841
98780b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,9 @@ | |
|
|
||
| using Aspire.Hosting; | ||
| using Aspire.Hosting.ApplicationModel; | ||
| using Microsoft.Data.SqlClient; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Aspire.Hosting; | ||
|
|
||
|
|
@@ -45,6 +47,42 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr | |
| } | ||
| }); | ||
|
|
||
| builder.Eventing.Subscribe<ResourceReadyEvent>(sqlServer, async (@event, ct) => | ||
| { | ||
| if (connectionString is null) | ||
| { | ||
| throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{sqlServer.Name}' resource but the connection string was null."); | ||
| } | ||
|
|
||
| using var sqlConnection = new SqlConnection(connectionString); | ||
| await sqlConnection.OpenAsync(ct).ConfigureAwait(false); | ||
|
|
||
| foreach (var sqlDatabase in sqlServer.DatabaseResources) | ||
| { | ||
| var quotedDatabaseIdentifier = new SqlCommandBuilder().QuoteIdentifier(sqlDatabase.DatabaseName); | ||
|
|
||
| try | ||
| { | ||
| if (sqlConnection.State != System.Data.ConnectionState.Open) | ||
| { | ||
| throw new InvalidOperationException($"Could not open connection to '{sqlServer.Name}'"); | ||
| } | ||
|
|
||
| var scriptAnnotation = sqlDatabase.Annotations.OfType<ScriptAnnotation>().LastOrDefault(); | ||
|
|
||
| using var command = sqlConnection.CreateCommand(); | ||
| command.CommandText = scriptAnnotation?.Script ?? | ||
| $"IF ( NOT EXISTS ( SELECT 1 FROM sys.databases WHERE name = @DatabaseName ) ) CREATE DATABASE {quotedDatabaseIdentifier};"; | ||
| command.Parameters.Add(new SqlParameter("@DatabaseName", sqlDatabase.DatabaseName)); | ||
| await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| @event.Services.GetRequiredService<ILogger<SqlServerServerResource>>().LogError(e, "Failed to create database '{DatabaseName}'", sqlDatabase.DatabaseName); | ||
|
||
| } | ||
| } | ||
| }); | ||
|
|
||
| var healthCheckKey = $"{name}_check"; | ||
| builder.Services.AddHealthChecks().AddSqlServer(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey); | ||
|
|
||
|
|
@@ -72,12 +110,33 @@ public static IResourceBuilder<SqlServerDatabaseResource> AddDatabase(this IReso | |
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentException.ThrowIfNullOrEmpty(name); | ||
|
|
||
| Dictionary<string, string> databaseConnectionStrings = []; | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Use the resource name as the database name if it's not provided | ||
| databaseName ??= name; | ||
|
|
||
| builder.Resource.AddDatabase(name, databaseName); | ||
| var sqlServerDatabase = new SqlServerDatabaseResource(name, databaseName, builder.Resource); | ||
| return builder.ApplicationBuilder.AddResource(sqlServerDatabase); | ||
|
|
||
| builder.Resource.AddDatabase(sqlServerDatabase); | ||
|
|
||
| builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(sqlServerDatabase, async (@event, ct) => | ||
| { | ||
| var databaseConnectionString = await sqlServerDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); | ||
|
|
||
| if (databaseConnectionString == null) | ||
| { | ||
| throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{name}' resource but the connection string was null."); | ||
| } | ||
|
|
||
| databaseConnectionStrings[name] = databaseConnectionString; | ||
| }); | ||
|
|
||
| var healthCheckKey = $"{name}_check"; | ||
| builder.ApplicationBuilder.Services.AddHealthChecks().AddSqlServer(sp => databaseConnectionStrings[name] ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey); | ||
|
|
||
| return builder.ApplicationBuilder | ||
| .AddResource(sqlServerDatabase) | ||
| .WithHealthCheck(healthCheckKey); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -112,4 +171,23 @@ public static IResourceBuilder<SqlServerServerResource> WithDataBindMount(this I | |
|
|
||
| return builder.WithBindMount(source, "/var/opt/mssql", isReadOnly); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Alters the JSON configuration document used by the emulator. | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// </summary> | ||
| /// <param name="builder">The builder for the <see cref="SqlServerDatabaseResource"/>.</param> | ||
| /// <param name="script">The SQL script used to create the database.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns> | ||
| /// <remarks> | ||
| /// <value>Default script is <code>IF ( NOT EXISTS ( SELECT 1 FROM sys.databases WHERE name = @DatabaseName ) ) CREATE DATABASE [<QUOTED_DATABASE_NAME%gt;];</code></value> | ||
| /// </remarks> | ||
| public static IResourceBuilder<SqlServerDatabaseResource> WithCreationScript(this IResourceBuilder<SqlServerDatabaseResource> builder, string script) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(script); | ||
|
|
||
| builder.WithAnnotation(new ScriptAnnotation(script)); | ||
|
|
||
| return builder; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Aspire.Hosting.ApplicationModel; | ||
|
|
||
| /// <summary> | ||
| /// Represents an annotation for defining a script to create a resource. | ||
| /// </summary> | ||
| public sealed class ScriptAnnotation : IResourceAnnotation | ||
|
||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="ScriptAnnotation"/> class. | ||
| /// </summary> | ||
| /// <param name="script">The script used to create the resource.</param> | ||
| public ScriptAnnotation(string script) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(script); | ||
| Script = script; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the script used to create the resource. | ||
| /// </summary> | ||
| public string Script { get; } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,8 @@ public async Task VerifyWaitForOnSqlServerBlocksDependentResources() | |
| [RequiresDocker] | ||
| public async Task VerifySqlServerResource() | ||
| { | ||
| const string databaseName = "newdb"; | ||
|
|
||
| var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); | ||
| var pipeline = new ResiliencePipelineBuilder() | ||
| .AddRetry(new() { MaxRetryAttempts = int.MaxValue, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2) }) | ||
|
|
@@ -68,18 +70,18 @@ public async Task VerifySqlServerResource() | |
| using var builder = TestDistributedApplicationBuilder.Create(o => { }, testOutputHelper); | ||
|
|
||
| var sqlserver = builder.AddSqlServer("sqlserver"); | ||
| var tempDb = sqlserver.AddDatabase("tempdb"); | ||
| var newDb = sqlserver.AddDatabase(databaseName); | ||
|
|
||
| using var app = builder.Build(); | ||
|
|
||
| await app.StartAsync(cts.Token); | ||
|
|
||
| var hb = Host.CreateApplicationBuilder(); | ||
|
|
||
| hb.Configuration[$"ConnectionStrings:{tempDb.Resource.Name}"] = await tempDb.Resource.ConnectionStringExpression.GetValueAsync(default); | ||
| hb.Configuration[$"ConnectionStrings:{newDb.Resource.Name}"] = await newDb.Resource.ConnectionStringExpression.GetValueAsync(default); | ||
|
|
||
| hb.AddSqlServerDbContext<TestDbContext>(tempDb.Resource.Name); | ||
| hb.AddSqlServerClient(tempDb.Resource.Name); | ||
| hb.AddSqlServerDbContext<TestDbContext>(newDb.Resource.Name); | ||
| hb.AddSqlServerClient(newDb.Resource.Name); | ||
|
|
||
| using var host = hb.Build(); | ||
|
|
||
|
|
@@ -119,9 +121,11 @@ await pipeline.ExecuteAsync(async token => | |
| [RequiresDocker] | ||
| public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) | ||
| { | ||
| const string databaseName = "db"; | ||
|
|
||
| string? volumeName = null; | ||
| string? bindMountPath = null; | ||
|
|
||
| var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); | ||
| var pipeline = new ResiliencePipelineBuilder() | ||
| .AddRetry(new() { MaxRetryAttempts = int.MaxValue, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2) }) | ||
|
|
@@ -132,7 +136,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) | |
| using var builder1 = TestDistributedApplicationBuilder.Create(o => { }, testOutputHelper); | ||
|
|
||
| var sqlserver1 = builder1.AddSqlServer("sqlserver"); | ||
| var masterdb1 = sqlserver1.AddDatabase("master"); | ||
| var db1 = sqlserver1.AddDatabase(databaseName); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not using |
||
|
|
||
| var password = sqlserver1.Resource.PasswordParameter.Value; | ||
|
|
||
|
|
@@ -149,7 +153,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) | |
| { | ||
| bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); | ||
|
|
||
| Directory.CreateDirectory(bindMountPath); | ||
| var directoryInfo = Directory.CreateDirectory(bindMountPath); | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (!OperatingSystem.IsWindows()) | ||
| { | ||
|
|
@@ -171,18 +175,18 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) | |
|
|
||
| await app1.StartAsync(); | ||
|
|
||
| await app1.ResourceNotifications.WaitForResourceHealthyAsync(masterdb1.Resource.Name, cts.Token); | ||
| await app1.ResourceNotifications.WaitForResourceHealthyAsync(db1.Resource.Name, cts.Token); | ||
|
|
||
| try | ||
| { | ||
| var hb1 = Host.CreateApplicationBuilder(); | ||
|
|
||
| hb1.Configuration.AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| [$"ConnectionStrings:{masterdb1.Resource.Name}"] = await masterdb1.Resource.ConnectionStringExpression.GetValueAsync(default), | ||
| [$"ConnectionStrings:{db1.Resource.Name}"] = await db1.Resource.ConnectionStringExpression.GetValueAsync(default), | ||
| }); | ||
|
|
||
| hb1.AddSqlServerClient(masterdb1.Resource.Name); | ||
| hb1.AddSqlServerClient(db1.Resource.Name); | ||
|
|
||
| using var host1 = hb1.Build(); | ||
|
|
||
|
|
@@ -239,7 +243,7 @@ await pipeline.ExecuteAsync(async token => | |
| var passwordParameter2 = builder2.AddParameter("pwd", password); | ||
|
|
||
| var sqlserver2 = builder2.AddSqlServer("sqlserver2", passwordParameter2); | ||
| var masterdb2 = sqlserver2.AddDatabase("master"); | ||
| var db2 = sqlserver2.AddDatabase(databaseName); | ||
|
|
||
| if (useVolume) | ||
| { | ||
|
|
@@ -254,18 +258,18 @@ await pipeline.ExecuteAsync(async token => | |
| { | ||
| await app2.StartAsync(); | ||
|
|
||
| await app2.ResourceNotifications.WaitForResourceHealthyAsync(masterdb2.Resource.Name, cts.Token); | ||
| await app2.ResourceNotifications.WaitForResourceHealthyAsync(db2.Resource.Name, cts.Token); | ||
|
|
||
| try | ||
| { | ||
| var hb2 = Host.CreateApplicationBuilder(); | ||
|
|
||
| hb2.Configuration.AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| [$"ConnectionStrings:{masterdb2.Resource.Name}"] = await masterdb2.Resource.ConnectionStringExpression.GetValueAsync(default), | ||
| [$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(default), | ||
| }); | ||
|
|
||
| hb2.AddSqlServerClient(masterdb2.Resource.Name); | ||
| hb2.AddSqlServerClient(db2.Resource.Name); | ||
|
|
||
| using (var host2 = hb2.Build()) | ||
| { | ||
|
|
@@ -319,4 +323,61 @@ await pipeline.ExecuteAsync(async token => | |
| } | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| [RequiresDocker] | ||
| public async Task AddDatabaseCreatesDatabaseWithCustomScript() | ||
| { | ||
| const string databaseName = "newdb"; | ||
|
|
||
| var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); | ||
| var pipeline = new ResiliencePipelineBuilder() | ||
| .AddRetry(new() { MaxRetryAttempts = 10, BackoffType = DelayBackoffType.Linear, Delay = TimeSpan.FromSeconds(2) }) | ||
| .Build(); | ||
|
|
||
| using var builder = TestDistributedApplicationBuilder.Create(o => { }, testOutputHelper); | ||
|
|
||
| var sqlserver = builder.AddSqlServer("sqlserver"); | ||
|
|
||
| // Create a datbase with Accent Insensitive collation | ||
sebastienros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var newDb = sqlserver.AddDatabase(databaseName) | ||
| .WithCreationScript($"CREATE DATABASE [{databaseName}] COLLATE French_CI_AI;"); | ||
|
|
||
| using var app = builder.Build(); | ||
|
|
||
| await app.StartAsync(cts.Token); | ||
|
|
||
| var hb = Host.CreateApplicationBuilder(); | ||
|
|
||
| hb.Configuration[$"ConnectionStrings:{newDb.Resource.Name}"] = await newDb.Resource.ConnectionStringExpression.GetValueAsync(default); | ||
|
|
||
| hb.AddSqlServerClient(newDb.Resource.Name); | ||
|
|
||
| using var host = hb.Build(); | ||
|
|
||
| await host.StartAsync(); | ||
|
|
||
| await app.ResourceNotifications.WaitForResourceHealthyAsync(newDb.Resource.Name, cts.Token); | ||
|
|
||
| // Test SqlConnection | ||
| await pipeline.ExecuteAsync(async token => | ||
| { | ||
| var conn = host.Services.GetRequiredService<SqlConnection>(); | ||
|
|
||
| if (conn.State != System.Data.ConnectionState.Open) | ||
| { | ||
| await conn.OpenAsync(token); | ||
| } | ||
|
|
||
| var selectCommand = conn.CreateCommand(); | ||
| selectCommand.CommandText = """ | ||
| CREATE TABLE [Modèles] ([Name] nvarchar(max) NOT NULL); | ||
| INSERT INTO [Modèles] ([Name]) VALUES ('BatMobile'); | ||
| SELECT * FROM [Modeles]; -- Incorrect accent to verify the database collation | ||
| """; | ||
|
|
||
| var results = await selectCommand.ExecuteReaderAsync(token); | ||
| Assert.True(results.HasRows); | ||
| }, cts.Token); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the difference between the name in the WHERE and the name used on the Create Database? Shouldn't they both use the same SqlParameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parameters can't be used for object names (such as database names, table names, or column names).
It's used in the query part because in that case it's a string value.