Showing posts with label dotnet. Show all posts
Showing posts with label dotnet. Show all posts

Wednesday, 5 November 2025

Diagnosing .NET Container Crashes in Kubernetes

Diagnosing .NET Container Crashes in Kubernetes: From Logs to Liveness Probes

Diagnosing .NET Container Crashes in Kubernetes

Debugging .NET in Kubernetes

From Logs to Liveness Probes: A Complete Troubleshooting Guide

Diagnosing container crashes in Kubernetes can be challenging, especially when .NET applications fail silently or enter crash loops. This comprehensive guide walks you through real-world debugging scenarios, covering container lifecycle, health probes, and systematic troubleshooting approaches.

You'll learn how to leverage Kubernetes events, container logs, health probes, and advanced diagnostic tools to quickly identify and resolve issues in your .NET containerized applications.

๐Ÿ” 1. Introduction

⚠️ The Problem

.NET containers crashing silently in Kubernetes environments is a common challenge that can be difficult to diagnose. Unlike traditional application debugging, containerized environments require a different approach to troubleshooting.

Why traditional logging isn't enough:

  • Logs may be lost when containers restart
  • Startup failures occur before logging is configured
  • Kubernetes events provide crucial context that logs alone don't capture
  • Health probe failures can mask underlying application issues

๐ŸŽฏ What You'll Learn

This guide provides an end-to-end diagnosis workflow:

  • Interpreting Kubernetes pod states and events
  • Extracting meaningful information from container logs
  • Configuring and troubleshooting health probes
  • Resolving CrashLoopBackOff scenarios
  • Using advanced diagnostic tools for live inspection
  • Implementing preventive measures and best practices

๐Ÿ’ฅ 2. Common Crash Scenarios in .NET Containers

Understanding common failure patterns helps you quickly identify root causes. Here are the most frequent scenarios:

๐Ÿ“š Native Library Mismatches

Missing or incorrect native dependencies can cause immediate container failures:

  • Missing .so files: Linux native libraries not included in the container image
  • Incorrect RID targeting: Runtime Identifier (RID) mismatch between build and runtime environments
  • Architecture mismatches: x64 vs ARM64 incompatibilities

Example Error:

// System.DllNotFoundException or System.TypeLoadException
Unhandled exception. System.DllNotFoundException: 
Unable to load DLL 'libgdiplus.so' or one of its dependencies

๐Ÿš€ Startup Exceptions

Configuration errors often manifest during application startup:

  • Misconfigured environment variables: Missing or incorrect values
  • Missing secrets: Kubernetes secrets not mounted or accessible
  • Database connection failures: Connection strings or network issues
  • Invalid configuration files: JSON or XML parsing errors

๐Ÿ’พ Memory Issues

Memory-related problems can cause containers to be terminated:

  • Memory leaks: Gradual memory consumption increase
  • OOMKilled events: Containers exceeding memory limits
  • Insufficient memory requests: Containers not allocated enough memory

๐Ÿฅ Health Probe Misconfigurations

Improperly configured probes can cause false failures:

  • Readiness probe failures: Pods marked as not ready, blocking traffic
  • Liveness probe failures: Pods being restarted unnecessarily
  • Wrong probe endpoints: Incorrect paths or ports
  • Timeout issues: Probes timing out before application is ready

๐ŸŒ Network and DNS Failures

Network-related issues during service bootstrapping:

  • DNS resolution failures: Service names not resolving
  • Network policy blocking connections
  • Service discovery issues

๐Ÿ“Š 3. Initial Triage: Kubernetes Events and Pod Status

Start your diagnosis by examining pod status and Kubernetes events. These provide the highest-level view of what's happening.

๐Ÿ”Ž Inspecting Pod Status

Use kubectl describe to get detailed information about a pod:

# Describe a specific pod
kubectl describe pod <pod-name> -n <namespace>

# Key fields to examine:
# - State: Current container state
# - Last State: Previous container state
# - Exit Code: Process exit code
# - Reason: Termination reason
# - Restart Count: Number of restarts

๐Ÿ’ก What to Look For:

  • Exit Code 0: Normal termination (may indicate application logic issue)
  • Exit Code 1-255: Application error (check logs)
  • OOMKilled: Out of memory (increase limits or fix memory leak)
  • Error: Container runtime error
  • CrashLoopBackOff: Pod restarting repeatedly

๐Ÿ“… Examining Events Timeline

Events provide a chronological view of pod lifecycle:

# Get events sorted by creation timestamp
kubectl get events --sort-by=.metadata.creationTimestamp -n <namespace>

# Filter events for a specific pod
kubectl get events --field-selector involvedObject.name=<pod-name> \
  --sort-by=.metadata.creationTimestamp -n <namespace>

# Save events to a file for postmortem analysis
kubectl get events --sort-by=.metadata.creationTimestamp -n <namespace> \
  > events-$(date +%Y%m%d-%H%M%S).log

⚠️ Audit Tip: Always pipe output to timestamped logs for postmortem analysis. Events are ephemeral and may be lost if the cluster is restarted or events are pruned.

๐Ÿ“ 4. Deep Dive into Container Logs

Container logs are your primary source of application-level errors. Here's how to extract maximum value from them.

๐Ÿ“‹ Retrieving Logs

Basic log retrieval commands:

# Get logs from a pod
kubectl logs <pod-name> -n <namespace>

# Get logs from a specific container in a multi-container pod
kubectl logs <pod-name> -c <container-name> -n <namespace>

# Follow logs in real-time
kubectl logs -f <pod-name> -n <namespace>

# Get logs from previous container instance
kubectl logs <pod-name> --previous -n <namespace>

# Get last 100 lines
kubectl logs --tail=100 <pod-name> -n <namespace>

๐Ÿณ Handling Multi-Container Pods

When pods contain multiple containers, you need to specify which container's logs to retrieve:

# List containers in a pod
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.containers[*].name}'

# Get logs from each container
for container in $(kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].name}'); do
  echo "=== Logs from $container ==="
  kubectl logs <pod-name> -c $container -n <namespace>
done

๐Ÿ“Š Using .NET Structured Logging

Leverage structured logging for better diagnostics. Configure Serilog or Microsoft.Extensions.Logging to output JSON:

// Program.cs - Configure JSON logging
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

// Configure structured JSON logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddJsonConsole(options =>
{
    options.JsonWriterOptions = new System.Text.Json.JsonWriterOptions
    {
        Indented = true
    };
});

// Or with Serilog
// builder.Host.UseSerilog((context, config) =>
// {
//     config.WriteTo.Console(new JsonFormatter());
// });

var app = builder.Build();
app.Run();

๐Ÿ” Decoding Common .NET Exceptions

Understanding exception types helps identify root causes:

System.TypeLoadException:

  • Missing assembly or version mismatch
  • Check NuGet package versions and dependencies
  • Verify all DLLs are included in the container image

System.DllNotFoundException:

  • Missing native library (.so on Linux, .dll on Windows)
  • Verify RID targeting matches container architecture
  • Check if native dependencies are included in the image

System.Net.Http.HttpRequestException:

  • Network connectivity issues
  • DNS resolution problems
  • Service endpoint not available

๐Ÿฅ 5. Probes: Readiness vs Liveness

Health probes are critical for Kubernetes to understand your application's state. Misconfiguration can cause unnecessary restarts or traffic routing issues.

๐Ÿ“– Definitions and Differences

