📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is an SDK-style .csproj, and how does it differ from an old-style project file?

The modern project file: the Sdk attribute, implicit imports, default globbing of source files, PackageReference, and why SDK-style projects are short, mergeable and cross-platform.

Beginner Theory Beginner csproj msbuild project-system sdk-style

🎯 Key points

  • An SDK-style project starts with <Project Sdk="Microsoft.NET.Sdk">, which imports the SDK's props and targets implicitly
  • Source files are included by default globbing (**/*.cs), so you don't list every file
  • Packages are declared with PackageReference instead of packages.config
  • Different SDKs add different defaults: Microsoft.NET.Sdk, Microsoft.NET.Sdk.Web, Microsoft.NET.Sdk.Worker, Microsoft.NET.Sdk.Razor
  • Key properties: TargetFramework, OutputType, Nullable, ImplicitUsings, RootNamespace
  • Old-style (.NET Framework) projects list every file, import targets explicitly, and are hard to merge
  • SDK-style projects work with the dotnet CLI on Windows, macOS and Linux

⏱ 30-second answer

An SDK-style project begins with <Project Sdk="Microsoft.NET.Sdk">. That one attribute pulls in all the standard build logic, so the file only holds what's specific to your project: the target framework, the output type, a few settings and its references. Source files are picked up automatically by globbing, so you don't list each .cs file. Packages are PackageReference items. Old-style .NET Framework project files listed every file, imported targets by hand and used packages.config. That made them long, noisy in merges and tied to Visual Studio on Windows.

🗣 Full answer (senior)

How I would answer this in an interview

"An SDK-style project is the modern .csproj format, and it starts with <Project Sdk="Microsoft.NET.Sdk">. That Sdk attribute imports the SDK's props before my content and its targets after it, so the default properties, the file globs and the build, pack and publish targets all come in implicitly. The file only holds what's specific to the project: TargetFramework, which sets the API surface I compile against, OutputType, Nullable, ImplicitUsings, RootNamespace and its references.

Compared with the old .NET Framework format, three things matter. Source files are globbed, so every .cs file under the folder is compiled and adding a file doesn't touch the project file. Packages are PackageReference items, alongside ProjectReference items for other projects, with transitive dependencies resolved automatically instead of packages.config and hint paths. And it works with the dotnet CLI on any OS. The old format listed every file, imported targets by hand, and conflicted on almost every merge.

The SDK I choose sets the defaults: Microsoft.NET.Sdk.Web for ASP.NET Core, .Worker for background services, .Razor for Razor libraries. The mistake I see is adding <Compile Include> for a file that globbing already includes, which fails with a duplicate-item error. I keep project files minimal and move shared settings into Directory.Build.props. When migrating legacy code, I convert to SDK-style first while still on net48, and change the framework as a separate step."


A minimal SDK-style project

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Serilog" Version="4.2.0" />
    <ProjectReference Include="..\Orders.Domain\Orders.Domain.csproj" />
  </ItemGroup>
</Project>

That's a complete, buildable project.

What the Sdk attribute does

Sdk="Microsoft.NET.Sdk" is shorthand for importing Sdk.props at the top of the file and Sdk.targets at the bottom. Those bring in:

  • Default properties (configuration, output paths, AssemblyName from the file name).
  • Default items: Compile for **/*.cs, EmbeddedResource for **/*.resx, None for other files, excluding bin/ and obj/.
  • The build, pack and publish targets.

Choosing an SDK

SDK Use for Adds
Microsoft.NET.Sdk Libraries, console apps, tests The basics
Microsoft.NET.Sdk.Web ASP.NET Core Framework reference to ASP.NET Core, web defaults, wwwroot publishing
Microsoft.NET.Sdk.Worker Background services Worker defaults
Microsoft.NET.Sdk.Razor Razor class libraries Razor compilation

Old-style versus SDK-style

Old-style (.NET Framework) SDK-style
Size Hundreds of lines Often under 20
Source files Each listed with <Compile Include> Globbed by default
Packages packages.config + HintPath PackageReference
Imports Explicit <Import> of targets Implicit via Sdk
Tooling Visual Studio, Windows dotnet CLI on any OS, all IDEs
Merge conflicts Frequent (every new file edits the project) Rare

.NET Framework projects can also use the SDK-style format (with TargetFramework set to net48), which is the usual first step of a migration.

Production example

Migrating a [40-project] .NET Framework solution to SDK-style project files (still on net48) removed [about 9,000 lines] of XML. Almost all merge conflicts in project files disappeared, and the solution could finally build with dotnet build on the Linux CI agents, before any code moved to .NET 10.

Best practices

  • Keep project files small. Move shared settings to Directory.Build.props.
  • Don't re-add <Compile Include> items for files that globbing already includes; that causes duplicate-item errors.
  • Enable Nullable and ImplicitUsings in new projects.

🔍 What the interviewer is checking

Checks basic understanding of the project file.

Green flags: explains the Sdk attribute and implicit imports; knows globbing; names a few SDKs and key properties; contrasts with old-style.

Red flags: thinks every file must be listed; only knows project files through IDE dialogs; doesn't know PackageReference.

📚 Learn in depth

1. What the Sdk attribute expands to

<Project Sdk="Microsoft.NET.Sdk"> is shorthand for two implicit imports, one before your content and one after it:

 <Project Sdk="Microsoft.NET.Sdk">        what MSBuild actually evaluates
                                         +--------------------------------------+
                                         | <Import Sdk.props />                 |
                                         |   Directory.Build.props (if found)   |
                                         |   default properties                 |
                                         |   default item globs (**/*.cs ...)   |
   <PropertyGroup> ... </PropertyGroup>  | --- your project content ----------- |
   <ItemGroup> ... </ItemGroup>          |                                      |
                                         | <Import Sdk.targets />               |
                                         |   Directory.Build.targets (if found) |
                                         |   build / pack / publish targets     |
 </Project>                              +--------------------------------------+

