Skip to content

Commit f315166

Browse files
Address feedback to WithUrls() (#8602)
* Address WithUrls feedback Fixes #8587 * Add more WithUrl tests * More tests * Fix test failure * Fix test * PR feedback
1 parent dbd0150 commit f315166

File tree

5 files changed

+289
-62
lines changed

5 files changed

+289
-62
lines changed

src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs

+13-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public ResourceSnapshotBuilder(DcpResourceState resourceState)
2020
public CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSnapshot previous)
2121
{
2222
var containerId = container.Status?.ContainerId;
23-
var urls = GetUrls(container);
23+
var urls = GetUrls(container, container.Status?.State);
2424
var volumes = GetVolumes(container);
2525

2626
var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env);
@@ -99,7 +99,7 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn
9999

100100
var state = executable.AppModelInitialState is "Hidden" ? "Hidden" : executable.Status?.State;
101101

102-
var urls = GetUrls(executable);
102+
var urls = GetUrls(executable, executable.Status?.State);
103103

104104
var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env);
105105

@@ -183,7 +183,7 @@ private static (ImmutableArray<string> Args, ImmutableArray<int>? ArgsAreSensiti
183183
return (launchArgsBuilder.ToImmutable(), argsAreSensitiveBuilder.ToImmutable(), anySensitive);
184184
}
185185

186-
private ImmutableArray<UrlSnapshot> GetUrls(CustomResource resource)
186+
private ImmutableArray<UrlSnapshot> GetUrls(CustomResource resource, string? resourceState)
187187
{
188188
var urls = ImmutableArray.CreateBuilder<UrlSnapshot>();
189189
var appModelResourceName = resource.AppModelResourceName;
@@ -199,21 +199,26 @@ private ImmutableArray<UrlSnapshot> GetUrls(CustomResource resource)
199199
var name = resource.Metadata.Name;
200200

201201
// Add the endpoint URLs
202-
foreach (var service in resourceServices)
202+
var serviceEndpoints = new HashSet<(string EndpointName, string ServiceMetadataName)>(resourceServices.Where(s => !string.IsNullOrEmpty(s.EndpointName)).Select(s => (s.EndpointName!, s.Metadata.Name)));
203+
foreach (var endpoint in serviceEndpoints)
203204
{
204-
if (endpointUrls.FirstOrDefault(u => string.Equals(service.EndpointName, u.Endpoint?.EndpointName, StringComparisons.EndpointAnnotationName)) is { Endpoint: { } } endpointUrl)
205+
var (endpointName, serviceName) = endpoint;
206+
var urlsForEndpoint = endpointUrls.Where(u => string.Equals(endpointName, u.Endpoint?.EndpointName, StringComparisons.EndpointAnnotationName)).ToList();
207+
208+
foreach (var endpointUrl in urlsForEndpoint)
205209
{
206-
var activeEndpoint = _resourceState.EndpointsMap.SingleOrDefault(e => e.Value.Spec.ServiceName == service.Metadata.Name && e.Value.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) == true).Value;
210+
var activeEndpoint = _resourceState.EndpointsMap.SingleOrDefault(e => e.Value.Spec.ServiceName == serviceName && e.Value.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) == true).Value;
207211
var isInactive = activeEndpoint is null;
208212

209-
urls.Add(new(Name: endpointUrl.Endpoint.EndpointName, Url: endpointUrl.Url, IsInternal: false) { IsInactive = isInactive, DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0) });
213+
urls.Add(new(Name: endpointUrl.Endpoint!.EndpointName, Url: endpointUrl.Url, IsInternal: false) { IsInactive = isInactive, DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0) });
210214
}
211215
}
212216

213217
// Add the non-endpoint URLs
218+
var resourceRunning = string.Equals(resourceState, KnownResourceStates.Running, StringComparisons.ResourceState);
214219
foreach (var url in nonEndpointUrls)
215220
{
216-
urls.Add(new(Name: null, Url: url.Url, IsInternal: false) { IsInactive = false, DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0) });
221+
urls.Add(new(Name: null, Url: url.Url, IsInternal: false) { IsInactive = !resourceRunning, DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0) });
217222
}
218223
}
219224

src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs

+41-26
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,13 @@ private async Task OnEndpointsAllocated(OnEndpointsAllocatedContext context)
9797
{
9898
await lifecycleHook.AfterEndpointsAllocatedAsync(_model, context.CancellationToken).ConfigureAwait(false);
9999
}
100-
101-
await ProcessUrls(context.CancellationToken).ConfigureAwait(false);
102100
}
103101

