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

Connect to Dynamics 365 CRM Online using a client ID and secret in a C# console app


1. Prerequisites

  • Install the Dynamics 365 SDK assemblies. You can install the necessary NuGet packages, such as:
  • Microsoft.CrmSdk.CoreAssemblies
  • Microsoft.CrmSdk.XrmTooling.CoreAssembly
  • Register your app in Azure Active Directory (AAD) to retrieve the client ID, client secret, and tenant ID.

2. Code Implementation

The following code demonstrates how to authenticate and interact with Dynamics 365 CRM using the CRM SDK:

using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Tooling.Connector;

class Program
{
    static void Main(string[] args)
    {
        string clientId = "Your_Client_ID";
        string clientSecret = "Your_Client_Secret";
        string tenantId = "Your_Tenant_ID";
        string crmUrl = "https://Your_CRM_Organization.crm.dynamics.com/";

        // Create connection string
        string connectionString = $@"
            AuthType=ClientSecret;
            ClientId={clientId};
            ClientSecret={clientSecret};
            TenantId={tenantId};
            Url={crmUrl};";

        // Establish connection
        CrmServiceClient serviceClient = new CrmServiceClient(connectionString);

        if (serviceClient.IsReady)
        {
            Console.WriteLine("Connected to CRM successfully!");

            // Example: Retrieve accounts
            IOrganizationService service = serviceClient.OrganizationServiceProxy;
            QueryExpression query = new QueryExpression("account")
            {
                ColumnSet = new ColumnSet("name", "accountnumber")
            };

            EntityCollection results = service.RetrieveMultiple(query);

            foreach (var entity in results.Entities)
            {
                Console.WriteLine($"Account Name: {entity.GetAttributeValue<string>("name")}, Account Number: {entity.GetAttributeValue<string>("accountnumber")}");
            }
        }
        else
        {
            Console.WriteLine($"Failed to connect: {serviceClient.LastCrmError}");
        }
    }
}

3. Explanation

  • Authentication: The connection string uses AAD authentication with the client ID, client secret, and tenant ID.
  • Connection: CrmServiceClient establishes a connection to Dynamics 365.
  • Query: The QueryExpression retrieves data from the CRM, such as accounts in this example.

Note : This article was created with assistance from AI and there could be mistakes / error