Skip to content
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 @@ -29,15 +29,35 @@ public static IResourceBuilder<TalkingClockResource> AddTalkingClock(
var tockHandResource = new ClockHandResource(name + "-tock-hand");
var clockResource = new TalkingClockResource(name, tickHandResource, tockHandResource);

builder.Eventing.Subscribe<InitializeResourceEvent>(clockResource, static async (@event, token) =>
// Add the resource instance to the Aspire application builder and configure it using fluent APIs.
var clockBuilder = builder.AddResource(clockResource)
// Use Aspire's ExcludeFromManifest to prevent this resource from being included in deployment manifests.
.ExcludeFromManifest()
// Set a URL for the resource, which will be displayed in the Aspire dashboard.
.WithUrl("https://www.speaking-clock.com/", "Speaking Clock")
// Use Aspire's WithInitialState to set an initial state snapshot for the resource.
// This provides initial metadata visible in the Aspire dashboard.
.WithInitialState(new CustomResourceSnapshot // Aspire type for custom resource state.
{
ResourceType = "TalkingClock", // A string identifying the type of resource for Aspire, this shows in the dashboard.
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted, // Use an Aspire well-known state.
// Add custom properties displayed in the Aspire dashboard's resource details.
Properties =
[
// Use Aspire's known property key for source information.
new(CustomResourceKnownProperties.Source, "Talking Clock")
]
});

clockBuilder.OnInitializeResource(static async (resource, @event, token) =>
{
// This event is published when the resource is initialized.
// You add custom logic here to establish the lifecycle for your custom resource.

var log = @event.Logger; // Get the logger for this resource instance.
var eventing = @event.Eventing; // Get the eventing service for publishing events.
var notification = @event.Notifications; // Get the notification service for state updates.
var resource = (TalkingClockResource)@event.Resource; // Get the resource instance.
var services = @event.Services; // Get the service provider for dependency injection.

// Publish an Aspire event indicating that this resource is about to start.
Expand Down Expand Up @@ -97,27 +117,6 @@ await notification.PublishUpdateAsync(resource.TockHand,
}
});

// Add the resource instance to the Aspire application builder and configure it using fluent APIs.
var clockBuilder = builder.AddResource(clockResource)
// Use Aspire's ExcludeFromManifest to prevent this resource from being included in deployment manifests.
.ExcludeFromManifest()
// Set a URL for the resource, which will be displayed in the Aspire dashboard.
.WithUrl("https://www.speaking-clock.com/", "Speaking Clock")
// Use Aspire's WithInitialState to set an initial state snapshot for the resource.
// This provides initial metadata visible in the Aspire dashboard.
.WithInitialState(new CustomResourceSnapshot // Aspire type for custom resource state.
{
ResourceType = "TalkingClock", // A string identifying the type of resource for Aspire, this shows in the dashboard.
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted, // Use an Aspire well-known state.
// Add custom properties displayed in the Aspire dashboard's resource details.
Properties =
[
// Use Aspire's known property key for source information.
new(CustomResourceKnownProperties.Source, "Talking Clock")
]
});

AddHandResource(tickHandResource);
AddHandResource(tockHandResource);

Expand Down
41 changes: 20 additions & 21 deletions playground/mongo/Mongo.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,26 @@

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

builder.Eventing.Subscribe<ResourceReadyEvent>(db.Resource, async (@event, ct) =>
{
// Artificial delay to demonstrate the waiting
await Task.Delay(TimeSpan.FromSeconds(10), ct);

// Seed the database with some data
var cs = await db.Resource.ConnectionStringExpression.GetValueAsync(ct);
using var client = new MongoClient(cs);

const string collectionName = "entries";

var myDb = client.GetDatabase("db");
await myDb.CreateCollectionAsync(collectionName, cancellationToken: ct);

for (int i = 0; i < 10; i++)
{
await myDb.GetCollection<Entry>(collectionName).InsertOneAsync(new Entry(), cancellationToken: ct);
}
});
.AddDatabase("db")
.OnResourceReady(async (db, @event, ct) =>{
// Artificial delay to demonstrate the waiting
await Task.Delay(TimeSpan.FromSeconds(10), ct);

// Seed the database with some data
//var cs = await db.Resource.ConnectionStringExpression.GetValueAsync(ct);
var cs = await db.ConnectionStringExpression.GetValueAsync(ct);
using var client = new MongoClient(cs);

const string collectionName = "entries";

var myDb = client.GetDatabase("db");
await myDb.CreateCollectionAsync(collectionName, cancellationToken: ct);

for (int i = 0; i < 10; i++)
{
await myDb.GetCollection<Entry>(collectionName).InsertOneAsync(new Entry(), cancellationToken: ct);
}
});

builder.AddProject<Projects.Mongo_ApiService>("api")
.WithExternalHttpEndpoints()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,9 @@ public static IResourceBuilder<AzureAIFoundryResource> RunAsFoundryLocal(this IR

private static IResourceBuilder<AzureAIFoundryResource> WithInitializer(this IResourceBuilder<AzureAIFoundryResource> builder)
{
builder.ApplicationBuilder.Eventing.Subscribe<InitializeResourceEvent>(builder.Resource, (@event, ct)
return builder.OnInitializeResource((resource, @event, ct)
=> Task.Run(async () =>
{
var resource = (AzureAIFoundryResource)@event.Resource;
var rns = @event.Services.GetRequiredService<ResourceNotificationService>();
var manager = @event.Services.GetRequiredService<FoundryLocalManager>();
var logger = @event.Services.GetRequiredService<ResourceLoggerService>().GetLogger(resource);
Expand Down Expand Up @@ -176,8 +175,6 @@ await rns.PublishUpdateAsync(resource, state => state with
}

}, ct));

return builder;
}

/// <summary>
Expand All @@ -187,9 +184,7 @@ internal static IResourceBuilder<AzureAIFoundryDeploymentResource> AsLocalDeploy
{
ArgumentNullException.ThrowIfNull(deployment, nameof(deployment));

var foundryResource = builder.Resource.Parent;

builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(foundryResource, (@event, ct) =>
builder.OnResourceReady((foundryResource, @event, ct) =>
{
var rns = @event.Services.GetRequiredService<ResourceNotificationService>();
var loggerService = @event.Services.GetRequiredService<ResourceLoggerService>();
Expand Down
12 changes: 5 additions & 7 deletions src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,18 @@ private static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResou
});

CosmosClient? cosmosClient = null;

builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(builder.Resource, async (@event, ct) =>
builder.OnConnectionStringAvailable(async (cosmosDb, @event, ct) =>
{
var connectionString = await builder.Resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
var connectionString = await cosmosDb.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);
});

builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(builder.Resource, async (@event, ct) =>
})
.OnResourceReady(async (cosmosDb, @event, ct) =>
{
if (cosmosClient is null)
{
Expand All @@ -110,7 +108,7 @@ private static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResou

await cosmosClient.ReadAccountAsync().WaitAsync(ct).ConfigureAwait(false);

foreach (var database in builder.Resource.Databases)
foreach (var database in cosmosDb.Databases)
{
var db = (await cosmosClient.CreateDatabaseIfNotExistsAsync(database.DatabaseName, cancellationToken: ct).ConfigureAwait(false)).Database;

Expand Down
64 changes: 33 additions & 31 deletions src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,32 +132,32 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
});

BlobServiceClient? blobServiceClient = null;
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (@event, ct) =>
{
// The BlobServiceClient is created before the health check is run.
// We can't use ConnectionStringAvailableEvent here because the resource doesn't have a connection string, so
// we use BeforeResourceStartedEvent

var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null.");
blobServiceClient = CreateBlobServiceClient(connectionString);
});

builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(builder.Resource, async (@event, ct) =>
{
// The ResourceReadyEvent of a resource is triggered after its health check is healthy.
// This means we can safely use this event to create the blob containers.

if (blobServiceClient is null)
builder
.OnBeforeResourceStarted(async (storage, @event, ct) =>
{
throw new InvalidOperationException("BlobServiceClient is not initialized.");
}

foreach (var container in builder.Resource.BlobContainers)
// The BlobServiceClient is created before the health check is run.
// We can't use ConnectionStringAvailableEvent here because the resource doesn't have a connection string, so
// we use BeforeResourceStartedEvent

var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null.");
blobServiceClient = CreateBlobServiceClient(connectionString);
})
.OnResourceReady(async (storage, @event, ct) =>
{
var blobContainerClient = blobServiceClient.GetBlobContainerClient(container.BlobContainerName);
await blobContainerClient.CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
}
});
// The ResourceReadyEvent of a resource is triggered after its health check is healthy.
// This means we can safely use this event to create the blob containers.

if (blobServiceClient is null)
{
throw new InvalidOperationException("BlobServiceClient is not initialized.");
}

foreach (var container in builder.Resource.BlobContainers)
{
var blobContainerClient = blobServiceClient.GetBlobContainerClient(container.BlobContainerName);
await blobContainerClient.CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
}
});

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

Expand Down Expand Up @@ -295,10 +295,6 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
var resource = new AzureBlobStorageResource(name, builder.Resource);

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

var healthCheckKey = $"{resource.Name}_check";

Expand All @@ -308,7 +304,13 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
return blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized."));
}, name: healthCheckKey);

return builder.ApplicationBuilder.AddResource(resource).WithHealthCheck(healthCheckKey);
return builder.ApplicationBuilder
.AddResource(resource)
.WithHealthCheck(healthCheckKey)
.OnConnectionStringAvailable(async (blobs, @event, ct) =>
{
connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
});
}

/// <summary>
Expand All @@ -329,9 +331,9 @@ public static IResourceBuilder<AzureBlobStorageContainerResource> AddBlobContain
builder.Resource.Parent.BlobContainers.Add(resource);

string? connectionString = null;
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
builder.OnConnectionStringAvailable(async (blobStorage, @event, ct) =>
{
connectionString = await resource.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
connectionString = await blobStorage.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
});

var healthCheckKey = $"{resource.Name}_check";
Expand Down
83 changes: 40 additions & 43 deletions src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,6 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr

string? connectionString = null;

builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(sqlServer, async (@event, ct) =>
{
connectionString = await sqlServer.GetConnectionStringAsync(ct).ConfigureAwait(false);

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

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);

if (sqlConnection.State != System.Data.ConnectionState.Open)
{
throw new InvalidOperationException($"Could not open connection to '{sqlServer.Name}'");
}

foreach (var sqlDatabase in sqlServer.DatabaseResources)
{
await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false);
}
});

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

