Skip to content

Commit d0d3ce5

Browse files
authored
WaitFor for Azure Storage (#5761)
1 parent 6c48677 commit d0d3ce5

File tree

12 files changed

+209
-62
lines changed

12 files changed

+209
-62
lines changed

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/AzureStorageEndToEnd.ApiService.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<AspireProjectOrPackageReference Include="Aspire.Azure.Storage.Blobs" />
11+
<AspireProjectOrPackageReference Include="Aspire.Azure.Storage.Queues" />
1112
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
1213
</ItemGroup>
1314

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Azure.Storage.Blobs;
5+
using Azure.Storage.Queues;
56

67
var builder = WebApplication.CreateBuilder(args);
78

89
builder.AddServiceDefaults();
910

1011
builder.AddAzureBlobClient("blobs");
12+
builder.AddAzureQueueClient("queues");
1113

1214
var app = builder.Build();
1315

1416
app.MapDefaultEndpoints();
15-
app.MapGet("/", async (BlobServiceClient bsc) =>
17+
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc) =>
1618
{
1719
var container = bsc.GetBlobContainerClient("mycontainer");
1820
await container.CreateIfNotExistsAsync();
@@ -29,6 +31,10 @@
2931
blobNames.Add(blob.Name);
3032
}
3133

34+
var queue = qsc.GetQueueClient("myqueue");
35+
await queue.CreateIfNotExistsAsync();
36+
await queue.SendMessageAsync("Hello, world!");
37+
3238
return blobNames;
3339
});
3440

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
3+
34
var builder = DistributedApplication.CreateBuilder(args);
45

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

1011
var blobs = storage.AddBlobs("blobs");
12+
var queues = storage.AddQueues("queues");
1113

1214
builder.AddProject<Projects.AzureStorageEndToEnd_ApiService>("api")
1315
.WithExternalHttpEndpoints()
14-
.WithReference(blobs);
16+
.WithReference(blobs).WaitFor(blobs)
17+
.WithReference(queues).WaitFor(queues);
1518

1619
#if !SKIP_DASHBOARD_REFERENCE
1720
// This project is only added in playground projects to support development/debugging

playground/mongo/Mongo.ApiService/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
var builder = WebApplication.CreateBuilder(args);
99

1010
builder.AddServiceDefaults();
11-
builder.AddMongoDBClient("mongo");
11+
builder.AddMongoDBClient("db");
1212

1313
var app = builder.Build();
1414

playground/mongo/Mongo.AppHost/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
var db = builder.AddMongoDB("mongo")
77
.WithMongoExpress(c => c.WithHostPort(3022))
8-
.PublishAsContainer();
8+
.AddDatabase("db");
99

1010
builder.AddProject<Projects.Mongo_ApiService>("api")
1111
.WithExternalHttpEndpoints()

src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -108,52 +108,9 @@ public static IResourceBuilder<AzureCosmosDBResource> AddAzureCosmosDB(this IDis
108108
};
109109

110110
var resource = new AzureCosmosDBResource(name, configureConstruct);
111-
112-
CosmosClient? cosmosClient = null;
113-
114-
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
115-
{
116-
var connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
117-
118-
if (connectionString == null)
119-
{
120-
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{resource.Name}' resource but the connection string was null.");
121-
}
122-
123-
cosmosClient = CreateCosmosClient(connectionString);
124-
});
125-
126-
var healthCheckKey = $"{name}_check";
127-
builder.Services.AddHealthChecks().AddAzureCosmosDB(sp =>
128-
{
129-
return cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized.");
130-
}, name: healthCheckKey);
131-
132111
return builder.AddResource(resource)
133112
.WithParameter(AzureBicepResource.KnownParameters.KeyVaultName)
134-
.WithManifestPublishingCallback(resource.WriteToManifest)
135-
.WithHealthCheck(healthCheckKey);
136-
137-
static CosmosClient CreateCosmosClient(string connectionString)
138-
{
139-
var clientOptions = new CosmosClientOptions();
140-
clientOptions.CosmosClientTelemetryOptions.DisableDistributedTracing = true;
141-
142-
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
143-
{
144-
return new CosmosClient(uri.OriginalString, new DefaultAzureCredential(), clientOptions);
145-
}
146-
else
147-
{
148-
if (CosmosUtils.IsEmulatorConnectionString(connectionString))
149-
{
150-
clientOptions.ConnectionMode = ConnectionMode.Gateway;
151-
clientOptions.LimitToEndpoint = true;
152-
}
153-
154-
return new CosmosClient(connectionString, clientOptions);
155-
}
156-
}
113+
.WithManifestPublishingCallback(resource.WriteToManifest);
157114
}
158115

