.NET 10: What's New, Performance & Long-Term Support

A deep dive into the key features of .NET 10 12/10/2025

.NET 10 has arrived, bringing with it a wave of performance improvements, developer experience enhancements, and new features that push the boundaries of what we can do with the platform. As an architect who has been riding the .NET wave since the early days, I can say that this release is particularly exciting. In this article, I will explore the key features that matter most in production environments, from performance gains to developer productivity tools.

Performance Improvements

Microsoft continues to make .NET one of the fastest frameworks available, and .NET 10 is no exception. The runtime team has focused on several key areas:

  • Reduced GC pressure — The garbage collector has been optimized to reduce pause times and improve throughput
  • Improved JIT compilation — Better inlining decisions and optimized code generation
  • Lower memory footprint — Reduced baseline memory usage across the board

One of the most noticeable improvements is in the area of string operations. The runtime has been optimized for common scenarios like concatenation and splitting. Here's a practical example showing the kind of improvements you can expect:


public class PerformanceBenchmark
{
    private readonly Random _random = new Random();
    private readonly string[] _words = new string[1000];

    public PerformanceBenchmark()
    {
        for (int i = 0; i < 1000; i++)
        {
            _words[i] = $"word{i:D6}";
        }
    }

    public string BuildMessage()
    {
        var result = new StringBuilder();
        for (int i = 0; i < 100; i++)
        {
            int index = _random.Next(_words.Length);
            result.Append(_words[index]).Append(' ');
        }
        return result.ToString().Trim();
    }
}
                

In .NET 10, this pattern benefits from improved StringBuilder resizing strategies and better string interning, resulting in up to 20-30% faster execution in typical scenarios.

JSON Patch in System.Text.Json

JSON Patch (RFC 6902) is a standardized format for describing JSON document modifications. .NET 10 brings native support for JSON Patch documents, making it much easier to implement partial updates and efficient APIs.

This is particularly useful for RESTful APIs where you want to support efficient partial updates without sending the entire resource. Let's look at a practical example:


using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

public class UserPatchExample
{
    public void ApplyPatch()
    {
        var document = new JsonObject
        {
            ["name"] = "John Doe",
            ["email"] = "john@example.com",
            ["age"] = JsonValue.Create(30)
        };

        var patchDocument = new JsonDocument("""
        [
            { "op": "replace", "path": "/name", "value": "John Smith" },
            { "op": "add", "path": "/phone", "value": "555-1234" }
        ]
        """).GetRootElement();

        document.ApplyPatch(patchDocument);

        // document now contains:
        // {
        //   "name": "John Smith",
        //   "email": "john@example.com",
        //   "age": 30,
        //   "phone": "555-1234"
        // }
    }
}
                

The ApplyPatch method is now available in System.Text.Json, providing a standardized way to apply JSON Patch operations. This is perfect for implementing PATCH endpoints that follow REST best practices.

Observability Metrics

Observability is crucial in modern distributed systems, and .NET 10 takes it seriously with enhanced metrics support. The new metrics APIs make it easier to track and expose telemetry data.

Here's how you can create custom metrics for your application:


using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Builder;

public class ObservabilityExample
{
    private readonly Meter _meter = new Meter("MyApp", "1.0.0");
    private readonly Histogram<decimal> _requestDuration;
    private readonly Counter _requestsReceived;

    public ObservabilityExample()
    {
        _requestDuration = _meter.CreateHistogram<decimal>(
            "app.request.duration",
            "milliseconds",
            "Request duration histogram"
        );

        _requestsReceived = _meter.CreateCounter<long>(
            "app.requests.total",
            "requests",
            "Total number of requests"
        );
    }

    public async Task HandleRequestAsync()
    {
        _requestsReceived.Add(1);

        var stopwatch = Stopwatch.StartNew();
        try
        {
            // Your business logic here
            await Task.Delay(100);
        }
        finally
        {
            _requestDuration.Record(stopwatch.ElapsedMilliseconds);
        }
    }
}
                

In .NET 10, these metrics can be exported to various backends including OpenTelemetry, Prometheus, and Azure Monitor. The integration with the OpenTelemetry SDK has been streamlined, making it easier to get started with cloud-native observability.

Passkeys Support

Passkeys represent the future of authentication, eliminating the need for passwords in favor of cryptographic credentials. .NET 10 brings native support for passkeys through the new WebAuthn APIs, making it easier to implement passwordless authentication.

