Skip to content
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

WaitFor for Azure Storage #5761

Merged
merged 16 commits into from
Oct 2, 2024
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 @@ -8,6 +8,7 @@

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Azure.Storage.Blobs" />
<AspireProjectOrPackageReference Include="Aspire.Azure.Storage.Queues" />
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Storage.Blobs;
using Azure.Storage.Queues;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.AddAzureBlobClient("blobs");
builder.AddAzureQueueClient("queues");

var app = builder.Build();

app.MapDefaultEndpoints();
app.MapGet("/", async (BlobServiceClient bsc) =>
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc) =>
{
var container = bsc.GetBlobContainerClient("mycontainer");
await container.CreateIfNotExistsAsync();
Expand All @@ -29,6 +31,10 @@
blobNames.Add(blob.Name);
}

var queue = qsc.GetQueueClient("myqueue");
await queue.CreateIfNotExistsAsync();
await queue.SendMessageAsync("Hello, world!");

return blobNames;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

var builder = DistributedApplication.CreateBuilder(args);

var storage = builder.AddAzureStorage("storage").RunAsEmulator(container =>
Expand All @@ -8,10 +9,12 @@
});

var blobs = storage.AddBlobs("blobs");
var queues = storage.AddQueues("queues");

builder.AddProject<Projects.AzureStorageEndToEnd_ApiService>("api")
.WithExternalHttpEndpoints()
.WithReference(blobs);
.WithReference(blobs).WaitFor(blobs)
.WithReference(queues).WaitFor(queues);

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
Expand Down
2 changes: 1 addition & 1 deletion playground/mongo/Mongo.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.AddMongoDBClient("mongo");
builder.AddMongoDBClient("db");

var app = builder.Build();

Expand Down
2 changes: 1 addition & 1 deletion playground/mongo/Mongo.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

var db = builder.AddMongoDB("mongo")
.WithMongoExpress(c => c.WithHostPort(3022))
.PublishAsContainer();
.AddDatabase("db");

builder.AddProject<Projects.Mongo_ApiService>("api")
.WithExternalHttpEndpoints()
Expand Down
88 changes: 44 additions & 44 deletions src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,52 +108,9 @@ public static IResourceBuilder<AzureCosmosDBResource> AddAzureCosmosDB(this IDis
};

var resource = new AzureCosmosDBResource(name, configureConstruct);

CosmosClient? cosmosClient = null;

builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
{
var connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{resource.Name}' resource but the connection string was null.");
}

cosmosClient = CreateCosmosClient(connectionString);
});

var healthCheckKey = $"{name}_check";
builder.Services.AddHealthChecks().AddAzureCosmosDB(sp =>
{
return cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized.");
}, name: healthCheckKey);

return builder.AddResource(resource)
.WithParameter(AzureBicepResource.KnownParameters.KeyVaultName)
.WithManifestPublishingCallback(resource.WriteToManifest)
.WithHealthCheck(healthCheckKey);

static CosmosClient CreateCosmosClient(string connectionString)
{
var clientOptions = new CosmosClientOptions();
clientOptions.CosmosClientTelemetryOptions.DisableDistributedTracing = true;

if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
return new CosmosClient(uri.OriginalString, new DefaultAzureCredential(), clientOptions);
}
else
{
if (CosmosUtils.IsEmulatorConnectionString(connectionString))
{
clientOptions.ConnectionMode = ConnectionMode.Gateway;
clientOptions.LimitToEndpoint = true;
}

return new CosmosClient(connectionString, clientOptions);
}
}
.WithManifestPublishingCallback(resource.WriteToManifest);
}

/// <summary>
Expand Down Expand Up @@ -182,6 +139,28 @@ public static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResour
Tag = "latest"
});

CosmosClient? cosmosClient = null;

builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(builder.Resource, async (@event, ct) =>
{
var connectionString = await builder.Resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
}

cosmosClient = CreateCosmosClient(connectionString);
});

var healthCheckKey = $"{builder.Resource.Name}_check";
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureCosmosDB(sp =>
{
return cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized.");
}, name: healthCheckKey);

builder.WithHealthCheck(healthCheckKey);

if (configureContainer != null)
{
var surrogate = new AzureCosmosDBEmulatorResource(builder.Resource);
Expand All @@ -190,6 +169,27 @@ public static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResour
}

return builder;

static CosmosClient CreateCosmosClient(string connectionString)
{
var clientOptions = new CosmosClientOptions();
clientOptions.CosmosClientTelemetryOptions.DisableDistributedTracing = true;

if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
return new CosmosClient(uri.OriginalString, new DefaultAzureCredential(), clientOptions);
}
else
{
if (CosmosUtils.IsEmulatorConnectionString(connectionString))
{
clientOptions.ConnectionMode = ConnectionMode.Gateway;
clientOptions.LimitToEndpoint = true;
}

return new CosmosClient(connectionString, clientOptions);
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.Azure.Storage.Blobs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
<PackageReference Include="Azure.Provisioning" />
Expand Down
41 changes: 38 additions & 3 deletions src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
using Aspire.Hosting.Azure;
using Aspire.Hosting.Azure.Storage;
using Aspire.Hosting.Utils;
using Azure.Identity;
using Azure.Provisioning;
using Azure.Provisioning.Storage;
using Azure.Storage.Blobs;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -115,6 +118,29 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
Tag = StorageEmulatorContainerImageTags.Tag
});

