Skip to main content

Newsletter Archive

Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.

Page 6 of 19 (56 editions)

June 28, 2026

params ReadOnlySpan: The Allocation-Free Params

Every time you call a method with params T[], the compiler sneaks in a brand-new array allocation behind your back. One call? No big deal. Thousands of calls on a hot path? You’re basically running a charity for the garbage collector. C# 13 finally lets you swap that T[] for ReadOnlySpan<T>, and the results are chef’s kiss.

The change is laughably simple. Just update your method signature:

// Before: every call allocates an array
public static int Sum(params int[] numbers)
{
var total = 0;
foreach (var n in numbers) total += n;
return total;
}
// After: zero allocations, stack-allocated span
public static int Sum(params ReadOnlySpan<int> numbers)
{
var total = 0;
foreach (var n in numbers) total += n;
return total;
}

Callers don’t change at all. Sum(1, 2, 3) compiles exactly the same way. But under the hood, the compiler now stack-allocates the arguments into an inline array and wraps them in a ReadOnlySpan<T>. No heap. No GC pressure. No drama.

This shines anywhere you have variadic helpers that get called frequently: logging, formatting, math utilities, builder patterns, you name it. And because ReadOnlySpan<T> is a supertype of arrays anyway, existing callers that pass an explicit int[] still work. It’s a free upgrade.

// All of these just work:
Sum(1, 2, 3); // inline span, zero alloc
Sum([10, 20, 30]); // collection expression, zero alloc
Sum(myArray); // existing array, still compiles

The takeaway? If you own a params method that lives anywhere near a hot path, slap ReadOnlySpan<T> on it and let the runtime do the rest. Your GC will send you a thank-you card.

Read the full tip + 9 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.

June 21, 2026

IExceptionHandler: Global Error Handling Done Right

Raise your hand if you’ve ever written an exception-handling middleware in ASP.NET Core that caught Exception, logged it, and returned a ProblemDetails response. Yeah, me too. We’ve all written that middleware. We’ve all copy-pasted it between projects. ASP.NET Core now has IExceptionHandler built in, and it’s cleaner, more composable, and actually designed for this.

Here’s a custom exception handler that turns known exceptions into proper Problem Details responses:

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddExceptionHandler<AppExceptionHandler>();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Something went sideways!");
});
app.MapGet("/not-found", () =>
{
throw new NotFoundException("The droids you're looking for");
});
app.Run();
// Custom exception for demo purposes
class NotFoundException(string item)
: Exception($"Could not find: {item}");
// The exception handler: clean and focused
class AppExceptionHandler(ILogger<AppExceptionHandler> logger)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
logger.LogError(exception, "Unhandled exception occurred");
var (statusCode, title) = exception switch
{
NotFoundException => (StatusCodes.Status404NotFound, "Not Found"),
ArgumentException => (StatusCodes.Status400BadRequest, "Bad Request"),
_ => (StatusCodes.Status500InternalServerError, "Server Error")
};
httpContext.Response.StatusCode = statusCode;
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = statusCode,
Title = title,
Detail = exception.Message,
Instance = httpContext.Request.Path
}, cancellationToken);
return true; // We handled it
}
}

The beauty of IExceptionHandler is the return value. If TryHandleAsync returns true, the exception is considered handled and the pipeline stops. Return false, and the next registered handler gets a shot. This means you can chain multiple handlers (one for validation errors, one for auth failures, one as a catch-all, each focused on a single responsibility).

// Register multiple handlers: they execute in order
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<FallbackExceptionHandler>();

This integrates seamlessly with AddProblemDetails(), which ensures your error responses conform to RFC 9457. Your API consumers get consistent, well-structured error payloads. Your code stays clean. And you never have to write app.Use(async (ctx, next) => { try { ... } catch { ... } }) again. That middleware has earned its retirement.

Read the full tip + 8 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.

June 14, 2026

Minimal API Route Groups: Organize Your Endpoints Like a Pro

Minimal APIs are great until your Program.cs looks like a 400-line scroll of app.MapGet, app.MapPost, and app.MapDelete. At that point you start to miss controllers, not because they were better, but because they at least grouped things. Enter MapGroup(), which gives you clean organization AND endpoint filters for cross-cutting concerns, all without going back to controller-land.

Here’s how you group a set of related endpoints under a shared prefix:

var app = WebApplication.Create(args);
var todos = app.MapGroup("/api/todos")
.RequireAuthorization()
.AddEndpointFilter<RequestLoggingFilter>();
todos.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todos.MapGet("/{id:int}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id) is { } todo
? Results.Ok(todo)
: Results.NotFound());
todos.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/api/todos/{todo.Id}", todo);
});
app.Run();

Notice that RequireAuthorization() and AddEndpointFilter are applied to the group, not to each individual endpoint. Every route under /api/todos automatically gets auth and logging. No repetition, no forgetting to secure that one endpoint you added at 4:57 PM on a Friday.

Endpoint filters are the secret weapon here. They’re like middleware, but scoped to specific routes. Here’s a simple one that logs request timing:

public class RequestLoggingFilter(ILogger<RequestLoggingFilter> logger)
: IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var stopwatch = Stopwatch.StartNew();
var result = await next(context);
logger.LogInformation("Request completed in {Elapsed}ms",
stopwatch.ElapsedMilliseconds);
return result;
}
}

You can stack filters, nest groups inside groups, and even apply different auth policies per group. It’s all the organization of controllers with all the simplicity of minimal APIs. Best of both worlds: your Program.cs will finally be something you’re proud to show in a code review.

Read the full tip + 7 curated links

Don't miss the next tip 💧

Get a .NET tip and curated links delivered to your inbox every week.