Readiness Probe:

  • Determines if a pod is ready to receive traffic
  • If it fails, the pod is removed from Service endpoints
  • Does not restart the pod
  • Use when the app needs time to initialize (database connections, cache warming, etc.)

Liveness Probe:

  • Determines if the application is running correctly
  • If it fails, Kubernetes restarts the pod
  • Use to detect deadlocks or hung applications
  • Should be more lenient than readiness probe

⚙️ How Misconfigured Probes Cause Issues

  • Too aggressive liveness probe: Restarts healthy pods unnecessarily
  • Too strict readiness probe: Pods never become ready, blocking all traffic
  • Wrong timeout values: Probes fail even when the app is healthy
  • Incorrect endpoint paths: Probes always fail

๐Ÿ“ YAML Configuration Examples

Proper probe configuration in a Kubernetes deployment:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-net-app
spec:
  template:
    spec:
      containers:
      - name: my-app
        image: myregistry/my-net-app:latest
        ports:
        - containerPort: 8080
        
        # Readiness probe - checks if app is ready for traffic
        readinessProbe:
          httpGet:
            path: /healthz/ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
          timeoutSeconds: 3
          failureThreshold: 3
        
        # Liveness probe - checks if app is alive
        livenessProbe:
          httpGet:
            path: /healthz/live
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3

๐Ÿ’ป Implementing Health Check Endpoints in .NET

Create health check endpoints in your .NET application:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add health checks
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy())
    .AddDbContextCheck<MyDbContext>()
    .AddUrlGroup(new Uri("http://external-service/health"), "external-api");

var app = builder.Build();

// Readiness endpoint - checks if app is ready
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

// Liveness endpoint - checks if app is alive
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = _ => false  // Only checks if the app is running
});

app.Run();

๐Ÿ’ก Audit Strategy: Log probe responses and status codes to understand probe behavior. Add middleware to log health check requests:

// Log health check requests
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/healthz"))
    {
        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
        logger.LogInformation("Health check: {Path} from {Ip}", 
            context.Request.Path, context.Connection.RemoteIpAddress);
    }
    await next();
});

๐Ÿ”„ 6. CrashLoopBackOff: Root Cause and Recovery

CrashLoopBackOff is a common state indicating a pod is restarting repeatedly. Understanding its mechanics helps you resolve issues quickly.

๐Ÿ“š What CrashLoopBackOff Means

When a pod fails repeatedly, Kubernetes implements an exponential backoff strategy:

  • Initial restart: Immediate
  • First backoff: 10 seconds
  • Subsequent backoffs: 20s, 40s, 80s, 160s (capped at 300s)
  • Maximum wait: 5 minutes between restart attempts

⚠️ Interpreting Backoff Timings: Longer backoff periods indicate the pod has been failing for an extended period. Check the restart count and recent events to understand the failure pattern.

๐Ÿ”ง Strategies for Resolution

1. Increase initialDelaySeconds

If your application needs more time to start, increase the initial delay:

livenessProbe:
  httpGet:
    path: /healthz/live
    port: 8080
  initialDelaySeconds: 30  # Increased from 10
  periodSeconds: 10

2. Add Retry Logic in App Startup

Implement retry logic for external dependencies:

// Program.cs - Retry logic for database connection
var retryPolicy = Policy
    .Handle<SqlException>()
    .WaitAndRetryAsync(
        retryCount: 5,
        sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
        onRetry: (exception, timeSpan, retryCount, context) =>
        {
            logger.LogWarning("Retry {RetryCount} after {Delay}s", retryCount, timeSpan.TotalSeconds);
        });

await retryPolicy.ExecuteAsync(async () =>
{
    // Initialize database connection
    await dbContext.Database.EnsureCreatedAsync();
});

3. Use postStart Lifecycle Hooks

Perform initialization tasks after container start:

lifecycle:
  postStart:
    exec:
      command:
      - /bin/sh
      - -c
      - sleep 10 && echo "Initialization complete"

๐Ÿ” Auto-Detect CrashLoopBackOff Pods

Use this script to quickly identify all pods in CrashLoopBackOff state:

#!/bin/bash
# Find all CrashLoopBackOff pods
kubectl get pods --all-namespaces | grep CrashLoopBackOff

# Get detailed information for each
for pod in $(kubectl get pods --all-namespaces \
  -o jsonpath='{range .items[?(@.status.containerStatuses[*].state.waiting.reason=="CrashLoopBackOff")]}{.metadata.namespace}{"\t"}{.metadata.name}{"\n"}{end}'); do
  namespace=$(echo $pod | cut -f1)
  name=$(echo $pod | cut -f2)
  echo "=== $namespace/$name ==="
  kubectl describe pod $name -n $namespace | grep -A 5 "State:"
  kubectl logs $name -n $namespace --tail=20
  echo ""
done

๐Ÿ”ฌ 7. Advanced Diagnostics

When standard logs and events aren't enough, use advanced diagnostic techniques to inspect running containers.

Using kubectl exec for Live Inspection

Execute commands inside running containers:

# Open an interactive shell
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh

# Or for bash
kubectl exec -it <pod-name> -n <namespace> -- /bin/bash

# Run a specific command
kubectl exec <pod-name> -n <namespace> -- ps aux
kubectl exec <pod-name> -n <namespace> -- env
kubectl exec <pod-name> -n <namespace> -- ls -la /app

๐Ÿณ Mounting Debug Sidecars

Add a debug container to your pod for troubleshooting:

spec:
  containers:
  - name: my-app
    image: my-app:latest
  - name: debug
    image: busybox:latest
    command: ["sleep", "3600"]
    volumeMounts:
    - name: app-volume
      mountPath: /shared

๐Ÿ“ Inspecting System Directories

Examine system files for additional context:

# Check process information
kubectl exec <pod-name> -- cat /proc/1/status

# View environment variables
kubectl exec <pod-name> -- env | sort

# Check mounted volumes
kubectl exec <pod-name> -- mount

# Inspect network configuration
kubectl exec <pod-name> -- cat /etc/resolv.conf

๐Ÿ› ️ .NET-Specific Diagnostic Tools

Use .NET diagnostic tools inside containers:

dotnet-dump

Capture memory dumps for analysis:

# Install dotnet-dump in container
kubectl exec <pod-name> -- dotnet tool install -g dotnet-dump

# Capture a dump
kubectl exec <pod-name> -- dotnet-dump collect -p 1

# Copy dump out of container
kubectl cp <namespace>/<pod-name>:/tmp/core_*.dmp ./core.dmp

dotnet-trace

Collect tracing information:

# Install dotnet-trace
kubectl exec <pod-name> -- dotnet tool install -g dotnet-trace

# Collect trace
kubectl exec <pod-name> -- dotnet-trace collect -p 1 --format speedscope

dotnet-counters

Monitor performance counters:

# Monitor counters in real-time
kubectl exec <pod-name> -- dotnet-counters monitor -p 1 \
  --counters System.Runtime,Microsoft.AspNetCore.Hosting

๐Ÿ›ก️ 8. Preventive Measures and Best Practices

Preventing issues is better than diagnosing them. Implement these practices to reduce container crash incidents.

๐Ÿฅ Use Health Check Endpoints in .NET

Always implement comprehensive health checks:

// Program.cs - Comprehensive health checks
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy())
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<CacheHealthCheck>("cache")
    .AddCheck<ExternalApiHealthCheck>("external-api");