The order matters. Anything you set in the project overrides the defaults from Sdk.props, but Sdk.targets runs after your content, so it sees your final property values. Run dotnet msbuild -preprocess:full.xml to see the fully expanded project; it is often several thousand lines.

The attribute can also name a versioned SDK from NuGet, for example <Project Sdk="Microsoft.Build.NoTargets/3.7.0">. MSBuild's SDK resolvers find it in the installed .NET SDK or download it as a package.

2. Old-style and SDK-style side by side

An old-style class library, abbreviated:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <ProjectGuid>{5A3B...}</ProjectGuid>
    <OutputType>Library</OutputType>
    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
    ...
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> ... </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> ... </PropertyGroup>
  <ItemGroup>
    <Reference Include="Newtonsoft.Json">
      <HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    ...
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Customer.cs" />
    <Compile Include="Order.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    ... one line per file ...
  </ItemGroup>
  <ItemGroup>
    <None Include="packages.config" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

The same library, SDK-style:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>
Concern Old-style SDK-style
Imports Explicit <Import> of Microsoft.Common.props and Microsoft.CSharp.targets Implicit through Sdk
Source files One <Compile Include> per file Globbed by default
Packages packages.config plus <Reference HintPath> into a packages/ folder PackageReference items, restored to the global packages folder
Transitive packages Every dependency listed by hand Resolved automatically
Assembly info Properties/AssemblyInfo.cs Generated from properties such as Version, Company
ProjectGuid, configuration blocks Required Not needed
Packing a NuGet package Separate .nuspec file dotnet pack from project properties
Multi-targeting Not supported <TargetFrameworks>net48;net10.0</TargetFrameworks>
Build tooling Visual Studio / MSBuild on Windows dotnet CLI on any OS, every IDE

3. Default items and globbing

