Skip to content

Remove dependencies on AfterEndpointsAllocatedEvent #9598

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

Merged
merged 4 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 4 additions & 7 deletions src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,16 @@ public static IResourceBuilder<KafkaServerResource> WithKafkaUI(this IResourceBu
.WithHttpEndpoint(targetPort: KafkaUIPort)
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(kafkaUi, (e, ct) =>
{
var kafkaResources = builder.ApplicationBuilder.Resources.OfType<KafkaServerResource>();

int i = 0;
foreach (var kafkaResource in kafkaResources)
{
if (kafkaResource.InternalEndpoint.IsAllocated)
{
var endpoint = kafkaResource.InternalEndpoint;
int index = i;
kafkaUiBuilder.WithEnvironment(context => ConfigureKafkaUIContainer(context, endpoint, index));
}
var endpoint = kafkaResource.InternalEndpoint;
int index = i;
kafkaUiBuilder.WithEnvironment(context => ConfigureKafkaUIContainer(context, endpoint, index));

i++;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
.WithHttpEndpoint(targetPort: 80, name: "http")
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(phpMyAdminContainer, (e, ct) =>
{
var mySqlInstances = builder.ApplicationBuilder.Resources.OfType<MySqlServerResource>();

Expand Down
19 changes: 8 additions & 11 deletions src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static IResourceBuilder<RedisResource> AddRedis(

// StackExchange.Redis doesn't support passwords with commas.
// See https://github.com/StackExchange/StackExchange.Redis/issues/680 and
// https://github.com/Azure/azure-dev/issues/4848
// https://github.com/Azure/azure-dev/issues/4848
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", special: false);

var redis = new RedisResource(name, passwordParameter);
Expand Down Expand Up @@ -158,7 +158,7 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
.WithHttpEndpoint(targetPort: 8081, name: "http")
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(resource, (e, ct) =>
{
var redisInstances = builder.ApplicationBuilder.Resources.OfType<RedisResource>();

Expand All @@ -172,17 +172,14 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB

foreach (var redisInstance in redisInstances)
{
if (redisInstance.PrimaryEndpoint.IsAllocated)
// Redis Commander assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.Name}:{redisInstance.PrimaryEndpoint.TargetPort}:0";
if (redisInstance.PasswordParameter is not null)
{
// Redis Commander assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.Name}:{redisInstance.PrimaryEndpoint.TargetPort}:0";
if (redisInstance.PasswordParameter is not null)
{
hostString += $":{redisInstance.PasswordParameter.Value}";
}
hostsVariableBuilder.Append(hostString);
hostString += $":{redisInstance.PasswordParameter.Value}";
}
hostsVariableBuilder.Append(hostString);
}

resourceBuilder.WithEnvironment("REDIS_HOSTS", hostsVariableBuilder.ToString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// </code>
/// </example>
/// </remarks>
[Obsolete("The AfterEndpointsAllocatedEvent is deprecated and will be removed in a future version. Use the resource specific events BeforeResourceStartedEvent or ResourceEndpointsAllocatedEvent instead depending on your needs.")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aaronontheweb @Alirexaa big change coming.

public class AfterEndpointsAllocatedEvent(IServiceProvider services, DistributedApplicationModel model) : IDistributedApplicationEvent
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken ca
/// <param name="appModel">The distributed application model.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>

Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
Expand Down
67 changes: 33 additions & 34 deletions src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public ApplicationOrchestrator(DistributedApplicationModel model,
dcpExecutorEvents.Subscribe<OnResourceStartingContext>(OnResourceStarting);
dcpExecutorEvents.Subscribe<OnResourceFailedToStartContext>(OnResourceFailedToStart);

_eventing.Subscribe<AfterEndpointsAllocatedEvent>(ProcessResourcesWithoutLifetime);
_eventing.Subscribe<ResourceEndpointsAllocatedEvent>(ProcessResourcesWithoutLifetime);
_eventing.Subscribe<ResourceEndpointsAllocatedEvent>(PublishInitialResourceUrls);
// Implement WaitFor functionality using BeforeResourceStartedEvent.
_eventing.Subscribe<BeforeResourceStartedEvent>(WaitForInBeforeResourceStartedEvent);
Expand Down Expand Up @@ -94,7 +94,9 @@ private async Task WaitForInBeforeResourceStartedEvent(BeforeResourceStartedEven

private async Task OnEndpointsAllocated(OnEndpointsAllocatedContext context)
{
#pragma warning disable CS0618 // Type or member is obsolete
var afterEndpointsAllocatedEvent = new AfterEndpointsAllocatedEvent(_serviceProvider, _model);
#pragma warning restore CS0618 // Type or member is obsolete
await _eventing.PublishAsync(afterEndpointsAllocatedEvent, context.CancellationToken).ConfigureAwait(false);

foreach (var lifecycleHook in _lifecycleHooks)
Expand Down Expand Up @@ -240,48 +242,45 @@ private async Task ProcessUrls(IResource resource, CancellationToken cancellatio
}
}

private Task ProcessResourcesWithoutLifetime(AfterEndpointsAllocatedEvent @event, CancellationToken cancellationToken)
private async Task ProcessResourcesWithoutLifetime(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a sketchiest change 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s definitely the one that stood out the most, but at least with the current event behavior/timing it should behave the same.

{
async Task ProcessValueAsync(IResource resource, IValueProvider vp)
if (@event.Resource is not IResourceWithoutLifetime resource)
{
try
{
var value = await vp.GetValueAsync(default).ConfigureAwait(false);

await _notificationService.PublishUpdateAsync(resource, s =>
{
return s with
{
Properties = s.Properties.SetResourceProperty("Value", value ?? "", resource is ParameterResource p && p.Secret)
};
})
.ConfigureAwait(false);
}
catch (Exception ex)
{
await _notificationService.PublishUpdateAsync(resource, s =>
{
return s with
{
State = new("Value missing", KnownResourceStateStyles.Error),
Properties = s.Properties.SetResourceProperty("Value", ex.Message)
};
})
.ConfigureAwait(false);
return;
}

_loggerService.GetLogger(resource.Name).LogError("{Message}", ex.Message);
}
if (resource is not IValueProvider valueProvider)
{
return;
}

foreach (var resource in _model.Resources.OfType<IResourceWithoutLifetime>())
try
{
if (resource is IValueProvider provider)
var value = await valueProvider.GetValueAsync(default).ConfigureAwait(false);

await _notificationService.PublishUpdateAsync(resource, s =>
{
_ = ProcessValueAsync(resource, provider);
}
return s with
{
Properties = s.Properties.SetResourceProperty("Value", value ?? "", resource is ParameterResource p && p.Secret)
};
})
.ConfigureAwait(false);
}
catch (Exception ex)
{
await _notificationService.PublishUpdateAsync(resource, s =>
{
return s with
{
State = new("Value missing", KnownResourceStateStyles.Error),
Properties = s.Properties.SetResourceProperty("Value", ex.Message)
};
})
.ConfigureAwait(false);

return Task.CompletedTask;
_loggerService.GetLogger(resource.Name).LogError("{Message}", ex.Message);
}
}

private async Task OnResourceChanged(OnResourceChangedContext context)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,7 +1312,7 @@ public static IResourceBuilder<T> WithHttpHealthCheck<T>(this IResourceBuilder<T

var endpointName = endpoint.EndpointName;

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((@event, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<ResourceEndpointsAllocatedEvent>(builder.Resource, (@event, ct) =>
{
if (!endpoint.Exists)
{
Expand Down
8 changes: 4 additions & 4 deletions tests/Aspire.Hosting.MySql.Tests/AddMySqlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,10 @@ public async Task SingleMySqlInstanceProducesCorrectMySqlHostsVariable()
// Add fake allocated endpoints.
mysql.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001));

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var myAdmin = builder.Resources.Single(r => r.Name.Equals("phpmyadmin"));

await builder.Eventing.PublishAsync<BeforeResourceStartedEvent>(new(myAdmin, app.Services));

var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(myAdmin, DistributedApplicationOperation.Run, TestServiceProvider.Instance);

var container = builder.Resources.Single(r => r.Name == "phpmyadmin");
Expand Down Expand Up @@ -271,9 +271,9 @@ public void WithPhpMyAdminProducesValidServerConfigFile()
using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var myAdmin = builder.Resources.Single(r => r.Name.Equals("phpmyadmin"));
builder.Eventing.PublishAsync<BeforeResourceStartedEvent>(new(myAdmin, app.Services));

var volume = myAdmin.Annotations.OfType<ContainerMountAnnotation>().Single();

using var stream = File.OpenRead(volume.Source!);
Expand Down
9 changes: 1 addition & 8 deletions tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,17 +369,14 @@ public async Task VerifyManifestWithParameters()
}

[Fact]
public async Task WithPgAdminAddsContainer()
public void WithPgAdminAddsContainer()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.AddPostgres("mypostgres").WithPgAdmin(pga => pga.WithHostPort(8081));

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// The mount annotation is added in the AfterEndpointsAllocatedEvent.
await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var container = builder.Resources.Single(r => r.Name == "pgadmin");
var createFile = container.Annotations.OfType<ContainerFileSystemCallbackAnnotation>().Single();

Expand Down Expand Up @@ -468,8 +465,6 @@ public async Task WithPostgresProducesValidServersJsonFile()

using var app = builder.Build();

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var pgadmin = builder.Resources.Single(r => r.Name.Equals("pgadmin"));

var createServers = pgadmin.Annotations.OfType<ContainerFileSystemCallbackAnnotation>().Single();
Expand Down Expand Up @@ -531,8 +526,6 @@ public async Task WithPgwebProducesValidBookmarkFiles()
using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var pgweb = builder.Resources.Single(r => r.Name.Equals("pgweb"));
var createBookmarks = pgweb.Annotations.OfType<ContainerFileSystemCallbackAnnotation>().Single();

Expand Down
4 changes: 3 additions & 1 deletion tests/Aspire.Hosting.Qdrant.Tests/QdrantFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,10 @@ public async Task AddQdrantWithDefaultsAddsUrlAnnotations()

var qdrant = builder.AddQdrant("qdrant");

var qdrantResource = builder.Resources.Single(r => r.Name.Equals("qdrant"));

var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(qdrantResource, (e, ct) =>
{
tcs.SetResult();
return Task.CompletedTask;
Expand Down
14 changes: 6 additions & 8 deletions tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,6 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables()
redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001));
redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002));

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var redisInsight = Assert.Single(builder.Resources.OfType<RedisInsightResource>());
var envs = await redisInsight.GetEnvironmentVariableValuesAsync();

Expand Down Expand Up @@ -490,10 +488,10 @@ public async Task SingleRedisInstanceWithoutPasswordProducesCorrectRedisHostsVar
// Add fake allocated endpoints.
redis.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001));

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var commander = builder.Resources.Single(r => r.Name.Equals("rediscommander"));

await builder.Eventing.PublishAsync<BeforeResourceStartedEvent>(new(commander, app.Services));

var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
commander,
DistributedApplicationOperation.Run,
Expand All @@ -514,10 +512,10 @@ public async Task SingleRedisInstanceWithPasswordProducesCorrectRedisHostsVariab
// Add fake allocated endpoints.
redis.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001));

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var commander = builder.Resources.Single(r => r.Name.Equals("rediscommander"));

await builder.Eventing.PublishAsync<BeforeResourceStartedEvent>(new(commander, app.Services));

var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(commander);

Assert.Equal($"myredis1:{redis.Resource.Name}:6379:0:{password}", config["REDIS_HOSTS"]);
Expand All @@ -535,10 +533,10 @@ public async Task MultipleRedisInstanceProducesCorrectRedisHostsVariable()
redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001));
redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002, "host2"));

await builder.Eventing.PublishAsync<AfterEndpointsAllocatedEvent>(new(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));

var commander = builder.Resources.Single(r => r.Name.Equals("rediscommander"));

await builder.Eventing.PublishAsync<BeforeResourceStartedEvent>(new(commander, app.Services));

var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
commander,
DistributedApplicationOperation.Run,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,15 @@ public async Task LifeycleHookAnalogousEventsFire()
beforeStartEventFired.Set();
return Task.CompletedTask;
});
#pragma warning disable CS0618 // Type or member is obsolete
builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
{
Assert.NotNull(e.Services);
Assert.NotNull(e.Model);
afterEndpointsAllocatedEventFired.Set();
return Task.CompletedTask;
});
#pragma warning restore CS0618 // Type or member is obsolete
builder.Eventing.Subscribe<AfterResourcesCreatedEvent>((e, ct) =>
{
Assert.NotNull(e.Services);
Expand Down
2 changes: 1 addition & 1 deletion tests/Aspire.Hosting.Tests/WithUrlsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task WithUrlsCallsCallbackAfterBeforeResourceStartedEvent()
.WithUrls(c => called = true);

var tcs = new TaskCompletionSource();
builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.Eventing.Subscribe<ResourceEndpointsAllocatedEvent>(projectA.Resource, (e, ct) =>
{
// Should not be called at this point
Assert.False(called);
Expand Down
Loading