By the end of this year, Microsoft plans to release .NET 10 (in November 2025). Incidentally, it will be a Long-Term Support (LTS) release. The preview version of .NET 10 is already available. This article presents an overview of the new features and enhancements in .NET 10. You can download a copy of .NET 10 Preview here: https://dotnet.microsoft.com/en-us/download/dotnet/10.0.
To work with the code examples discussed in this article, the following software must be available:
- Visual Studio 2022 Preview
- .NET 10.0 Preview
- ASP.NET 10.0 Preview Runtime
If you don't already have a copy of Visual Studio 2022 Preview installed on your computer, you can download it from here: https://visualstudio.microsoft.com/downloads/.
In this article, after exploring the new features, you'll implement a simple application that leverages ASP.NET Core 10, Blazor 10, EF Core 10, and C# 14.
New Features and Enhancements in .NET 10
This section examines the new features and enhancements in .NET 10.
.NET Runtime Improvements
.NET 10 comes with enhanced support for optimized JIT compilation, thereby facilitating significant performance and efficiency improvements. For example, improved support for method inlining and loop unrolling optimizations allows for faster execution of frequently used code paths, thereby enhancing performance for applications that run for extended periods.
The following are the key performance enhancements in .NET runtime 10:
- De-virtualization of array interface methods
- Inlining late de-virtualized methods
- Allocation of small-sized arrays in the stack
- De-abstraction of array enumeration
New Features and Enhancements in .NET 10 Libraries
Microsoft released .NET 10 with a slew of new features and updated libraries meant to boost speed, security, and developer efficiency. Incidentally, .NET 10 also introduces additional APIs that facilitate working with spans, thereby helping you to reduce unnecessary allocations, as shown in the following code snippet:
public static class StringNormalizationExtensions
{
public static int GetNormalizedLength(this ReadOnlySpan<char> source,
NormalizationForm normalizationForm = NormalizationForm.FormC);
public static bool IsNormalized(this ReadOnlySpan<char> source,
NormalizationForm normalizationForm = NormalizationForm.FormC);
public static bool TryNormalize(this ReadOnlySpan<char> source,
Span<char> destination, out int charsWritten,
NormalizationForm normalizationForm = NormalizationForm.FormC);
}
There are a plethora of new features and enhancements in .NET 10 Libraries, such as the following:
- ZipArchive performance improvements
- JSON serialization updates
- Numerical string comparisons via the
CompareOptions.NumericOrdering
enum - Improvements to
OrderedDictionary
- New AOT-safe constructor included in the
ValidationContext
class - New overloaded method for
TimeSpan.FromMilliseconds
- String normalization APIs for spans
In the previous versions of the C# programming language, all ZipArchiveEntry instances were loaded in the memory and then rewritten, which increased resource usage and created performance issues. With .NET 10, resource usage for ZipArchiveEntry instances have been optimized, enabling you to load only the entries required in the memory, thereby improving performance. The following piece of code shows how you can create a new entry in a zip archive and then write data to it using a file stream instance.
using (FileStream fileStream = new FileStream(@"D:\test.zip", FileMode.Open))
{
using (ZipArchive zipArchive = new ZipArchive(fileStream,
ZipArchiveMode.Update))
{
ZipArchiveEntry zipArchiveEntry = zipArchive.CreateEntry("Sample.txt");
using (StreamWriter writer = new StreamWriter(zipArchiveEntry.Open()))
{
writer.WriteLine("This is a sample piece of text.");
}
}
}
The following new overloaded methods have been added to the ISOWeek class to work with the DateOnly
type.
public static class ISOWeek
{
// Additional overloaded methods
public static int GetWeekOfYear(DateOnly date);
public static int GetYear(DateOnly date);
public static DateOnly ToDateOnly(int year, int week,
DayOfWeek dayOfWeek);
}
A new constructor has been introduced in the ValidationContext
class to promote AOT safety. The following code snippet shows what's changed in this class:
public sealed class ValidationContext
{
public ValidationContext(object instance, string displayName,
IServiceProvider? serviceProvider = null,
IDictionary<object, object?>? items = null);
}
With .NET 10, additional overloaded methods have been introduced for working with OrderedDictionary<TKey,TValue>,
as shown in the code snippet given below:
public class OrderedDictionary<TKey, TValue>
{
//Additional overloads
public bool TryAdd(TKey key, TValue value, out int index);
public bool TryGetValue(TKey key, out TValue value, out int index);
}
Numeric string comparison is a much-awaited feature in C#. With C# 14, it's possible to compare strings numerically instead of lexicographically. The CompareOptions
enum is defined in the System.Globalization namespace, as shown in the code snippet given below:
using System;
using System.Globalization;
[Flags, Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
public enum CompareOptions
{
None = 0x00000000,
IgnoreCase = 0x00000001,
IgnoreNonSpace = 0x00000002,
IgnoreSymbols = 0x00000004,
IgnoreKanaType = 0x00000008,
IgnoreWidth = 0x00000010,
OrdinalIgnoreCase = 0x10000000,
StringSort = 0x20000000,
Ordinal = 0x40000000
}
To take advantage of the CompareOptions
enum, you should install the System.Globalization NuGet package into your project and include the System.Globalization assembly in your programs. Consider the following piece of code that shows how this can be achieved:
StringComparer numericStringComparer =
StringComparer.Create(CultureInfo.CurrentCulture,
CompareOptions.NumericOrdering);
foreach (string str in new[] { "05", "01", "03", "02", "04" }
.Order(numericStringComparer))
{
Console.WriteLine(str);
}
Console.ReadKey();
When the preceding piece of code is executed, the numbers in the string array will be displayed in the ascending order, as shown in Figure 1.

With .NET 10, the TimeSpan.FromMilliseconds
method includes a new overload that accepts a single parameter, as shown in the code snippet given below:
public readonly struct TimeSpan
{
public static TimeSpan FromMilliseconds(long milliseconds,
long microseconds);
public static TimeSpan FromMilliseconds(long milliseconds);
}
In the preceding piece of code, there are two overloaded variations of the FromMilliseconds
method in the TimeSpan
struct. Now, the second parameter of the first overloaded method is no longer optional. The additional overloaded variation of the FromMilliseconds
method accepts a single parameter that wasn't available in the previous versions of the C# programming language.
Stack Allocation for Small Fixed-Sized Arrays
Thanks to its support for stack allocation of smaller immutable arrays, .NET 10 can considerably reduce garbage collection overheads, thereby improving array memory allocation overhead by eliminating the need for heap memory allocation. Additionally, .NET 10 reduces garbage collection (GC) overheads and enhances overall system efficiency by storing small arrays in the stack.
Support for Advanced Vector Extensions
Thanks to its support for AVX 10.2 intrinsics, .NET 10 is poised to support high-performance CPU optimizations. Although disabled by default, this feature will significantly enhance numeric, AI, and graphics computations once compatible processors become available. These improvements will greatly benefit developers working on scientific computing, image processing, and ML-based applications.
Garbage Collection (GC) Enhancements
Although managed objects are created in the managed heap and removed from the managed heap during a garbage collection cycle, you'd need to write code to delete unmanaged objects explicitly. Garbage collection is a strategy adopted by the CLR to clear the memory occupied by managed objects that have become “garbage.” Both .NET Framework and .NET Core include garbage collection as a built-in feature for cleaning up memory occupied by managed objects.
.NET 10 provides enhanced support for better memory management, reducing latency in real-life applications. The background GC is optimized to eliminate memory fragmentation through memory compaction enhancements, improving overall application performance.
Enhanced Code Layout
With .NET 10, the JIT compiler now has a new way of organizing method code into basic blocks to optimize runtime performance. In earlier versions of .NET Framework and .NET Core, the JIT performed an initial layout of the program's flowgraph using reverse-postorder (RPO) traversal. Although this approach has some degree of performance improvement, it has its downsides as well.
It should be noted that block reordering is done using a model based on a reduction to the asymmetric Travelling Salesman Problem. With these techniques, .NET 10 optimizes hot path density while decreasing branch distances, thereby improving overall performance.
New Features in C# 14
C# 14 introduces a plethora of new features and enhancements to improve developer productivity and code quality. In this section I'll examine some of the key features and enhancements in C# 14, such as the following:
- Using the preview features in Visual Studio
- The
Field
keyword - Enhanced support for working with lambda expressions
- Enhanced partial capabilities
- Modifiers for simple lambda parameters
- Enable inlining for de-virtualization
- De-virtualization of array interface methods
- Allocation of Value Type arrays in the stack
- Support for null conditional assignment
- Support for implicit span conversions
- Support for stack allocation of small arrays of reference types
Create a New .NET 10 Console Application Project in Visual Studio 2022 Preview
Let's create a console application project to work with the C# 14 code examples given in the subsequent sections of this article. You can create a project in Visual Studio 2022 in several ways. When you launch Visual Studio 2022, you'll see the Start window. You can choose Continue without code to launch the main screen of the Visual Studio 2022 IDE.
To create a new Console Application Project in Visual Studio 2022:
- Start the Visual Studio 2022 Preview IDE.
- In the Create a new project window, select Console App, and click Next to move on.
- Specify the project name and the path where it should be created in the Configure your new project window.
- If you want the solution file and project to be created in the same directory, optionally check the Place solution and project in the same directory checkbox. Click Next to move on.
- In the Additional information screen, specify the target framework as .NET 10.0 (Preview), as shown in Figure 2.
- Click Create to complete the process.

You'll use this application in the subsequent sections of this article.
Using the Preview Features in Visual Studio
To use preview features in Visual Studio, edit the project file and add the LangVersion
element, as shown in the code snippet given below:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
If there are multiple C# projects that need to be configured to use preview features, create a Directory.Build.props
file and add the following code in there:
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
Do not use the Directory.Build.Props
file in a folder where the subdirectories of the folder contain projects that have been written in both C# and VB because the versions are different.
The Field Keyword
Prior to C# 14, you had to declare a backing field and implement the Get and Set accessors to check whether a property of type string is set to null, as shown in the code snippet below:
private string _address;
public string Address
{
get => _address;
set => _address = value ?? throw new ArgumentNullException(nameof(value));
}
C# 14 introduces the Field
keyword to make your code much cleaner, more readable, and readily maintainable. You can declare these fields at the top of the class and initialize them in the constructor of the class. The following code snippet shows how to modify the preceding piece of code in C# 14 to ensure the Address
property is never assigned a null
value and make the source code lean and clean.
public string Address
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
Additionally, C# 14 enables declaring a body for your Get and Set accessors pertaining to a field-backed property. Note that if a type already contains a symbol named field, use @field
or this.field
to distinguish it from the keyword and avoid ambiguity.
Enhanced Support for Working with Lambda Expressions
With C# 14, you can use parameter modifiers such as ref
, scoped
, in
, out
, or ref readonly
on your lambda expression parameters without requiring that you specify the parameter types explicitly. Let's understand this with a code example. Consider the following piece of code:
TryParse<int> parse1 = (text, out result) =>
{
Int32.TryParse(text, out result);
};
The statement is perfectly valid in C# 14. You longer need to specify the type of the parameters explicitly.
Partial Events and Partial Constructors
The partial keyword has long been in use in the C# programming language. You've seen partial methods, properties, and indexers thus far. With C# 14, you can use the partial keyword with events and constructors, i.e., you can now have partial events and partial constructors. C# now provides enhanced support for working with partial members of a class.
You can now include the partial keyword in the signature of a constructor or event enabling isolation of their declaration and implementation code. You need exactly one definition and one implementation for your partial constructors and events. You can use the this()
or base()
initializers only in the implementation of a partial constructor.
If a constructor needs to invoke another constructor in the same class or in a base class using this()
or base()
, the initializer can only be mentioned in the implementing declaration of the partial constructor. The following code snippet shows how to declare a partial method in a partial class and write the implementation code of the method in a partial class having identical name.
public partial class MyExamplePartialClass
{
public MyExamplePartialClass()
{
MyExamplePartialMethod();
}
partial void MyExamplePartialMethod();
}
public partial class MyExamplePartialClass
{
partial void MyExamplePartialMethod()
{
//Write your code here to implement
//the body of the method
}
}
In the preceding code snippet, the MyExamplePartialClass
type can reside in the same or another file. The following code snippet illustrates how partial constructors can be used in C# 14.
public partial class Author
{
public partial Author(Guid id); //This represents
//declaring the partial constructor, i.e.,
//i.e., it contains only its signature
}
public partial class Author
{
public partial Author(Guid id)
{
//This shows a partial constructor, i.e.,
//it contains the actual body
}
}
In the preceding code snippet, a partial class named Author
is shown in which a partial constructor is declared. Note how I've declared the signature of the partial constructor without any method body. The partial constructor of the Author
class is implemented in another partial class having an identical name, i.e., both these partial classes have the same names.
Here are the key rules for using partial constructors in C# 14 at a glance:
- A partial constructor can have only one defining and implementing declaration.
- Partial constructors can be used only in partial classes.
- A partial constructor cannot be abstract, i.e., it cannot have an abstract modifier.
In C#, a partial event represents an event declaration in which the event definition is isolated from its implementation. Partial events can be used when the definition of a C# class is split so that the event handling logic is isolated from the class definition. As a result, you can define your event signature and have a source code generator create the implementation. This can help separate the concerns of the application while allowing for cleaner, manageable code.
Similar to partial constructors, you can declare partial events only twice—once with its definition (without any body) and the other having its implementation. You must provide both Add and Remove accessors in the implementing declaration of a partial event. Like partial constructors, partial events can only be used in partial classes, i.e., classes that are marked with the partial keyword in the class declaration. You cannot mark a partial event as abstract or use the partial keyword to implement an event in an interface.
Here are the key rules for using partial events in C# 14 at a glance:
- There can be one defining and one implementing declaration of a partial event.
- An implementing declaration of a partial event must contain one or more accessors.
- Partial events can be used only in partial classes but not in abstract classes or implementations of interfaces.
Listing 1 shows how to implement partial events in C# 14.
Listing 1: Implementing partial events
public partial class PartialEventDeclarationExample
{
public partial event EventHandler DataReceived;
}
public partial class PartialEventImplementationExample
{
private EventHandler _dataReceivedExampleHandler;
public partial event EventHandler DataReceivedEventExample
{
add
{
Console.WriteLine("A new subscriber has been added to
DataReceivedEventExample");
_dataReceivedExampleHandler += value;
}
remove
{
Console.WriteLine("An existing subscriber has been
removed from DataReceivedEventExample");
_dataReceivedExampleHandler -= value;
}
}
protected void OnDataReceived(EventArgs e)
{
_dataReceivedExampleHandler?.Invoke(this, e);
}
}
Enhanced Readability of Lambda Expressions
With C# 14, it's possible to add parameter modifiers such as ref
, ref readonly
, scoped
, in
, and out
to your lambda expressions without requiring that you specify explicit type declarations, and enhancing readability of your lambda expressions. For example, you'd be required to specify explicit types for the modifiers in C# 13, as shown in the following code snippet:
TryParse<int> parse = (string text, out int result) =>
{
return int.TryParse(text, out result);
};
Because .NET 10 provides support for type inference, it's possible to write the same piece of code like this:
delegate bool TryParse<T>
(
string text, out T result
);
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);
Enable Inlining for De-Virtualization
Virtual methods in C# are used to implement runtime polymorphism. Despite the flexibility they offer, there are performance drawbacks as well. Under the hood, the C# programming language uses a virtual method table, also known as vtable, to determine the exact method to be called at runtime. It should be noted that vtable is class specific, i.e., you have one vtable per class that contains at least one virtual method. The just-in-time (JIT) compiler in .NET Core 10 is capable of inlining methods that are eligible for de-virtualization.
De-Virtualization of Array Interface Methods
With .NET 10, the JIT compiler provides support for de-virtualization based on inlining observations. It now modifies the types of temporary variables containing return values that are being inlined. The .NET 10 runtime provides enhanced support for improving performance and reducing abstraction overheads in your source code. This is accomplished by enabling the JIT compiler to de-virtualize array interface methods. Prior to .NET 10, the JIT was capable of optimizing loops in .NET 9 by removing bounds checks in methods such as the following:
static int AddIntegers(int[] integers)
{
int sum = 0;
for (int i = 0; i < integers.Length; i++)
{
sum += integers[i];
}
return sum;
}
That said, if you're to iterate a foreach loop of IEnumerable<int>
, virtual calls will be introduced, thereby preventing you from optimizing the source code using inlining or eliminating of bounds checks in the code.
static int AddIntegers(int[] integers)
{
int sum = 0;
IEnumerable<int> temp = integers;
foreach (var n in temp)
{
sum += n;
}
return sum;
}
Allocation of Value Type Arrays in the Stack
Allocation and de-allocation of objects in the stack memory reduces GC overheads because the GC needs to track fewer objects. Once an object has been allocated in the stack memory, the JIT replaces it with its scalar values. With .NET 10, the JIT can allocate small, fixed-size arrays of value types in the stack memory.
With .NET 9, you were able to allocate objects on the stack, thereby reducing the garbage collection overhead and improving performance. With .NET 10, this has been further enhanced by enabling allocation of small, fixed-size arrays of value types that don't have any GC references in the stack.
Consider the following piece of code:
static int AddIntegers()
{
int[] integers = { 1, 2, 3 };
int sum = 0;
for (int i = 0; i < integers.Length; i++)
{
sum += integers[i];
}
return sum;
}
In the preceding piece of code, because the array of named integers is of fixed size, the JIT allocates it on the stack instead of the heap, thereby improving performance.
Null Conditional Assignment
With C# 14, there's no need to check for null
value when assigning a variable to a property. You can now use null
conditional member access operators on the left-hand side of an assignment, as shown in the code snippet:
employee?.DepartmentId = GetDepartmentId(employeeId);
Prior to C# 14, you'd have to write the following code instead:
if (employee is not null)
{
employee.DepartmentId = GetDepartmentId(employeeId);
}
This feature reduces verbose null checks in your code, which makes your code more efficient, concise, and error free. Let's illustrate this with another code example. Consider the following piece of code that demonstrates a simple C# class named MyExampleClass:
public class MyExampleClass
{
public string MyExampleProperty
{
get;
set;
}
}
The following static class named MyExampleFactory
contains a static method that returns an instance of the MyExampleClass:
public static class MyExampleFactory
{
public static MyExampleClass MyExampleMethod()
{
return new MyExampleClass();
}
}
Typically, you'd use the following piece of code to instantiate the MyExampleClass
class and assign a value to its property:
MyExampleClass?
myExampleObject = MyExampleFactory.MyExampleMethod();
if (myExampleObject is not null)
{
myExampleObject.MyExampleProperty = "Hello World!";
}
With C# 14, the preceding piece of code may be shortened to the following:
MyExampleClass?
myExampleObject = MyExampleFactory.MyExampleMethod();
myExampleObject?.MyExampleProperty = "Hello World!";
When the preceding piece is executed, the runtime checks whether the myExampleObject
instance is null. If the instance is not null, the assignment is made, i.e., the string “Hello World!” is assigned to its string property named MyExampleProperty
. If the instance null, the assignment statement is ignored. The MyExampleFactory
class is a factory class, i.e., a class that follows the factory design pattern and returns an instance of type MyExampleClass
.
Implicit Span Conversions
Span<T>
, Memory<T>
, ReadOnlySpan
, and ReadOnlyMemory
are types defined in the System namespace and have been added in the newer versions of the C# programming language. They represent a contiguous block of memory and provide a type-safe way to access contiguous regions of arbitrary memory.
With C# 14, Microsoft has introduced first-class support for System.Span<T>
and System.ReadOnlySpan<T>
in the language itself. As a result, you can now leverage implicit conversions of these types leading to enhanced readability and flexibility of your code. These enhancements improve performance, security, and interoperability, and facilitate a much more natural way to program spans in C#.
Both System.Span<T>
and System.ReadOnlySpan<T>
types help improve the performance of your applications considerably by reducing allocations, thereby decreasing the strain on the garbage collector. Span<T>
is defined in the System namespace, as shown in the code snippet below:
public readonly ref struct Span<T>
{
internal readonly ByReference<T> _pointer;
private readonly int _length;
// Other members
}
ReadOnlySpan<T>
is defined in the System namespace, as shown below:
public readonly ref struct ReadOnlySpan<T>
{
internal readonly ref T _reference;
private readonly int _length;
//Other members
}
It's now possible to convert the following types to Span<T>
:
- Arrays
- Pointers
- IntPtr
- stackalloc
Also, you can convert the following types to ReadOnlySpan<T>:
- Arrays
- Pointers
- IntPtr
- stackalloc
- string
The following code snippet shows how you can convert an array to a Span<T>
in C# 14:
string[] words =
{
"This", "is", "a", "sample", "text."
};
ReadOnlySpan<string> readOnlySpan = words;
Stack Allocation of Small Arrays of Reference Types
With .NET 10, Microsoft introduces improvements in allocation of objects in the stack memory. Prior to .NET 10, arrays of reference types were always allocated in the heap memory irrespective of their lifetime. With .NET 10, the JIT can allocate arrays of reference types in the stack memory when these arrays don't outlive their creation context.
Consider the following piece of code:
static void DisplayText()
{
string[] array = { "A", "B", "C", "D", "E" };
foreach (var str in array)
{
Console.WriteLine(str);
}
}
In the preceding piece of code, the string array named “array” will be allocated on the stack memory.
To work with C# 14, you should have .NET 10 SDK, and Visual Studio 2022 (version 17.14 or later) installed in your computer. Additionally, you should set
<LangVersion>14</LangVersion>
in your.csproj
file in Visual Studio to enable C# 14 features.
New Features in Entity Framework Core (EF Core) 10
Entity Framework Core (EF Core) is a cross-platform, lightweight, extensible, Object Relational Mapper from Microsoft that simplifies how to work with databases. There are several new features and enhancements in Entity Framework Core (EF Core) 10 such as the following:
- Enhancements to LINQ and SQL Translation
- Support for full-text search on Azure Cosmos DB
- Support for Hybrid search
- Support for LeftJoin and RightJoin operators
- Enhanced support when evolving the model on Azure Cosmos DB
- Enhanced support for mapping Stored Procedures
- Support for non-expression lambda in an
ExecuteUpdateAsync
method
Install Entity Framework Core
Before getting started using EF Core, it should be installed in your project from NuGet. To install the necessary NuGet package(s) for working with Entity Framework Core and SQL Server, install these packages into your project, right-click on the solution and then select Manage NuGet Packages for Solution….
Once the window pops up, search for the NuGet packages to add to your project. To do this, type in the packages named Microsoft.EntityFrameworkCore,
Microsoft.EntityFrameworkCore.Design
, Microsoft.EntityFrameworkCore.Tools,
and Microsoft.EntityFrameworkCore.SqlServer
in the search box of the NuGet Package Manager screen and install them one after the other.
Alternatively, you can type the commands shown below at the NuGet Package Manager command prompt:
PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.Design
PM> Install-Package Microsoft.EntityFrameworkCore.Tools
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
You can also install these packages by executing the following commands at the Windows Shell:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Enhancements to LINQ and SQL Translation
In EF Core 10, LINQ queries optimize execution and minimize redundant database calls to improve indexing mechanisms. New changes to the query compiler optimizations add more efficient translation of LINQ expressions, resulting in quicker data access and better concurrency management.
Support for Full-Text Search on Azure Cosmos DB
With .NET 10, Azure Cosmos DB has introduced support for full-text search for NoSQL, enabling comprehensive and advanced text searching. You can use it with vector search to improve precision in some AI use cases. The support for this feature on EF Core 10 enables modeling the database with full-text search-enabled properties.
The following code snippet shows how to configure full-text search in EF Core 10:
public class ApplicationDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext>
options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>(b =>
{
b.Property(e => e.City).EnableFullTextSearch();
b.HasIndex(e => e.City).IsFullTextIndex();
});
}
}
Assuming that full-text search has been configured already, the following piece of code can be used to leverage full-text search capabilities in EF Core 10:
var data = await context.Persons
.Where(x => EF.Functions.FullTextContains(x.City, "Texas"))
.ToListAsync();
Support for Hybrid Search
EF Core 10 provides support for a combination of vector similarity search and full-text search (i.e., hybrid search). The following piece of code shows how you can work with hybrid search.
float[] vectorArray;
/* Assume that the vector array is populated with vector data
derived from text, image, etc. */
var hybrid = await context
.Persons
.OrderBy(x => EF.Functions.Rrf(
EF.Functions.FullTextScore(x.City, "database"),
EF.Functions.VectorDistance(x.Vector, vectorArray)
))
.Take(25)
.ToListAsync();
Support for LeftJoin and RightJoin Operators
In the previous versions of EF Core, it was quite challenging to implement LeftJoin in LINQ operations. With EF Core 10, you now have support for both LeftJoin and RightJoin operations in LINQ.
The following query shows how to use the LeftJoin operator in EF Core 10:
var query = context.Payroll
.LeftJoin(
context.Department,
employee => employee.DepartmentID,
department => department.ID,
(employee, department) => new
{
employee.ID,
employee.FirstName,
employee.LastName,
employee.JoiningDate,
employee.EmailAddress,
Department = department.Name ?? ""
});
Enhanced Support When Evolving the Model on Azure Cosmos DB
An entity with a new required property added with EF Core in Azure Cosmos DB will have materialization issues as EF cannot materialize that entity. Because the document was created before the change, those values were unavailable to EF Core. To solve this, mark the property initially as optional, add the default values manually, and then make the desired changes.
Enhanced Support for Mapping Stored Procedures
With EF Core 10, you can map CUD (Create, Update, Delete) operations in the stored procedures much more easily than before, enabling more efficient data management. Before this release of EF Core, you had to run through numerous steps to run stored procedures, which required great effort.
You can now enable EF Core to leverage stored procedures instead of generating SQL commands when working with CUD operations. The following code snippet shows how this can be accomplished:
modelBuilder.Entity<Person>()
.InsertUsingStoredProcedure("Employee_Insert", spe =>
{
spe.HasParameter(e => e.FirstName);
spe.HasResultColumn(e => e.Id);
})
.UpdateUsingStoredProcedure("Employe_Update", spe =>
{
spe.HasOriginalValueParameter(e => e.Id);
spe.HasParameter(e => e.FirstName);
spe.HasRowsAffectedResultColumn();
})
.DeleteUsingStoredProcedure("Employe_Delete", spe =>
{
spe.HasOriginalValueParameter(e => e.Id);
spe.HasRowsAffectedResultColumn();
});
In EF Core, InsertUsingStoredProcedure
, UpdateUsingStoredProcedure
, and DeleteUsingStoredProcedure
are implemented as extension methods that call corresponding stored procedures on the underlying database for inserting, updating, and deleting data in a relational database. Typically, these extension methods are used inside the OnModelCreating
method of the custom DbContext class.
Support for Non-Expression Lambda in ExecuteUpdateAsync Method
In the recent versions of EF Core, the newly introduced ExecuteUpdateAsync
and ExecuteDeleteAsync
methods facilitate asynchronous batch update or delete operations. These improvements provide greater scalability, especially for cloud applications that manage large volumes of data.
In the earlier versions of EF Core, the ExecuteUpdateAsync
method required expression trees, which made it difficult to work with dynamic updates. With EF Core 10, LINQ query translation and the ExecuteUpdateAsync
method have been enhanced, thereby enabling more flexible and efficient database operations. EF Core 10 provides support for regular, non-expression lambda in the ExecuteUpdateAsync
method.
The following code snippet shows how EF Core 10 enables working with regular lambda expressions, thereby enabling you to easily perform conditional updates dynamically:
await context.Blogs.ExecuteUpdateAsync(s =>
{
if (isNameChanged)
{
s.SetProperty(a => a.FirstName, "Joydip");
s.SetProperty(a => a.LastName, "Kanjilal");
}
});
A few other improvements made in EF Core 10 include support for some string functions taking char as arguments, support for MAX/MIN/ORDER BY using decimal on SQLite, translation of COALESCE as ISNULL, and replacing AsyncLocal to ThreadId for better Lazy loader performance.
Install Entity Framework Core 10
Install the necessary NuGet Package(s) for working with Entity Framework Core and SQL Server. To install these packages into your project, right-click on the solution and the select Manage NuGet Packages for Solution….
Now search for the NuGet packages named Microsoft.EntityFrameworkCore
and Microsoft.EntityFrameworkCore.InMemory
packages in the search box and install them one after the other. Select the latest prerelease version of EF Core, as shown in Figure 3. As of this writing, the latest prerelease version of EF Core is 10.0.0-preview.4.25258.110.