// Custom health check implementation
public class DatabaseHealthCheck : IHealthCheck
{
    private readonly MyDbContext _dbContext;

    public DatabaseHealthCheck(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        try
        {
            await _dbContext.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Database connection failed", ex);
        }
    }
}

Validate Native Dependencies During CI

Add checks to your CI/CD pipeline:

# .github/workflows/validate-native-deps.yml
name: Validate Native Dependencies

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Build and test
      run: |
        dotnet build
        dotnet test
    - name: Check RID targeting
      run: |
        dotnet publish -r linux-x64 --self-contained
        ldd ./bin/Release/net9.0/linux-x64/publish/MyApp | grep "not found" && exit 1 || exit 0

๐Ÿงช Container Startup Smoke Tests

Test containers before deployment:

# smoke-test.sh
#!/bin/bash
set -e

IMAGE=$1
PORT=8080

# Start container
CONTAINER_ID=$(docker run -d -p $PORT:8080 $IMAGE)

# Wait for startup
sleep 10

# Test health endpoint
curl -f http://localhost:$PORT/healthz/ready || exit 1

# Test liveness endpoint
curl -f http://localhost:$PORT/healthz/live || exit 1

# Cleanup
docker stop $CONTAINER_ID
docker rm $CONTAINER_ID

echo "Smoke tests passed!"

☁️ Infrastructure as Code Integration

Use Terraform or Helm to inject probe configurations:

# terraform - kubernetes_deployment.tf
resource "kubernetes_deployment" "app" {
  metadata {
    name = "my-app"
  }
  spec {
    template {
      spec {
        container {
          name  = "my-app"
          image = "my-app:${var.image_tag}"
          
          liveness_probe {
            http_get {
              path = "/healthz/live"
              port = 8080
            }
            initial_delay_seconds = var.liveness_initial_delay
            period_seconds        = var.liveness_period
          }
          
          readiness_probe {
            http_get {
              path = "/healthz/ready"
              port = 8080
            }
            initial_delay_seconds = var.readiness_initial_delay
            period_seconds        = var.readiness_period
          }
        }
      }
    }
  }
}

๐Ÿ’ก Best Practices Summary:

  • Always implement health check endpoints
  • Test native dependencies in CI/CD
  • Run smoke tests before deployment
  • Use structured logging for better observability
  • Configure appropriate resource limits and requests
  • Monitor probe response times and success rates
  • Document troubleshooting procedures for common issues

๐Ÿ“š 9. Conclusion

Diagnosing .NET container crashes in Kubernetes requires a systematic approach that combines multiple diagnostic techniques. By following the workflow outlined in this guide, you can quickly identify and resolve issues.

๐Ÿ”„ Recap of Diagnostic Flow

  1. Initial Triage: Check pod status and Kubernetes events
  2. Container Logs: Examine logs for application-level errors
  3. Health Probes: Verify probe configuration and endpoints
  4. CrashLoopBackOff: Understand backoff mechanics and apply appropriate fixes
  5. Advanced Diagnostics: Use exec and diagnostic tools for deeper inspection
  6. Prevention: Implement health checks, validation, and monitoring

๐Ÿ’ก Key Takeaways:

  • Always start with kubectl describe pod and events
  • Use structured logging for better diagnostics
  • Configure health probes appropriately - don't be too aggressive
  • Implement retry logic for external dependencies
  • Test containers locally before deployment
  • Monitor and log probe responses

๐Ÿš€ Next Steps

Continue improving your container debugging skills:

  • Set up comprehensive monitoring and alerting
  • Create diagnostic scripts for common issues
  • Document your troubleshooting procedures
  • Share knowledge with your team
  • Contribute diagnostic tools and scripts to open-source projects

๐Ÿ“– Resources:

.NET 9 Feature Guide - A Leap Toward Cloud-Native Excellence

.NET 9 Feature Guide - A Leap Toward Cloud-Native Excellence

.NET 9 — A Leap Toward Cloud-Native Excellence

Exploring the most impactful features with practical examples

.NET 9 represents a significant milestone in Microsoft's journey toward cloud-native, high-performance application development. This release focuses on Native AOT, cloud-optimized tooling, performance enhancements, and developer productivity improvements. Whether you're building microservices, APIs, or enterprise applications, .NET 9 provides the tools and runtime optimizations you need to build faster, more efficient applications.

In this comprehensive guide, we'll explore the most impactful features of .NET 9, with practical examples and insights into how they transform real-world development.

๐Ÿงญ .NET 9 Feature Highlights

Here are the key features we'll be diving into:

  1. Native AOT for ASP.NET Core — Official support for ahead-of-time compilation with faster startup and reduced memory.
  2. Cloud-Optimized Templates — Built-in OpenTelemetry, health checks, and Kubernetes support.
  3. Performance Improvements — Enhanced HTTP/3, System.Text.Json, and LINQ optimizations.
  4. Unified Test Experience — Improved CLI testing tools and better integration.
  5. NuGet and SDK Enhancements — Better package management and extensibility.
  6. C# 13 Language Features — Collection expressions, params Span<T>, and more.
  7. Enhanced Serialization — System.Text.Json source generator improvements.
  8. Improved Container Support — Better Docker and Kubernetes integration.

1. Native AOT for ASP.NET Core

What is Native AOT?

Native Ahead-of-Time (AOT) compilation eliminates the need for Just-In-Time (JIT) compilation at runtime. Your application is compiled directly to native machine code, resulting in:

  • Faster startup times — No JIT compilation overhead
  • Reduced memory usage — Smaller memory footprint
  • Better container performance — Ideal for microservices and serverless
  • Smaller deployment size — Single executable deployment

๐Ÿงช Example: Creating a Native AOT Web API

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();
app.Run();

๐Ÿ’ป Publishing with Native AOT

// Publish for Linux x64 with Native AOT
dotnet publish -c Release -r linux-x64 --aot

// Publish for Windows x64 with Native AOT
dotnet publish -c Release -r win-x64 --aot

// Publish for macOS ARM64 with Native AOT
dotnet publish -c Release -r osx-arm64 --aot

๐Ÿ“Š Performance Comparison

๐Ÿ’ก Key Benefits:

  • Startup time: Up to 90% faster than JIT-compiled apps
  • Memory usage: 30-50% reduction in memory footprint
  • Cold start: Perfect for serverless and containerized workloads
  • Deployment: Single executable with no runtime dependencies

⚠️ Considerations

Native AOT has some limitations:

  • No dynamic code generation (reflection is limited)
  • Some libraries may not be compatible
  • Larger build times during compilation
  • Platform-specific builds required

2. Cloud-Optimized Templates

☁️ What Are Cloud-Optimized Templates?

.NET 9 introduces new project templates specifically designed for cloud-native development. These templates include:

  • OpenTelemetry — Built-in observability and tracing
  • Health checks — Ready-to-use health endpoints
  • Structured logging — JSON logging configured by default
  • Metrics — Prometheus-compatible metrics
  • Kubernetes support — First-class container orchestration

๐Ÿงช Example: Creating a Cloud-Optimized Web API

// Create a new cloud-optimized web API
dotnet new webapi --use-cloud-setup

// This automatically includes:
// - OpenTelemetry configuration
// - Health check endpoints
// - Structured logging
// - Metrics collection

๐Ÿ’ก Example: Cloud-Optimized Program.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

// OpenTelemetry is configured automatically
builder.Services.AddOpenTelemetry()
    .WithTracing(b => b.AddAspNetCoreInstrumentation())
    .WithMetrics(b => b.AddAspNetCoreInstrumentation());