Expand All @@ -88,7 +57,36 @@ public static IResourceBuilder<SqlServerServerResource> AddSqlServer(this IDistr
{
context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = sqlServer.PasswordParameter;
})
.WithHealthCheck(healthCheckKey);
.WithHealthCheck(healthCheckKey)
.OnConnectionStringAvailable(async (sqlServer, @event, ct) =>
{
connectionString = await sqlServer.GetConnectionStringAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{sqlServer.Name}' resource but the connection string was null.");
}
})
.OnResourceReady(async (sqlServer, @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);

if (sqlConnection.State != System.Data.ConnectionState.Open)
{
throw new InvalidOperationException($"Could not open connection to '{sqlServer.Name}'");
}

foreach (var sqlDatabase in sqlServer.DatabaseResources)
{
await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false);
}
});
}

/// <summary>
Expand All @@ -112,22 +110,21 @@ public static IResourceBuilder<SqlServerDatabaseResource> AddDatabase(this IReso

string? connectionString = null;

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

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

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

return builder.ApplicationBuilder
.AddResource(sqlServerDatabase)
.WithHealthCheck(healthCheckKey);
.WithHealthCheck(healthCheckKey)
.OnConnectionStringAvailable(async (sqlServerDatabase, @event, ct) =>
{
connectionString = await sqlServerDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);

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

/// <summary>
Expand Down
Loading
Loading