A retry policy that doesn’t account for idempotency turns one dropped connection into a duplicated bulk insert. A log statement that doesn’t know about data classification writes a customer’s SSN to a file three teams can read. dotnet/extensions closes gaps like these: patterns Microsoft built out of running Teams and Microsoft 365 at internet scale.

The core .NET runtime gives you the primitives: HttpClient, ILogger, and IOptions<T>. It doesn’t give you a retry that knows about idempotency, a log line that knows about data classification, or a way to tell an orchestrator your service is still alive.

If you’ve worked with ASP.NET Core you already know Microsoft.Extensions for dependency injection, logging, and configuration. dotnet/extensions sits a layer above that, shipped as nine independently installable NuGet packages.

dotnet/extensions

AI

Resilience

Telemetry

Compliance

Diagnostics

Contextual options

ASP.NET Core extensions

Static analysis

Testing

Every package below lives under the Microsoft.Extensions namespace unless noted otherwise.

PackagePurposeKey API
AIProvider-agnostic generative AI abstractionsIChatClient
Http.ResiliencePolly-based resilience pipelines for HttpClientAddStandardResilienceHandler
TelemetryStructured logging, metering, tracing, latency[LogProperties]
Compliance.RedactionData classification and log redactionAddRedaction
Diagnostics (health checks)Liveness and readiness reporting for orchestratorsAddHealthChecks
Options.ContextualOptions that resolve differently per callerIContextualOptions<T>
ASP.NET Core extensionsHigh-performance request-pipeline middlewareAddRequestLatencyTelemetry
StaticAnalysisCurated Roslyn analyzer configurationPackageReference
TimeProvider.TestingDeterministic time and logging fakesFakeLogger, FakeTimeProvider

AI

Microsoft.Extensions.AI defines provider-agnostic interfaces for generative AI: IChatClient for chat completions, IEmbeddingGenerator<TInput, TEmbedding> for embeddings, and an experimental IImageGenerator.

A basic call looks like this:

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(
    new Uri("http://localhost:11434/"), "phi3:mini");

Console.WriteLine(await client.GetResponseAsync("What is AI?"));

ChatClientBuilder wraps that client with caching, function invocation, and OpenTelemetry, the same way ASP.NET Core middleware wraps a request:

IChatClient client = new ChatClientBuilder(
    new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"))
    .UseDistributedCache(new MemoryDistributedCache(
        Options.Create(new MemoryDistributedCacheOptions())))
    .UseFunctionInvocation()
    .UseOpenTelemetry(
        sourceName: sourceName,
        configure: c => c.EnableSensitiveData = true)
    .Build();

App code

IChatClient

Distributed cache

Function invocation

OpenTelemetry

Model provider

AddChatClient registers that same pipeline as an injectable IChatClient:

builder.Services.AddDistributedMemoryCache();
builder.Services
    .AddChatClient(new OllamaApiClient(
        new Uri("http://localhost:11434"), "llama3.1"))
    .UseDistributedCache();

Semantic Kernel and the .NET Agent Framework both build on these same abstractions, so code written against IChatClient isn’t locked to one library.

Resilience

If you’ve used Polly, Microsoft.Extensions.Http.Resilience will feel familiar because it’s built on Polly. For a case where a retry policy has to be paired with idempotent writes, see Dapper Plus or SqlBulkCopy?.

The standard resilience handler adds five strategies to an HttpClient in one line:

IHttpClientBuilder httpClientBuilder = builder.Services.AddHttpClient<PaymentsClient>(
    client => client.BaseAddress = new("https://api.example.com"));

httpClientBuilder.AddStandardResilienceHandler();

Request

Rate limiter

Total timeout

Retry

Circuit breaker

Attempt timeout

HttpClient

The documented defaults chain five strategies:

  • Rate limiter: 1,000 concurrent permits
  • Total timeout: 30 seconds for the whole call, retries included
  • Retry: 3 attempts, exponential backoff with jitter
  • Circuit breaker: opens at a 10% failure ratio over a 30-second sampling window
  • Attempt timeout: 10 seconds per individual attempt

When the defaults don’t fit, AddResilienceHandler gives you the same building blocks with full control:

httpClientBuilder.AddResilienceHandler("CustomPipeline", builder =>
{
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        BackoffType = DelayBackoffType.Exponential,
        MaxRetryAttempts = 5,
        UseJitter = true
    });

    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        SamplingDuration = TimeSpan.FromSeconds(10),
        FailureRatio = 0.2,
        MinimumThroughput = 3
    });

    builder.AddTimeout(TimeSpan.FromSeconds(5));
});

There’s also a hedging handler for calling multiple endpoints and falling back to healthy ones when some degrade. Its circuit breakers are keyed by URL authority, so each endpoint gets its own isolation boundary:

httpClientBuilder.AddStandardHedgingHandler();

Telemetry

Microsoft.Extensions.Telemetry ships a source generator that replaces the default LoggerMessage generator and knows how to log an entire object’s public properties as individual structured fields, not just its ToString().

The [LogProperties] attribute does the work:

[LoggerMessage(Level = LogLevel.Debug, Message = "Generated forecast")]
private static partial void GeneratedForecast(
    ILogger logger, [LogProperties] WeatherForecast forecast);

internal record WeatherForecast(DateOnly Date, int TemperatureC)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Calling GeneratedForecast(logger, forecast) produces structured output with every property broken out as its own field:

forecast.TemperatureF = 125
forecast.TemperatureC = 52
forecast.Date = 11/27/2023

Compliance