// Health checks
builder.Services.AddHealthChecks();

// Controllers
builder.Services.AddControllers();

var app = builder.Build();

// Health check endpoint
app.MapHealthChecks("/health");

app.MapControllers();
app.Run();

๐Ÿณ Docker Integration

// Dockerfile for cloud-optimized app
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

3. Performance Improvements

๐Ÿš€ HTTP/3 Support

.NET 9 brings stable and fast HTTP/3 support, enabling better performance for web applications:

var builder = WebApplication.CreateBuilder(args);

// Enable HTTP/3
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(8080, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});

var app = builder.Build();
app.Run();

๐Ÿ“ฆ System.Text.Json Enhancements

Improved serialization performance with source generator enhancements:

// Using source generators for better performance
[JsonSerializable(typeof(Product))]
internal partial class ProductJsonContext : JsonSerializerContext
{
}

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Serialize with source-generated context
var product = new Product { Name = "Widget", Price = 29.99m };
string json = JsonSerializer.Serialize(product, ProductJsonContext.Default.Product);

๐Ÿ” LINQ Optimizations

Runtime-level optimizations for common LINQ operations:

// These operations are now optimized at runtime
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Faster filtering and projection
var evens = numbers.Where(n => n % 2 == 0).Select(n => n * 2);

// Optimized aggregation
var sum = numbers.Sum();
var average = numbers.Average();

// Better performance for large collections
var grouped = numbers.GroupBy(n => n % 3);

๐Ÿ’ก Performance Gains:

  • HTTP/3: Up to 30% faster for high-latency connections
  • JSON Serialization: 20-40% faster with source generators
  • LINQ: 15-25% improvement for common operations
  • GC: Reduced latency in high-throughput scenarios

4. Unified Test Experience

๐Ÿงช Enhanced CLI Testing

.NET 9 improves the testing experience with better CLI tools:

// List all tests
dotnet test --list-tests

// Filter tests by category
dotnet test --filter Category=Integration

// Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"

// Run specific test method
dotnet test --filter FullyQualifiedName~MyTestClass.MyTestMethod

๐Ÿ“ Example: Minimal Test Project

// Create a minimal test project
dotnet new xunit -n MyTests

// Test file with attributes
using Xunit;

public class CalculatorTests
{
    [Fact]
    [Trait("Category", "Unit")]
    public void Add_ShouldReturnCorrectSum()
    {
        // Arrange
        var calculator = new Calculator();

        // Act
        var result = calculator.Add(2, 3);

        // Assert
        Assert.Equal(5, result);
    }
}

5. NuGet and SDK Enhancements

๐Ÿ“ฆ Improved Package Management

Better package discovery and management:

// Add package with version suggestions
dotnet add package Newtonsoft.Json

// The CLI now shows:
// - Available versions
// - Package metadata
// - Compatibility information

// Add package with specific version
dotnet add package Newtonsoft.Json --version 13.0.3

// List packages with details
dotnet list package --include-transitive

๐Ÿ”ง SDK Extensibility

Cleaner extensibility points for project SDKs:

// .csproj with custom SDK
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
  
  <!-- Source generators are easier to use -->
  <ItemGroup>
    <Analyzer Include="path/to/analyzer.dll" />
  </ItemGroup>
</Project>

6. C# 13 Language Features

Collection Expressions

Simplified syntax for creating collections:

// Create arrays and lists more concisely
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];

// Spread operator for combining collections
int[] combined = [..numbers, 6, 7, 8];

// Dictionary initialization
Dictionary<string, int> ages = ["Alice", 30], ["Bob", 25]];

๐Ÿ”— Params Span<T>

More efficient parameter passing:

// Use Span<T> for params instead of arrays
public void ProcessItems(params Span<int> items)
{
    foreach (var item in items)
    {
        Console.WriteLine(item);
    }
}

// Can be called with array or stackalloc
ProcessItems(1, 2, 3, 4, 5);

int[] array = [1, 2, 3];
ProcessItems(array);

Span<int> stack = stackalloc int[3] { 1, 2, 3 };
ProcessItems(stack);

7. Enhanced Serialization

๐Ÿ”„ System.Text.Json Source Generator Improvements

Better source generation for JSON serialization:

using System.Text.Json.Serialization;

[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? Email { get; set; }
}

// Fast serialization with source-generated context
var user = new User { Name = "John", Age = 30 };
string json = JsonSerializer.Serialize(user, AppJsonContext.Default.User);

8. Improved Container Support

๐Ÿณ Better Docker Integration

Enhanced support for containerized applications:

// .dockerignore example
**/.dockerignore
**/.git
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/obj
**/node_modules

// Optimized Dockerfile for .NET 9
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["MyApi.csproj", "./"]
RUN dotnet restore "./MyApi.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./MyApi.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./MyApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

☸️ Kubernetes Integration

First-class support for Kubernetes deployments:

// Kubernetes deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapi
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapi
  template:
    metadata:
      labels:
        app: myapi
    spec:
      containers:
      - name: myapi
        image: myapi:latest
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10

๐Ÿš€ Conclusion

.NET 9 represents a significant step forward in cloud-native development and performance optimization. From Native AOT compilation that dramatically improves startup times to cloud-optimized templates that simplify observability and deployment, .NET 9 provides the tools you need to build modern, scalable applications.

The focus on performance improvements, enhanced serialization, and better container support makes .NET 9 an excellent choice for microservices, APIs, and enterprise applications. Combined with C# 13 language features, this release offers a powerful and productive development experience.

๐Ÿ“š Next Steps:

  • Upgrade your existing projects to .NET 9
  • Experiment with Native AOT for new projects
  • Try the cloud-optimized templates for microservices
  • Explore the performance improvements in your workloads
  • Stay tuned for more in-depth explorations of each feature

Monday, 3 November 2025

What's new in c# 14?



C# 14 — A Leap Toward Cleaner, Smarter Code

Exploring the most impactful features with practical examples

C# 14 marks a thoughtful evolution in the language, focusing on expressiveness, modularity, and developer productivity. While not a radical overhaul, this release introduces subtle yet powerful enhancements that make everyday coding more intuitive and maintainable. Whether you're building high-performance systems or crafting elegant APIs, C# 14 helps you write less boilerplate and more meaningful logic.

In this guide, we'll explore the most impactful features of C# 14, with practical examples and insights into how they improve real-world development.

๐Ÿงญ C# 14 Feature Highlights

Here are the key features we'll be diving into:

  1. Extension Members — Add properties and indexers to existing types via extension blocks.
  2. Field Keyword in Properties — Access auto-generated backing fields directly with field.
  3. Null-Conditional Assignment (??=) — Assign only when the target is null.
  4. Unbound Generic Types in nameof — Use open generic types for cleaner diagnostics.
  5. Implicit Span Conversions — Seamless transitions between Span<T> and ReadOnlySpan<T>.
  6. Lambda Parameter Modifiers — Use ref, in, or out in lambda expressions.
  7. Partial Events and Constructors — Modularize event and constructor logic across files.
  8. User-Defined Compound Assignment Operators — Customize behavior for +=, -=, etc.
  9. Improved Interpolated Strings — Enhanced formatting and performance for string interpolation.
  10. Collection Expressions (Preview) — Concise syntax for initializing collections.

1. Extension Members

๐Ÿ”ง What Are Extension Members?

