Skip to content

    Back to articles

    5 min read

    Pin C# language behavior separately from the .NET SDK

    An SDK upgrade is not a language policy

    The .NET SDK supplies the compiler, but the target framework helps determine the default C# language version. That distinction matters now: the .NET 11 overview says C# 15 is the default for projects targeting .NET 11, and its current release-candidate notes make that a live upgrade concern rather than a distant one.

    I would make two fleet-wide decisions, not one: which SDK builds the repositories, and which C# language version their code is allowed to use. Those decisions interact, but they answer different questions. The SDK determines which build tools are available. The language version sets the syntax and language features the compiler accepts. The target framework determines the APIs the application can compile against and, by default, influences the language version. Microsoft describes this relationship in its C# language version reference.

    That means an SDK upgrade by itself should not be treated as an intentional language-policy change. But an SDK upgrade that enables a move to .NET 11 can bring a new default along with the target-framework change. If the repository has no explicit policy, its accepted code can change as a side effect of a framework upgrade. I would rather make that change deliberate than ask every repository to notice it independently.

    Put the language choice where the fleet can see it

    I would keep SDK selection in global.json and language policy in shared MSBuild configuration. The global.json reference documents how the CLI selects an SDK from that file; it is the right place to express which SDK a repository expects. It is not where I would express which C# syntax the repository permits.

    For example, a repository that has not approved C# 15 can set its policy in Directory.Build.props:

    <Project>
      <PropertyGroup>
        <LangVersion>14.0</LangVersion>
      </PropertyGroup>
    </Project>

    That setting is intentionally not inferred from the SDK or target framework. When the team chooses to adopt C# 15, it can change the value as a reviewed policy update. The MSBuild documentation on customizing builds by directory explains how Directory.Build.props can provide settings to projects beneath it. I would check how each repository is structured before relying on one shared file: nested files, imports, and later property assignments can affect what a project actually receives.

    I would also avoid setting the language version to latest. “Latest” describes a moving compiler choice, not a stable policy. Two builds using different SDKs could then accept different language features without a change to the repository’s stated intent. An explicit version makes the decision visible in code review and gives the build a consistent rule to enforce. The compiler still comes from the SDK; the language version is an independent input to that compiler.

    This separation does not mean every project should use the same language version forever. A repository targeting .NET 11 may intentionally adopt C# 15 while another stays on C# 14. The point is that those differences should be chosen, not inherited accidentally from a default. Shared configuration can establish a baseline, while a deliberate, documented exception can serve a project with a different migration plan.

    Roll out a change as a compatibility check

    I would not change the SDK, target framework, and language version across a fleet in one opaque step. Each change can expose a different class of issue. An SDK change changes the build toolchain; a target-framework change changes the platform surface; a language-version change permits new syntax and can affect how the compiler interprets code. Keeping those decisions distinct makes failures easier to diagnose and makes the review answerable: which policy changed, and why?

    A practical rollout starts by recording each repository’s current SDK, target framework, and effective language version. Then I would apply the shared language setting and verify that the existing build still passes under the existing toolchain. Next, I would test the proposed SDK and framework update against a representative set of repositories, chosen to cover different project shapes and build paths. The compatibility check should include the actual restore, build, and test steps used by CI, not just a local compile of one project.

    When the change is ready, I would make the language-policy update explicit in the same review that adopts the new language version. I would check for projects that override the central property and for build paths that do not consume the shared configuration. If the fleet has reusable pipelines, I would put the check there so a repository cannot silently escape the policy merely because its local build happens to use a different SDK installation.

    A pinned language version is not a promise that every new language feature works with every target framework. It controls what the compiler accepts; it does not supply runtime or framework APIs that the application may need. I would therefore test new syntax against the project’s actual target and dependencies, and keep framework compatibility review separate from the language-version change. The compiler’s own language-version guidance is the reference for supported values and the relationship between language versions and target frameworks.

    Make the default change reviewable

    C# 15 becoming the default for .NET 11 projects is a useful reminder that defaults are policy, even when nobody wrote them down. I would pin the SDK so builds are reproducible, and pin the language version so accepted code changes only when the team chooses to change it. Those pins solve different problems; treating them as interchangeable hides the decision that matters.

    When I am ready to adopt C# 15, I want the pull request to say so directly, the shared configuration to enforce it, and targeted compatibility checks to show that the affected repositories still build and test as intended. That costs some coordination up front. It costs less than discovering, repository by repository, that an infrastructure upgrade quietly changed the rules for writing application code.

    Drafted with AI assistance, reviewed and edited by Ali.