newsletter

New Features in .NET 10 and C# 14

9 min read

Newsletter Sponsors

When EF Core Can't Keep Up, This Extension Does.
Insert data up to 14x faster and cut save time by 94% β€” with Bulk Insert, Update, Delete & Merge.
πŸ‘‰ Try Entity Framework Extensions

Build Safer, Scalable APIs with Protobuf

Building APIs seems simple β€” until you need to evolve them safely across teams and languages.

Breaking changes, inconsistent validation, and fragmented toolchains can turn API evolution into a painful, high-risk process.

Join the Buf engineering team for a free, 1-hour interactive workshop on November 19th (9:00 AM PDT | 12:00 PM EDT | 5:00 PM BST) to learn how to design production-ready APIs with Protobuf β€” used by top teams worldwide for its performance, safety, and flexibility.

Here is what you will learn:
➑️ Safer API development workflows – Prevent breaking changes and evolve APIs confidently with Protobuf's schema-driven validation.
➑️ Battle-tested API design patterns – Discover practical techniques for building scalable, maintainable APIs used in production today.
➑️ Beyond network APIs – Learn how Protobuf powers event streams, data pipelines, and analytics systems with guaranteed data consistency.

Whether you are integrating Protobuf for the first time or refining your API strategy, this session will show you how Buf simplifies the entire lifecycle β€” from design to deployment.

πŸ‘‰ Reserve your FREE seat for the Buf Workshop

.NET 10 and C# 14 is out today (November 11, 2025).

As a Long-Term Support (LTS) release, .NET 10 will receive three years of support until November 14, 2028. This makes it a solid choice for production applications that need long-term stability.

In this post, we will explore:

  • What's New in .NET 10
  • What's New in C# 14
  • What's New in ASP.NET Core in .NET 10
  • What's New in EF Core 10
  • Other Changes in .NET 10

Let's dive in!

Copied

What's New in .NET 10

Copied

File-Based Apps

The biggest addition in .NET 10 is support for file-based apps. This feature changes how you can write C# code for scripts and small utilities.

Traditionally, even the simplest C# application required three things: a solution file (sln), a project file (csproj), and your source code file (*.cs). You would then use your IDE or the dotnet run command to build and run the app.

Starting with .NET 10, you can create a single *.cs file and run it directly:

bash
dotnet run main.cs

This puts C# on equal with Python, JavaScript, TypeScript and other scripting languages. This makes C# a good option for CLI utilities, automation scripts, and tooling, without a project setup.

File-based apps can reference NuGet packages and SDKs using special # directives at the top of your file. This lets you include any library you need without a project file.

You can even create a single-file app that uses EF Core and runs a Minimal API:

csharp
#:sdk Microsoft.NET.Sdk.Web #:package [email protected] using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(); builder.Services.AddDbContext<OrderDbContext>(options => { options.UseSqlite("Data Source=orders.db"); }); var app = builder.Build(); app.MapGet("/orders", async (OrderDbContext db) => { return await db.Orders.ToListAsync(); }); app.Run(); return; public record Order(string OrderNumber, decimal Amount); public class OrderDbContext : DbContext { public OrderDbContext(DbContextOptions<OrderDbContext> options) : base(options) { } public DbSet<Order> Orders { get; set; } }

You can also reference existing project files from your script:

csharp
#:project ../ClassLib/ClassLib.csproj

Cross-Platform Shell Scripts

You can write cross-platform C# shell scripts that are executed directly on Unix-like systems. Use the #! directive to specify the command to run the script:

bash
#!/usr/bin/env dotnet

Then make the file executable and run it:

bash
chmod +x app.cs ./app.cs

Converting to a Full Project

When your script grows and needs more structure, you can convert it to a regular project using the dotnet project convert command:

bash
dotnet project convert app.cs

Note: Support for file-based apps with multiple files will likely come in future .NET releases.

You can see the complete list of new features in .NET 10 here.

Copied

What's New in C# 14

C# 14 is one of the most significant releases in recent years.

Let's explore the key features:

Copied

Extension Members

Extension members are my favorite feature in C# 14. They represent a modern evolution of extension methods that have been part of C# since version 3.0.

Traditional Extension Method Syntax

Before C# 14, you created extensions by writing static methods with a this parameter:

csharp
public static class StringExtensions { public static bool IsNullOrEmpty(this string value) { return string.IsNullOrEmpty(value); } public static string Truncate(this string value, int maxLength) { if (string.IsNullOrEmpty(value) || value.Length <= maxLength) { return value; } return value.Substring(0, maxLength); } }

New Extension Keyword Syntax

The new syntax separates the receiver (the type you're extending) from the members you're adding. Instead of putting this on each method parameter, you declare an extension block that specifies the receiver once:

csharp
public static class StringExtensions { extension(string value) { public bool IsNullOrEmpty() { return string.IsNullOrEmpty(value); } public string Truncate(int maxLength) { if (string.IsNullOrEmpty(value) || value.Length <= maxLength) return value; return value.Substring(0, maxLength); } } }

The extension block takes the receiver as a parameter. Inside the block, you write your methods and properties just like they were actual members of the type. The value parameter is available to all members within the block.

Both old and a new syntax compile to identical code, so they work the same way. You can even use both styles in the same static class.

The new extension syntax supports:

  • Instance methods
  • Instance properties
  • Static methods
  • Static properties
Copied

Extension Properties

Extension properties make your code more readable and expressive. Instead of calling methods, you can use properties that feel more natural.

For example, when working with collections, you frequently check if they're empty. Instead of writing !items.Any() everywhere, you can create an IsEmpty property:

csharp
public static class CollectionExtensions { extension<T>(IEnumerable<T> source) { public bool IsEmpty => !source.Any(); public bool HasItems => source.Any(); public int Count => source.Count(); } } public void ProcessOrders(IEnumerable<Order> orders) { if (orders.IsEmpty) { Console.WriteLine("No orders to process"); return; } foreach (var order in orders) { // Process order } }
Copied

Private Fields and Caching

Inside an extension block, you can define private fields and methods, just like in a regular class. This is useful when you need to cache expensive calculations:

csharp
public static class CollectionExtensions { extension<T>(IEnumerable<T> source) { private List<T>? _materializedList; public List<T> MaterializedList => _materializedList ??= source.ToList(); public bool IsEmpty => MaterializedList.Count == 0; public T FirstItem => MaterializedList[0]; } }

The backing field _materializedList ensures that ToList() is only called once, no matter how many times you access the properties.

Copied

Static Extension Members

Static extensions let you add factory methods or utility functions to a type, not the instance. To create static extensions, use extension without naming the receiver parameter:

csharp
public static class ProductExtensions { extension(Product) { public static Product CreateDefault() => new Product { Name = "Unnamed Product", Price = 0, StockQuantity = 0, Category = "Uncategorized", CreatedDate = DateTime.UtcNow }; public static bool IsValidPrice(decimal price) => price >= 0 && price <= 1000000; public static string DefaultCategory => "General"; } }

You can call these static members directly on the type:

csharp
var product = Product.CreateDefault(); if (Product.IsValidPrice(999.99m)) { product.Price = 999.99m; }
Copied

Null-Conditional Assignment

The null-conditional operators (?. and ?[]) can now be used for assignment, not just for accessing members.

Before C# 14, you needed an explicit null check before assigning to a property:

csharp
if (user is not null) { user.Profile = LoadProfile(); }

Now you can simplify this using the ?. operator:

csharp
user?.Profile = LoadProfile();

This is cleaner and more consistent with how null-conditional operators work for reading values.

Copied

The Field Keyword

The field keyword eliminates the need for explicit backing fields in many common property scenarios.

Previously, you needed to declare a private backing field:

csharp
public class Record { private string _msg; public string Message { get => _msg; set => _msg = value ?? throw new ArgumentNullException(nameof(value)); } }

With the field keyword, you can reference the compiler-generated backing field directly:

csharp
public class Record { public string Message { get; set => field = value ?? throw new ArgumentNullException(nameof(value)); } }

You can use field in any accessor: get, set, or init.

If you already use "field" as a variable name in your code, you can avoid conflicts by using @field or this.field, or by renaming your existing variable.

The field keyword is particularly useful for lazy initialization and default values:

csharp
public class ConfigReader { public string FilePath { get => field ??= "data/config.json"; set => field = value; } public Dictionary<string, string> ConfigValues { get => field ??= new Dictionary<string, string>(); set => field = value; } }

This avoids declaring separate backing fields for FilePath and ConfigValues.

Copied

Lambda Parameters with Modifiers

C# 14 allows you to use parameter modifiers like out, ref, in, scoped, and ref readonly in lambda expressions without specifying parameter types.

Previously, you had to include complete type declarations when using modifiers:

csharp
delegate bool TryParse<T>(string text, out T result); TryParse<int> parse = (string text, out int result) => Int32.TryParse(text, out result);

Now you can omit the types and let the compiler infer them:

csharp
delegate bool TryParse<T>(string text, out T result); TryParse<int> parse = (text, out result) => Int32.TryParse(text, out result);

This makes lambda expressions more concise while maintaining type safety.

Copied

Partial Constructors and Events

C# 14 extends partial member support to include constructors and events. This is particularly useful for source generators.

Partial constructors must have one defining declaration and one implementing declaration:

csharp
public partial class User { // This is the defining part public partial User(string name); } public partial class User { // This is the implementing part public partial User(string name) : this() // Calls another constructor in the same class { Name = name; } public User() { } public string Name { get; set; } }

Rules for partial constructors:

  • Only the implementing part can use this() or base() to call another constructor
  • Only one part of the class can use a primary constructor

Partial events also require one defining and one implementing declaration:

csharp
public partial class Downloader { public partial event Action<string> DownloadCompleted; } public partial class Downloader { public partial event Action<string> DownloadCompleted { add { } remove { } } }

The implementing part must include both add and remove accessors.

You can see the complete list of new features in C# 14 here.

Copied

What's New in ASP.NET Core in .NET 10

Copied

Validation Support in Minimal APIs

ASP.NET Core 10 adds built-in validation support for Minimal APIs. This feature automatically validates data sent to your endpoints, including query parameters, headers, and request bodies.

Register validation services by calling AddValidation:

csharp
builder.Services.AddValidation();

The validation system automatically discovers types used in your Minimal API handlers and validates them using attributes from the System.ComponentModel.DataAnnotations namespace.

You can apply validation attributes directly to endpoint parameters:

csharp
app.MapPost("/products", ([Range(1, int.MaxValue)] int productId, [Required] string name) => { return TypedResults.Ok(new { productId, name }); });

Record types work with validation too:

csharp
public record Product( [Required] string Name, [Range(1, 1000)] int Quantity); app.MapPost("/products", (Product product) => { return TypedResults.Ok(product); });

When validation fails, the runtime automatically returns a 400 Bad Request response with details about the validation errors.

You can disable validation for specific endpoints:

csharp
app.MapPost("/products", (int productId, string name) => TypedResults.Ok(productId)) .DisableValidation();

You can customize error responses by implementing IProblemDetailsService and registering it in the dependency injection container. This lets you create consistent, user-friendly error messages across your application.

Copied

JSON Patch Support in Minimal APIs

To enable JSON Patch support with System.Text.Json, install the package:

bash
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
Copied

Server-Sent Events (SSE)

Server-Sent Events (SSE) provide a lightweight, reliable way for ASP.NET Core apps to push continuous streams of data without the complexity of bidirectional protocols.

Server-Sent Events (SSE) is a web standard that enables a server to push real-time data to web clients over a single HTTP connection. Unlike traditional request-response patterns where clients must repeatedly poll the server for updates, SSE allows the server to initiate communication and send data whenever new information becomes available.

For sending Server-Sent Events, you need to provide a data stream via IAsyncEnumerable<T>.

Here's a service that generates stock price updates:

csharp
public record StockPriceEvent(string Id, string Symbol, decimal Price, DateTime Timestamp); public class StockService { public async IAsyncEnumerable<StockPriceEvent> GenerateStockPrices( [EnumeratorCancellation] CancellationToken cancellationToken) { var symbols = new[] { "MSFT", "AAPL", "GOOG", "AMZN" }; while (!cancellationToken.IsCancellationRequested) { var symbol = symbols[Random.Shared.Next(symbols.Length)]; var price = Math.Round((decimal)(100 + Random.Shared.NextDouble() * 50), 2); var id = DateTime.UtcNow.ToString("o"); yield return new StockPriceEvent(id, symbol, price, DateTime.UtcNow); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } } }

Use TypedResults.ServerSentEvents to create an endpoint that streams data:

csharp
builder.Services.AddSingleton<StockService>(); app.MapGet("/stocks", (StockService stockService, CancellationToken ct) => { return TypedResults.ServerSentEvents( stockService.GenerateStockPrices(ct), eventType: "stockUpdate" ); });

Clients can connect to this endpoint and receive real-time updates as they're generated.

You can read an in-depth guide on Server-Sent Events with ASP.NET Core here.

Copied

OpenAPI 3.1 Support

ASP.NET Core 10 adds support for generating OpenAPI 3.1 documents. While the version number suggests a minor update, OpenAPI 3.1 is significant because it includes full support for JSON Schema draft 2020-12.

Key Changes in OpenAPI 3.1:

  • Nullable types use a type array that includes null instead of a nullable: true property
  • Integer types (int and long) may appear without the type: integer field and use a pattern field instead, depending on your JSON serialization settings
  • The default OpenAPI version is now 3.1

You can explicitly set the OpenAPI version when adding OpenAPI support:

csharp
builder.Services.AddOpenApi(options => { options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1; });

YAML Format Support

ASP.NET Core now supports serving OpenAPI documents in YAML format. YAML is more concise than JSON and supports multi-line strings, making it easier to read long descriptions.

To serve YAML documents, specify a .yaml or .yml extension:

csharp
if (app.Environment.IsDevelopment()) { app.MapOpenApi("/openapi/{documentName}.yaml"); }
Copied

What's New in Blazor

Blazor receives several improvements in .NET 10:

  • Hot Reload for Blazor WebAssembly and .NET on WebAssembly
  • Environment configuration in standalone Blazor WebAssembly apps
  • Performance profiling and diagnostic counters for Blazor WebAssembly
  • NotFoundPage parameter for the Blazor router
  • Static asset preloading in Blazor Web Apps
  • Improved form validation

You can see the complete list of ASP.NET Core 10 features here.

Copied

What's New in EF Core 10

Copied

Complex Types

Complex types let you model data that belongs to an entity but doesn't have its own identity. While entity types map to database tables, complex types can map to columns in the same table (table splitting) or to a JSON column.

Complex types enable document modeling patterns that can improve performance by avoiding JOINs and simplifying your database schema.

Table Splitting

Map a customer's addresses as complex types:

csharp
modelBuilder.Entity<Customer>(b => { b.ComplexProperty(c => c.ShippingAddress); b.ComplexProperty(c => c.BillingAddress); });

Optional Complex Types

EF 10 adds support for optional complex types:

csharp
public class Customer { public int Id { get; set; } public Address ShippingAddress { get; set; } public Address? BillingAddress { get; set; } }

JSON Mapping

Map complex types to JSON columns:

csharp
modelBuilder.Entity<Customer>(b => { b.ComplexProperty(c => c.ShippingAddress, c => c.ToJson()); b.ComplexProperty(c => c.BillingAddress, c => c.ToJson()); });

Struct Support

Complex types support .NET structs:

csharp
public struct Address { public required string Street { get; set; } public required string City { get; set; } public required string ZipCode { get; set; } }
Copied

LeftJoin and RightJoin Operators

LEFT JOIN is common in database queries, but previous versions of EF Core made it complicated to write. You needed to combine SelectMany, GroupJoin, and DefaultIfEmpty in a specific way.

.NET 10 adds first-class LINQ support for LeftJoin, making these queries much simpler:

csharp
var query = context.Students .LeftJoin( context.Departments, student => student.DepartmentID, department => department.ID, (student, department) => new { student.FirstName, student.LastName, Department = department.Name ?? "[NONE]" });

EF 10 also supports RightJoin, which keeps all data from the second collection and only matching data from the first.

Both methods translate to the appropriate SQL JOIN operations.

Note that C# query syntax (from x select x.Id) doesn't yet support these operators.

Copied

ExecuteUpdate for JSON Columns

While EF Core has supported JSON columns for several versions, ExecuteUpdate couldn't modify them. EF 10 changes this by allowing you to update JSON properties in bulk operations.

Given this model with a JSON column:

csharp
public class Blog { public int Id { get; set; } public BlogDetails Details { get; set; } } public class BlogDetails { public string Title { get; set; } public int Views { get; set; } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>().ComplexProperty(b => b.Details, bd => bd.ToJson()); }

You can now update properties within the JSON column:

csharp
await context.Blogs.ExecuteUpdateAsync(s => s.SetProperty(b => b.Details.Views, b => b.Details.Views + 1));

This generates efficient SQL that updates the JSON data directly in the database.

Copied

Named Query Filters

Global query filters let you define conditions that apply to all queries for an entity type. This simplifies implementing patterns like soft deletion and multi-tenancy.

Previously, EF Core supported only one query filter per entity type.

EF 10 introduces named query filters, letting you define multiple filters and control them independently:

csharp
modelBuilder.Entity<Blog>() .HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted) .HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);

You can selectively disable specific filters in individual queries:

csharp
var allBlogs = await context.Blogs .IgnoreQueryFilters(["SoftDeletionFilter"]) .ToListAsync();

This gives you fine-grained control over which filters apply to each query.

Copied

Regular Lambdas in ExecuteUpdateAsync

ExecuteUpdateAsync lets you perform bulk updates directly in the database without loading entities into memory.

In previous versions, you had to provide changes as expression trees, making it challenging to build dynamic updates.

EF 10 allows regular lambdas with conditional logic:

csharp
await context.Blogs.ExecuteUpdateAsync(s => { s.SetProperty(b => b.Views, 8); if (nameChanged) { s.SetProperty(b => b.Name, "foo"); } });

This is much simpler than manually building expression trees and makes conditional updates straightforward.

You can see the complete list of EF Core 10 features here.

Copied

Other Changes in .NET 10

Additional resources for .NET 10:

Copied

Summary

.NET 10 and C# 14 represent a significant step forward for the .NET ecosystem.

The file-based apps feature makes C# viable for scripting scenarios that were previously dominated by Python, Node.js and JavaScript.

Extension members give you flexibility by letting you add properties to types, not just methods.

The improvements to Minimal APIs, EF Core, and ASP.NET Core make building modern web applications faster and more intuitive.

As an LTS release, .NET 10 provides a stable foundation for both new and existing applications.

Whether you're building Web APIs, desktop applications, or CLI tools, this release offers features that improve productivity and code quality.

I've been testing .NET 10 in personal projects and will begin migrating commercial projects this month.

Are you excited about .NET 10? Hit reply and let me know.

Hope you find this newsletter useful. See you next time.

Whenever you're ready, here's how I can help you:

The .NET Senior Playbook is built to:

  • Fast-track you from junior or mid-level to senior
  • Keep you growing as a senior
  • Help you beat any .NET interview

Covers everything: C#, ASP.NET Core, EF Core, system design β€” answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.

The .NET Senior Playbook
View the Playbook

Not sure where you stand? Take the free .NET Interview Run:

  • Find out your real level β€” Junior to Senior+
  • A realistic mock .NET interview β€” across 13 areas of C#, .NET, ASP.NET Core and System Design

No credit card required. When you finish, you get a personalized report: your level, your strongest and weakest areas, and where to focus next β€” the perfect way to benchmark yourself before diving into the Playbook.

Start your free run

Enjoyed this article? Share it with your network

Improve Your .NET and Architecture Skills

Join my community of 25,000+ developers and architects.

Each week you will get 1 practical tip with best practices and real-world examples.

Learn how to craft better software with source code available for my newsletter.

Join 25,000+ developers already reading
No spam. Unsubscribe any time.