Newsletter Archive
Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.
Page 1 of 19 (56 editions)
Minimal APIs have always made the happy path look good.
The unhappy path took more work. You could decorate a request model with Data Annotations, but nothing automatically validated it. Most of us wrote an endpoint filter, pulled in another package, or checked every property by hand.
ASP.NET Core 10 adds validation directly to Minimal APIs.
Try It
Create a .NET 10 empty web app:
dotnet new web -n MinimalValidationDemo -f net10.0cd MinimalValidationDemoReplace Program.cs with this:
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation();
var app = builder.Build();
app.MapPost("/products", (CreateProduct request) =>{ var product = new Product( Id: Random.Shared.Next(1, 10_000), request.Name, request.Quantity);
return TypedResults.Created($"/products/{product.Id}", product);});
app.Run();
public sealed record CreateProduct( [Required, StringLength(100, MinimumLength = 3)] string Name,
[Range(1, 1_000)] int Quantity);
public sealed record Product(int Id, string Name, int Quantity);Run it:
dotnet runSend an invalid request:
curl -i -X POST http://localhost:5000/products -H "Content-Type: application/json" -d "{\"name\":\"\",\"quantity\":0}"Use the HTTP URL shown by dotnet run if your port is different. ASP.NET Core returns 400 Bad Request before your handler runs.
Send a valid request:
curl -i -X POST http://localhost:5000/products -H "Content-Type: application/json" -d "{\"name\":\"Mechanical Keyboard\",\"quantity\":4}"Validation Isn’t Limited to Request Bodies
The generated validation filter can validate values from the request body, query string, and headers:
app.MapGet("/inventory", ( [Range(1, 100)] int pageSize, [Required] string warehouse) =>{ return TypedResults.Ok(new { pageSize, warehouse });});The Multi-Assembly Gotcha
AddValidation() uses a source generator. It discovers validatable endpoint types in the assembly where you call it.
If your endpoints live in a separate class library, call AddValidation() from that assembly. Otherwise your attributes can look correct while the generated validation metadata never sees them.
You can disable validation for a specific endpoint with .DisableValidation(), but use that on purpose. Turning it off because a model is inconvenient usually means the model needs another look.
Key Takeaway
ASP.NET Core 10 can validate Minimal API inputs before your handler runs. Add AddValidation(), decorate the model you already have, and let the framework produce the 400 response.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
You need to run git status from a .NET tool and capture the result.
That sounds like five lines of code. The old Process API could turn it into redirection settings, output events, error events, task completion sources, and a small prayer that both streams finish before the process exits.
.NET 11 adds one-shot process helpers for the common case.
This tip uses .NET 11 Preview 7. Preview APIs can change before the final release.
Try It
Install the .NET 11 Preview 7 SDK, then create a console app:
dotnet new console -n ProcessCaptureDemo -f net11.0cd ProcessCaptureDemoReplace Program.cs with this:
using System.Diagnostics;
ProcessTextOutput result = await Process.RunAndCaptureTextAsync( "git", ["status", "--short"]);
Console.WriteLine($"Exit code: {result.ExitStatus.ExitCode}");
if (!string.IsNullOrWhiteSpace(result.StandardOutput)){ Console.WriteLine("Standard output:"); Console.WriteLine(result.StandardOutput);}
if (!string.IsNullOrWhiteSpace(result.StandardError)){ Console.WriteLine("Standard error:"); Console.WriteLine(result.StandardError);}Run it from any Git repository:
dotnet runYou get standard output, standard error, and the exit code in one result. No event handlers required.
Stream Output as It Arrives
When you need live progress instead of one final string, .NET 11 also adds ReadAllLinesAsync:
using var process = Process.Start(new ProcessStartInfo{ FileName = "dotnet", Arguments = "--info", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false})!;
await foreach (ProcessOutputLine line in process.ReadAllLinesAsync(CancellationToken.None)){ var label = line.StandardError ? "ERR" : "OUT"; Console.WriteLine($"[{label}] {line.Content}");}The output type tells you which stream produced each line, so you don’t have to merge two event pipelines yourself.
When Not to Use It
RunAndCaptureTextAsync holds the captured text in memory. That is perfect for a command such as git status. It is not the right choice for a process that can print gigabytes or run forever. Stream those lines or redirect them to a file instead.
Key Takeaway
.NET 11 gives Process a much friendlier path for running commands and collecting results. Use the one-shot helper for bounded output. Use the async line stream when you need progress as it happens.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
You have a large folder to archive inside a web request or desktop app. The ZIP APIs do the job, but the synchronous call keeps a thread occupied while the disk does all the real work.
The usual workaround was to wrap it in Task.Run. That moves the blocked thread. It doesn’t make the file work asynchronous.
.NET 10 adds real async APIs for creating, opening, updating, and extracting ZIP archives.
Try It
Create a .NET 10 console app:
dotnet new console -n AsyncZipDemo -f net10.0cd AsyncZipDemoReplace Program.cs with this:
using System.IO.Compression;
var workspace = Path.Combine(Path.GetTempPath(), "async-zip-demo");var source = Path.Combine(workspace, "source");var extracted = Path.Combine(workspace, "extracted");var archive = Path.Combine(workspace, "backup.zip");
if (Directory.Exists(workspace)){ Directory.Delete(workspace, recursive: true);}
Directory.CreateDirectory(source);
await File.WriteAllTextAsync( Path.Combine(source, "hello.txt"), "Hello from .NET 10!");
await File.WriteAllTextAsync( Path.Combine(source, "settings.json"), """{"theme":"dark","fontSize":18}""");
await ZipFile.CreateFromDirectoryAsync( source, archive, CompressionLevel.Optimal, includeBaseDirectory: false, cancellationToken: CancellationToken.None);
await ZipFile.ExtractToDirectoryAsync( archive, extracted, overwriteFiles: true, cancellationToken: CancellationToken.None);
foreach (var file in Directory.EnumerateFiles(extracted)){ Console.WriteLine($"{Path.GetFileName(file)}: {await File.ReadAllTextAsync(file)}");}Run it:
dotnet runMore Than Two Helper Methods
.NET 10 also adds async APIs lower in the stack:
await using var file = File.OpenRead("backup.zip");using var archive = await ZipArchive.CreateAsync( file, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null, cancellationToken: cancellationToken);
foreach (var entry in archive.Entries){ await using var entryStream = await entry.OpenAsync(cancellationToken); // Read the entry without blocking a thread on file I/O.}That matters when you are working with uploaded streams, cloud storage, or archives that never touch a local filename.
When Not to Use It
Async doesn’t make compression itself free. The CPU work still has to happen. The win is that file and stream waits no longer pin a thread.
For a tiny ZIP created once in a console tool, the synchronous API is perfectly fine. Reach for async when the archive is large, the storage is slow, cancellation matters, or blocking a request thread would hurt.
Key Takeaway
.NET 10 gives ZIP files honest async support. You can create, extract, open, and update archives without hiding synchronous I/O inside Task.Run.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.