Microsoft.Extensions.Compliance.Redaction strips or masks sensitive data before it reaches a log, once you’ve tagged the field with a classification. A classification is just a DataClassification value on a static class you define:

public static class MyTaxonomyClassifications
{
    public static string Name => "MyTaxonomy";

    public static DataClassification Private => new(Name, nameof(Private));
    public static DataClassification Personal => new(Name, nameof(Personal));
}

AddRedaction maps each classification to the redactor that handles it:

services.AddRedaction(redactionBuilder =>
{
    redactionBuilder.SetRedactor<StarRedactor>(
        MyTaxonomyClassifications.Private,
        MyTaxonomyClassifications.Personal);
});

StarRedactor below just replaces whatever it’s given with four asterisks:

public sealed class StarRedactor : Redactor
{
    private const string Stars = "****";

    public override int GetRedactedLength(ReadOnlySpan<char> input) => Stars.Length;

    public override int Redact(ReadOnlySpan<char> source, Span<char> destination)
    {
        Stars.CopyTo(destination);
        return Stars.Length;
    }
}

Tag a customer’s SSN with Private once, and every log call that touches it goes through the redactor, whether the developer writing that log line remembers to think about it or not.

Diagnostics

Kubernetes won’t route traffic to a pod that never answers a health check, and it can’t tell a stuck service from a healthy one without one. AddHealthChecks gives it something to ask:

builder.Services.AddHealthChecks()
    .AddCheck<PaymentsProviderHealthCheck>("payments-provider");

app.MapHealthChecks("/health");

A custom check implements IHealthCheck and reports Healthy, Degraded, or Unhealthy based on whatever condition matters, a database connection, a downstream API, disk space. MapHealthChecks exposes the aggregate result at a route your orchestrator can poll.

Contextual options

This one extends the standard options pattern to resolve differently depending on context, the same option key returning different values for different users, tenants, or request metadata. That’s the mechanism behind A/B tests and per-tenant feature flags.

A context is just a partial class marked with an attribute:

[OptionsContext]
internal partial class WeatherForecastContext
{
    public Guid UserId { get; set; }
    public string? Country { get; set; }
}

Resolving options against that context goes through IContextualOptions<T> instead of IOptions<T>:

public sealed class WeatherForecastService(
    IContextualOptions<WeatherForecastOptions> contextualOptions)
{
    public async Task<WeatherForecastOptions> GetOptionsAsync(
        WeatherForecastContext context, CancellationToken cancellationToken)
    {
        return await contextualOptions.GetAsync(context, cancellationToken);
    }
}

Global configuration still applies as the baseline. The context only overrides what needs to differ per caller.

ASP.NET Core extensions

A collection of middleware for high-performance services, operating at the application level rather than inside MVC’s filter pipeline. Request latency tracking is the one you’ll reach for most: it measures where time actually goes within a request, not just the total.

builder.Services.AddRequestLatencyTelemetry();
builder.Services.AddRequestCheckpoint(options => { });

var app = builder.Build();

app.UseRequestCheckpoint();
app.UseRequestLatencyTelemetry();

Named checkpoints get registered at startup. Mark one wherever a request crosses a stage in your pipeline, and the middleware flushes the measured latencies to your exporter when the request finishes.

Static analysis

Microsoft.Extensions.StaticAnalysis is a curated set of Roslyn analyzer configurations, shipped as a package instead of a .editorconfig file everyone has to remember to copy:

<PackageReference Include="Microsoft.Extensions.StaticAnalysis" Version="*">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

Add it once and every project referencing it gets the same code quality baseline, enforced at build time instead of at review time.

Testing

Two small utilities that solve two specific pain points.

FakeLogger gives you an ILogger you can make deterministic assertions against, log level, message, structured properties, without mocking:

var fakeLogger = new FakeLogger<MyComponent>();
var componentUnderTest = new MyComponent(fakeLogger);

componentUnderTest.DoWork();

FakeLogCollector collector = fakeLogger.Collector;
IReadOnlyList<FakeLogRecord> logs = collector.GetSnapshot();

FakeTimeProvider does the same for anything built on TimeProvider, letting you advance time instantly instead of waiting on real clocks:

var fakeTimeProvider = new FakeTimeProvider();
var operation = new DelayedOperation(fakeTimeProvider);

Task task = operation.ExecuteAsync(TimeSpan.FromMinutes(5));
Assert.False(task.IsCompleted);

fakeTimeProvider.Advance(TimeSpan.FromMinutes(5));
await task;

Assert.True(task.IsCompleted);

Both slot into the same DI container as their real counterparts, so the code under test doesn’t know it’s running against a fake.

Should you adopt this?

Adopt selectively, not wholesale.

Microsoft.Extensions.Http.Resilience is close to a free win if your service talks to external dependencies. Wiring it up takes one line, and it prevents the failure modes that page someone at 3 a.m.

Add Microsoft.Extensions.AI early if you’re building AI capabilities. The abstraction costs almost nothing, and the major providers already support it.

FakeLogger earns its place the first time you’ve fought to assert on logged output in a unit test.

Compliance, Contextual options, and the ASP.NET Core middleware can wait until you hit the specific problem they solve. A service with no sensitive fields doesn’t need redaction yet.

Summary

Nine packages, each solving one problem Microsoft hit running Teams and Microsoft 365 at scale. None require the others: pull in just the resilience handler, or just the AI abstractions, and the rest stays out of your dependency tree. But they compose when you need them to. Resilience telemetry flows into the same pipeline as the AI middleware’s OpenTelemetry export, and FakeTimeProvider slots into a DI container built for the real TimeProvider without either one knowing the other exists.

Review the repository at github.com/dotnet/extensions.