Traditionally, C# extension methods let you "add" methods to existing types without modifying them. But with C# 14, you can now define:

  • Properties
  • Indexers
  • Events
  • Even operators

…all inside an extension block scoped to a specific type.

๐Ÿงช Example: Extension Methods for IEnumerable<T>

public static class EnumerableExtensions
{
    // Method: Check if the collection is empty
    public static bool IsEmpty<T>(this IEnumerable<T> source)
        => !source.Any();

    // Method: Access element by index safely (returns default if out of range)
    public static T? ElementAtOrDefaultSafe<T>(this IEnumerable<T> source, int index)
        => source.Skip(index).DefaultIfEmpty(default!).FirstOrDefault();

    // Method: Get the last element safely
    public static T? LastOrDefaultSafe<T>(this IEnumerable<T> source)
        => source.LastOrDefault();
}

๐Ÿ’ก Usage Example

var numbers = new[] { 1, 2, 3, 4, 5 };

// Using the extension methods
if (!numbers.IsEmpty())
{
    Console.WriteLine($"Third element: {numbers.ElementAtOrDefaultSafe(2)}");
    Console.WriteLine($"Last element: {numbers.LastOrDefaultSafe()}");
}

๐Ÿ’ก Key Benefits:

  • Cleaner API surface — Access properties and indexers naturally without method calls
  • Better IntelliSense — IDE suggestions now include extension properties and indexers
  • Type safety — Compile-time checks ensure proper usage

2. Field Keyword in Properties

๐Ÿ”‘ Direct Access to Backing Fields

The new field keyword allows you to access the auto-generated backing field directly in property accessors, eliminating the need to manually declare private fields.

๐Ÿงช Example: Smart Properties with Validation

public class Product
{
    public string Name { get; set; }

    public decimal Price
    {
        get => field;
        set
        {
            if (value < 0)
                throw new ArgumentException("Price cannot be negative");
            field = value;
        }
    }

    public int Quantity
    {
        get => field;
        set
        {
            field = Math.Max(0, value); // Ensure non-negative
            Console.WriteLine($"Stock updated: {field} units");
        }
    }
}

๐Ÿ’ก Why This Matters: Before C# 14, you had to declare a separate backing field and manually manage it. Now, you can add validation and logging to auto-properties without the boilerplate!

3. Null-Conditional Assignment (??=)

๐ŸŽฏ Assign Only When Null

The ??= operator assigns a value to a variable only if it's currently null. This simplifies lazy initialization patterns.

private Logger? _logger;

public Logger Logger
{
    get
    {
        // Initialize only if null
        _logger ??= new Logger();
        return _logger;
    }
}

Comparison with traditional approach:

// Old way
if (_logger == null)
{
    _logger = new Logger();
}

// New way
_logger ??= new Logger();

4. Unbound Generic Types in nameof

๐Ÿ“ What Are Unbound Generic Types?

Prior to C# 14, you couldn't use nameof with open generic types directly. You had to specify type parameters even when you just wanted the type's name. Now, you can reference unbound generic types cleanly.

๐Ÿงช Example: Cleaner Type Name References

// Before C# 14 - Had to provide type arguments
string typeName = nameof(Dictionary<string, int>); // "Dictionary"

// C# 14 - Can use unbound generic types
string typeName = nameof(Dictionary<,>); // "Dictionary"
string listName = nameof(List<>);       // "List"

๐Ÿ’ก Real-World Usage: Generic Factory with Logging

public class GenericFactory<T> where T : new()
{
    public T Create()
    {
        // Log the factory name without specifying T
        var factoryName = nameof(GenericFactory<>);
        Console.WriteLine($"Creating instance in {factoryName}");
        
        return new T();
    }
}

// Usage in error handling
public void ProcessRepository<TRepo, TEntity>()
{
    try
    {
        // Processing logic...
    }
    catch (Exception ex)
    {
        // Clean error messages without type parameters
        throw new InvalidOperationException(
            $"Failed in {nameof(ProcessRepository<,>)}", ex);
    }
}

๐Ÿ’ก Key Benefits:

  • Cleaner code — No need to provide arbitrary type arguments just to get a name
  • Better refactoring — Generic type names update automatically when renamed
  • Improved diagnostics — More readable error messages and logs
  • API documentation — Reference generic types clearly in generated docs

๐Ÿ“Š Use Cases

  • Logging: Log generic type names without concrete type parameters
  • Diagnostics: Create meaningful exception messages referencing generic methods
  • Reflection: Build type metadata utilities more elegantly
  • Code generation: Generate code based on generic type names

5. Implicit Span Conversions

Seamless Span Type Transitions

C# 14 introduces implicit conversions between Span<T> and ReadOnlySpan<T>, making it much easier to work with high-performance memory operations. You can now pass Span<T> to methods expecting ReadOnlySpan<T> without explicit casting.

๐Ÿงช Example: Before vs. After

// Before C# 14 - Manual casting required
void ProcessData(ReadOnlySpan<byte> data)
{
    // Process read-only data
    Console.WriteLine($"Processing {data.Length} bytes");
}

Span<byte> buffer = new byte[1024];
ProcessData(buffer); // ❌ Compiler error in older versions

// Had to explicitly cast:
ProcessData((ReadOnlySpan<byte>)buffer);

// C# 14 - Implicit conversion
Span<byte> buffer = new byte[1024];
ProcessData(buffer); // ✅ Works seamlessly!

๐Ÿ’ก Real-World Example: Stream Processing

public class DataProcessor
{
    // Method that reads data without modifying it
    public int CalculateChecksum(ReadOnlySpan<byte> data)
    {
        int checksum = 0;
        foreach (var b in data)
        {
            checksum ^= b;
        }
        return checksum;
    }

    // Method that modifies data
    public void EncryptData(Span<byte> data, byte key)
    {
        for (int i = 0; i < data.Length; i++)
        {
            data[i] ^= key;
        }
    }

    public void ProcessStream()
    {
        Span<byte> buffer = stackalloc byte[512];
        
        // Fill buffer with data...
        
        // Implicit conversion to ReadOnlySpan<byte>
        var checksum = CalculateChecksum(buffer); // ✅ Works!
        
        // Modify the buffer
        EncryptData(buffer, 0x5A);
        
        // Calculate checksum again
        var newChecksum = CalculateChecksum(buffer); // ✅ Still works!
        
        Console.WriteLine($"Checksums: {checksum} → {newChecksum}");
    }
}

๐ŸŽฏ Performance Benefits

// Stack-allocated buffer for zero-allocation processing
public static bool ValidateData(ReadOnlySpan<char> input)
{
    return input.Length > 0 && input[0] == '{' && input[^1] == '}';
}

// Can now pass strings directly!
string json = "{\"name\":\"John\"}";
if (ValidateData(json)) // Implicit string → ReadOnlySpan<char>
{
    Console.WriteLine("Valid JSON structure");
}

// Stack-allocated buffer also works
Span<char> buffer = stackalloc char[100];
int length = FormatData(buffer);
if (ValidateData(buffer.Slice(0, length))) // Span → ReadOnlySpan
{
    Console.WriteLine("Buffer validated");
}

๐Ÿ’ก Key Benefits:

  • Zero-copy operations — Pass data without allocations or copying
  • Cleaner API design — Methods can accept ReadOnlySpan for immutability guarantees
  • Better performance — Stack-allocated buffers with minimal overhead
  • Improved interop — Easier integration with native code and unsafe contexts
  • Type safety — Compiler enforces read-only vs. mutable semantics