159116
/// <summary>
@@ -182,6 +139,28 @@ public static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResour
182139
Tag = "latest"
183140
});
184141

142+
CosmosClient? cosmosClient = null;
143+
144+
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(builder.Resource, async (@event, ct) =>
145+
{
146+
var connectionString = await builder.Resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
147+
148+
if (connectionString == null)
149+
{
150+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
151+
}
152+
153+
cosmosClient = CreateCosmosClient(connectionString);
154+
});
155+
156+
var healthCheckKey = $"{builder.Resource.Name}_check";
157+
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureCosmosDB(sp =>
158+
{
159+
return cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized.");
160+
}, name: healthCheckKey);
161+
162+
builder.WithHealthCheck(healthCheckKey);
163+
185164
if (configureContainer != null)
186165
{
187166
var surrogate = new AzureCosmosDBEmulatorResource(builder.Resource);
@@ -190,6 +169,27 @@ public static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResour
190169
}
191170

192171
return builder;
172+
173+
static CosmosClient CreateCosmosClient(string connectionString)
174+
{
175+
var clientOptions = new CosmosClientOptions();
176+
clientOptions.CosmosClientTelemetryOptions.DisableDistributedTracing = true;
177+
178+
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
179+
{
180+
return new CosmosClient(uri.OriginalString, new DefaultAzureCredential(), clientOptions);
181+
}
182+
else
183+
{
184+
if (CosmosUtils.IsEmulatorConnectionString(connectionString))
185+
{
186+
clientOptions.ConnectionMode = ConnectionMode.Gateway;
187+
clientOptions.LimitToEndpoint = true;
188+
}
189+
190+
return new CosmosClient(connectionString, clientOptions);
191+
}
192+
}
193193
}
194194

195195
/// <summary>

src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
1717
</ItemGroup>
1818

19+
<ItemGroup>
20+
<PackageReference Include="AspNetCore.HealthChecks.Azure.Storage.Blobs" />
21+
</ItemGroup>
22+
1923
<ItemGroup>
2024
<ProjectReference Include="..\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
2125
<PackageReference Include="Azure.Provisioning" />

src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
using Aspire.Hosting.Azure;
77
using Aspire.Hosting.Azure.Storage;
88
using Aspire.Hosting.Utils;
9+
using Azure.Identity;
910
using Azure.Provisioning;
1011
using Azure.Provisioning.Storage;
12+
using Azure.Storage.Blobs;
13+
using Microsoft.Extensions.DependencyInjection;
1114

1215
namespace Aspire.Hosting;
1316

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

121+
BlobServiceClient? blobServiceClient = null;
122+
123+
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (@event, ct) =>
124+
{
125+
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
126+
127+
if (connectionString == null)
128+
{
129+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
130+
}
131+
132+
blobServiceClient = CreateBlobServiceClient(connectionString);
133+
});
134+
135+
var healthCheckKey = $"{builder.Resource.Name}_check";
136+
137+
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp =>
138+
{
139+
return blobServiceClient ?? throw new InvalidOperationException("BlobServiceClient is not initialized.");
140+
}, name: healthCheckKey);
141+
142+
builder.WithHealthCheck(healthCheckKey);
143+
118144
if (configureContainer != null)
119145
{
120146
var surrogate = new AzureStorageEmulatorResource(builder.Resource);
@@ -123,6 +149,18 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
123149
}
124150

125151
return builder;
152+
153+
static BlobServiceClient CreateBlobServiceClient(string connectionString)
154+
{
155+
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
156+
{
157+
return new BlobServiceClient(uri, new DefaultAzureCredential());
158+
}
159+
else
160+
{
161+
return new BlobServiceClient(connectionString);
162+
}
163+
}
126164
}
127165

