Skip to content

Commit

Permalink
WaitFor for Azure Storage (#5761)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchdenny authored Oct 2, 2024
1 parent 6c48677 commit d0d3ce5
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 62 deletions.
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

0 comments on commit d0d3ce5

Please sign in to comment.