๐Ÿ“‹ Common Patterns

// Pattern 1: Parse without allocation
public static int ParseNumber(ReadOnlySpan<char> text)
{
    return int.Parse(text);
}

string input = "12345";
var number = ParseNumber(input); // No substring allocation!

// Pattern 2: Slice and process
void ProcessSegments(ReadOnlySpan<char> data)
{
    var firstPart = data.Slice(0, 10);
    var secondPart = data.Slice(10);
    // Process without creating new strings
}

// Pattern 3: Safe buffer operations
void SafeReadBuffer()
{
    Span<byte> writeBuffer = new byte[100];
    FillBuffer(writeBuffer);
    
    // Pass as read-only to prevent modifications
    VerifyBuffer(writeBuffer); // Implicit → ReadOnlySpan
}

6. Lambda Parameter Modifiers

๐Ÿ”„ Enhanced Lambda Expressions

C# 14 allows you to use parameter modifiers (ref, in, out) in lambda expressions, enabling more efficient parameter passing and better performance in high-throughput scenarios.

๐Ÿงช Example: Using ref in Lambdas

// Before C# 14 - Had to use delegates explicitly
delegate void RefAction<T>(ref T value);

void ProcessWithRef(RefAction<int> action)
{
    int value = 10;
    action(ref value);
    Console.WriteLine(value);
}

// C# 14 - Use ref directly in lambda
var incrementer = (ref int x) => x++;

int number = 5;
incrementer(ref number);
Console.WriteLine(number); // Output: 6

๐Ÿ’ก Real-World Example: High-Performance Data Processing

public class DataTransformer
{
    // Transform large structs in-place without copying
    public void TransformInPlace<T>(
        List<T> items,
        RefAction<T> transformer) where T : struct
    {
        for (int i = 0; i < items.Count; i++)
        {
            var temp = items[i];
            transformer(ref temp);
            items[i] = temp;
        }
    }
}

// Usage: Transform points without allocation
struct Point
{
    public double X, Y;
}

var points = new List<Point> { new() { X = 1, Y = 2 } };
var transformer = new DataTransformer();

// Lambda with ref modifier - modifies in place
transformer.TransformInPlace(points, (ref Point p) => 
{
    p.X *= 2;
    p.Y *= 2;
});

๐Ÿ“Š Using in for Read-Only References

// Use 'in' to pass large structs efficiently without copying
struct Matrix4x4
{
    public double[,] Data;
    // ... large struct with 16 doubles
}

// Custom delegate type with 'in' parameter
public delegate double InFunc<T>(in T value);

// Lambda with 'in' parameter - read-only, no copy
InFunc<Matrix4x4> calculateDeterminant = 
    (in Matrix4x4 matrix) =>
    {
        // Access matrix without copying the entire struct
        return matrix.Data[0, 0] * matrix.Data[1, 1];
    };

var matrix = new Matrix4x4();
var det = calculateDeterminant(in matrix);

๐ŸŽฏ Using out for Output Parameters

// Custom delegate type with 'out' parameter
public delegate bool TryParseFunc<T>(string input, out T result);

// Lambda that returns multiple values via out parameter
TryParseFunc<int> tryParse = 
    (string input, out int result) =>
    {
        return int.TryParse(input, out result);
    };

if (tryParse("42", out var value))
{
    Console.WriteLine($"Parsed: {value}");
}

// Practical example: Validation with details
public delegate bool TryValidate(string input, out string errorMessage);

TryValidate validateEmail = 
    (string email, out string errorMessage) =>
    {
        if (string.IsNullOrEmpty(email))
        {
            errorMessage = "Email cannot be empty";
            return false;
        }
        if (!email.Contains("@"))
        {
            errorMessage = "Invalid email format";
            return false;
        }
        errorMessage = string.Empty;
        return true;
    };

๐Ÿ’ก Key Benefits:

  • Better performance — Avoid copying large structs
  • More expressive — Write functional code with side effects when needed
  • Cleaner APIs — Use lambdas in more scenarios without wrapper methods
  • Type safety — Compiler enforces parameter semantics

7. Partial Events and Constructors

๐Ÿ“‚ Modular Code Organization

C# 14 extends the partial keyword to events and constructors, allowing you to split their implementation across multiple files. This is especially useful for generated code scenarios and large class implementations.

๐Ÿงช Example: Partial Constructors

// File: Person.cs - Main implementation
public partial class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    // Partial constructor declaration
    public partial Person(string firstName, string lastName);
}

// File: Person.Generated.cs - Auto-generated validation logic
public partial class Person
{
    // Partial constructor implementation
    public partial Person(string firstName, string lastName)
    {
        // Generated validation and initialization
        if (string.IsNullOrWhiteSpace(firstName))
            throw new ArgumentException(nameof(firstName));
        
        if (string.IsNullOrWhiteSpace(lastName))
            throw new ArgumentException(nameof(lastName));
        
        FirstName = firstName;
        LastName = lastName;
        Age = 0;
        
        // Call generated initialization logic
        InitializeGeneratedProperties();
    }

    partial void InitializeGeneratedProperties();
}

๐Ÿ’ก Example: Partial Events

// File: DataService.cs - Main class
public partial class DataService
{
    // Partial event declaration
    public partial event EventHandler<DataChangedEventArgs> DataChanged;

    public void UpdateData(string newData)
    {
        // Business logic
        ProcessData(newData);
        
        // Raise the partial event
        DataChanged?.Invoke(this, new DataChangedEventArgs(newData));
    }

    private void ProcessData(string data) { /* ... */ }
}

// File: DataService.Logging.cs - Logging concerns
public partial class DataService
{
    // Partial event implementation with logging
    public partial event EventHandler<DataChangedEventArgs> DataChanged
    {
        add
        {
            Console.WriteLine($"Subscriber added to DataChanged event");
            field += value; // C# 14 field keyword
        }
        remove
        {
            Console.WriteLine($"Subscriber removed from DataChanged event");
            field -= value;
        }
    }
}

๐ŸŽฏ Real-World Use Case: Separating Generated Code

// File: ViewModel.cs - Hand-written code
public partial class UserViewModel
{
    private string _username;
    
    // Partial event for property changes
    public partial event PropertyChangedEventHandler PropertyChanged;

    public string Username
    {
        get => _username;
        set
        {
            if (_username != value)
            {
                _username = value;
                OnPropertyChanged(nameof(Username));
            }
        }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

// File: ViewModel.Generated.cs - Source generated code
public partial class UserViewModel
{
    // Generated partial constructor with dependency injection
    public partial UserViewModel(
        ILogger logger,
        IDataService dataService);

    public partial UserViewModel(
        ILogger logger,
        IDataService dataService)
    {
        // Generated initialization
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
        
        _logger.LogInformation("UserViewModel created");
    }

    // Generated partial event implementation with weak references
    public partial event PropertyChangedEventHandler PropertyChanged
    {
        add { _weakEventManager.AddHandler(value); }
        remove { _weakEventManager.RemoveHandler(value); }
    }

    private readonly WeakEventManager _weakEventManager = new();
    private readonly ILogger _logger;
    private readonly IDataService _dataService;
}

๐Ÿ’ก Key Benefits:

  • Clean separation — Keep generated code separate from hand-written code
  • Better maintainability — Organize large classes across multiple files
  • Source generators — Perfect for code generation scenarios
  • Team collaboration — Different team members can work on different aspects

8. User-Defined Compound Assignment Operators

๐Ÿ”ง Customize Compound Operations

C# 14 allows you to define custom behavior for compound assignment operators like +=, -=, *=, etc. This enables more intuitive APIs for custom types.

๐Ÿงช Example: Custom Vector Type

public struct Vector3
{
    public double X, Y, Z;

    public Vector3(double x, double y, double z)
    {
        X = x; Y = y; Z = z;
    }

    // Define the fundamental operators
    public static Vector3 operator +(Vector3 a, Vector3 b)
        => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);

    public static Vector3 operator *(Vector3 v, double scalar)
        => new(v.X * scalar, v.Y * scalar, v.Z * scalar);
}

// Usage — Compound assignments use the defined operators
var velocity = new Vector3(1, 2, 3);
var acceleration = new Vector3(0.1, 0.2, 0.3);

velocity += acceleration; // Uses operator + under the hood
velocity *= 0.5;          // Uses operator * under the hood

๐Ÿ’ก Example: StringBuilder-like Builder Pattern

public class QueryBuilder
{
    private StringBuilder _query = new();