Here's a basic example of how you can implement passkey registration:


using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.WebUtilities;

public class PasskeyRegistrationService
{
    private readonly IUserRepository _userRepository;
    private readonly ICredentialStorage _credentialStorage;

    public PasskeyRegistrationService(
        IUserRepository userRepository,
        ICredentialStorage credentialStorage)
    {
        _userRepository = userRepository;
        _credentialStorage = credentialStorage;
    }

    public async Task<RegistrationResponse> RegisterPasskeyAsync(
        Guid userId,
        RegistrationRequest request)
    {
        // Generate attestation challenge
        var challenge = RandomNumberGenerator.GetBytes(32);

        var attestationOptions = new AuthenticatorAttachment
        {
            Challenge = challenge,
            UserVerification = UserVerificationRequirement.Discouraged,
            ExcludeCredentials = await _credentialStorage.GetExistingCredentialsAsync(userId)
        };

        // Send to client for passkey creation
        return new RegistrationResponse
        {
            CredentialId = await _credentialStorage.CreateCredentialAsync(userId, request.CredentialData),
            PublicKey = attestationOptions.PublicKey,
            Attestation = attestationOptions.AttestationObject
        };
    }
}
                

Passkeys provide a significantly better user experience while improving security. They're resistant to phishing, support biometric authentication, and can be synced across devices through cloud providers.

C# Script Mode

C# script mode has been enhanced in .NET 10, making it even easier to write quick scripts and tools. You can now execute C# code files directly without needing to wrap them in a full application structure.

The script engine now supports implicit references to common namespaces, making scripts more concise:


// example.csx - Run with: dotnet script example.csx

// Implicit references to common namespaces
var data = Directory.GetFiles(@"C:\Users", "*.txt")
    .Select(Path.GetFileName)
    .OrderBy(f => f);

Console.WriteLine($"Found {data.Count()} text files:");
foreach (var file in data)
{
    Console.WriteLine(file);
}
                

Scripts can now also access project references and NuGet packages, making them more powerful for development tasks and automation.

C# 14 Extension Members

One of the most anticipated features in C# 14 is extension members. This allows extension methods to access private members of the type they extend, which was previously impossible.

Let's see how this enables more powerful code patterns:


public class CollectionExtensions
{
    public static T GetPrivateField<T>(this object obj, string fieldName)
    {
        var field = obj.GetType().GetField(fieldName,
            BindingFlags.NonPublic | BindingFlags.Instance);
        return (T)field?.GetValue(obj)!;
    }

    // Now you can use extension methods to work with private members
    public static void ResetState<T>(this T instance) where T : class
    {
        var currentState = instance.GetPrivateField<string>("_currentState");
        Console.WriteLine($"Current state before reset: {currentState}");

        // Reset private state
        var stateField = typeof(T).GetField("_currentState",
            BindingFlags.NonPublic | BindingFlags.Instance);
        stateField?.SetValue(instance, "initialized");
    }
}
                

This feature is particularly useful for refactoring legacy code and implementing design patterns that were previously difficult with extension methods.

C# 14 Field Keyword

C# 14 introduces the field keyword for primary constructor parameters, making it explicit when a parameter creates a field:


public class User
{
    // Old syntax (implicit field creation)
    public User(string name, int id)
    {
        Name = name;
        Id = id;
    }

    public string Name { get; }
    public int Id { get; }

    // New syntax with field keyword
    public class ModernUser(int field id, string field name)
    {
        public int Id => id;
        public string Name => name;
    }

    // Or with explicit field keyword for clarity
    public partial class EnhancedUser
    {
        public string Name { get; }
        public int Id { get; }

        public EnhancedUser(int field id, string field name)
        {
            Id = id;
            Name = name;
        }
    }
}
                

The field keyword makes it crystal clear which constructor parameters create backing fields, improving code readability and maintainability.

Numeric String Comparer

Sorting strings that contain numbers has always been a challenge (e.g., "file10" should come after "file2", not before). .NET 10 introduces NumericStringComparer to handle this naturally:


using System.Globalization;