104102
private async Task OnResourceStarting(OnResourceStartingContext context)
105103
{
104+
// Call the callbacks to configure resource URLs
105+
await ProcessUrls(context.Resource, context.CancellationToken).ConfigureAwait(false);
106+
106107
switch (context.ResourceType)
107108
{
108109
case KnownResourceTypes.Project:
@@ -152,44 +153,58 @@ private async Task OnResourcesPrepared(OnResourcesPreparedContext _)
152153
await PublishResourcesWithInitialStateAsync().ConfigureAwait(false);
153154
}
154155

155-
private async Task ProcessUrls(CancellationToken cancellationToken)
156+
private async Task ProcessUrls(IResource resource, CancellationToken cancellationToken)
156157
{
157-
// Project endpoints to URLS
158-
foreach (var resource in _model.Resources.OfType<IResourceWithEndpoints>())
158+
if (resource is not IResourceWithEndpoints resourceWithEndpoints)
159159
{
160-
var urls = new List<ResourceUrlAnnotation>();
160+
return;
161+
}
161162

162-
if (resource.TryGetEndpoints(out var endpoints))
163+
// Project endpoints to URLS
164+
var urls = new List<ResourceUrlAnnotation>();
165+
166+
if (resource.TryGetEndpoints(out var endpoints))
167+
{
168+
foreach (var endpoint in endpoints)
163169
{
164-
foreach (var endpoint in endpoints)
170+
// Create a URL for each endpoint
171+
if (endpoint.AllocatedEndpoint is { } allocatedEndpoint)
165172
{
166-
// Create a URL for each endpoint
167-
if (endpoint.AllocatedEndpoint is { } allocatedEndpoint)
168-
{
169-
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = new EndpointReference(resource, endpoint) };
170-
urls.Add(url);
171-
}
173+
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = new EndpointReference(resourceWithEndpoints, endpoint) };
174+
urls.Add(url);
172175
}
173176
}
177+
}
174178

175-
// Run the URL callbacks
176-
if (resource.TryGetAnnotationsOfType<ResourceUrlsCallbackAnnotation>(out var callbacks))
179+
// Run the URL callbacks
180+
if (resource.TryGetAnnotationsOfType<ResourceUrlsCallbackAnnotation>(out var callbacks))
181+
{
182+
var urlsCallbackContext = new ResourceUrlsCallbackContext(new(DistributedApplicationOperation.Run), resource, urls, cancellationToken)
177183
{
178-
var urlsCallbackContext = new ResourceUrlsCallbackContext(new(DistributedApplicationOperation.Run), resource, urls, cancellationToken)
179-
{
180-
Logger = _loggerService.GetLogger(resource.Name)
181-
};
182-
foreach (var callback in callbacks)
183-
{
184-
await callback.Callback(urlsCallbackContext).ConfigureAwait(false);
185-
}
184+
Logger = _loggerService.GetLogger(resource.Name)
185+
};
186+
foreach (var callback in callbacks)
187+
{
188+
await callback.Callback(urlsCallbackContext).ConfigureAwait(false);
186189
}
190+
}
187191

188-
foreach (var url in urls)
192+
// Clear existing URLs
193+
if (resource.TryGetUrls(out var existingUrls))
194+
{
195+
var existing = existingUrls.ToArray();
196+
for (var i = existing.Length - 1; i >= 0; i--)
189197
{
190-
resource.Annotations.Add(url);
198+
var url = existing[i];
199+
resource.Annotations.Remove(url);
191200
}
192201
}
202+
203+
// Add URLs
204+
foreach (var url in urls)
205+
{
206+
resource.Annotations.Add(url);
207+
}
193208
}
194209

195210
private Task ProcessResourcesWithoutLifetime(AfterEndpointsAllocatedEvent @event, CancellationToken cancellationToken)

src/Aspire.Hosting/ResourceBuilderExtensions.cs

+52-1
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,58 @@ public static IResourceBuilder<T> WithUrl<T>(this IResourceBuilder<T> builder, s
799799
}
800800

