Skip to main content

Command Palette

Search for a command to run...

C# Primary Constructors vs Traditional Constructors in .NET: Which Should Your Team Use in 2026?

Updated
โ€ข11 min read
C# Primary Constructors vs Traditional Constructors in .NET: Which Should Your Team Use in 2026?

C# primary constructors, introduced in C# 12, have quietly become one of the most debated syntax choices in enterprise .NET teams. The pitch is compelling: fewer lines, less boilerplate, cleaner service classes. But the controversy is real โ€” mutable capture semantics, readability trade-offs, and a very different mental model from what most .NET developers learned first. If your team is deciding whether to adopt primary constructors across an ASP.NET Core codebase, the answer depends on where and how you use them, not on a blanket rule.

The complete patterns and trade-off comparisons covered in this article are explored inside a production-ready codebase on Patreon โ€” with real service classes, handler implementations, and annotated examples showing exactly when each approach pays off in a working ASP.NET Core API.

Understanding how constructors fit into clean, layered code is also a core theme in Chapter 11 of the ASP.NET Core Web API: Zero to Production course โ€” which covers Clean Architecture with CQRS and MediatR, where constructor design choices directly affect how handlers, repositories, and domain services wire together.

ASP.NET Core Web API: Zero to Production

What Are Primary Constructors in C#?

Primary constructors let you declare constructor parameters directly on the class declaration, making them available throughout the type body without requiring explicit field declarations. They've existed for records since C# 9, but C# 12 extended them to classes and structs.

For a service class with injected dependencies, a primary constructor collapses what used to be four or five lines of boilerplate field declaration, constructor signature, and field assignment into a single line at the class declaration.

In ASP.NET Core, the most common use case is constructor injection. The DI container resolves the dependencies and passes them to the constructor โ€” and with primary constructors, that process is identical at the container level. The difference is entirely in how you write and read the class definition.

The Core Difference: How Each Approach Wires Dependencies

With a traditional constructor, you explicitly declare private readonly fields, write a constructor that accepts parameters, and assign each parameter to its corresponding field. The result is verbose but explicit: every dependency has a visible, named, immutable home in the type.

With a primary constructor, the parameters are declared on the class line and captured implicitly. You reference them directly in the class body โ€” no field assignment needed. The parameter name itself becomes your reference throughout the type.

The critical difference that trips up enterprise teams: primary constructor parameters are captured as mutable variables, not readonly fields. If you reassign a primary constructor parameter inside the class body, the compiler will not stop you. Traditional constructors with private readonly fields enforce immutability at compile time.

Side-by-Side Comparison

Dimension Primary Constructor Traditional Constructor
Verbosity Lower โ€” no explicit field declarations Higher โ€” requires field + assignment per dependency
Immutability enforcement Not enforced โ€” parameters are mutable Enforced โ€” private readonly prevents reassignment
Readability in large classes Can obscure which parameters are used where Each field is explicitly visible at the top
Debugger experience Parameters may not appear as clearly in locals Fields appear with their assigned values
Tool support (Roslyn analyzers) Improving โ€” still less mature than field-based patterns Fully supported โ€” mature tooling ecosystem
XML doc integration Cannot document primary constructor parameters with <param> docs on the class Can document constructor parameters on the constructor declaration
Suitability for record types Natural fit โ€” records were designed for this Not applicable โ€” records favour primary constructor syntax
Risk in refactoring Higher โ€” accidental reassignment is silent Lower โ€” readonly fields catch reassignment at compile time
DI container compatibility Identical โ€” no difference at runtime Identical โ€” no difference at runtime

When Primary Constructors Are the Right Choice

Record types and immutable DTOs. Primary constructors were designed for records. If you are writing a result type, a request model, or a value object, primary constructors are natural and idiomatic. There is no meaningful downside here.

Simple, focused service classes. If a class has one or two dependencies, does one job, and is unlikely to grow significantly, primary constructors reduce noise without adding risk. Small query handlers, simple validators, and adapter classes are good candidates.

Test classes. When using a framework like xUnit, primary constructors on test classes clean up test fixture setup considerably. The risk of accidental reassignment is low in test contexts, and the readability gain is real.

Teams with strong Roslyn analyser coverage. If your team runs a linter or analyser that flags primary constructor parameter reassignment (such as rules from Roslynator or JetBrains Rider/ReSharper), the safety concern diminishes significantly.

When Traditional Constructors Are the Right Choice

Service classes with three or more dependencies. When a class grows, the implicit capture model of primary constructors makes it harder to see at a glance which dependencies a type holds. A traditional constructor with declared fields gives any developer an immediate inventory of what the class depends on.

Classes where immutability matters explicitly. Any service, repository, or domain object where you want the compiler to enforce that injected dependencies cannot be replaced mid-object-lifetime should use private readonly fields. This is a non-trivial safety property in long-lived singleton services.

Public or protected classes in library or SDK code. If external consumers can inherit from your class, the implicit parameter capture model can confuse inheriting developers. Traditional constructors with clearly documented parameters are the safer public API surface.

Classes with complex initialization logic. If your constructor does more than simple field assignment โ€” configuring options, validating preconditions, or initialising derived state โ€” a traditional constructor body is cleaner and more intentional.

Teams working across a mix of C# versions. If parts of your codebase target older runtimes or contributors use older tooling that has limited primary constructor support in editors and analysers, consistency may favour staying with traditional constructors project-wide.

The Mutable Capture Problem in Detail

The single biggest risk with primary constructors in service classes is the mutable capture semantics. In a traditional constructor, once you write _service = service, the field is readonly and cannot be overwritten. The compiler enforces this.

