The big headline features of each .NET release tend to hog the spotlight, yet a handful of lower-profile components-actual classes, structs, and helpers-quietly remove boiler-plate and unlock serious performance. Below are five worth adding to your toolbox, with release version, tips, and code snippets.
1. JsonSerializerContext
Source-generation for System.Text.Json
Introduced: .NET 6 / improved .NET 8
Source: How to use source generation in System.Text.Json

Why you should care
- Reflection-free (de)serialization > ~40-60% faster cold-start in micro-benchmarks.
- Compile-time schema validation eliminates run-time surprises.
- Works in AOT, Blazor, Azure Functions-anywhere reflection is costly.
using System.Text.Json;
using System.Text.Json.Serialization;
// Annotate a partial context
[JsonSerializable(typeof(WeatherForecast))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = false)]
public partial class WeatherForecastContext : JsonSerializerContext { }
// Use in serialization (no reflection!)
var forecast = new WeatherForecast();
var options = new JsonSerializerOptions();
var json = JsonSerializer.Serialize(forecast, WeatherForecastContext.Default.WeatherForecast);
Tips
- Keep one context per bounded domain to avoid megabyte-scale metadata.
- For version-tolerant APIs, pin an explicit schema file in your repo.
2. TimeProvider
Test-friendly abstraction of now
Introduced: .NET 8
Source: What is TimeProvider?

Why you should care
- Eliminates home-grown
IDateTimeService wrappers.
- Ships with
FakeTimeProvider for deterministic unit & integration tests.
- Also available to .NET Framework 4.7+/Standard 2.0 via NuGet.
// In DI registration
services.AddSingleton<TimeProvider>(TimeProvider.System);
// In your service
public class WeatherService
{
private readonly TimeProvider _timeProvider;
public WeatherService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public DateTimeOffset GetCurrentTime()
{
return _timeProvider.GetUtcNow();
}
}
Tips
- Prefer
TimeProvider.Current rather than passing an injected instance through every layer.
- Use
OffsetTimeProvider for deterministic "wall clock" simulations.
3. FrozenDictionary<TKey,TValue>
Read-only lookup with extreme throughput
Introduced: .NET 8
Source: FrozenDictionary<TKey,TValue> Class

Why you should care
- 2-8x faster look-ups than mutable or
ImmutableDictionary.
- Compact, contiguous memory layout; GC-friendly.
- Perfect for feature-flags, routing tables, country/locale maps.
using System.Collections.Frozen;
// Build once, freeze forever
var countryCodes = new[]
{
("UA", "Ukraine"),
("TR", "Türkiye"),
("DE", "Germany"),
}.ToFrozenDictionary(k => k.Item1, v => v.Item2);
// O(1) look-ups
if (countryCodes.TryGetValue("DE", out var name))
{
// Germany
Console.WriteLine(name);
}
Tips
- Construction cost is higher—create during app start-up or background warm-up.
- Store as a static/singleton to guarantee single-instance memory usage.
4. Keyed DI Services
Multiple implementations, no factories required
Introduced: .NET 8
Source: .NET 8 Dependency Injection Changes: Keyed Services

Why you should care
- Strategy, tenant, or payment‐gateway patterns become one-line registrations.
- Reduces reflection and keeps DI graph explicit.
// 1. Register
builder.Services.AddKeyedTransient<IMessageSender, EmailSender>("email");
builder.Services.AddKeyedTransient<IMessageSender, SmsSender>("sms");
// 2. Resolve in controller
[ApiController]
public class NotifyController(IMessageSender email,
[FromKeyedServices("sms")] IMessageSender sms)
{
// email & sms are resolved by key automatically
}
// 3. Resolve everywhere else
_serviceProvider.GetKeyedService<IMessageSender>("email")
Tips
- Combine with options-pattern to configure each implementation separately.
- Use enums converted to strings for compile-time key safety.
5. HybridCache
Tier-1 in-memory + tier-2 distributed caching with tag invalidation
Introduced: .NET 9
Source: Hello HybridCache! Streamlining Cache Management for ASP.NET Core Applications

Why you should care
- Hits local memory first (micro-seconds) before falling back to Redis or SQL.
- Tag-based invalidation evicts entire groups in a single call.
- Drop-in replacement for
IDistributedCache.
// Setup (Program.cs)
builder.Services
.AddMemoryCache()
.AddStackExchangeRedisCache(o => o.Configuration = "redis:6379")
.AddHybridCache(cfg =>
{
cfg.WithMemoryCache();
cfg.WithDistributedCache();
});
// Domain usage
public sealed class ProductService(HybridCache cache, DbContext db)
{
public async Task<Product[]> GetAllAsync() =>
await cache.GetOrCreateAsync(
"products::all",
entry =>
{
entry.SetTags("products");
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
return db.Products.AsNoTracking().ToArrayAsync();
});
public Task InvalidateAsync() =>
cache.InvalidateByTagsAsync("products");
}
Tips
- Keep in-memory expiration shorter than distributed TTL to maximize locality.
- Use tags that map to aggregate roots (
orders-{tenantId}, products, etc.).
- For event-driven apps, pair tag invalidation with a message bus to sync nodes.
Final Thoughts
Adopting these hidden components often pays bigger dividends than headline features:
- Less code (no more hand-rolled abstractions).
- Better tests (time control & deterministic state).
- Higher throughput (reflection-free serialization, frozen collections, locality-first caching).