From 1cb7e445d810f2c4c2c4ac52e38aebe046a1c027 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 8 Jan 2024 11:27:21 +1100 Subject: [PATCH 1/4] Add Cosmos database resource. --- ...smosDBCloudApplicationBuilderExtensions.cs | 13 ++++++++ .../AzureCosmosDBDatabaseResource.cs | 22 +++++++++++++ .../ManifestGenerationTests.cs | 31 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs index ec8c0392dc..18618aa731 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs @@ -29,6 +29,19 @@ public static IResourceBuilder AddAzureCosmosDB( .WithManifestPublishingCallback(context => WriteCosmosDBToManifest(context, connection)); } + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) + { + var database = new AzureCosmosDBDatabaseResource(name, builder.Resource); + return builder.ApplicationBuilder.AddResource(database) + .WithManifestPublishingCallback(context => WriteCosmosDBDatabaseToManifest(context, database)); + } + + private static void WriteCosmosDBDatabaseToManifest(ManifestPublishingContext context, AzureCosmosDBDatabaseResource database) + { + context.Writer.WriteString("type", "azure.cosmosdb.database.v0"); + context.Writer.WriteString("parent", database.Parent.Name); + } + private static void WriteCosmosDBToManifest(ManifestPublishingContext context, AzureCosmosDBResource cosmosDb) { var connectionString = cosmosDb.GetConnectionString(); diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs new file mode 100644 index 0000000000..e1ae33486a --- /dev/null +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure.Data.Cosmos; + +/// +/// Represents a Azure Cosmos DB database. +/// +/// The resource name. +/// Parent Azure Cosmos DB account +public class AzureCosmosDBDatabaseResource(string name, AzureCosmosDBResource parent) : Resource(name), IResourceWithConnectionString, IAzureResource, IResourceWithParent +{ + public AzureCosmosDBResource Parent { get; } = parent; + + /// + /// Gets the connection string to use for this database. + /// + /// The connection string to use for this database. + public string? GetConnectionString() => Parent.GetConnectionString(); +} diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs index 7d4f342ecf..900dbe09b4 100644 --- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs +++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs @@ -407,6 +407,37 @@ public void EnsureAllAzureAppConfigurationManifestTypesHaveVersion0Suffix() Assert.Equal("azure.appconfiguration.v0", config.GetProperty("type").GetString()); } + [Fact] + public void EnsureAllAzureCosmosDBManifestTypesHaveVersion0Suffix() + { + var program = CreateTestProgramJsonDocumentManifestPublisher(); + + program.AppBuilder.AddAzureCosmosDB("cosmosconnection", "a connection string").AddDatabase("mydb1"); + program.AppBuilder.AddAzureCosmosDB("cosmosaccount").AddDatabase("mydb2"); + + // Build AppHost so that publisher can be resolved. + program.Build(); + var publisher = program.GetManifestPublisher(); + + program.Run(); + + var resources = publisher.ManifestDocument.RootElement.GetProperty("resources"); + + var connection = resources.GetProperty("cosmosconnection"); + Assert.Equal("azure.cosmosdb.connection.v0", connection.GetProperty("type").GetString()); + + var account = resources.GetProperty("cosmosaccount"); + Assert.Equal("azure.cosmosdb.account.v0", account.GetProperty("type").GetString()); + + var db1 = resources.GetProperty("mydb1"); + Assert.Equal("azure.cosmosdb.database.v0", db1.GetProperty("type").GetString()); + Assert.Equal("cosmosconnection", db1.GetProperty("parent").GetString()); + + var db2 = resources.GetProperty("mydb2"); + Assert.Equal("azure.cosmosdb.database.v0", db2.GetProperty("type").GetString()); + Assert.Equal("cosmosaccount", db2.GetProperty("parent").GetString()); + } + [Fact] public void NodeAppIsExecutableResource() { From 83d70b29eb0e6c0f3019bf9036d6aa91f2c155ca Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 9 Jan 2024 15:14:36 +1100 Subject: [PATCH 2/4] Add XML doc for AddDatabase. --- .../AzureCosmosDBCloudApplicationBuilderExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs index 18618aa731..009dc8652d 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs @@ -29,6 +29,12 @@ public static IResourceBuilder AddAzureCosmosDB( .WithManifestPublishingCallback(context => WriteCosmosDBToManifest(context, connection)); } + /// + /// Adds a resource which represents a database in the associated Cosmos DB account resource. + /// + /// AzureCosmosDB resource builder. + /// Name of database. + /// public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) { var database = new AzureCosmosDBDatabaseResource(name, builder.Resource); From e53f3d42c06f64ba786c27faa22d1dde01c66de1 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 10 Jan 2024 10:13:29 +1100 Subject: [PATCH 3/4] Provisioner working for Cosmos databases. --- .../Provisioners/AzureCosmosDBProvisioner.cs | 50 ++++++++++++++++++- .../AzureCosmosDBDatabaseResource.cs | 24 ++++++--- .../AzureCosmosDBResource.cs | 14 ++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs index f53252f661..8b55a22ef4 100644 --- a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs +++ b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs @@ -17,7 +17,24 @@ public override bool ConfigureResource(IConfiguration configuration, AzureCosmos { if (configuration.GetConnectionString(resource.Name) is string connectionString) { - resource.ConnectionString = connectionString; + if (resource.Databases.Count == 0) + { + resource.ConnectionString = connectionString; + return true; + } + + foreach (var database in resource.Databases) + { + if (configuration.GetConnectionString(database.Name) is string databaseConnectionString) + { + database.ConnectionString = databaseConnectionString; + } + else + { + return false; + } + } + return true; } @@ -76,5 +93,36 @@ public override async Task GetOrCreateResourceAsync( var connectionStrings = context.UserSecrets.Prop("ConnectionStrings"); connectionStrings[resource.Name] = resource.ConnectionString; + + foreach (var database in resource.Databases) + { + await CreateDatabaseIfNotExists(cosmosResource, database, cancellationToken).ConfigureAwait(false); + connectionStrings[database.Name] = resource.ConnectionString; // Same as parent resource. + } + } + + private async Task CreateDatabaseIfNotExists( + CosmosDBAccountResource cosmosResource, + AzureCosmosDBDatabaseResource database, + CancellationToken cancellationToken) + { + + var exists = await cosmosResource.GetCosmosDBSqlDatabases().ExistsAsync(database.Name, cancellationToken).ConfigureAwait(false); + + if (!exists) + { + logger.LogInformation("Creating Cosmos DB SQL database {CosmosAccountName}/{CosmosDatabaseName} in {Location}", cosmosResource.Data.Name, database.Name, cosmosResource.Data.Location); + + var cosmosDatabaseContent = new CosmosDBSqlDatabaseCreateOrUpdateContent(cosmosResource.Data.Location, new CosmosDBSqlDatabaseResourceInfo(database.Name)); + cosmosDatabaseContent.Tags.Add(AzureProvisioner.AspireResourceNameTag, database.Name); + + var sw = ValueStopwatch.StartNew(); + var operation = await cosmosResource.GetCosmosDBSqlDatabases() + .CreateOrUpdateAsync(WaitUntil.Completed, database.Name, cosmosDatabaseContent, cancellationToken) + .ConfigureAwait(false); + var cosmosDatabaseResource = operation.Value; + + logger.LogInformation("Cosmos DB SQL database {CosmosAccountName}/{CosmosDatabaseName} created in {Elapsed}", cosmosResource.Data.Name, cosmosDatabaseResource.Data.Name, sw.GetElapsedTime()); + } } } diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs index e1ae33486a..fafc43cc07 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBDatabaseResource.cs @@ -5,18 +5,26 @@ namespace Aspire.Hosting.Azure.Data.Cosmos; -/// -/// Represents a Azure Cosmos DB database. -/// -/// The resource name. -/// Parent Azure Cosmos DB account -public class AzureCosmosDBDatabaseResource(string name, AzureCosmosDBResource parent) : Resource(name), IResourceWithConnectionString, IAzureResource, IResourceWithParent +public class AzureCosmosDBDatabaseResource : Resource, IResourceWithConnectionString, IResourceWithParent { - public AzureCosmosDBResource Parent { get; } = parent; + /// + /// Constructor for AzureCosmosDBDatabaseResource. + /// + /// The resource name. + /// Parent Azure Cosmos DB account + public AzureCosmosDBDatabaseResource(string name, AzureCosmosDBResource parent) : base(name) + { + Parent = parent; + parent.AddDatabase(this); + } + + public string? ConnectionString { get; set; } + + public AzureCosmosDBResource Parent { get; } /// /// Gets the connection string to use for this database. /// /// The connection string to use for this database. - public string? GetConnectionString() => Parent.GetConnectionString(); + public string? GetConnectionString() => ConnectionString; } diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs index abdc063255..44350f6346 100644 --- a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.ObjectModel; using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Azure.Data.Cosmos; @@ -23,4 +24,17 @@ public class AzureCosmosDBResource(string name, string? connectionString) /// /// The connection string to use for this database. public string? GetConnectionString() => ConnectionString; + + private readonly Collection _databases = new(); + + public IReadOnlyCollection Databases => _databases; + + internal void AddDatabase(AzureCosmosDBDatabaseResource database) + { + if (database.Parent != this) + { + throw new ArgumentException("Database belongs to another server", nameof(database)); + } + _databases.Add(database); + } } From d5edc8dc74266c715667a9fda3c5d23f5861ff89 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 10 Jan 2024 18:41:28 +1100 Subject: [PATCH 4/4] Add deletion logic to provisioner for cosmos databases. --- .../Provisioners/AzureCosmosDBProvisioner.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs index 8b55a22ef4..fc23745f58 100644 --- a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs +++ b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/AzureCosmosDBProvisioner.cs @@ -94,6 +94,17 @@ public override async Task GetOrCreateResourceAsync( var connectionStrings = context.UserSecrets.Prop("ConnectionStrings"); connectionStrings[resource.Name] = resource.ConnectionString; + var existingDatabases = cosmosResource.GetCosmosDBSqlDatabases().GetAllAsync(cancellationToken); + + await foreach (var existingDatabase in existingDatabases) + { + if (!resource.Databases.Any(d => d.Name == existingDatabase.Data.Name)) + { + logger.LogInformation("Deleting database {DatabaseName}", existingDatabase.Data.Name); + await existingDatabase.DeleteAsync(WaitUntil.Completed, cancellationToken).ConfigureAwait(false); + } + } + foreach (var database in resource.Databases) { await CreateDatabaseIfNotExists(cosmosResource, database, cancellationToken).ConfigureAwait(false);