With a primary constructor, the parameter service is not backed by a readonly field. You can write service = null; inside any method body and the compiler will accept it silently. The DI container still injects correctly. The mutable capture only becomes a problem if someone accidentally or intentionally reassigns the parameter โ€” but in large codebases with many contributors, silent mutability is a footgun.

Teams that adopt primary constructors for ASP.NET Core services should pair them with a Roslyn analyser rule that flags any reassignment of primary constructor parameters in non-record class bodies.

Does It Affect Performance?

No. Primary constructors and traditional constructors compile to identical IL in practice for simple injection scenarios. The JIT sees the same constructor invocation, the same field storage (even if the C# source doesn't show it explicitly), and the same method calls. There is no performance argument for either approach.

What Does Microsoft's Own Codebase Do?

Microsoft's ASP.NET Core team has adopted primary constructors selectively in .NET 9 and .NET 10 source. The pattern appears most in simple internal service implementations, test helpers, and record-like types. The more complex services in the framework โ€” those with several dependencies and complex lifecycles โ€” continue to use traditional constructors with explicit field declarations.

This mirrors the practical recommendation for enterprise teams: use primary constructors where they simplify the code without introducing implicit state concerns, and stick with traditional constructors where explicitness has value.

Is There a Middle Path?

Some teams adopt a hybrid convention:

  • Records and simple value types: primary constructors always

  • Small handler and adapter classes (โ‰ค2 dependencies): primary constructors allowed

  • Service classes with โ‰ฅ3 dependencies or complex lifecycle: traditional constructors required

  • Abstract base classes or public library types: traditional constructors required

This approach captures most of the readability gains from primary constructors while preserving explicitness where it matters most. The key is documenting the convention and enforcing it through code review, not leaving it to individual judgment.

How to Choose: Decision Framework

Ask these questions before reaching for primary constructors in a new class:

  1. Is this a record type or immutable DTO? โ†’ Primary constructor is the natural choice.

  2. Does this class have more than two injected dependencies? โ†’ Traditional constructor preserves clarity.

  3. Will this class be inherited or used as a public API surface? โ†’ Traditional constructor is safer.

  4. Does your team have an analyser rule for primary constructor parameter reassignment? โ†’ If yes, primary constructors are safer. If no, weigh the risk.

  5. Is this a test fixture or adapter class? โ†’ Primary constructor typically wins on readability.

There is no wrong answer that applies to all scenarios. The goal is intentional consistency, not a fleet-wide mandate in either direction.

Refactoring Existing Codebases

If you are considering migrating an existing ASP.NET Core codebase from traditional to primary constructors, go incrementally. Target the simplest, lowest-risk classes first โ€” small handlers, validators, and adapters. Leave the large, stateful service classes on traditional constructors until your team has built confidence with the new pattern and your analyser coverage is solid.

Rider and Visual Studio both offer automated refactoring to convert traditional constructors to primary constructors. Running these transformations in bulk across a large codebase is risky โ€” it is better to migrate class by class, reviewing each transformation for the mutable capture concern before merging.

For teams following the Domain Events vs Integration Events in .NET architectural pattern, domain event handlers are typically excellent candidates for primary constructors given their focused, single-dependency nature. Larger aggregates and orchestration services are better left on traditional constructors.

For the DI lifetime decisions that interact with constructor design, see the post on ASP.NET Core DI Lifetimes: Singleton vs Scoped vs Transient โ€” lifetime mismatches remain the most common constructor-related runtime error in ASP.NET Core, regardless of which constructor style you use.


โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!

FAQ

Are C# primary constructors safe to use with ASP.NET Core dependency injection? Yes. The DI container resolves and passes dependencies to primary constructors exactly as it does with traditional constructors. The difference is entirely in how the C# source is written, not how the runtime wires dependencies. There is no compatibility or registration difference.

Why are primary constructor parameters mutable in C# class types? This was a deliberate language design decision to keep primary constructors general-purpose. Unlike record types, where primary constructor parameters map directly to init-only properties, class primary constructors were designed to support scenarios beyond immutable data โ€” including cases where the parameter value needs to change during initialisation. The downside is that you lose the compiler-enforced immutability that private readonly fields provide.

Should I convert all my existing ASP.NET Core service classes to use primary constructors? No โ€” not as a bulk operation. Evaluate each class individually. Simple, small service classes are good candidates. Large services with multiple dependencies, complex state, or inheritance chains are better left on traditional constructors. Mass conversion without an analyser to catch mutable capture issues introduces subtle risk.

Do primary constructors work with ASP.NET Core options like IOptions<T>? Yes. IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> are injected through primary constructors in exactly the same way as through traditional constructors. The registration in Program.cs is unchanged.

Can I use primary constructors in ASP.NET Core controllers? Yes. Controller classes support primary constructors in C# 12 and later. The DI container injects action-injected dependencies normally. However, controllers tend to have more dependencies than simple service classes, so the clarity trade-off often favours traditional constructors for controllers in enterprise codebases.

What is the impact of primary constructors on code coverage and debugging? Some debuggers and coverage tools have slightly less mature support for primary constructor parameters compared to traditional fields. JetBrains Rider and Visual Studio 2026 have improved this significantly, but if you rely on field-watching in complex debugging scenarios, traditional constructors with named readonly fields give a more predictable experience.

Do primary constructors affect XML documentation in ASP.NET Core libraries? Yes, in a minor way. You cannot use <param> tags on the class declaration to document primary constructor parameters in the standard way. This is a real limitation for library authors and public SDK surfaces. Traditional constructors with explicit /// <summary> and <param> documentation on the constructor itself are better for documentation-heavy codebases.

More from this blog

C

Coding Droplets

223 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.