public class NaturalSortExample
{
    public void DemonstrateNumericComparer()
    {
        var files = new[] { "file1.txt", "file10.txt", "file2.txt" };

        // Natural order with NumericStringComparer
        var sorted = files.OrderBy(f => f, StringComparer.Numeric);

        Console.WriteLine("Natural order:");
        foreach (var file in sorted)
        {
            Console.WriteLine(file);
        }

        // Output:
        // file1.txt
        // file2.txt
        // file10.txt

        // You can also customize the comparer
        var caseInsensitive = StringComparer.Numeric(ComparisonType.CaseInsensitive);
        var caseSensitive = StringComparer.Numeric(ComparisonType.CaseSensitive);
    }
}
                

The NumericStringComparer intelligently identifies numeric sequences within strings and compares them numerically, not lexicographically. This is perfect for file explorers, list views, or any scenario where natural sorting is expected.

Minimal APIs ProblemDetails

Error handling in Minimal APIs has been significantly improved with native support for ProblemDetails (RFC 7807). This standardized format for error responses makes APIs more consistent and easier to consume.

Here's how you can use ProblemDetails in Minimal APIs:


using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.Extensions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Custom ProblemDetails handler
app.MapExceptionHandler<ProblemDetails>((exception, context) =>
{
    var problemDetails = new ProblemDetails
    {
        Status = StatusCodes.Status500InternalServerError,
        Title = "An unexpected error occurred",
        Type = "https://api.example.com/errors/internal-server-error",
        Instance = context.Request.Path,
        Detail = exception.Message
    };

    if (exception is KeyNotFoundException)
    {
        problemDetails.Status = StatusCodes.Status404NotFound;
        problemDetails.Title = "Resource not found";
        problemDetails.Type = "https://api.example.com/errors/resource-not-found";
    }
    else if (exception is ArgumentException)
    {
        problemDetails.Status = StatusCodes.Status400BadRequest;
        problemDetails.Title = "Invalid request";
        problemDetails.Type = "https://api.example.com/errors/invalid-request";
    }

    context.Response.StatusCode = problemDetails.Status.Value;
    return problemDetails;
});
                

When errors occur, the response follows the RFC 7807 standard:


{
    "type": "https://api.example.com/errors/resource-not-found",
    "title": "Resource not found",
    "status": 404,
    "instance": "/api/users/123"
}
                

This standardized format makes it easier for API consumers to handle errors programmatically.

OpenAPI 3.1 Support

.NET 10 brings OpenAPI 3.1 support to Minimal APIs and Swagger. This is the latest version of the OpenAPI specification and brings several improvements:

  • Better JSON Schema support — Native support for JSON Schema features
  • Improved documentation — More detailed parameter and response descriptions
  • Enhanced examples — Better support for example values

Here's how you can configure your application for OpenAPI 3.1:


using Microsoft.OpenApi.Any;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(options =>
{
    options.DocumentName = "v3-1";
    options.AddDocumentTransformer<OpenApiTransformer31>();
    options.AddDocumentTransformer<ExampleEnricher>();
});

var app = builder.Build();

app.MapOpenApi();

// Minimal API with rich OpenAPI documentation
app.MapPost("/products", (ProductDto product, HttpContext context) =>
{
    // Validate and create product
    return Results.Created($"/products/{product.Id}", product);
})
.WithName("CreateProduct")
.WithOpenApi(operation =>
{
    var openApiOperation = new OpenApiOperation
    {
        Summary = "Create a new product",
        Description = "Creates a product with the provided details",
        Tags = new[] { new OpenApiTag { Name = "Products" } }
    };

    operation.Responses["201"] = new OpenApiResponse
    {
        Description = "Product created successfully",
        Content = new Dictionary<string, OpenApiMediaType>
        {
            ["application/json"] = new OpenApiMediaType
            {
                Schema = new OpenApiSchema
                {
                    Type = "object",
                    Example = new OpenApiObject
                    {
                        ["id"] = new OpenApiInteger(123),
                        ["name"] = new OpenApiString("Product Name"),
                        ["price"] = new OpenApiNumber(99.99m)
                    }
                }
            }
        }
    };

    return openApiOperation;
});
                

With OpenAPI 3.1, your API documentation is more accurate and helpful for consumers, automatically generated from your Minimal APIs.

EF Core Named Query Filters

Entity Framework Core 9 (included in .NET 10) introduces named query filters, which allow you to define reusable query filters that can be applied and removed dynamically.

This is particularly useful for implementing features like soft deletes or tenant isolation:


using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Named query filter for soft deletes
        modelBuilder.Entity<Order>()
            .HasQueryFilter("IsDeleted", o => !o.IsDeleted);
    }
}

public class Order
{
    public int Id { get; set; }
    public string Description { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public bool IsDeleted { get; set; } = false;
}
                

To use a named query filter:


public class OrderService
{
    private readonly AppDbContext _context;

    public OrderService(AppDbContext context)
    {
        _context = context;
    }

    public async Task<List<Order>> GetAllSoftDeletedOrdersAsync()
    {
        // Get all orders including soft-deleted ones
        return await _context.Orders
            .IgnoreQueryFilter("IsDeleted")
            .ToListAsync();
    }

    public async Task<List<Order>> GetActiveOrdersAsync()
    {
        // Get only active orders (default behavior)
        return await _context.Orders
            .WithQueryFilter("IsDeleted")
            .ToListAsync();
    }
}
                

Named filters can be conditionally enabled/disabled at runtime, giving you fine-grained control over query behavior.

EF Core LeftJoin

Entity Framework Core finally gets what many developers have been asking for: native LeftJoin support! While you could always work around this with subqueries, native join support makes the code more readable and often more efficient.

Here's a practical example:


public class CustomerOrderExample
{
    private readonly AppDbContext _context;

    public CustomerOrderExample(AppDbContext context)
    {
        _context = context;
    }

    public async Task<IReadOnlyList<(Customer Customer, Order? Order)>>
        GetCustomersWithOrdersAsync()
    {
        var query = _context.Customers
            .LeftJoin(
                _context.Orders,
                customer => customer.Id,
                order => order.CustomerId,
                (customer, order) => (customer, order)
            )
            .ToList();

        return await Task.FromResult(query);
    }

    public async Task<IReadOnlyList<Customer>>
        GetCustomersWithNoOrdersAsync()
    {
        // Filter for customers with no matching orders
        return await _context.Customers
            .LeftJoin(
                _context.Orders,
                customer => customer.Id,
                order => order.CustomerId,
                (customer, order) => (customer, order)
            )
            .Where(x => x.Order is null)
            .Select(x => x.Customer)
            .ToListAsync();
    }
}
                

The LeftJoin method takes:

  • The source collection
  • The inner collection to join
  • The key selector for the outer collection
  • The key selector for the inner collection
  • The result selector function

This makes complex queries much more readable compared to the previous subquery approach.

Long-Term Support

.NET 10 is not a Long-Term Support (LTS) version. Microsoft follows a release cadence where:

  • LTS versions are released every two years and supported for three years
  • Current versions are released every year and supported for one year

The current LTS version is .NET 8, which will be supported until November 2026. For production environments requiring extended support, you should consider .NET 8 or wait for the next LTS release.

Here's the current .NET support lifecycle:

Version Type Release Date End of Support
.NET 8 LTS November 2023 November 2026
.NET 9 Current November 2024 November 2025
.NET 10 Current November 2025 November 2026

When planning upgrades, keep in mind the support timeline and plan your migration strategy accordingly.

Summary

.NET 10 brings significant improvements across the board. From performance optimizations that will benefit production workloads, to developer experience enhancements like JSON Patch and C# script mode improvements, this release has something for everyone.

Key takeaways:

  • Performance improvements in GC, JIT, and string operations will benefit all .NET applications
  • JSON Patch support in System.Text.Json enables efficient partial updates
  • Enhanced observability with improved metrics and OpenTelemetry integration
  • Passkeys support brings modern, passwordless authentication to .NET
  • C# script mode improvements make quick scripts and automation easier
  • Extension members and field keyword in C# 14 improve code clarity
  • NumericStringComparer for natural sorting of strings with numbers
  • Minimal APIs ProblemDetails for standardized error responses
  • OpenAPI 3.1 support for better API documentation
  • EF Core named query filters for reusable, dynamic query filtering
  • EF Core LeftJoin for more readable complex queries

For migration advice, I recommend the official .NET 10 migration guide as well as the .NET Blog which has detailed posts on each new feature.

The .NET platform continues to evolve, and .NET 10 demonstrates Microsoft's commitment to performance, developer productivity, and modern standards. Whether you're starting a new project or updating an existing one, there are many compelling reasons to adopt .NET 10.

Happy coding with .NET 10!

Share On