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
3 changes: 2 additions & 1 deletion examples-Aspire/AspireApp1.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService);
.WithReference(apiService)
.WaitFor(apiService);

builder.Build().Run();
44 changes: 44 additions & 0 deletions src/WireMock.Net.Aspire/WireMockHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright © WireMock.Net

using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using WireMock.Client;

namespace WireMock.Net.Aspire;

/// <summary>
/// WireMockHealthCheck
/// </summary>
public class WireMockHealthCheck(WireMockServerResource resource) : IHealthCheck
{
private const string HealthStatusHealthy = "Healthy";

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (!await IsHealthyAsync(resource.AdminApi.Value, cancellationToken))
{
return HealthCheckResult.Unhealthy("WireMock.Net is not healthy");
}

if (resource.ApiMappingState == WireMockMappingState.NotSubmitted)
{
return HealthCheckResult.Unhealthy("WireMock.Net has not received mappings");
}

return HealthCheckResult.Healthy();
}

private static async Task<bool> IsHealthyAsync(IWireMockAdminApi adminApi, CancellationToken cancellationToken)
{
try
{
var status = await adminApi.GetHealthAsync(cancellationToken);
return string.Equals(status, HealthStatusHealthy, StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
}
10 changes: 10 additions & 0 deletions src/WireMock.Net.Aspire/WireMockMappingState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright © WireMock.Net

namespace WireMock.Net.Aspire;

internal enum WireMockMappingState
{
NoMappings,
NotSubmitted,
Submitted,
}
12 changes: 12 additions & 0 deletions src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.WireMock;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Stef.Validation;
using WireMock.Client.Builders;
Expand Down Expand Up @@ -53,11 +54,21 @@ public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistrib
Guard.NotNull(arguments);

var wireMockContainerResource = new WireMockServerResource(name, arguments);

var healthCheckKey = $"{name}_check";
var healthCheckRegistration = new HealthCheckRegistration(
healthCheckKey,
_ => new WireMockHealthCheck(wireMockContainerResource),
failureStatus: null,
tags: null);
builder.Services.AddHealthChecks().Add(healthCheckRegistration);

var resourceBuilder = builder
.AddResource(wireMockContainerResource)
.WithImage(DefaultLinuxImage)
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort)
.WithHealthCheck(healthCheckKey)
.WithWireMockInspectorCommand();

if (!string.IsNullOrEmpty(arguments.MappingsPath))
Expand Down Expand Up @@ -172,6 +183,7 @@ public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(thi

wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted;

return wiremock;
}
Expand Down
43 changes: 29 additions & 14 deletions src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,47 @@ internal class WireMockServerLifecycleHook(ILoggerFactory loggerFactory) : IDist
{
private readonly CancellationTokenSource _shutdownCts = new();

public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
private CancellationTokenSource? _linkedCts;
private Task? _mappingTask;

var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.ToArray();
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);

foreach (var wireMockServerResource in wireMockServerResources)
_mappingTask = Task.Run(async () =>
{
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.ToArray();

var endpoint = wireMockServerResource.GetEndpoint();
if (endpoint.IsAllocated)
foreach (var wireMockServerResource in wireMockServerResources)
{
await wireMockServerResource.WaitForHealthAsync(cts.Token);
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());

var endpoint = wireMockServerResource.GetEndpoint();
System.Diagnostics.Debug.Assert(endpoint.IsAllocated);

await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token);

wireMockServerResource.StartWatchingStaticMappings(cts.Token);
await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token);

wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token);
}
}
}, _linkedCts.Token);

return Task.CompletedTask;
}

public async ValueTask DisposeAsync()
{
await _shutdownCts.CancelAsync();

_linkedCts?.Dispose();
_shutdownCts.Dispose();

if (_mappingTask is not null)
{
await _mappingTask;
}
}
}
4 changes: 4 additions & 0 deletions src/WireMock.Net.Aspire/WireMockServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Net.Aspire;
using WireMock.Util;

// ReSharper disable once CheckNamespace
Expand All @@ -19,6 +20,7 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis

internal WireMockServerArguments Arguments { get; }
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
internal WireMockMappingState ApiMappingState { get; set; } = WireMockMappingState.NoMappings;

private ILogger? _logger;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
Expand Down Expand Up @@ -64,6 +66,8 @@ internal async Task CallApiMappingBuilderActionAsync(CancellationToken cancellat

var mappingBuilder = AdminApi.Value.GetMappingBuilder();
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);

ApiMappingState = WireMockMappingState.Submitted;
}

internal void StartWatchingStaticMappings(CancellationToken cancellationToken)
Expand Down
2 changes: 2 additions & 0 deletions test/WireMock.Net.Aspire.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace WireMock.Net.Aspire.Tests;

public class IntegrationTests(ITestOutputHelper output)

Check warning on line 11 in test/WireMock.Net.Aspire.Tests/IntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run Tests on Linux

Parameter 'output' is unread.
{
private record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);

Expand All @@ -19,6 +19,7 @@
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
await app.ResourceNotifications.WaitForResourceHealthyAsync("wiremock-service");

using var httpClient = app.CreateHttpClient("wiremock-service");

Expand Down Expand Up @@ -46,6 +47,7 @@
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
await app.ResourceNotifications.WaitForResourceHealthyAsync("wiremock-service");

var adminClient = app.CreateWireMockAdminClient("wiremock-service");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void AddWireMock()
MappingsPath = null,
HttpPort = port
});
wiremock.Resource.Annotations.Should().HaveCount(5);
wiremock.Resource.Annotations.Should().HaveCount(6);

var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
Expand Down
Loading