Telerik blogs

The .NET Aspire series continues with an overview of the service defaults, a set of extension methods that compress all the OpenTelemetry wiring, health probes, discovery and Polly policies into one method call.

This is the third part of a six-part exploratory series on .NET Aspire.

In the first installment of this series, we said hi to .NET Aspire, Microsoft’s opinionated toolkit for building cloud-ready applications. In the sequel, we peeked at the Developer Dashboard to show how we can add observability with zero extra code.

Now, we want to answer the following question from your boss: “How do we make every service in our solution have consistency without having to copy and paste hundreds of lines of boilerplate into every project?”

“It depends” won’t get you out of this one, friend. The answer lies with Aspire’s service defaults. With it, you can attach a single builder.AddServiceDefaults() to your app and it immediately receives:

  • OpenTelemetry tracing, logging and metrics wired to your favorite exporter
  • Health-check endpoints that distinguish “alive” from “ready”
  • Service discovery so in-cluster, service-to-service calls work without hard-coded URLs
  • An opinionated Polly-powered resilience pipeline with time-outs, retries and circuit breakers

Let’s get started, shall we?

Why Bother with Service Defaults?

As we discussed in our first post, building services can get quite complicated with you having to sprinkle logging, tracing, retries, Docker configs, health checks and whatever else you need.

With .NET Aspire’s premise to be cloud-ready by default, you can instead reference ServiceDefaults once and instantly have an opinionated baseline that works the same everywhere, whether it’s on your laptop or inside Azure Container Apps. These conventions are tightly coupled with integrations that spin up the infrastructure you need (like SQL Server, Redis and Key Vault)—all with a single line of C#!

Everything is then expressed as C# code inside your AppHost project, so you can version control your application’s topology just like application logic.

Since conventions are expressed in one location, a future change—like changing from Honeycomb to Azure Monitor—is a one-line edit.

Let’s now look at some code and see how it all works.

Looking Inside the ServiceDefaults Project

When you create a new Aspire app, or add Aspire to your existing solution, you’ll see a project named ServiceDefaults.

Let’s look in the ServiceDefaults project. Open the file Extensions.cs (or something similar). You should see an AddServiceDefaults extension method that looks something like this:

public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
    builder.ConfigureOpenTelemetry();
    builder.AddDefaultHealthChecks();
    builder.Services.AddServiceDiscovery();
    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        http.AddStandardResilienceHandler();
        http.AddServiceDiscovery();
    });

    return builder;
}

Let’s unpack this code.

Note: The resiliency and service discovery parts deserve a treatment of their own and will be discussed in detail in Part 5 of our series.

OpenTelemetry Configuration

When we look at builder.ConfigureOpenTelemetry, .NET Aspire provides a set of opinionated defaults for OpenTelemetry.

By default, Aspire adds:

SignalInstrumentations
LogsMicrosoft.Extensions.Logging OTLP exporter
Traces and spansASP.NET Core, HttpClient, gRPC, SqlClient, EF Core, Redis
MetricsRuntime, ASP.NET Core, HttpClient, SqlClient

These are all viewable from the Aspire dashboard that we reviewed in the previous post. For the OTLP exporter, it easily integrates with your telemetry. For example, in Azure Container Apps the exporter points at Azure Monitor.

None of this is magic. If you scroll down in your Extensions.cs, you can inspect the ConfigureOpenTelemetry extension method to learn more.

public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
    builder.Logging.AddOpenTelemetry(logging =>
    {
        logging.IncludeFormattedMessage = true;
        logging.IncludeScopes = true;
    });

    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics =>
        {
            metrics.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation();
        })
        .WithTracing(tracing =>
        {
            tracing.AddSource(builder.Environment.ApplicationName)
                .AddAspNetCoreInstrumentation()
                // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
                //.AddGrpcClientInstrumentation()
                .AddHttpClientInstrumentation();
        });

    builder.AddOpenTelemetryExporters();

    return builder;
}

Configuring your exporter is super easy. Because the exporter URL comes from OTEL_EXPORTER_OTLP_ENDPOINT, changing where telemetry flows is an easy one-liner:

export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io

In the AddOpenTelemetryExporters, you’ll see it in action. Need Azure Monitor instead? Uncomment the relevant block in AddOpenTelemetryExporters, supply an APPLICATIONINSIGHTS_CONNECTION_STRING, and call it a day.

private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
    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;
}

Default Health Checks

Moving on: What does builder.AddDefaultHealthChecks() get us? This powerful line adds a set of opinionated defaults for health checks, which can be used by systems and applications to see if your app is ready.

The AddDefaultHealthChecks method adds a default liveness check to check if the application is responsive.

public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
    builder.Services.AddHealthChecks()
        // Add a default liveness check to ensure app is responsive
        .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);

    return builder;
}

You can see how this all works in the MapDefaultEndpoints extension method, where it exposes a /health endpoint and an /alive endpoint. The /health endpoint illustrates if the app is running and ready to receive requests. The /alive endpoint indicates if the app is running or must be restarted because of a crash.

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.
    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;
}

As you can see the defaults are for development only. You must opt into production exposure.

The health checks also come built in with Aspire’s various integrations. For example, the health check for SQL Server verifies that SQL Server is running and that a connection can be established.

Create a Custom Service Defaults Project

If you look at the MyApp.ServiceDefaults.csproj, you’ll notice a FrameworkReference dependency on Microsoft.AspNetCore.App.

<FrameworkReference Include="Microsoft.AspNetCore.App" />

That reference brings in the entire ASP.NET Core shared framework (with Kestrel, MVC, SignalR and so on).

In certain situations, you might not want this:

  • Non-HTTP workloads: Background workers or queue processors don’t need Kestrel.
  • Serverless hosts: Azure Functions ships their own ASP.NET Core copy. A second copy can cause binding conflicts.
  • Reusable class libraries: Transitive framework references force consumer apps to carry ASP.NET Core. What if the library is UI-agnostic?

In that case, you can create a custom service defaults project. According to the Microsoft Docs, you can create a new class library project and add its dependencies to the project file. Check out the docs for more information.

Wrapping Up

Service Defaults are the unsung hero of .NET Aspire. They compress all the OpenTelemetry wiring, health probes, discovery and Polly policies into one method call. As a result, every service behaves predictably, telemetry is always on and your new hire ramps up quickly (think minutes, not days).

In the next installment we’ll explore integrations. We’ll see how Aspire spins up SQL Server and Redis containers—all orchestrated from our AppHost. Stay tuned!


Dave-Brock-headshot-sq
About the Author

Dave Brock

Dave Brock is a software engineer, writer, speaker, open-source contributor and former Microsoft MVP. With a focus on Microsoft technologies, Dave enjoys advocating for modern and sustainable cloud-based solutions.

Related Posts

Comments

Comments are disabled in preview mode.