    // Define fundamental operators
    public static QueryBuilder operator +(QueryBuilder builder, string clause)
    {
        if (builder._query.Length > 0)
            builder._query.Append(" ");
        builder._query.Append(clause);
        return builder;
    }

    public static QueryBuilder operator &(QueryBuilder builder, string condition)
    {
        if (!builder._query.ToString().Contains("WHERE"))
            builder._query.Append(" WHERE ");
        else
            builder._query.Append(" AND ");
        builder._query.Append(condition);
        return builder;
    }

    public override string ToString() => _query.ToString();
}

// Usage — Intuitive query building
var query = new QueryBuilder();
query += "SELECT * FROM Users";
query &= "Age > 18";
query &= "IsActive = 1";
query += "ORDER BY Name";

Console.WriteLine(query);
// Output: SELECT * FROM Users WHERE Age > 18 AND IsActive = 1 ORDER BY Name

๐ŸŽฏ Example: Collection Builder with Validation

public class ValidatedCollection<T>
{
    private List<T> _items = new();
    private Func<T, bool> _validator;

    public ValidatedCollection(Func<T, bool> validator)
    {
        _validator = validator;
    }

    // Define fundamental operators
    public static ValidatedCollection<T> operator +(
        ValidatedCollection<T> collection, T item)
    {
        if (collection._validator(item))
        {
            collection._items.Add(item);
            Console.WriteLine($"✓ Added: {item}");
        }
        else
        {
            Console.WriteLine($"✗ Rejected: {item}");
        }
        return collection;
    }

    public static ValidatedCollection<T> operator -(
        ValidatedCollection<T> collection, T item)
    {
        collection._items.Remove(item);
        return collection;
    }

    public int Count => _items.Count;
}

// Usage
var numbers = new ValidatedCollection<int>(x => x > 0);
numbers += 5;   // ✓ Added: 5
numbers += -3;  // ✗ Rejected: -3
numbers += 10;  // ✓ Added: 10

๐Ÿ’ก Key Benefits:

  • Intuitive syntax — Make custom types behave like built-in types
  • Performance — Optimize in-place operations without temporary allocations
  • Fluent APIs — Create chainable, readable code
  • Domain modeling — Express domain concepts naturally

9. Improved Interpolated Strings

๐Ÿ“ Enhanced String Formatting

C# 14 brings significant improvements to string interpolation, including better performance, enhanced formatting options, and support for custom interpolated string handlers.

๐Ÿงช Example: Advanced Formatting

// Enhanced format specifiers
var price = 1234.567;
var date = new DateTime(2025, 11, 3);

// Multiple format specifiers with alignment
var formatted = $"Price: {price,15:C2} | Date: {date:yyyy-MM-dd}";
// Output: "Price:      $1,234.57 | Date: 2025-11-03"

// C# 14: Conditional formatting within interpolation
var stock = 5;
var message = $"Stock: {stock} ({(stock > 0 ? \"Available\" : \"Out of stock\")})";

// Nested interpolations (improved in C# 14)
var items = new[] { "Apple", "Banana", "Cherry" };
var list = $"Items: {string.Join(", ", items.Select(x => $"'{x}'"))}";
// Output: "Items: 'Apple', 'Banana', 'Cherry'"

๐Ÿ’ก Performance: Custom Interpolated String Handlers

// Custom handler for SQL query building with parameter safety
[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct SqlInterpolatedStringHandler
{
    private System.Text.StringBuilder _builder;
    private System.Collections.Generic.List<object> _parameters;

    public SqlInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    _builder = new System.Text.StringBuilder(literalLength);
    _parameters = new System.Collections.Generic.List<object>(formattedCount);
    }

    public void AppendLiteral(string s) => _builder.Append(s);

    public void AppendFormatted<T>(T value)
    {
        _parameters.Add(value);
        _builder.Append($"@p{_parameters.Count - 1}");
    }

    public (string Query, object[] Parameters) GetResult()
        => (_builder.ToString(), _parameters.ToArray());
}

// Usage - Safe parameterized queries
public static class SqlQuery
{
    public static (string Query, object[] Parameters) Create(
        SqlInterpolatedStringHandler handler)
    {
        return handler.GetResult();
    }
}

// Create safe SQL queries
var userId = 42;
var userName = "John";

var (query, parameters) = SqlQuery.Create(
    $"SELECT * FROM Users WHERE Id = {userId} AND Name = {userName}");

Console.WriteLine(query);
// Output: "SELECT * FROM Users WHERE Id = @p0 AND Name = @p1"
Console.WriteLine(string.Join(", ", parameters));
// Output: "42, John"

๐ŸŽฏ Example: Logging with Conditional Evaluation

// Custom logger that only evaluates expensive operations if needed
public enum LogLevel { Debug = 1, Info = 2, Warn = 3, Error = 4 }

public class SmartLogger
{
    private LogLevel _currentLevel = LogLevel.Info;