Another option is to type the commands shown below at the NuGet Package Manager command prompt:
PM> Install-Package Microsoft.EntityFrameworkCore -v 10.0.0-preview.3.25171.6
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer -v
10.0.0-preview.3.25171.6
Alternatively, install these packages by executing the following commands at the Windows Shell:
dotnet add package Microsoft.EntityFrameworkCore -v 10.0.0-preview.3.25171.6
dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v
10.0.0-preview.3.25171.6
New Features and Enhancements in ASP.NET Core 10
With ASP.NET Core 10, Microsoft has introduced several new enhancements in Blazor, SignalR, minimal APIs, and OpenAPI 3.1. ASP.NET Core 10 promises ease of development, seamless integration with modern web standards, and improved performance over its earlier counterparts. This section highlights the new features and enhancements introduced in ASP.NET Core 10:
- Blazor enhancements
- Convert empty strings in Form Posts to null for nullable value types
- Support for accessing an OpenApiDocument using the
IOpenApiDocumentProvider
interface - Improvements to ASP.NET Core OpenAPI document generation
- Support for JSON Patch using
System.Text.Json
- Enhanced support for OpenAPI XML documentation
- Support for generating OpenAPI schemas in transformers
- Support for JavaScript interops
- Support for validation in minimal APIs
- Enhanced support for Server-Sent Events (SSE)
- Support for specifying response description for your API controllers
- Authentication and authorization enhancements
- The
RedirectHttpResult.IsLocalUrl
helper method
Before I delve into these enhancements, let's first learn how you can create a new ASP.NET Core 10 project in Visual Studio.
Create a New ASP.NET Core 10 Project in Visual Studio 2022 Preview
You can create a project in Visual Studio 2022 in several ways, such as from the Visual Studio 2022 Developer Command prompt or by launching the Visual Studio 2022 IDE. When you launch Visual Studio 2022 Preview, you'll see the Start window. You can choose Continue without code to launch the main screen of the Visual Studio 2022 Preview IDE.
Now that you know the basics, let's start setting up the project. To create a new ASP.NET Core 8 Project in Visual Studio 2022:
- Start the Visual Studio 2022 IDE Preview.
- In the Create a new project window, select ASP.NET Core Web API and click Next to move on.
- Specify the project name and the path where it should be created in the “Configure your new project” window.
- If you want the solution file and project to be created in the same directory, you can optionally check the Place solution and project in the same directory checkbox. Click Next to move on.
- In the next screen, specify the target framework as .NET 10.0 (Preview) and authentication type as None from the respective drop-down controls.
- Ensure that the Configure for HTTPS, Enable Docker Support, Do not use top-level statements, and the Enable OpenAPI support checkboxes are unchecked because you won't use any of these in this example.
- Remember to leave the Use controllers checkbox checked because you won't use minimal API in this example.
- Click Create to complete the process.
A new ASP.NET Core Web API project is created. You'll use this project in the sections that follow.
Blazor Enhancements
Blazor is an open-source, modern web framework from Microsoft that empowers developers to create interactive web applications using C# and .NET. It was initially included in .NET 5 and is used for building interactive web applications using C# and .NET.
The QuickGrid
component in ASP.NET Core 10 Blazor now includes a RowClass parameter, as shown in the code snippet given below:
<QuickGrid Items="employee" RowClass="ApplyRowStyle">
<PropertyColumn Property="@(e => e.EmployeeId)" Sortable="true" />
<PropertyColumn Property="@(e => e.FirstName)" Sortable="true" />
<PropertyColumn Property="@(e => e.FirstName)" Sortable="true" />
<PropertyColumn Property="@(e => p.JoiningDate)" Format="yyyy-MM-dd"
Sortable="true" />
</QuickGrid>
Additionally, you can take advantage of the CloseColumnOptionsAsync
method to close the QuickGrid
column options UI, as shown in the following code example:
<QuickGrid @ref="employeeGrid"
Items="employees">
<PropertyColumn Property="@(e => e.City)"
Title="City">
<ColumnOptions>
<input type="search"
@bind="cityFilter"
placeholder="Filter by city"
@bind:after="@(() => employeeGrid.CloseColumnOptionsAsync())" />
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Property="@(e => e.FirstName)"
Title="First Name" />
<PropertyColumn Property="@(e => e.LastName)"
Title="Last Name" />
<PropertyColumn Property="@(e => e.City)"
Title="City" />
</QuickGrid>
If you're using EF Core to bind data to a QuickGrid component in Blazor, add a reference to the Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter
NuGet package. The QuickGrid
component in Blazor is capable of resolving queries that return IQueryable data. To use this capability, invoke the AddQuickGridEntityFrameworkAdapter
on the service collection in the Program.cs
file, as shown in the code snippet below:
builder.Services.AddQuickGridEntityFrameworkAdapter();
This enables you to register an IAsyncQueryExecutor implementation that's compliant with EF Core. Listing 2 shows how to use the QuickGrid
component in ASP.NET Core Blazor to bind data.
Listing 2: Data binding using QuickGrid
@page "/examplegrid"
@using Microsoft.AspNetCore.Components.QuickGrid
<h3>Demonstrating QuickGrid in ASP.NET Core Blazor</h3>
<QuickGrid Items="@people" TGridItem="Employee">
<PropertyColumn Property="@(employee => employee.Id)" Title="ID" />
<PropertyColumn Property="@(employee => employee.FirstName)"
Title="FirstName" />
<PropertyColumn Property="@(employee => employee.LastName)" Title="LastName" />
</QuickGrid>
@code {
public List<Employee> employee = new()
{
new Employee { Id = 1, FirstName = "Steve", LastName = "Smith" },
new Employee { Id = 2, FirstName = "Rex", LastName = "Wills" },
new Employee { Id = 3, FirstName = "Michelle", LastName = "Stevens" }
};
public class Employee {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
With .NET 10, in Blazor web applications, the framework static assets are now preloaded automatically using Link headers, thereby enabling the web browser to load resources ahead of time before the initial page is fetched and rendered. The standalone Blazor WebAssembly application template has been updated to allow prefetching of state framework assets alongside a generated JavaScript import map.
Convert Empty Strings in Form Posts to Null for Nullable Value Types
When working with [Form]
attribute using minimal APIs in ASP.NET Core 10, empty string values in a form post operation are converted to null, as shown in the code snippet below:
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/book",
([FromForm] Book book) => TypedResults.Ok(book));
app.Run();
public class Book
{
public int Id
{
get; set;
}
public DateOnly? PublicationDate
{
get; set;
}
public string Title
{
get; set;
}
}
Refer to the preceding code snippet. The field PublicationDate
will be mapped to null if it's empty.
Support for Accessing an OpenAPI Document Using the IOpenApiDocumentProvider Interface
With .NET 10, ASP.NET Core allows you to inject a reference of type IOpenApiDocumentProvider
using dependency injection. As a result, it's now possible to access an OpenAPI document instance outside the context of an HTTP context, such as, background services or custom middleware. The following piece of code shows how an instance of type IOpenAPiDocumentProvider
is injected using constructor injection.
public class OrderService
(
IOpenAPiDocumentProvider openApiDocumentProvider
)
{
public async Task SomeMethod()
{
OpenApiDocument doc = await
openApiDocumentProvider.GetOpenApiDocumentAsync();
}
}
Improvements to ASP.NET Core OpenAPI Document Generation
ASP.NET Core 10 provides support for generating OpenAPI documents, which is, in turn, aligned with the 2020-12 JSON Schema Draft specification. Although this enhancement makes API documentation better structured and more accurate, there are a few changes introduced in this version. With OpenAPI 3.1, nullable properties use a type keyword in lieu of using nullable : true. Additionally, although you can still use OpenAPI 3.0 for generating your documents, OpenAPI 3.1 is now considered the default version for generated documents henceforth.
You can use the following code to configure OpenAPI version 3.0:
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});
Alternatively, specify the same using MSBuild options:
<OpenApiGenerateDocumentsOptions> --openapi-version OpenApi3_0
</OpenApiGenerateDocumentsOptions>
Another critical change made in OpenAPI 3.1 is replacing OpenApiAny
by JsonNode
. Consider the following piece of code that shows how to use OpenApiObject
in the earlier versions of .NET Core when generating OpenAPI documents:
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.JsonTypeInfo.Type == typeof(Employee))
{
schema.Example = new OpenApiObject
{
["joiningdate"] = new OpenApiString(
DateTime.Now.AddDays(1).ToString("yyyy-MM-dd")),
["comments"] = new OpenApiString("New Employee")
};
}
return Task.CompletedTask;
});
With .NET 10, you can write the following code instead by replacing OpenApiObject
by JsonObject
:
options.AddSchemaTransformer(
(schema, context, cancellationToken) =>
{
if (context.JsonTypeInfo.Type == typeof(Employee))
{
schema.Example = new JsonObject
{
["joiningdate"] = new OpenApiString(
DateTime.Now.AddDays(1).ToString("yyyy-MM-dd")),
["comments"] = new OpenApiString("New Employee"),
};
}
return Task.CompletedTask;
});
Remember to enable XML document generation in your project file to take advantage of this feature. The following code snippet shows how this can be achieved:
<PropertyGroup>
<GenerateDocumentationFile>
true
</GenerateDocumentationFile>
</PropertyGroup>
Another interesting feature in ASP.NET Core 10 is the support for generating OpenAPI documents in YAML format. To configure this in the Program.cs
file, use the following piece of code:
app.MapOpenApi("/openapi/{documentName}.yaml");
With ASP.NET Core 10, the ASP.NET Core Web API (Native AOT) project template provides support for OpenAPI document generation using the
Microsoft.AspNetCore.OpenApi
package by default. To turn off this feature, use the--no-openapi
flag.
Support for JSON Patch Using System.Text.Json
JSON Patch is accepted as a standard for any alterations made to a JSON document. It encapsulates a series of actions (add, remove, replace, move, copy, test) that can be executed to change a JSON document. In web applications, JSON Patch is typically used during PATCH operations for partial attribute updates of a resource. To reduce payload size and improve performance, clients can send a JSON Patch document comprising just the modifications instead of the resource object in its entirety.
You can enable JSON Patch support with System.Text.Json by installing the Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet package onto your project. The JsonPatchDocument<T>
class pertaining to this package can be used to represent a JSON Patch document for instances that are of type T.
Let's understand this with a code example. Consider the following piece of code that shows how an Author
object is defined:
var author = new Author
{
FirstName = "Joydip",
LastName = "Kanjilal",
Address = "Hyderabad, India",
Email = "jkanjilal@gmail.com"
};
A typical JSON Patch document will look like this:
string jsonPatch = """
[
{ "op": "replace", "path": "/Address", "value": "Hyderabad" }
]""";
Next, deserialize the JSON patch document, as shown in the following piece of code:
var deserializedDocument =
JsonSerializer.Deserialize<JsonPatchDocument<Author>>(jsonPatch);
Finally, apply the JSON patch document as shown in the code snippet given below:
deserializedDocument!.ApplyTo(author);
Enhanced Support for OpenAPI XML Documentation
With .NET 10, ASP.NET Core provides enhanced support for OpenAPI XML documentation comments. Configuring XML document processing to document types outside the current assembly is now possible.
The following code snippet illustrates how to include XML comments for types in Microsoft.AspNetCore.Http assembly:
<Target Name="AddOpenApiDependencies" AfterTargets="ResolveReferences">
<ItemGroup>
<!-- Write your XML documentation here -->
<AdditionalFiles
Include="@(ReferencePath->'%(RootDir)%(Directory)%(Filename).xml')"
Condition="'%(ReferencePath.Filename)' ==
'Microsoft.AspNetCore.Http.Abstractions'"
KeepMetadata="Identity;HintPath" />
</ItemGroup>
</Target>
Support for Generating OpenAPI Schemas in Transformers
With .NET 10, you can create schemas for C# types and include them in the OpenAPI document. As a result, it will be possible to reference the schema from anywhere in the OpenAPI document. The context for document, operation, and schema transformers now contains the GetOrCreateSchemaAsync
method.
You can use this method to create a schema for a given type. It also provides an optional ApiParameterDescription
parameter to add details to the created schema. To take advantage of this feature in a document, operation, or schema transformer, you just have to generate the schema using the GetOrCreateSchemaAsync
method and include it in the OpenAPI document by calling the AddComponent
method. The following piece of code illustrates how this can be achieved:
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info = new()
{
Title = "Order Processing API",
Version = "v1.0",
Description = "API for processing orders."
};
return Task.CompletedTask;
});
});
Support for JavaScript Interop for Invoking Constructors and Properties
With .NET 10, you can leverage the newly added InvokeNewAsync
, GetValueAsync
, and SetValueAsync
methods to invoke JavaScript constructors and properties from your .NET Core application. The newly added interfaces include the IJSRuntime and IJSObjectReference for asynchronous calls, and the IJSInProcessRuntime
and IJSInProcessObjectReference
interfaces for synchronous calls.
The code snippet given below shows how an instance of a JavaScript class is created and then its properties and methods are called using this instance:
window.MyClass = class {
constructor(text) {
this.text = text;
}
getLength() {
return this.text.length;
}
}
var obj = await JSRuntime.InvokeNewAsync("jsInterop.MyClass",
"Calling a JavaScript method from Blazor");
var text = await obj.GetValueAsync<string>("text");
var length = await obj.InvokeAsync<int>("getLength");
Support for Validation in Minimal APIs
In the previous versions of ASP.NET Core, there was no support for a dedicated validation API. With ASP.NET Core 10, you can now validate the data sent to your minimal API endpoints. This feature enables the runtime to automatically validate query parameters, header information, and the request body. You can now define validation rules using DataAnnotations attributes or through custom validation logic.
I often use DataAnnotations attributes to decorate business model types, i.e., classes, structs, or record types. Refer to the following code snippet that illustrates how you can take advantage of these attributes to implement validation logic:
public class Employee
{
[Required]
[StringLength(50)]
public string FirstName;
[Required]
[StringLength(50)]
public string LastName;
}
You can define the validations you need for your minimal API endpoints using attributes. There are two ways in which you can implement validations in your minimal APIs in ASP.NET Core 10: by creating your custom validator or implementing the IValidatableObject
interface if the validation logic is complex. To add validation support in minimal APIs, you should call the AddValidation
extension method in the Program.cs
file, as shown in the code snippet below:
builder.Services.AddValidation();
The following code snippet shows how to validate your model object in a HttpPOST
method in minimal APIs:
app.MapPost("/create", ([FromBody] Employee employee)
{
// Write your custom code
// to validate the employee object
return Results.Ok(employee);
});
You can also implement custom validators by extending the AbstractValidator
class as shown below:
public class EmployeeValidator : AbstractValidator<Employee>
{
public EmployeeValidator()
{
RuleFor(e => e.Code).MaximumLength(5);
RuleFor(e => e.FirstName).NotNull();
RuleFor(e => e.LastName).NotNull();
}
}
The following code snippet shows how to use the custom validator in your minimal API:
app.MapPost("/create", async ([FromBody] Employee employee,
[FromServices] IValidator<Employee> validator) =>
{
var validationResult = await validator.ValidateAsync(employee);
if (!validationResult.IsValid)
{
return Results.BadRequest(validationResult.Errors);
}
// Validation is successful,
// so proceed as usual
});
You can now take advantage of this support for validation in minimal APIs to validate data defined in the Query, Header, and the Request body. To disable validation for a specific minimal API endpoint, use the DisableValidation
extension method, as shown in the code snippet below:
app.MapPost("/addemployee", async (Employee employee)
{
// Process request without
// validation of the model
return Results.Ok(
new
{
message = "Received with validation disabled.", employee
}
);
}).DisableValidation();
With .NET 10, similar to class types, you can also validate record type parameters in your minimal APIs.
Enhanced Support for Server-Sent Events (SSE)
With ASP.NET Core 10, you can take advantage of the TypedResults.ServerSentEvents
API to return ServerSentEvents result. The following code snippet illustrates how this can be achieved:
app.MapGet("/getall", (CancellationToken cancellationToken) =>
{
return TypedResults.ServerSentEvents(GetAllEmployee(
cancellationToken), eventType: "getAll");
});
This feature is available for both minimal APIs and controller-based APIs in ASP.NET Core 10.
Support for Specifying Response Description for Your API Controllers
With ASP.NET Core 10, the ProducesAttribute
, ProducesResponseTypeAttribute
, and ProducesDefaultResponseType
attributes now accept an optional string parameter named Description
. Use it to specify the description of the response as shown in the code snippet below:
[HttpGet(Name = "GetAllEmployee")]
[ProducesResponseType<IEnumerable<Employee>>(StatusCodes.Status200OK,
Description = "This endpoint returns all employee records.")]
public IEnumerable<Employee> GetAllEmployee()
{
// Write your code here to
// return all employee records
}
Authentication and Authorization Enhancements
With ASP.NET Core 10, support for additional metrics has been added. This will help obtain metrics for each of the events listed here.
Authentication:
- Request duration
- Challenge count
- Forbid count
- Sign in count
- Sign out count
Authorization:
- Number of requests that need authorization
RedirectHttpResult.IsLocalUrl Helper Method
With ASP.NET Core 10, you can determine whether a URL is local using the RedirectHttpResult.IsLocalUrl(url)
helper method, as shown in the code snippet below:
if (RedirectHttpResult.IsLocalUrl(url))
{
return Results.LocalRedirect(url);
}
New Features in WPF 10
Windows Presentation Foundation, (WPF) is a flexible user interface framework developed by Microsoft and included as part of .NET Framework 3.0 initially. With WPF 10, several enhancements and bug fixes are primarily focused on improving performance and fluent style support. In this release, fluent styles have been added for controls such as Label, Hyperlink, GroupBox, GridSplitter, and NavigationWindow.
Performance enhancements have been achieved by optimizing cache operations, moving the font collection loader to managed code, and improving array handling. Besides this, several bug fixes and changes have been made that can enhance the framework's functionality and developer experience.
New Features in Windows Forms 10
Windows Forms (also known as WinForms) in .NET 10 comes up with several enhancements aimed at improving developer productivity, modernizing the design experience, and enhancing interoperability. WinForms and Windows Presentation Foundation (WPF) now share a common clipboard implementation, streamlining data exchange between the two frameworks. There have been several enhancements to improve compatibility with screen readers like NVDA.
Several new methods have been added to boost JSON-based serialization and phase out the obsolete BinaryFormatter. These advancements enhance security and modernize the Windows Forms applications and make it aligned with Microsoft's objectives of enhancing security and maintainability.
Take-Aways
.NET 10 aims to improve stability and speed and lessen the abstraction overhead caused by C#'s growing high-level features. The introduction of a plethora of new features and enhancements help make C# a more expressive, flexible, and efficient programming language. Incidentally, .NET 10 is a long-term release with significant improvements to the runtime, language features, and class libraries, as well as a three-year support window.
Although introducing these new features and enhancements will help businesses and developers build flexible, manageable, and efficient applications, you must consider the performance, scalability, and security benefits before choosing a particular feature. The best way to get started is analyzing the features, benchmarking performance, and then applying them as appropriate in your applications.