128166
/// <summary>
@@ -196,7 +234,6 @@ public static IResourceBuilder<AzureStorageEmulatorResource> WithTablePort(this
196234
public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResourceBuilder<AzureStorageResource> builder, [ResourceName] string name)
197235
{
198236
var resource = new AzureBlobStorageResource(name, builder.Resource);
199-
200237
return builder.ApplicationBuilder.AddResource(resource);
201238
}
202239

@@ -209,7 +246,6 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
209246
public static IResourceBuilder<AzureTableStorageResource> AddTables(this IResourceBuilder<AzureStorageResource> builder, [ResourceName] string name)
210247
{
211248
var resource = new AzureTableStorageResource(name, builder.Resource);
212-
213249
return builder.ApplicationBuilder.AddResource(resource);
214250
}
215251

@@ -222,7 +258,6 @@ public static IResourceBuilder<AzureTableStorageResource> AddTables(this IResour
222258
public static IResourceBuilder<AzureQueueStorageResource> AddQueues(this IResourceBuilder<AzureStorageResource> builder, [ResourceName] string name)
223259
{
224260
var resource = new AzureQueueStorageResource(name, builder.Resource);
225-
226261
return builder.ApplicationBuilder.AddResource(resource);
227262
}
228263
}

src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ IDistributedApplicationEventing eventing
6868
return azureResources;
6969
}
7070

71+
private ILookup<IResource, IResourceWithParent>? _parentChildLookup;
72+
7173
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
7274
{
7375
var azureResources = GetAzureResourcesFromAppModel(appModel);
@@ -92,8 +94,15 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
9294
return;
9395
}
9496

95-
// Create a map of parents to their children used to propagate state changes later.
96-
var parentChildLookup = appModel.Resources.OfType<IResourceWithParent>().ToLookup(r => r.Parent);
97+
static IResource? SelectParentResource(IResource resource) => resource switch
98+
{
99+
IAzureResource ar => ar,
100+
IResourceWithParent rp => SelectParentResource(rp.Parent),
101+
_ => null
102+
};
103+
104+
// Create a map of parents to their children used to propogate state changes later.
105+
_parentChildLookup = appModel.Resources.OfType<IResourceWithParent>().ToLookup(r => r.Parent);
97106

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

111120
// We basically want child resources to be moved into the same state as their parent resources whenever
112121
// there is a state update. This is done for us in DCP so we replicate the behavior here in the Azure Provisioner.
113-
var childResources = parentChildLookup[resource.Resource];
122+
var childResources = _parentChildLookup[resource.Resource];
114123
foreach (var child in childResources)
115124
{
116125
await notificationService.PublishUpdateAsync(child, stateFactory).ConfigureAwait(false);
@@ -327,6 +336,15 @@ async Task PublishConnectionStringAvailableEventAsync()
327336
{
328337
var connectionStringAvailableEvent = new ConnectionStringAvailableEvent(resource.Resource, serviceProvider);
329338
await eventing.PublishAsync(connectionStringAvailableEvent, cancellationToken).ConfigureAwait(false);
339+
340+
if (_parentChildLookup![resource.Resource] is { } children)
341+
{
342+
foreach (var child in children.OfType<IResourceWithConnectionString>())
343+
{
344+
var childConnectionStringAvailableEvent = new ConnectionStringAvailableEvent(child, serviceProvider);
345+
await eventing.PublishAsync(childConnectionStringAvailableEvent, cancellationToken).ConfigureAwait(false);
346+
}
347+
}
330348
}
331349
}
332350

src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ public static IResourceBuilder<MongoDBDatabaseResource> AddDatabase(this IResour
101101
builder.Resource.AddDatabase(name, databaseName);
102102
var mongoDBDatabase = new MongoDBDatabaseResource(name, databaseName, builder.Resource);
103103

104+
string? connectionString = null;
105+
106+
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(mongoDBDatabase, async (@event, ct) =>
107+
{
108+
connectionString = await mongoDBDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
109+
110+
if (connectionString == null)
111+
{
112+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{mongoDBDatabase.Name}' resource but the connection string was null.");
113+
}
114+
});
115+
116+
var healthCheckKey = $"{name}_check";
117+
builder.ApplicationBuilder.Services.AddHealthChecks().AddMongoDb(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);
118+
104119
return builder.ApplicationBuilder
105120
.AddResource(mongoDBDatabase);
106121
}

0 commit comments

Comments
 (0)