Skip to content

Instantly share code, notes, and snippets.

@sliekens
Last active July 12, 2024 12:07
Show Gist options
  • Save sliekens/4177534ed54bb34bc8ce674be6563f89 to your computer and use it in GitHub Desktop.
Save sliekens/4177534ed54bb34bc8ce674be6563f89 to your computer and use it in GitHub Desktop.
Aspire split defaults for web and workers

The Aspire ServiceDefaults project template provided by the SDK requires the ASP.NET Core shared runtime to be installed in production for all your applications. This is problematic for solutions that include other application types like Worker applications.

Specifically, Worker applications (IHostedService) typically only require the dotnet shared runtime.

// Does NOT require the aspnetcore shared runtime
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

If you run this in Docker, you would use the dotnet base image, which doesn't include the aspnetcore runtime.

FROM mcr.microsoft.com/dotnet/runtime:8.0

Adding the service defaults introduces a dependency on Microsoft.AspNetCore.App, which requires the aspnetcore shared runtime.

// DOES require the aspnetcore shared runtime
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHostedService<Worker>();

So, the docker base image would have to be changed.

-FROM mcr.microsoft.com/dotnet/runtime:8.0
+FROM mcr.microsoft.com/dotnet/aspnet:8.0

Instead, you can split the ServiceDefaults project into DotNetDefaults which only requires the dotnet runtime, and AspNetDefaults for code that requires the aspnetcore runtime. The AspNetDefaults extend the DotNetDefaults for maximal code reuse.

The updated worker application:

var builder = Host.CreateApplicationBuilder(args);

// Add project reference to DotNetDefaults.csproj
// Included in this Gist
builder.AddDotNetDefaults();

var host = builder.Build();
host.Run();

Web applications:

var builder = WebApplication.CreateBuilder(args);

// Add project reference to AspNetDefaults.csproj
// Included in this Gist
builder.AddAspNetDefaults();

var app = builder.Build();

app.MapDefaultEndpoints();

app.Run();
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DotNetDefaults\DotNetDefaults.csproj" />
</ItemGroup>
</Project>
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
public static class AspNetExtensions
{
public static IHostApplicationBuilder AddAspNetDefaults(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.AddDotNetDefaults();
builder.AddAspNetCoreInstrumentation();
builder.AddDefaultHealthChecks();
return builder;
}
public static IHostApplicationBuilder AddAspNetCoreInstrumentation(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation();
});
return builder;
}
public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
ArgumentNullException.ThrowIfNull(app);
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
</ItemGroup>
</Project>
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
public static class DotNetExtensions
{
public static IHostApplicationBuilder AddDotNetDefaults(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.ConfigureOpenTelemetry();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
// Turn on service discovery by default
http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment