Skip to content

Commit 852af12

Browse files
Make WithHttpHealthCheck work like WithHttpCommand (#9133)
Fixes #8765
1 parent 0e182c9 commit 852af12

File tree

5 files changed

+142
-58
lines changed

5 files changed

+142
-58
lines changed

playground/TestShop/TestShop.AppHost/Program.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,8 @@
7171
// Don't show the non-HTTPS link on the resources page (details only)
7272
.WithUrlForEndpoint("http", url => url.DisplayLocation = UrlDisplayLocation.DetailsOnly)
7373
// Add health relative URL (show in details only)
74-
.WithUrlForEndpoint("https", ep => new() { Url = "/health", DisplayText = "Health", DisplayLocation = UrlDisplayLocation.DetailsOnly });
75-
76-
var _ = frontend.GetEndpoint("https").Exists ? frontend.WithHttpsHealthCheck("/health") : frontend.WithHttpHealthCheck("/health");
74+
.WithUrlForEndpoint("https", ep => new() { Url = "/health", DisplayText = "Health", DisplayLocation = UrlDisplayLocation.DetailsOnly })
75+
.WithHttpHealthCheck("/health");
7776

7877
builder.AddProject<Projects.OrderProcessor>("orderprocessor", launchProfileName: "OrderProcessor")
7978
.WithReference(messaging).WaitFor(messaging);

src/Aspire.Hosting/ResourceBuilderExtensions.cs

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using Aspire.Hosting.ApplicationModel;
99
using Aspire.Hosting.Publishing;
1010
using Aspire.Hosting.Utils;
11-
using HealthChecks.Uris;
1211
using Microsoft.Extensions.DependencyInjection;
1312
using Microsoft.Extensions.Diagnostics.HealthChecks;
1413
using Microsoft.Extensions.Logging;
@@ -1259,21 +1258,58 @@ public static IResourceBuilder<T> WithHttpHealthCheck<T>(this IResourceBuilder<T
12591258
{
12601259
ArgumentNullException.ThrowIfNull(builder);
12611260

1262-
endpointName = endpointName ?? "http";
1263-
return builder.WithHttpHealthCheckInternal(
1264-
path: path,
1265-
desiredScheme: "http",
1266-
endpointName: endpointName,
1267-
statusCode: statusCode
1268-
);
1261+
var endpointSelector = endpointName is not null
1262+
? NamedEndpointSelector(builder, [endpointName], "HTTP health check")
1263+
: NamedEndpointSelector(builder, s_httpSchemes, "HTTP health check");
1264+
1265+
return WithHttpHealthCheck(builder, endpointSelector, path, statusCode);
12691266
}
12701267

1271-
internal static IResourceBuilder<T> WithHttpHealthCheckInternal<T>(this IResourceBuilder<T> builder, string desiredScheme, string endpointName, string? path = null, int? statusCode = null) where T : IResourceWithEndpoints
1268+
/// <summary>
1269+
/// Adds a health check to the resource which is mapped to a specific endpoint.
1270+
/// </summary>
1271+
/// <typeparam name="T">A resource type that implements <see cref="IResourceWithEndpoints" />.</typeparam>
1272+
/// <param name="builder">A resource builder.</param>
1273+
/// <param name="endpointSelector"></param>
1274+
/// <param name="path">The relative path to test.</param>
1275+
/// <param name="statusCode">The result code to interpret as healthy.</param>
1276+
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
1277+
/// <remarks>
1278+
/// <para>
1279+
/// This method adds a health check to the health check service which polls the specified endpoint on a periodic basis.
1280+
/// The base address is dynamically determined based on the endpoint that was selected. By default the path is set to "/"
1281+
/// and the status code is set to 200.
1282+
/// </para>
1283+
/// <example>
1284+
/// This example shows adding an HTTP health check to a backend project.
1285+
/// The health check makes sure that the front end does not start until the backend is
1286+
/// reporting a healthy status based on the return code returned from the
1287+
/// "/health" path on the backend server.
1288+
/// <code lang="C#">
1289+
/// var builder = DistributedApplication.CreateBuilder(args);
1290+
/// var backend = builder.AddProject&lt;Projects.Backend&gt;("backend");
1291+
/// backend.WithHttpHealthCheck(() => backend.GetEndpoint("https"), path: "/health")
1292+
/// builder.AddProject&lt;Projects.Frontend&gt;("frontend")
1293+
/// .WithReference(backend).WaitFor(backend);
1294+
/// </code>
1295+
/// </example>
1296+
/// </remarks>
1297+
public static IResourceBuilder<T> WithHttpHealthCheck<T>(this IResourceBuilder<T> builder, Func<EndpointReference>? endpointSelector, string? path = null, int? statusCode = null) where T : IResourceWithEndpoints
12721298
{
1273-
path = path ?? "/";
1274-
statusCode = statusCode ?? 200;
1299+
endpointSelector ??= DefaultEndpointSelector(builder);
1300+
1301+
var endpoint = endpointSelector()
1302+
?? throw new DistributedApplicationException($"Could not create HTTP health check for resource '{builder.Resource.Name}' as the endpoint selector returned null.");
1303+
1304+
if (endpoint.Scheme != "http" && endpoint.Scheme != "https")
1305+
{
1306+
throw new DistributedApplicationException($"Could not create HTTP health check for resource '{builder.Resource.Name}' as the endpoint with name '{endpoint.EndpointName}' and scheme '{endpoint.Scheme}' is not an HTTP endpoint.");
1307+
}
1308+
1309+
path ??= "/";
1310+
statusCode ??= 200;
12751311

1276-
var endpoint = builder.Resource.GetEndpoint(endpointName);
1312+
var endpointName = endpoint.EndpointName;
12771313

12781314
builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((@event, ct) =>
12791315
{
@@ -1282,11 +1318,6 @@ internal static IResourceBuilder<T> WithHttpHealthCheckInternal<T>(this IResourc
12821318
throw new DistributedApplicationException($"The endpoint '{endpointName}' does not exist on the resource '{builder.Resource.Name}'.");
12831319
}
12841320

1285-
if (endpoint.Scheme != desiredScheme)
1286-
{
1287-
throw new DistributedApplicationException($"The endpoint '{endpointName}' on resource '{builder.Resource.Name}' was not using the '{desiredScheme}' scheme.");
1288-
}
1289-
12901321
return Task.CompletedTask;
12911322
});
12921323

@@ -1302,7 +1333,7 @@ internal static IResourceBuilder<T> WithHttpHealthCheckInternal<T>(this IResourc
13021333

13031334
builder.ApplicationBuilder.Services.SuppressHealthCheckHttpClientLogging(healthCheckKey);
13041335

1305-
builder.ApplicationBuilder.Services.AddHealthChecks().AddUrlGroup((UriHealthCheckOptions options) =>
1336+
builder.ApplicationBuilder.Services.AddHealthChecks().AddUrlGroup(options =>
13061337
{
13071338
if (uri is null)
13081339
{
@@ -1346,16 +1377,12 @@ internal static IResourceBuilder<T> WithHttpHealthCheckInternal<T>(this IResourc
13461377
/// </code>
13471378
/// </example>
13481379
/// </remarks>
1380+
[Obsolete("This method is obsolete and will be removed in a future version. Use the WithHttpHealthCheck method instead.")]
13491381
public static IResourceBuilder<T> WithHttpsHealthCheck<T>(this IResourceBuilder<T> builder, string? path = null, int? statusCode = null, string? endpointName = null) where T : IResourceWithEndpoints
13501382
{
13511383
ArgumentNullException.ThrowIfNull(builder);
13521384

1353-
endpointName = endpointName ?? "https";
1354-
return builder.WithHttpHealthCheckInternal(
1355-
path: path,
1356-
desiredScheme: "https",
1357-
endpointName: endpointName,
1358-
statusCode: statusCode);
1385+
return builder.WithHttpHealthCheck(path, statusCode, endpointName ?? "https");
13591386
}
13601387

13611388
/// <summary>
@@ -1551,8 +1578,8 @@ public static IResourceBuilder<TResource> WithHttpCommand<TResource>(
15511578
path: path,
15521579
displayName: displayName,
15531580
endpointSelector: endpointName is not null
1554-
? NamedEndpointSelector(builder, [endpointName])
1555-
: NamedEndpointSelector(builder, s_httpSchemes),
1581+
? NamedEndpointSelector(builder, [endpointName], "HTTP command")
1582+
: NamedEndpointSelector(builder, s_httpSchemes, "HTTP command"),
15561583
commandName: commandName,
15571584
commandOptions: commandOptions);
15581585

@@ -1626,6 +1653,11 @@ public static IResourceBuilder<TResource> WithHttpCommand<TResource>(
16261653
var endpoint = endpointSelector()
16271654
?? throw new DistributedApplicationException($"Could not create HTTP command for resource '{builder.Resource.Name}' as the endpoint selector returned null.");
16281655

1656+
if (endpoint.Scheme != "http" && endpoint.Scheme != "https")
1657+
{
1658+
throw new DistributedApplicationException($"Could not create HTTP command for resource '{builder.Resource.Name}' as the endpoint with name '{endpoint.EndpointName}' and scheme '{endpoint.Scheme}' is not an HTTP endpoint.");
1659+
}
1660+
16291661
builder.ApplicationBuilder.Services.AddHttpClient();
16301662

16311663
commandOptions ??= HttpCommandOptions.Default;
@@ -1716,7 +1748,7 @@ public static IResourceBuilder<TResource> WithHttpCommand<TResource>(
17161748
// if found.
17171749
private static readonly string[] s_httpSchemes = ["https", "http"];
17181750

1719-
private static Func<EndpointReference> NamedEndpointSelector<TResource>(IResourceBuilder<TResource> builder, string[] endpointNames)
1751+
private static Func<EndpointReference> NamedEndpointSelector<TResource>(IResourceBuilder<TResource> builder, string[] endpointNames, string errorDisplayNoun)
17201752
where TResource : IResourceWithEndpoints
17211753
=> () =>
17221754
{
@@ -1731,15 +1763,15 @@ private static Func<EndpointReference> NamedEndpointSelector<TResource>(IResourc
17311763
{
17321764
if (!s_httpSchemes.Contains(matchingEndpoint.Scheme, StringComparers.EndpointAnnotationUriScheme))
17331765
{
1734-
throw new DistributedApplicationException($"Could not create HTTP command for resource '{builder.Resource.Name}' as the endpoint with name '{matchingEndpoint.EndpointName}' and scheme '{matchingEndpoint.Scheme}' is not an HTTP endpoint.");
1766+
throw new DistributedApplicationException($"Could not create {errorDisplayNoun} for resource '{builder.Resource.Name}' as the endpoint with name '{matchingEndpoint.EndpointName}' and scheme '{matchingEndpoint.Scheme}' is not an HTTP endpoint.");
17351767
}
17361768
return matchingEndpoint;
17371769
}
17381770
}
17391771

17401772
// No endpoint found with the specified names
17411773
var endpointNamesString = string.Join(", ", endpointNames);
1742-
throw new DistributedApplicationException($"Could not create HTTP command for resource '{builder.Resource.Name}' as no endpoint was found matching one of the specified names: {endpointNamesString}");
1774+
throw new DistributedApplicationException($"Could not create {errorDisplayNoun} for resource '{builder.Resource.Name}' as no endpoint was found matching one of the specified names: {endpointNamesString}");
17431775
};
17441776

17451777
private static Func<EndpointReference> DefaultEndpointSelector<TResource>(IResourceBuilder<TResource> builder)

src/Aspire.ProjectTemplates/templates/aspire-starter/9.3/Aspire-StarterApplication.1.AppHost/AppHost.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,11 @@
55

66
#endif
77
var apiService = builder.AddProject<Projects.GeneratedClassNamePrefix_ApiService>("apiservice")
8-
#if HasHttpsProfile
9-
.WithHttpsHealthCheck("/health");
10-
#else
118
.WithHttpHealthCheck("/health");
12-
#endif
139

1410
builder.AddProject<Projects.GeneratedClassNamePrefix_Web>("webfrontend")
1511
.WithExternalHttpEndpoints()
16-
#if HasHttpsProfile
17-
.WithHttpsHealthCheck("/health")
18-
#else
1912
.WithHttpHealthCheck("/health")
20-
#endif
2113
#if UseRedisCache
2214
.WithReference(cache)
2315
.WaitFor(cache)

tests/Aspire.Hosting.Tests/HealthCheckTests.cs

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,61 @@ public class HealthCheckTests(ITestOutputHelper testOutputHelper)
1616
{
1717
[Fact]
1818
[RequiresDocker]
19-
public async Task WithHttpHealthCheckThrowsIfReferencingEndpointThatIsNotHttpScheme()
19+
public void WithHttpHealthCheckThrowsIfReferencingEndpointByNameThatIsNotHttpScheme()
2020
{
2121
using var builder = TestDistributedApplicationBuilder.Create();
22-
builder.AddContainer("resource", "dummycontainer")
23-
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp")
24-
.WithHttpHealthCheck(endpointName: "nonhttp");
2522

26-
using var app = builder.Build();
23+
var container = builder.AddContainer("resource", "dummycontainer")
24+
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp");
2725

28-
var ex = await Assert.ThrowsAsync<DistributedApplicationException>(async () =>
26+
var ex = Assert.Throws<DistributedApplicationException>(() =>
2927
{
30-
await app.StartAsync();
31-
}).DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout);
28+
container.WithHttpHealthCheck(endpointName: "nonhttp");
29+
});
3230

3331
Assert.Equal(
34-
"The endpoint 'nonhttp' on resource 'resource' was not using the 'http' scheme.",
32+
"Could not create HTTP health check for resource 'resource' as the endpoint with name 'nonhttp' and scheme 'tcp' is not an HTTP endpoint.",
3533
ex.Message
3634
);
3735
}
3836

3937
[Fact]
4038
[RequiresDocker]
41-
public async Task WithHttpsHealthCheckThrowsIfReferencingEndpointThatIsNotHttpsScheme()
39+
public void WithHttpHealthCheckThrowsIfReferencingEndpointThatIsNotHttpScheme()
4240
{
4341
using var builder = TestDistributedApplicationBuilder.Create();
44-
builder.AddContainer("resource", "dummycontainer")
45-
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp")
46-
.WithHttpsHealthCheck(endpointName: "nonhttp");
4742

48-
using var app = builder.Build();
43+
var container = builder.AddContainer("resource", "dummycontainer")
44+
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp");
4945

50-
var ex = await Assert.ThrowsAsync<DistributedApplicationException>(async () =>
46+
var ex = Assert.Throws<DistributedApplicationException>(() =>
5147
{
52-
await app.StartAsync();
53-
}).DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout);
48+
container.WithHttpHealthCheck(() => container.GetEndpoint("nonhttp"));
49+
});
50+
51+
Assert.Equal(
52+
"Could not create HTTP health check for resource 'resource' as the endpoint with name 'nonhttp' and scheme 'tcp' is not an HTTP endpoint.",
53+
ex.Message
54+
);
55+
}
56+
57+
[Fact]
58+
[RequiresDocker]
59+
public void WithHttpsHealthCheckThrowsIfReferencingEndpointThatIsNotHttpsScheme()
60+
{
61+
using var builder = TestDistributedApplicationBuilder.Create();
62+
63+
var ex = Assert.Throws<DistributedApplicationException>(() =>
64+
{
65+
#pragma warning disable CS0618 // Type or member is obsolete
66+
builder.AddContainer("resource", "dummycontainer")
67+
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp")
68+
.WithHttpsHealthCheck(endpointName: "nonhttp");
69+
#pragma warning restore CS0618 // Type or member is obsolete
70+
});
5471

5572
Assert.Equal(
56-
"The endpoint 'nonhttp' on resource 'resource' was not using the 'https' scheme.",
73+
"Could not create HTTP health check for resource 'resource' as the endpoint with name 'nonhttp' and scheme 'tcp' is not an HTTP endpoint.",
5774
ex.Message
5875
);
5976
}

tests/Aspire.Hosting.Tests/WithHttpCommandTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,50 @@ public void WithHttpCommand_AddsHttpClientFactory()
3232
Assert.NotNull(httpClientFactory);
3333
}
3434

35+
[Fact]
36+
public void WithHttpCommand_Throws_WhenEndpointByNameIsNotHttp()
37+
{
38+
// Arrange
39+
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
40+
41+
var container = builder.AddContainer("name", "image")
42+
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp");
43+
44+
// Act
45+
var ex = Assert.Throws<DistributedApplicationException>(() =>
46+
{
47+
container.WithHttpCommand("/some-path", "Do The Thing", endpointName: "nonhttp");
48+
});
49+
50+
// Assert
51+
Assert.Equal(
52+
"Could not create HTTP command for resource 'name' as the endpoint with name 'nonhttp' and scheme 'tcp' is not an HTTP endpoint.",
53+
ex.Message
54+
);
55+
}
56+
57+
[Fact]
58+
public void WithHttpCommand_Throws_WhenEndpointIsNotHttp()
59+
{
60+
// Arrange
61+
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
62+
63+
var container = builder.AddContainer("name", "image")
64+
.WithEndpoint(targetPort: 9999, scheme: "tcp", name: "nonhttp");
65+
66+
// Act
67+
var ex = Assert.Throws<DistributedApplicationException>(() =>
68+
{
69+
container.WithHttpCommand("/some-path", "Do The Thing", () => container.GetEndpoint("nonhttp"));
70+
});
71+
72+
// Assert
73+
Assert.Equal(
74+
"Could not create HTTP command for resource 'name' as the endpoint with name 'nonhttp' and scheme 'tcp' is not an HTTP endpoint.",
75+
ex.Message
76+
);
77+
}
78+
3579
[Fact]
3680
public void WithHttpCommand_AddsResourceCommandAnnotation_WithDefaultValues()
3781
{

0 commit comments

Comments
 (0)