Introduction
We've all inherited codebases where constructors chain for ten parameters, where JSON strings live in constants with escaped quotes, or where simple auto-properties need hidden backing fields just to add a line of validation. C# has been quietly solving these problems for the last few releases. The C# features worth adopting now: best practices explained in this post represent real pain points that senior teams face every day—and they're either already available in your production environment or coming very soon. We'll walk through each one with concrete examples and show you where they fit into typical e-commerce, SaaS, and API codebases.
Collection expressions (C# 12) — One syntax for arrays, lists and spans
Before C# 12, initializing collections meant reaching for different syntax depending on the target type. You'd use array initializers, collection initializers, or reach for LINQ. Now we have a single, consistent expression syntax that works for arrays, lists, spans, and even custom collection types.
In real production code, this matters most when you're building test fixtures or seeding default data. Instead of:
var userIds = new List { 101, 102, 103, 104 };
var productSkus = new string[] { "WIDGET-A", "GADGET-B", "TOOL-C" };
var prioritySpan = new Span(new[] { 1, 2, 3, 4 });You now write:
List userIds = [101, 102, 103, 104];
string[] productSkus = ["WIDGET-A", "GADGET-B", "TOOL-C"];
Span prioritySpan = [1, 2, 3, 4];The compiler infers the target type from context. This also works beautifully when spreading existing collections:
var existingIds = new[] { 1, 2, 3 };
var allIds = [..existingIds, 4, 5, 6]; // Result: [1, 2, 3, 4, 5, 6]In a ride-hailing or travel platform where you're often combining route segments, waypoints, or booking IDs from multiple sources, this pattern appears constantly. The spread operator paired with collection expressions cuts down on intermediate variables and temporary allocations.
Required members — Enforce initialization without giant constructors
Required members landed in C# 11 and remain underused. Instead of building a constructor that takes fifteen parameters, mark properties as required. The compiler then forces callers to set them during object initialization.
Consider an order entity in an e-commerce system:
public class Order
{
public required int OrderId { get; init; }
public required int CustomerId { get; init; }
public required DateTime CreatedAt { get; init; }
public required List LineItems { get; init; }
public decimal ShippingCost { get; init; } // Optional
}
// Usage
var order = new Order
{
OrderId = 5001,
CustomerId = 42,
CreatedAt = DateTime.UtcNow,
LineItems = [/* items */]
// ShippingCost can be omitted
};
// This fails to compile if CreatedAt is missing
var incomplete = new Order { OrderId = 5001 }; // ErrorThe power here is that you avoid overloaded constructors and factories while maintaining compile-time safety. When paired with init properties (read-only after construction), you get immutability without boilerplate. Teams working on CRM systems with complex entity hierarchies find this especially valuable—payment details, contact info, and address records all benefit from required initialization.
Raw string literals — JSON and SQL without escape hell
Raw string literals, introduced in C# 11, use triple quotes and treat everything inside literally. No need to escape backslashes, quotes, or braces. This is life-changing for embedded JSON schemas, SQL queries, and GraphQL documents.
Before:
string jsonSchema = "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}";
string sqlQuery = "SELECT * FROM orders WHERE status = 'PENDING' AND created_at > '2024-01-01'";After:
string jsonSchema = """
{
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
""";
string sqlQuery = """
SELECT *
FROM orders
WHERE status = 'PENDING'
AND created_at > '2024-01-01'
""";
In trading platforms and real-time data pipelines, raw strings let you keep complex query logic legible. You can also include leading whitespace and it's preserved, making indentation match your code style. For configuration-as-code scenarios or when you're embedding templates, this removes an entire class of escaping bugs.
The field keyword (C# 14) — Add logic to auto-properties without backing fields
C# 14 introduces the field keyword, which lets you reference a compiler-generated backing field inside property accessors without explicitly declaring it. This sounds small, but it enables validation and side effects on simple properties without the verbosity of full property declarations.
Before C# 14, if you needed to add a setter with validation:
public class Product
{
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (value < 0) throw new ArgumentException("Price cannot be negative");
_price = value;
}
}
}
With C# 14 and the field keyword:
public class Product
{
public decimal Price
{
get => field;
set
{
if (value < 0) throw new ArgumentException("Price cannot be negative");
field = value;
}
}
}
This is particularly useful in SaaS platforms where entities carry business rules—discount percentages must stay within bounds, email addresses must be validated, inventory quantities can't go negative. You keep your code clean and readable while embedding domain logic directly in properties.
Extension members (C# 14) — Properties and static members, not just methods
C# 11 and earlier limited extensions to methods. C# 14 expands the concept to include extension properties and extension static members, opening new patterns for fluent APIs and utility layers.
A common use case in distributed systems: extending a span or queue to check common conditions:
public static class CollectionExtensions
{
public static bool HasItems(this List source) => source.Count > 0;
// Now with C# 14, also extension properties
public static int SafeCount(this List? source)
{
get => source?.Count ?? 0;
}
}
// Usage
if (orders.SafeCount > 0)
{
// process
}
In practice, extension properties let you add computed or cached properties to third-party types or sealed types without inheritance. For a ride-hailing app, you might extend a Location type with distance calculations or cached geocoding results. For payment processing, you might extend a transaction record with formatted currency display or audit trail helpers—all without touching the original class definition.
Common mistakes
Overusing required on optional fields
Mark a property as required only if the entity is genuinely incomplete without it. Shipping address in an order? Required. Promo code? Optional. Marking everything as required defeats the purpose and creates friction during object construction for legitimate scenarios like API responses that include sparse data.
Raw strings in code review friction
Raw strings are readable during development but can surprise reviewers if indentation changes the intent. Always think about whether leading whitespace matters for your use case. For JSON payloads parsed by a library, indentation is stripped—no problem. For SQL templates that might preserve whitespace in string concatenation, be careful.
Forgetting that collection expressions require C# 12
If your team targets .NET Framework 4.8 or older .NET versions, collection expressions won't compile. Check your LangVersion in the project file. Upgrade your baseline if possible, but don't break CI with syntax that your build environment doesn't support.
Extension properties on interfaces
Extension properties work on sealed classes and structs, but don't expect to extend interface contracts—that's not their purpose. Use them for concrete types where you're adding convenience, not for architectural extension points.
Key takeaways
- Collection expressions (C# 12) unify syntax across arrays, lists, and spans, reducing friction when initializing or combining collections.
- Required members enforce initialization at compile time without constructor overload chains, keeping entity definitions clean and safe.
- Raw string literals eliminate escaping hell for JSON, SQL, and embedded templates, improving readability and reducing bugs.
- The field keyword (C# 14) lets you add validation and logic to simple properties without explicit backing fields, keeping code concise.
- Extension members (C# 14) expand beyond methods to include properties and static members, enabling fluent utilities and computed values on external types.
- Each feature solves a real pain point in production codebases—adopt them where they reduce boilerplate without adding complexity to your team's mental model.






Comments
Loading comments…