C# 14 Is Here: Everything You Need to Know (With Code Examples)

Microsoft has shipped C# 14 alongside .NET 10, and it’s packed with quality-of-life improvements that make everyday code cleaner, safer, and more expressive. Let’s walk through every major feature — with fresh, practical examples you can try right now.


1. Extension Members — Properties, Statics, and Operators on Any Type

C# has long supported extension methods. C# 14 takes it much further with the new extension block — you can now add properties, static members, and even operators to types you don’t own.

Imagine you work with strings a lot and want a clean .IsValidEmail check, a .WordCount property, and the ability to repeat a string with *:

public static class StringExtensions
{
extension(string text)
{
// Extension property — use it like: email.IsValidEmail
public bool IsValidEmail => text.Contains('@') && text.Contains('.');
// Extension property — use it like: sentence.WordCount
public int WordCount => text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
extension(string)
{
// Static extension property — use it like: string.Placeholder
public static string Placeholder => "N/A";
// User-defined operator — use it like: "hello" * 3 => "hellohellohello"
public static string operator *(string text, int times) =>
string.Concat(Enumerable.Repeat(text, times));
}
}
// Usage:
string email = "dev@example.com";
Console.WriteLine(email.IsValidEmail); // True
Console.WriteLine("hello world".WordCount); // 2
Console.WriteLine(string.Placeholder); // N/A
Console.WriteLine("ping! " * 3); // ping! ping! ping!

This is a game-changer for library authors — your APIs can now feel like true language primitives.


2. The field Keyword — Say Goodbye to Backing Fields

Every C# developer has written a property with validation that forced them to declare a private backing field. The new field keyword lets the compiler generate it for you.

Here’s a UserProfile class that enforces age and username rules — no manual backing fields needed:

public class UserProfile
{
// Compiler synthesizes the backing field automatically
public string Username
{
get;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Username cannot be blank.")
: value.Trim().ToLower();
}
public int Age
{
get;
set => field = (value is < 0 or > 150)
? throw new ArgumentOutOfRangeException(nameof(Age), "Age must be between 0 and 150.")
: value;
}
}
// Usage:
var profile = new UserProfile();
profile.Username = " Alice "; // stored as "alice"
profile.Age = 30; // fine
profile.Age = 200; // throws ArgumentOutOfRangeException

Cleaner classes, less noise — and full control over validation logic without the ceremony.


3. Null-Conditional Assignment — Assign Only When It Makes Sense

The ?. operator has been a null-safety staple for reading values. In C# 14, you can use it on the left side of an assignment too — so the right side only executes when the target isn’t null.

Consider an e-commerce scenario where you conditionally update a shopping cart:

public class Cart
{
public string PromoCode { get; set; }
public List<string> Items { get; set; } = new();
public decimal Discount { get; set; }
}
Cart activeCart = GetActiveCart(); // might return null
// Old way — manual null guard required:
if (activeCart != null)
{
activeCart.PromoCode = "SAVE20";
activeCart.Discount += 20m;
}
// C# 14 — clean and concise:
activeCart?.PromoCode = "SAVE20";
activeCart?.Discount += 20m; // compound assignment works too!
// The promo code lookup won't even run if activeCart is null:
activeCart?.PromoCode = FetchBestPromoFromApi();

Notice the last line — FetchBestPromoFromApi() is not called at all if activeCart is null. That’s a real benefit when those calls are expensive.


4. Implicit Span Conversions — High-Performance Code, Less Friction

Span<T> and ReadOnlySpan<T> are the go-to tools for zero-allocation data processing in .NET. C# 14 introduces implicit conversions so you can pass arrays where spans are expected — and vice versa — without manual casting.

// A method that processes data efficiently using ReadOnlySpan
static double AverageTemperature(ReadOnlySpan<double> readings)
{
double sum = 0;
foreach (var temp in readings) sum += temp;
return sum / readings.Length;
}
double[] dailyReadings = { 22.5, 23.1, 21.8, 24.0, 22.9 };
// C# 14: the array converts implicitly — no .AsSpan() call needed
double avg = AverageTemperature(dailyReadings);
Console.WriteLine($"Average: {avg:F1}°C"); // Average: 22.9°C
// Span and ReadOnlySpan also compose more naturally with generics
ReadOnlySpan<char> greeting = "Hello, World!";
Span<char> buffer = new char[greeting.Length];
greeting.CopyTo(buffer);

Less ceremony around high-performance code means it’s easier to adopt these patterns throughout your codebase.


5. nameof with Unbound Generics — Cleaner Type Names

Previously, nameof required a closed generic type like nameof(Dictionary<string, int>) just to get back "Dictionary". In C# 14, you can pass the unbound form directly.

// Before C# 14 — you had to supply dummy type arguments:
string name1 = nameof(Dictionary<string, int>); // "Dictionary"
string name2 = nameof(List<object>); // "List"
// C# 14 — just use the unbound form:
string name3 = nameof(Dictionary<,>); // "Dictionary"
string name4 = nameof(List<>); // "List"
string name5 = nameof(Func<,,>); // "Func"
// Real-world use: building a generic cache key or log label
public static string CacheKey<T>() => $"cache:{nameof(List<>)}:{typeof(T).Name}";
Console.WriteLine(CacheKey<int>()); // cache:List:Int32
Console.WriteLine(CacheKey<string>()); // cache:List:String

6. Lambda Parameters with Modifiers — No Full Types Required

When a lambda parameter needed a modifier like ref or out, you previously had to write the full type for every parameter, even the ones you didn’t care about. Not anymore.

// A delegate for a try-parse style conversion
delegate bool TryConvert<TIn, TOut>(TIn input, out TOut result);
// Before C# 14 — all parameter types had to be spelled out:
TryConvert<string, int> parseOld = (string text, out int result) =>
int.TryParse(text, out result);
// C# 14 — just add the modifier, skip the type:
TryConvert<string, int> parseInt = (text, out result) => int.TryParse(text, out result);
TryConvert<string, bool> parseBool = (text, out result) => bool.TryParse(text, out result);
// Works great for inline swap utilities with ref params:
delegate void Swapper<T>(ref T a, ref T b);
Swapper<int> swap = (ref a, ref b) => (a, b) = (b, a);
int x = 10, y = 20;
swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}"); // x=20, y=10

