Newsletter Archive
Browse through our collection of past newsletters. Each edition is packed with C# and .NET insights.
Page 3 of 12 (36 editions)
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:
- Slow first call while it builds metadata and converters
- 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 trimmingstring 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:
| Method | First call | Subsequent 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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
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.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.
Extension Members: C# 14 Breaks the ‘this’ Barrier
For over 15 years, extension methods have been one of C#‘s most beloved features. But they’ve always had a frustrating limitation in that you can only add methods. Want an extension property? An extension operator? A static helper that feels like it belongs on the type? Sorry, best we can do is another this parameter.
C# 14 changes that. The new extension block syntax lets you add properties, operators, and static members to types you don’t own.
The Old Way
We’ve all written utility classes like this:
public static class StringExtensions{ public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
public static string Truncate(this string s, int maxLength) => s.Length <= maxLength ? s : s[..maxLength];}It works, but IsNullOrEmpty should be a property. There’s no argument, no verb, it’s clearly a state check. And if you wanted a static helper like string.Join but custom? Impossible through extensions.
The New Way
C# 14 introduces the extension block. You declare it inside a static class, specify the receiver type, and then define members as if you were writing them inside the type itself:
public static class StringExtensions{ extension(string? s) { // Extension property — no more parentheses for simple checks public bool IsNullOrEmpty => string.IsNullOrEmpty(s);
// Extension method — works just like before, cleaner grouping public string Truncate(int maxLength) => s is null || s.Length <= maxLength ? s! : s[..maxLength]; }}Now you call IsNullOrEmpty the way it always should have been:
string? name = GetName();
if (name.IsNullOrEmpty) Console.WriteLine("No name provided");
string preview = name.Truncate(50);No parentheses, no “is this a method or a property?” confusion. It just reads like a member.
Static Extension Members
Want to add a factory method or constant that lives on the type itself? Use the parameterless receiver syntax:
public static class GuidExtensions{ extension(Guid) { public static Guid CreateFrom(string input) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); return new Guid(hash.AsSpan(0, 16)); } }}Now it’s called like a real static member:
No GuidHelper.CreateFrom(...). No hunting for which utility class has the method. It’s right where you’d expect it.
Generics Work Too
Extension blocks support generic type parameters and constraints:
public static class EnumerableExtensions{ extension<T>(IEnumerable<T> source) { public bool IsEmpty => !source.Any();
public IEnumerable<T> WhereGreaterThan(T value) where T : IComparable<T> => source.Where(x => x.CompareTo(value) > 0); }}var numbers = new List<int> { 1, 2, 3, 4, 5 };
if (!numbers.IsEmpty){ var big = numbers.WhereGreaterThan(3); // [4, 5]}What You Can’t Do (Yet)
- No backing fields - extension properties are computed only, no stored state.
- No constructors or events - you’re extending the surface, not the internals.
- Old-style extensions still work - the classic
thisparameter syntax isn’t going anywhere. You can mix both in the same project.
The Bottom Line
Extension members take the most-used workaround in C# and make them real. Properties feel like properties. Static helpers live on the type. Your code reads more naturally, and your consumers don’t need to know the difference.
If you maintain a library or a shared “Extensions” folder, C# 14 just made it a lot more expressive.
Don't miss the next tip 💧
Get a .NET tip and curated links delivered to your inbox every week.