801801
/// <summary>
802-
/// Registers a callback to customize the URL displayed for the endpoint with the specified name.
802+
/// Adds a URL to be displayed for the resource.
803+
/// </summary>
804+
/// <typeparam name="T">The resource type.</typeparam>
805+
/// <param name="builder">The builder for the resource.</param>
806+
/// <param name="url">The interpolated string that produces the URL.</param>
807+
/// <param name="displayText">The display text to show when the link is displayed.</param>
808+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
809+
/// <remarks>
810+
/// Use this method to add a URL to be displayed for the resource.<br/>
811+
/// Note that any endpoints on the resource will automatically get a corresponding URL added for them.
812+
/// </remarks>
813+
public static IResourceBuilder<T> WithUrl<T>(this IResourceBuilder<T> builder, in ReferenceExpression.ExpressionInterpolatedStringHandler url, string? displayText = null)
814+
where T : IResource
815+
{
816+
ArgumentNullException.ThrowIfNull(builder);
817+
818+
var expression = url.GetExpression();
819+
820+
return builder.WithUrl(expression, displayText);
821+
}
822+
823+
/// <summary>
824+
/// Adds a URL to be displayed for the resource.
825+
/// </summary>
826+
/// <typeparam name="T">The resource type.</typeparam>
827+
/// <param name="builder">The builder for the resource.</param>
828+
/// <param name="url">A <see cref="ReferenceExpression"/> that will produce the URL.</param>
829+
/// <param name="displayText">The display text to show when the link is displayed.</param>
830+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
831+
/// <remarks>
832+
/// Use this method to add a URL to be displayed for the resource.<br/>
833+
/// Note that any endpoints on the resource will automatically get a corresponding URL added for them.
834+
/// </remarks>
835+
public static IResourceBuilder<T> WithUrl<T>(this IResourceBuilder<T> builder, ReferenceExpression url, string? displayText = null)
836+
where T : IResource
837+
{
838+
ArgumentNullException.ThrowIfNull(builder);
839+
ArgumentNullException.ThrowIfNull(url);
840+
841+
return builder.WithAnnotation(new ResourceUrlsCallbackAnnotation(async c =>
842+
{
843+
var endpoint = url.ValueProviders.OfType<EndpointReference>().FirstOrDefault();
844+
var urlValue = await url.GetValueAsync(c.CancellationToken).ConfigureAwait(false);
845+
if (!string.IsNullOrEmpty(urlValue))
846+
{
847+
c.Urls.Add(new() { Endpoint = endpoint, Url = urlValue, DisplayText = displayText });
848+
}
849+
}));
850+
}
851+
852+
/// <summary>
853+
/// Registers a callback to update the URL displayed for the endpoint with the specified name.
803854
/// </summary>
804855
/// <typeparam name="T">The resource type.</typeparam>
805856
/// <param name="builder">The builder for the resource.</param>

tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs

+25-25
Original file line numberDiff line numberDiff line change
@@ -177,26 +177,26 @@ public async Task ExplicitStart_StartExecutable()
177177
var notStartedResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.NotStarted).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
178178
var dependentResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
179179

180-
// Inactive URLs and source should be populated on non-started resources.
180+
// Source should be populated on non-started resources.
181181
Assert.Contains("TestProject.ServiceA.csproj", notStartedResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
182-
Assert.Collection(notStartedResourceEvent.Snapshot.Urls, u =>
183-
{
184-
Assert.Equal("http://localhost:5156", u.Url);
185-
Assert.True(u.IsInactive);
186-
});
187182
Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
188-
Assert.Collection(dependentResourceEvent.Snapshot.Urls, u =>
189-
{
190-
Assert.Equal("http://localhost:5254", u.Url);
191-
Assert.True(u.IsInactive);
192-
});
193183

194184
logger.LogInformation("Start explicit start resource.");
195185
await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
196-
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
186+
var runningResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
187+
Assert.Collection(runningResourceEvent.Snapshot.Urls, u =>
188+
{
189+
Assert.Equal("http://localhost:5156", u.Url);
190+
Assert.Equal("http", u.Name);
191+
});
197192

198193
// Dependent resource should now run.
199-
await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
194+
var dependentResourceRunningEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
195+
Assert.Collection(dependentResourceRunningEvent.Snapshot.Urls, u =>
196+
{
197+
Assert.Equal("http://localhost:5254", u.Url);
198+
Assert.Equal("http", u.Name);
199+
});
200200

201201
logger.LogInformation("Stop resource.");
202202
await orchestrator.StopResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
@@ -239,27 +239,27 @@ public async Task ExplicitStart_StartContainer()
239239
var notStartedResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.NotStarted).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
240240
var dependentResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
241241

242-
// Inactive URLs and source should be populated on non-started resources.
242+
// Source should be populated on non-started resources.
243243
Assert.Equal(RedisImageSource, notStartedResourceEvent.Snapshot.Properties.Single(p => p.Name == "container.image").Value?.ToString());
244-
Assert.Collection(notStartedResourceEvent.Snapshot.Urls, u =>
244+
Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
245+
246+
logger.LogInformation("Start explicit start resource.");
247+
await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
248+
var runningResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
249+
Assert.Collection(runningResourceEvent.Snapshot.Urls, u =>
245250
{
246251
Assert.Equal("tcp://localhost:6379", u.Url);
247252
Assert.True(u.IsInactive);
248253
});
249-
Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
250-
Assert.Collection(dependentResourceEvent.Snapshot.Urls, u =>
254+
255+
// Dependent resource should now run.
256+
var dependentRunningResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
257+
Assert.Collection(dependentRunningResourceEvent.Snapshot.Urls, u =>
251258
{
252259
Assert.Equal("http://localhost:5254", u.Url);
253-
Assert.True(u.IsInactive);
260+
Assert.Equal("http", u.Name);
254261
});
255262

256-
logger.LogInformation("Start explicit start resource.");
257-
await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
258-
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
259-
260-
// Dependent resource should now run.
261-
await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
262-
263263
logger.LogInformation("Stop resource.");
264264
await orchestrator.StopResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
265265
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Exited).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);

0 commit comments

Comments
 (0)