7. Partial Constructors and Events — Better Source Generator Support

Source generators are a cornerstone of modern .NET tooling (think: EF Core, SignalR, logging). C# 14 extends the partial keyword to constructors and events, making generator-heavy classes much easier to split between generated and hand-written code.

// File: OrderProcessor.cs (your hand-written code)
public partial class OrderProcessor
{
// Defining declaration — no body, no initializer
public partial OrderProcessor(string region);
public void Process(Order order) => Console.WriteLine($"Processing in {_region}");
}
// File: OrderProcessor.Generated.cs (source generator output)
public partial class OrderProcessor
{
private readonly string _region;
private readonly ILogger _logger;
// Implementing declaration — has the body and base/this call
public partial OrderProcessor(string region) : base()
{
_region = region;
_logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger<OrderProcessor>();
_logger.LogInformation("OrderProcessor created for region: {Region}", region);
}
}

Your hand-written file stays clean and readable. The generated file handles all the plumbing. No mismatch, no duplication.


8. User-Defined Compound Assignment Operators

Custom types can now define their own behavior for compound operators like +=, -=, and *=. Previously this was inferred from the binary operator; now you can override it directly for full control.

public struct Budget
{
public decimal Amount { get; private set; }
public Budget(decimal amount) => Amount = amount;
// Standard addition
public static Budget operator +(Budget a, Budget b) => new(a.Amount + b.Amount);
// User-defined compound: += can now enforce a cap rule
public static Budget operator +=(Budget current, Budget extra)
{
var newAmount = current.Amount + extra.Amount;
return new Budget(Math.Min(newAmount, 100_000m)); // cap at 100k
}
public override string ToString() => $"${Amount:N0}";
}
var budget = new Budget(90_000m);
budget += new Budget(15_000m); // would be 105k — capped to 100k
Console.WriteLine(budget); // $100,000

Getting Started with C# 14 Today

All of these features are available now. To try them out:

C# 14 continues the language team’s philosophy of removing friction without adding complexity. Each of these features solves a real daily annoyance — and the cumulative effect on code readability is significant.

Source: What’s new in C# 14 — Microsoft Learn

Dataverse Long-Term Data Retention

Mastering Data verse Long-Term Data Retention: A Quick Guide

Managing database growth in Microsoft Dataverse just got easier with long-term data retention policies. Instead of permanently deleting your historical “cold” data or letting it bloat your active environment, you can securely archive it.

Here is everything you need to know to set up a data retention policy in Dataverse.

📋 Phase 1: The Prerequisites

Before you can schedule a retention policy, you need to prepare your tables and data criteria.

  • Enable the Parent Table: Go to the properties of your target table in Power Apps, expand Advanced options, and check Enable long term retention.
  • Note: Turning this on automatically enables all related child tables (like notes or tasks). It takes about 15–30 minutes to activate.
  • Create a Criteria View: Dataverse uses standard system views to determine which records get archived.
  • Best Practice: Build a dedicated view (e.g., “Cases Closed Before 2015”). Microsoft strongly recommends testing this view with a TOP N statement (like TOP 10) first to ensure your query pulls exactly what you expect before applying it to millions of rows.

⚙️ Phase 2: Setting Up the Policy

Once your tables are enabled and your view is ready, an administrator can deploy the policy:

  1. Sign into Power Apps and navigate to Retention policies on the left menu.
  2. Select New retention policy.
  3. Fill out the required details:
  • Table: Select your root parent table.
  • Name: Give your policy a clear name.
  • Criteria: Choose the Dataverse view you tested earlier.
  • Schedule & Frequency: Choose a start date and set how often it should run (Once, Daily, Weekly, Monthly, or Yearly).
  1. Hit Save.

⚠️ Critical Gotchas & Limitations

Before you hit go, keep these unique platform behaviors in mind:

  • It’s a One-Way Street: Once data is moved to long-term storage, it cannot be moved back to the active data store.
  • API Limits Apply: Running retention policies counts against your Microsoft Power Platform API request allocations.
  • Background Throttling: These policies are treated as low priority by the platform to avoid slowing down your active apps and flows. As a result, a single policy run can take 72 to 96 hours, regardless of the data volume.
  • Known Timeout Issue: If a parent table has a massive cascade chain (25+ child tables), the process might time out. The workaround is to manually enable a few child tables for retention first, then enable the parent table.