    public void Log(
        LogLevel level,
        [System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute("this", "level")]
        LogInterpolatedStringHandler handler)
    {
        if (level >= _currentLevel)
        {
            Console.WriteLine(handler.ToString());
        }
    }
}

[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct LogInterpolatedStringHandler
{
    private System.Text.StringBuilder? _builder;
    private bool _enabled;

    public LogInterpolatedStringHandler(
        int literalLength,
        int formattedCount,
        SmartLogger logger,
        LogLevel level,
        out bool isEnabled)
    {
        _enabled = level >= logger._currentLevel;
        isEnabled = _enabled;
        _builder = _enabled ? new System.Text.StringBuilder(literalLength) : null;
    }

    public void AppendLiteral(string s)
    {
        if (_enabled) _builder!.Append(s);
    }

    public void AppendFormatted<T>(T value)
    {
        if (_enabled) _builder!.Append(value);
    }

    public override string ToString() => _builder?.ToString() ?? string.Empty;
}

// Usage - Expensive operations only run if logging is enabled
var logger = new SmartLogger();
var data = new List<int> { 1, 2, 3 };

string ExpensiveOperation(IEnumerable<int> items)
{
    return string.Join(",", items.Select(i => (i * i).ToString()));
}

// This expensive operation only runs if Debug level is enabled
logger.Log(LogLevel.Debug,
    $"Processing {data.Count} items, details: {ExpensiveOperation(data)}");

๐Ÿ“Š Real-World Example: URL Builder

public class UrlBuilder
{
    public static string Build(UrlInterpolatedStringHandler handler)
        => handler.ToString();
}

[System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
public ref struct UrlInterpolatedStringHandler
{
    private System.Text.StringBuilder _builder;

    public UrlInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    _builder = new System.Text.StringBuilder(literalLength);
    }

    public void AppendLiteral(string s) => _builder.Append(s);

    public void AppendFormatted(string value)
    {
        // Automatically URL-encode interpolated values
        _builder.Append(Uri.EscapeDataString(value ?? string.Empty));
    }

    public override string ToString() => _builder.ToString();
}

// Usage - Automatic URL encoding
var searchTerm = "C# programming";
var category = "Books & Media";

var url = UrlBuilder.Build(
    $"https://example.com/search?q={searchTerm}&category={category}");

Console.WriteLine(url);
// Output: "https://example.com/search?q=C%23%20programming&category=Books%20%26%20Media"

๐Ÿ’ก Key Benefits:

  • Zero-allocation — Custom handlers can optimize memory usage
  • Conditional evaluation — Skip expensive operations when not needed
  • Type safety — Enforce formatting rules at compile time
  • Domain-specific — Create specialized string builders (SQL, URLs, etc.)
  • Better performance — Reduced allocations in hot paths

10. Collection Expressions (Preview)

๐Ÿ“ฆ Concise Collection Initialization

C# 14 introduces collection expressions, providing a unified, concise syntax for creating and initializing collections of any type. This feature simplifies code and makes collection creation more intuitive.

๐Ÿงช Example: Basic Collection Expressions

// Traditional way
var oldList = new List<int> { 1, 2, 3, 4, 5 };
var oldArray = new int[] { 1, 2, 3, 4, 5 };

// C# 14: Collection expressions - unified syntax
List<int> list = [1, 2, 3, 4, 5];
int[] array = [1, 2, 3, 4, 5];
Span<int> span = [1, 2, 3, 4, 5];

// Empty collections
List<string> empty = [];
int[] emptyArray = [];

๐Ÿ’ก Example: Spread Operator

// Combine collections with spread operator ..
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] third = [7, 8, 9];

// Spread and combine
int[] combined = [..first, ..second, ..third];
// Result: [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Mix literals and spreads
int[] mixed = [0, ..first, 10, ..second, 20];
// Result: [0, 1, 2, 3, 10, 4, 5, 6, 20]

// Spread with LINQ
var numbers = [1, 2, 3, 4, 5];
var doubled = [..numbers.Select(x => x * 2)];
// Result: [2, 4, 6, 8, 10]

๐ŸŽฏ Example: Pattern Matching with Collections

// Pattern matching with collection expressions
string DescribeCollection(int[] numbers) => numbers switch
{
    [] => "Empty collection",
    [var single] => $"Single element: {single}",
    [var first, var second] => $"Two elements: {first}, {second}",
    [var first, .., var last] => $"First: {first}, Last: {last}",
    _ => $"Collection with {numbers.Length} elements"
};

// Usage
Console.WriteLine(DescribeCollection([]));           // "Empty collection"
Console.WriteLine(DescribeCollection([42]));         // "Single element: 42"
Console.WriteLine(DescribeCollection([1, 2]));       // "Two elements: 1, 2"
Console.WriteLine(DescribeCollection([1, 2, 3, 4])); // "First: 1, Last: 4"

๐Ÿ”ฅ Real-World Example: API Response Building

public record ApiResponse(string Status, object[] Data);

public class UserService
{
    public ApiResponse GetUsers(bool includeAdmin, bool includeGuests)
    {
        var regularUsers = GetRegularUsers();
        var adminUsers = includeAdmin ? GetAdminUsers() : [];
        var guestUsers = includeGuests ? GetGuestUsers() : [];

        // Build response with collection expressions
        return new ApiResponse(
            "success",
            [
                ..regularUsers,
                ..adminUsers,
                ..guestUsers
            ]
        );
    }

    private object[] GetRegularUsers() => [
        new { Id = 1, Name = "Alice", Role = "User" },
        new { Id = 2, Name = "Bob", Role = "User" }
    ];

    private object[] GetAdminUsers() => [
        new { Id = 100, Name = "Admin", Role = "Admin" }
    ];

    private object[] GetGuestUsers() => [
        new { Id = 200, Name = "Guest", Role = "Guest" }
    ];
}

๐Ÿ“Š Example: Building Test Data

// Easy test data creation
public class Product
{
    public string Name { get; set; } = string.Empty;
    public int Price { get; set; }
}

public static class TestDataBuilder
{
    public static List<Product> CreateTestProducts()
    {
        var electronics = CreateElectronics();
        var books = CreateBooks();
        var featured = new Product { Name = "Featured Item", Price = 999 };

        // Combine with collection expressions
        return [featured, ..electronics, ..books];
    }

    private static Product[] CreateElectronics() => [
        new Product { Name = "Laptop", Price = 1200 },
        new Product { Name = "Phone", Price = 800 }
    ];

    private static Product[] CreateBooks() => [
        new Product { Name = "C# Guide", Price = 45 },
        new Product { Name = "Design Patterns", Price = 55 }
    ];
}

// Simple check without external test frameworks
int Sum(int[] numbers) => numbers.Sum();
Console.WriteLine(Sum([1,2,3]));   // 6
Console.WriteLine(Sum([10,20,30])); // 60

Performance Example: Stack Allocation

// Collection expressions work with Span for stack allocation
public static void ProcessData()
{
    // Stack-allocated span with collection expression
    Span<int> numbers = [1, 2, 3, 4, 5];
    
    // Zero heap allocation!
    foreach (var num in numbers)
    {
        Console.WriteLine(num * 2);
    }

    // Combine with stackalloc for larger data
    Span<byte> buffer = stackalloc byte[256];
    Span<byte> header = [0xFF, 0xFE, 0xFD];
    
    // Copy header to buffer
    header.CopyTo(buffer);
}

๐Ÿ’ก Key Benefits:

  • Unified syntax — Same syntax for arrays, lists, spans, and more
  • More readable — Cleaner, less verbose collection initialization
  • Spread operator — Easy collection combination and manipulation
  • Pattern matching — Powerful collection destructuring
  • Performance — Works with stack-allocated spans
  • Type inference — Compiler figures out the target type

๐Ÿš€ Conclusion

C# 14 continues the language's tradition of thoughtful, developer-friendly improvements. From extension members that extend types more naturally, to the field keyword that eliminates boilerplate, these features help you write code that's both more expressive and maintainable.

The addition of lambda parameter modifiers enables more efficient functional programming patterns, while partial events and constructors improve code organization in large projects. User-defined compound assignment operators make custom types feel more natural, and improved interpolated strings provide both performance and flexibility.

Finally, collection expressions bring a modern, unified syntax for working with collections, making code more concise and readable. As you adopt C# 14 in your projects, you'll find these small enhancements add up to significant productivity gains. The language continues to evolve in ways that respect existing code while opening new possibilities for elegant solutions.

๐Ÿ“š Next Steps:

  • Experiment with these features in your own projects
  • Explore the official C# 14 documentation for more details
  • Share your experiences and patterns with the community
  • Stay tuned for more advanced use cases and best practices

Happy coding with C# 14! ๐ŸŽ‰

What Is the Internet?

What Is the Internet? What Is the Internet? The internet is a global ne...