C# 12 (released November 2023, shipped with .NET 8) introduced several features that reduce boilerplate for the patterns most common in ASP.NET Core. Primary constructors on classes and structs extend the convenience previously reserved for records to all type declarations — particularly useful for service classes that receive dependencies via constructor injection. Collection expressions unify the syntax for creating all collection types. Together these features make ASP.NET Core service and controller code noticeably cleaner without changing any runtime semantics.
Primary Constructors on Classes (C# 12)
// ── Traditional constructor injection boilerplate ─────────────────────────
public class PostService
{
private readonly IPostRepository _repo;
private readonly IEmailSender _email;
private readonly ILogger<PostService> _logger;
public PostService(
IPostRepository repo,
IEmailSender email,
ILogger<PostService> logger)
{
_repo = repo;
_email = email;
_logger = logger;
}
}
// ── Primary constructor (C# 12) — eliminates the boilerplate ─────────────
public class PostService(
IPostRepository repo,
IEmailSender email,
ILogger<PostService> logger)
{
// Parameters are directly available throughout the class
public async Task<Post> GetByIdAsync(int id, CancellationToken ct = default)
{
logger.LogInformation("Fetching post {Id}", id);
return await repo.GetByIdAsync(id, ct)
?? throw new NotFoundException(nameof(Post), id);
}
public async Task PublishAsync(int id, string authorEmail)
{
var post = await GetByIdAsync(id);
post.IsPublished = true;
await repo.UpdateAsync(post);
await email.SendAsync(authorEmail, "Your post is live!", $"'{post.Title}' is published.");
}
}
// Note: if you need to store a parameter as a field (e.g., for mutation),
// still declare the field explicitly:
public class CachedPostService(IPostRepository repo, IMemoryCache cache)
{
private readonly IMemoryCache _cache = cache; // explicit field for cache
// repo is used directly as the primary constructor parameter
}
init properties.ArgumentNullException.ThrowIfNull(repo), put that in an explicit constructor body. The sweet spot for primary constructors is simple services with 2–4 injected dependencies and no validation — exactly the most common pattern in ASP.NET Core service layers.Collection Expressions (C# 12)
// ── Unified syntax for creating collections with [element, element, ...] ──
int[] array = [1, 2, 3, 4, 5];
List<string> list = ["alpha", "beta", "gamma"];
ImmutableArray<int> immutable = [10, 20, 30];
Span<byte> span = [0xFF, 0xFE, 0x00];
// Spread operator .. — include elements from another collection
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] merged = [..first, ..second]; // [1, 2, 3, 4, 5, 6]
int[] prepend = [0, ..first]; // [0, 1, 2, 3]
int[] append = [..first, 7, 8]; // [1, 2, 3, 7, 8]
// In method calls
ProcessItems([1, 2, 3]); // passes a ReadOnlySpan<int> or List<int> depending on param type
SetTags(["dotnet", "csharp"]); // IEnumerable<string>, List<string>, or string[]
Using Aliases for Complex Types (C# 12)
// using alias for complex generic types — C# 12 extends this to any type
using StringDictionary = Dictionary<string, string>;
using PostLookup = Dictionary<int, List<Post>>;
using ValidationErrors = Dictionary<string, string[]>;
using UserIdSet = HashSet<string>;
// Use the alias anywhere in the file
StringDictionary config = new();
config["key"] = "value";
ValidationErrors errors = new()
{
["Title"] = ["Title is required", "Title max 200 chars"],
["Body"] = ["Body cannot be empty"],
};
Common Mistakes
Mistake 1 — Expecting primary constructor parameters to be stored as fields automatically
❌ Wrong — the parameter may not be a field; assigning to it does not persist:
public class Service(IRepository repo)
{
public void Replace(IRepository newRepo) { repo = newRepo; } // does NOT update a stored field!
}
✅ Correct — declare an explicit private field if you need mutation.
Mistake 2 — Confusing collection expressions with array literals (they look similar)
Collection expressions [1, 2, 3] adapt to the target type — they can produce arrays, lists, spans, or immutable collections. Old array literals new[] { 1, 2, 3 } always produce arrays. Prefer collection expressions in modern code; use new[] only when array type is specifically required.