Skip to main content

Newsletter Archive

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

Page 5 of 15 (43 editions)

April 19, 2026

Server-Sent Events in ASP.NET Core Minimal APIs

You need to push real-time updates from server to browser. SignalR is the default answer, but sometimes you don’t need bidirectional communication. You just need the server to stream events to the client: a progress bar, a live feed, a notification stream.

Server-Sent Events (SSE) does exactly that. It’s a native browser API built on plain HTTP. No WebSocket upgrade, no SignalR hub, no client library.

The Basics

Set the content type to text/event-stream and write data: lines followed by double newlines:

app.MapGet("/events/progress", async (HttpContext context, CancellationToken ct) =>
{
context.Response.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
string[] steps = ["Validating", "Processing", "Confirming"];
for (int i = 0; i < steps.Length; i++)
{
var json = JsonSerializer.Serialize(new { step = steps[i], percent = (i + 1) * 33 });
await context.Response.WriteAsync($"data: {json}\n\n", ct);
await context.Response.Body.FlushAsync(ct);
await Task.Delay(1500, ct);
}
});

The CancellationToken fires when the client disconnects, so you stop doing work immediately.

Named Events

Add an event: line to categorize messages:

await context.Response.WriteAsync($"event: progress\ndata: {json}\n\n", ct);

The client can then listen for specific event types.

The Client Side

The browser’s native EventSource API handles connection and auto-reconnection:

const source = new EventSource('/events/progress');
source.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
updateProgressBar(data.percent);
});
source.onerror = () => console.log('Reconnecting...');

Auto-reconnect is built into the spec. You get resilience for free.

Streaming with Channels

For fan-out scenarios like live dashboards, pair SSE with System.Threading.Channels:

app.MapGet("/events/orders", async (HttpContext ctx, OrderEventBus bus, CancellationToken ct) =>
{
ctx.Response.ContentType = "text/event-stream";
ctx.Response.Headers.CacheControl = "no-cache";
await foreach (var evt in bus.Subscribe(ct))
{
var json = JsonSerializer.Serialize(evt);
await ctx.Response.WriteAsync($"data: {json}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}
});

POST events into the channel from elsewhere, and every connected SSE client receives them in real time.

SSE vs. SignalR

SSESignalR
DirectionServer to client onlyBidirectional
TransportPlain HTTPWebSocket (with fallbacks)
ClientNative EventSourceRequires SignalR library
Auto-reconnectBuilt into the specBuilt into the client
Binary dataText onlyText and binary

Choose SSE when you only need server-to-client streaming and want the simplest possible setup. Choose SignalR when you need bidirectional messaging, binary data, or group management.

Key Takeaway

SSE is the lightest way to push real-time updates from ASP.NET Core to a browser. Set text/event-stream, write data: lines, flush, and the browser handles the rest, including automatic reconnection.

Read the full tip + 10 curated links

Don't miss the next tip 💧

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

April 12, 2026

System.Text.Json Source Generation: AOT-Ready Serialization Without Reflection

System.Text.Json works great out of the box. You call JsonSerializer.Serialize(myObject) and it figures everything out at runtime via reflection. But that runtime discovery has two costs:

  1. Slow first call while it builds metadata and converters
  2. Incompatible with Native AOT/trimming because the trimmer strips the metadata reflection needs

Source generation moves all of that to compile time.

The Problem

// Reflection-based: slow first call, breaks under trimming
string json = JsonSerializer.Serialize(product);

The Fix

Create a partial class extending JsonSerializerContext and annotate each type you need:

[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(Product[]))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext;

Use the generated type info instead of relying on reflection:

string json = JsonSerializer.Serialize(product, AppJsonContext.Default.Product);
var roundTripped = JsonSerializer.Deserialize(json, AppJsonContext.Default.Product);

AppJsonContext.Default.Product is the generated JsonTypeInfo<Product> - no reflection at any point.

Wiring into ASP.NET Core

builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

Inserting at position 0 means source-generated metadata is checked first. Uncovered types fall back to reflection. For fully AOT-safe apps, set it as the sole resolver:

options.SerializerOptions.TypeInfoResolver = AppJsonContext.Default;

Now any unregistered type produces a clear error instead of a silent reflection fallback.

Polymorphism

Use [JsonDerivedType] on the base type:

[JsonDerivedType(typeof(ElectronicProduct), "electronic")]
[JsonDerivedType(typeof(ClothingProduct), "clothing")]
public record Product(int Id, string Name, decimal Price);
public record ElectronicProduct(int Id, string Name, decimal Price, string Warranty)
: Product(Id, Name, Price);

The serializer emits a $type discriminator and the source generator handles the rest.

Performance

Serializing an array of 100 products:

MethodFirst callSubsequent calls
Reflection-based~12 ms~45 us
Source-generated~0.3 ms~30 us

The big win is that first call: source generation eliminates the reflection warmup entirely. Subsequent calls are comparable or slightly faster, with roughly 15% fewer allocations on the fast path.

Key Takeaway

A partial class, a few [JsonSerializable] attributes, and you get instant first-call performance plus trim/AOT compatibility. If you’re targeting AOT, serverless, or just want faster startup, this is the path.

Read the full tip + 13 curated links

Don't miss the next tip 💧

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

April 5, 2026

Compiled Bindings in .NET MAUI: Faster UI with Compile-Time Safety

Data binding in .NET MAUI resolves property names through reflection at runtime. Misspell a property in XAML and nothing breaks at build time. The binding just silently fails, and you stare at an empty label wondering why.

Compiled bindings fix both the safety and the speed problem.

The Problem

<!-- Typo: "Naem" instead of "Name". No build error. Silent failure at runtime. -->
<Label Text="{Binding Naem}" />

The Fix

Add x:DataType to tell the XAML compiler what type the BindingContext is:

<ContentPage xmlns:vm="clr-namespace:MyApp.ViewModels"
x:DataType="vm:ProductViewModel">
<Label Text="{Binding Name}" FontSize="24" />
<Label Text="{Binding Price, StringFormat='${0:F2}'}" />
<Button Text="Add to Cart" Command="{Binding AddToCartCommand}" />
</ContentPage>

Now {Binding Naem} is a build error. The compiler generates direct property access instead of using reflection, so bindings are faster too.

Scoping in Templates

In a CollectionView, the item template binds to a different type than the page. Scope x:DataType at the template level:

<ContentPage x:DataType="vm:ProductListViewModel">
<CollectionView ItemsSource="{Binding Products}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Grid ColumnDefinitions="*, Auto">
<Label Text="{Binding Name}" />
<Label Grid.Column="1"
Text="{Binding Price, StringFormat='${0:F2}'}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>

Each scope is type-checked independently. Page bindings resolve against ProductListViewModel, item bindings against Product.

Opting Out for Ancestor Bindings

Sometimes you need to reach a parent ViewModel from inside a template. Set x:DataType="" on that element to skip type checking:

<DataTemplate x:DataType="models:Product">
<Button x:DataType=""
Text="Remove"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:ProductListViewModel}},
Path=RemoveCommand}"
CommandParameter="{Binding .}" />
</DataTemplate>

Use this sparingly. Every opt-out is a binding that can silently fail again.

The Performance Win

Compiled bindings skip reflection-based property lookups. In a CollectionView with hundreds of items, this means faster initial load, smoother scrolling, and less memory overhead from cached reflection metadata. The difference is most noticeable on mid-range Android devices with multiple bound properties per cell.

Key Takeaway

Add x:DataType and your bindings get compile-time error checking and faster runtime performance. Typos become build errors. Reflection disappears. One attribute, two wins.

Read the full tip + 11 curated links

Don't miss the next tip 💧

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