Item type Default include Notes
Compile **/*.cs (the language's extension) Controlled by EnableDefaultCompileItems
EmbeddedResource **/*.resx Controlled by EnableDefaultEmbeddedResourceItems
None **/* minus the above Shown in the IDE, not compiled or copied unless you say so
Web SDK Content wwwroot/**, **/*.json, **/*.config and more Published with the app

Every glob excludes bin/, obj/, **/*.user, **/*.*proj, **/*.sln and other files listed in DefaultItemExcludes. You can extend that list:

<PropertyGroup>
  <DefaultItemExcludes>$(DefaultItemExcludes);scratch/**</DefaultItemExcludes>
</PropertyGroup>

The three item operations:

Operation Example Effect
Include <Compile Include="..\Shared\Clock.cs" Link="Shared\Clock.cs" /> Adds an item that isn't already globbed
Remove <Compile Remove="Legacy/**" /> Takes globbed items out
Update <None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> Changes metadata on an item already globbed

Using Include for a file that is already globbed adds it twice, which fails with NETSDK1022 (duplicate items).

4. The SDKs and what each adds

SDK Use for Adds
Microsoft.NET.Sdk Libraries, console apps, test projects The basics
Microsoft.NET.Sdk.Web ASP.NET Core apps and APIs Microsoft.AspNetCore.App framework reference, web implicit usings, Content items for wwwroot and config files, launch settings
Microsoft.NET.Sdk.Worker Background services Worker defaults and Microsoft.Extensions.Hosting usings
Microsoft.NET.Sdk.Razor Razor class libraries Razor compilation
Microsoft.NET.Sdk.BlazorWebAssembly Standalone Blazor WebAssembly apps WebAssembly build and publish
Aspire.AppHost.Sdk .NET Aspire app hosts Orchestration of the referenced projects
MSTest.Sdk MSTest projects Test framework and Microsoft.Testing.Platform defaults

5. Properties you will set most often

Property Typical value What it does
TargetFramework / TargetFrameworks net10.0 or net8.0;net10.0 Which .NET API surface you compile against
OutputType Exe or Library (default) Executable or class library
Nullable enable Turns on nullable reference type analysis
ImplicitUsings enable Generates global using directives for common namespaces, in obj/.../<Project>.GlobalUsings.g.cs
RootNamespace Orders.Api Default namespace for new files and embedded resource names
AssemblyName defaults to the file name Name of the output DLL or EXE
LangVersion normally unset The C# version; the default follows the target framework (C# 14 for net10.0)
TreatWarningsAsErrors true in CI Fails the build on any warning
GenerateAssemblyInfo true (default) Generates assembly attributes from Version, Company and similar properties

6. What ImplicitUsings generates

For Microsoft.NET.Sdk the generated file contains global using directives for System, System.Collections.Generic, System.IO, System.Linq, System.Net.Http, System.Threading and System.Threading.Tasks. The Web SDK adds ASP.NET Core namespaces such as Microsoft.AspNetCore.Builder. You can add or remove entries:

<ItemGroup>
  <Using Include="Orders.Domain" />
  <Using Remove="System.Net.Http" />
</ItemGroup>

7. Migrating an old project

  1. Convert the project file to SDK-style while keeping the same target (net48). Remove the <Compile> list, ProjectGuid and the configuration blocks.
  2. Convert packages.config to PackageReference (Visual Studio offers "Migrate packages.config to PackageReference"). Keep only top-level packages.
  3. Delete Properties/AssemblyInfo.cs or set GenerateAssemblyInfo to false, otherwise you get CS0579 duplicate attribute errors.
  4. Build with dotnet build and fix differences such as files that were deliberately not in the old <Compile> list but are now globbed.
  5. Get it building in CI on the CLI, then change the target framework in a separate step.

The .NET Upgrade Assistant and the newer AI-assisted modernisation tooling in Visual Studio can automate parts of this, but review the result as you would any other change.

8. File-based apps in .NET 10

.NET 10 can run a single C# file without a project file: dotnet run app.cs. Directives at the top of the file, such as #:package [email protected] and #:sdk Microsoft.NET.Sdk.Web, play the role of the project file. When the script grows, dotnet project convert app.cs turns it into a normal SDK-style project. It's useful for scripts and samples, but production code still belongs in projects.

9. Diagnosing project-file problems

Symptom Likely cause Check
NETSDK1022 duplicate items Include of a globbed file Change it to Update, or remove it
A stray file is compiled Globbing picked up a backup or scratch file dotnet msbuild -getItem:Compile lists every compiled file
CS0579 duplicate assembly attribute Old AssemblyInfo.cs plus generated attributes Delete the file or set GenerateAssemblyInfo=false
A property seems to be ignored Set in the wrong place or overridden later dotnet msbuild -getProperty:Nullable, or a binary log (-bl)
Type not found in a web project Wrong SDK (Microsoft.NET.Sdk instead of .Web) Check the Sdk attribute

10. .NET 11 awareness

The .NET 11 SDK (preview now) keeps the same project format. Moving a project later is usually a one-line change to TargetFramework (net11.0), plus fixing any new warnings. Keep production projects on net10.0 (LTS) for now.

11. Try it yourself

  1. Run dotnet new console -o Demo and open Demo.csproj.
  2. Add Extra.cs with a class and build. You didn't touch the project file.
  3. Run dotnet msbuild -getItem:Compile and find Extra.cs in the list.
  4. Add <Compile Include="Extra.cs" /> and build to see NETSDK1022. Change it to <Compile Remove="Extra.cs" /> and see the class disappear.
  5. Run dotnet msbuild -preprocess:full.xml and search for Sdk.props and Sdk.targets.
  6. Change Sdk to Microsoft.NET.Sdk.Web, preprocess again, and compare the size.
  7. Open obj/Debug/net10.0/Demo.GlobalUsings.g.cs to see what ImplicitUsings generated.

💻 Code

<!-- An ASP.NET Core API project -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>Orders.Api</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <!-- Exclude a folder of legacy files from compilation -->
    <Compile Remove="Legacy/**" />
    <!-- Change metadata on a file that globbing already includes -->
    <None Update="seed-data.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Orders.Domain\Orders.Domain.csproj" />
  </ItemGroup>
</Project>
// Program.cs: with ImplicitUsings, common namespaces need no using directives
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/health", () => Results.Ok(new { status = "ok", region = "australiaeast" }));
app.Run();

⚠ Common mistakes

  • Mistake: listing source files explicitly in an SDK-style project. The default glob already includes every .cs file, so an extra <Compile Include> adds it twice and the build fails with NETSDK1022. Instead: let globbing include source files, and use Remove only for files you really want excluded.

  • Mistake: using Include instead of Update to change a globbed file. <None Include="appsettings.json" CopyToOutputDirectory="..." /> creates a second item instead of changing the existing one. Instead: use <None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> for files that globbing already picks up.

  • Mistake: copying the old project file into the new format. Carrying across ProjectGuid, configuration blocks, HintPath references and file lists keeps the noise and often breaks the build. Instead: start from a clean SDK-style file and add back only what is specific to the project.

  • Mistake: keeping Properties/AssemblyInfo.cs after migrating. The SDK generates the same assembly attributes, so the build fails with CS0579 duplicate attribute errors. Instead: delete the file and set Version, Company and similar properties in the project or Directory.Build.props, or set GenerateAssemblyInfo to false if you must keep it.

  • Mistake: picking the wrong SDK. Using Microsoft.NET.Sdk for a web app and then adding ASP.NET Core packages by hand misses the framework reference, the web defaults and wwwroot publishing. Instead: use Microsoft.NET.Sdk.Web for ASP.NET Core and the matching SDK for workers, Razor libraries and Blazor.

  • Mistake: leaving stray files in the project folder. Backups, scratch .cs files or copied folders are compiled silently, causing duplicate types or unexpected code in the build. Instead: keep the folder clean, add Remove or DefaultItemExcludes for anything that must stay, and check with dotnet msbuild -getItem:Compile.

  • Mistake: ignoring transitive dependencies. With PackageReference, a project also gets every package its references depend on. Relying on those without referencing them directly, or letting versions drift between projects, leads to conflicts such as NU1605 downgrade errors. Instead: reference packages you use directly, and keep versions consistent with central package management (Directory.Packages.props).

  • Mistake: repeating the same properties in every project file. Nullable, TreatWarningsAsErrors, LangVersion and company metadata copied into 50 projects drift apart over time. Instead: put shared settings in Directory.Build.props at the repo root, and keep each project file to what is specific to it.

  • Mistake: thinking SDK-style means .NET Core only. SDK-style projects can target .NET Framework, so teams sometimes delay the format change until a full framework migration. Instead: convert project files first while keeping net48, get them building with the CLI, and change the target framework later in a separate step.

  • Mistake: setting LangVersion to latest or preview without a reason. It lets developers use language features that the target runtime or other tools may not support, and the result changes when the SDK changes. Instead: leave LangVersion unset so it follows the target framework (C# 14 for net10.0).

🏛 Senior insight: trade-offs

The SDK-style format turned project files from IDE-owned artefacts into small, reviewable source code. A senior engineer keeps them that way: minimal project files, shared settings in Directory.Build.props, versions in Directory.Packages.props, and no hand-maintained file lists. Because packages are now plain items in the project, keeping them current is a routine job: automated update pull requests, NuGet Audit warnings treated as work, and a periodic look at dotnet list package --outdated, so projects don't rot on old, vulnerable versions. When migrating legacy solutions, they convert project files to SDK-style first (keeping net48), get them building with the CLI in CI, and only then move target frameworks, so each step is small and reversible.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions repeating the same properties in every project. Why: Moves from one project file to sharing settings across many.
    How to answer

    Directory.Build.props and Directory.Build.targets are ordinary MSBuild files that the SDK imports automatically into every project below them. For each project, MSBuild searches upwards and uses the nearest one it finds. .props is imported early, before the project's own content, so it's for default properties and items: target framework, Nullable, analysers, package metadata. Projects can still override those values. .targets is imported late, after the project, so it's for targets and logic that need final values. Only the nearest file is picked up. To layer them, the inner file imports the outer one with GetPathOfFileAbove. I debug them with dotnet msbuild -preprocess or a binary log.

  2. When: Candidate names TargetFramework as a key property. Why: Checks what the TargetFramework property actually controls.
    How to answer

    A target framework moniker, or TFM, such as net10.0, tells the SDK which API surface to compile against and which runtime the app needs. OS-specific TFMs like net10.0-windows add platform APIs such as WPF. For applications I target one framework, normally the newest LTS: that's net10.0 today, supported until November 2028. .NET 8 and .NET 9 both reach end of support on 10 November 2026, so they need upgrading now. Libraries can multi-target with <TargetFrameworks>net8.0;net10.0</TargetFrameworks> while they still have consumers on older versions. That builds the library once per framework, and #if NET10_0_OR_GREATER handles API differences. .NET 11 is a preview, so it's for experiments only.

  3. When: Candidate mentions PackageReference or ProjectReference items. Why: Probes the references section of the project file.
    How to answer

    A PackageReference pulls a specific version of a NuGet package from a feed. A ProjectReference builds another project in the same repo and references its output. You use project references inside a codebase and package references across teams or repos that release separately. Both are transitive by default: whoever references your project also gets your dependencies. PrivateAssets="all" stops that for things consumers shouldn't inherit, such as analysers or Source Link. A plain Version="4.2.0" means "4.2.0 or higher". NuGet then picks the lowest version that satisfies every constraint, and the nearest declaration wins. dotnet list package --include-transitive shows the full graph.

↩ Usually asked after

More in Project System

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me