BlobServiceClient? blobServiceClient = null;

builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (@event, ct) =>
{
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
}

blobServiceClient = CreateBlobServiceClient(connectionString);
});

var healthCheckKey = $"{builder.Resource.Name}_check";

builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp =>
{
return blobServiceClient ?? throw new InvalidOperationException("BlobServiceClient is not initialized.");
}, name: healthCheckKey);

builder.WithHealthCheck(healthCheckKey);

if (configureContainer != null)
{
var surrogate = new AzureStorageEmulatorResource(builder.Resource);
Expand All @@ -123,6 +149,18 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
}

return builder;

static BlobServiceClient CreateBlobServiceClient(string connectionString)
{
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
return new BlobServiceClient(uri, new DefaultAzureCredential());
}
else
{
return new BlobServiceClient(connectionString);
}
}
}

/// <summary>
Expand Down Expand Up @@ -196,7 +234,6 @@ public static IResourceBuilder<AzureStorageEmulatorResource> WithTablePort(this
public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResourceBuilder<AzureStorageResource> builder, [ResourceName] string name)
{
var resource = new AzureBlobStorageResource(name, builder.Resource);

return builder.ApplicationBuilder.AddResource(resource);
}

Expand All @@ -209,7 +246,6 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
public static IResourceBuilder<AzureTableStorageResource> AddTables(this IResourceBuilder<AzureStorageResource> builder, [ResourceName] string name)
{
var resource = new AzureTableStorageResource(name, builder.Resource);

return builder.ApplicationBuilder.AddResource(resource);
}

Expand All @@ -222,7 +258,6 @@ public static IResourceBuilder<AzureTableStorageResource> AddTables(this IResour
public static IResourceBuilder<AzureQueueStorageResource> AddQueues(this IResourceBuilder<AzureStorageResource> builder, [ResourceName] string name)
{
var resource = new AzureQueueStorageResource(name, builder.Resource);

return builder.ApplicationBuilder.AddResource(resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ IDistributedApplicationEventing eventing
return azureResources;
}

private ILookup<IResource, IResourceWithParent>? _parentChildLookup;

public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var azureResources = GetAzureResourcesFromAppModel(appModel);
Expand All @@ -92,8 +94,15 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
return;
}

// Create a map of parents to their children used to propagate state changes later.
var parentChildLookup = appModel.Resources.OfType<IResourceWithParent>().ToLookup(r => r.Parent);
static IResource? SelectParentResource(IResource resource) => resource switch
{
IAzureResource ar => ar,
IResourceWithParent rp => SelectParentResource(rp.Parent),
_ => null
};

// Create a map of parents to their children used to propogate state changes later.
_parentChildLookup = appModel.Resources.OfType<IResourceWithParent>().ToLookup(r => r.Parent);

// Sets the state of the resource and all of its children
async Task UpdateStateAsync((IResource Resource, IAzureResource AzureResource) resource, Func<CustomResourceSnapshot, CustomResourceSnapshot> stateFactory)
Expand All @@ -110,7 +119,7 @@ async Task UpdateStateAsync((IResource Resource, IAzureResource AzureResource) r

// We basically want child resources to be moved into the same state as their parent resources whenever
// there is a state update. This is done for us in DCP so we replicate the behavior here in the Azure Provisioner.
var childResources = parentChildLookup[resource.Resource];
var childResources = _parentChildLookup[resource.Resource];
foreach (var child in childResources)
{
await notificationService.PublishUpdateAsync(child, stateFactory).ConfigureAwait(false);
Expand Down Expand Up @@ -327,6 +336,15 @@ async Task PublishConnectionStringAvailableEventAsync()
{
var connectionStringAvailableEvent = new ConnectionStringAvailableEvent(resource.Resource, serviceProvider);
await eventing.PublishAsync(connectionStringAvailableEvent, cancellationToken).ConfigureAwait(false);

if (_parentChildLookup![resource.Resource] is { } children)
{
foreach (var child in children.OfType<IResourceWithConnectionString>())
{
var childConnectionStringAvailableEvent = new ConnectionStringAvailableEvent(child, serviceProvider);
await eventing.PublishAsync(childConnectionStringAvailableEvent, cancellationToken).ConfigureAwait(false);
}
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ public static IResourceBuilder<MongoDBDatabaseResource> AddDatabase(this IResour
builder.Resource.AddDatabase(name, databaseName);
var mongoDBDatabase = new MongoDBDatabaseResource(name, databaseName, builder.Resource);

string? connectionString = null;

builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(mongoDBDatabase, async (@event, ct) =>
{
connectionString = await mongoDBDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{mongoDBDatabase.Name}' resource but the connection string was null.");
}
});

var healthCheckKey = $"{name}_check";
builder.ApplicationBuilder.Services.AddHealthChecks().AddMongoDb(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);

return builder.ApplicationBuilder
.AddResource(mongoDBDatabase);
}
Expand Down
Loading