SDK-Style Project Files
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
An SDK-style project is an MSBuild project whose root element names a project SDK:
<Project Sdk="Microsoft.NET.Sdk">. That one attribute imports thousands of lines of MSBuild logic, so a project file can
state only what is particular to the project — its target framework, its packages, its metadata — and inherit
everything else: which files to compile, where outputs go, how to restore, pack and publish. All .NET (Core) and
.NET 5+ projects are SDK-style; the format can also target .NET Framework and .NET Standard. This page dissects the
format with the Ledgerline projects and .NET SDK 10.0.402. The introduction is in
Namespaces, Assemblies and Projects — The SDK-Style Project;
the MSBuild elements themselves (PropertyGroup, ItemGroup, Target, Import) are covered on
Project File Anatomy.
The smallest project
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
These five lines build a library from every *.cs file under the project folder. The Sdk attribute tells MSBuild to
add two implicit imports: Sdk.props before the first line of the project body and Sdk.targets after the last.
dotnet msbuild -pp:<file> ("preprocess") writes the project with every import inlined and shows them:
dotnet msbuild -pp:pp.xml
wc -l pp.xml
<!--
============================================================================================================================================
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk">
This import was added implicitly because the Project element's Sdk attribute specified "Microsoft.NET.Sdk".
<sdk>/Sdks/Microsoft.NET.Sdk/Sdk/Sdk.props
============================================================================================================================================
-->
…
<!--
============================================================================================================================================
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk">
This import was added implicitly because the Project element's Sdk attribute specified "Microsoft.NET.Sdk".
<sdk>/Sdks/Microsoft.NET.Sdk/Sdk/Sdk.targets
============================================================================================================================================
-->
17284 pp.xml
The five-line project is 17,284 lines once evaluated. The preprocessed file is the first thing to read when a property "has no effect": search it for the property name and the last assignment wins.
The three equivalent forms
The Sdk attribute is shorthand. The same project can be written with explicit imports, which is the only way to put
something after Sdk.targets — for example a target that overrides one the SDK defines:
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<!-- Anything here can override what Sdk.targets defines, because it comes after it. -->
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<Target Name="ShowSdk" AfterTargets="Build">
<Message Importance="high" Text="Built with $(NETCoreSdkVersion) for $(TargetFramework)" />
</Target>
</Project>
Determining projects to restore...
Restored ~/journal/Journal.csproj (in 82 ms).
Journal -> ~/journal/bin/Debug/net10.0/Journal.dll
Built with 10.0.402 for net10.0
Build succeeded.
The third form is the <Sdk> element, used to add a second SDK on top of the first ("additive SDKs") or to give a
version: <Sdk Name="Aspire.AppHost.Sdk" Version="13.6.3" /> inside a <Project Sdk="Microsoft.NET.Sdk">. A version can
also be written in the attribute itself, <Project Sdk="Microsoft.Build.NoTargets/3.7.134">.
How a project SDK is found
When MSBuild evaluates an SDK import it asks a chain of SDK resolvers:
-
the .NET SDK resolver finds the SDKs that ship with the selected .NET SDK (
Microsoft.NET.Sdk,Microsoft.NET.Sdk.Web, …) under<sdk>/Sdks/; -
the NuGet SDK resolver downloads an SDK published as a NuGet package (
Microsoft.Build.Traversal,Microsoft.Build.NoTargets, your own) — only when a version is given, in the project or in themsbuild-sdkssection ofglobal.json; -
the default resolver looks next to MSBuild itself.
Without a version, a NuGet-delivered SDK fails to resolve, and the error lists what each resolver said:
<Project Sdk="Microsoft.Build.NoTargets">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
~/demo/Docs.proj : error : Could not resolve SDK "Microsoft.Build.NoTargets". Exactly one of the probing messages below indicates why we could not resolve the SDK. Investigate and resolve that message to correctly specify the SDK.
~/demo/Docs.proj : error : SDK resolver "Microsoft.DotNet.MSBuildWorkloadSdkResolver" returned null.
~/demo/Docs.proj : error : The NuGetSdkResolver did not resolve this SDK because there was no version specified in the project or global.json.
~/demo/Docs.proj : error MSB4236: The SDK 'Microsoft.Build.NoTargets' specified could not be found.
The fix is a version in one place for the whole repository:
{
"msbuild-sdks": {
"Microsoft.Build.NoTargets": "3.7.134",
"Microsoft.Build.Traversal": "4.1.82"
}
}
Only one version of each project SDK can be used in a build; MSBuild warns when two projects ask for different ones. Traversal and NoTargets are covered on Imports and Project SDKs.
The SDKs that ship with .NET
| SDK | For | Adds on top of Microsoft.NET.Sdk |
|---|---|---|
|
libraries, console apps, tests, tools; also WinForms and WPF with |
— |
|
ASP.NET Core apps |
the ASP.NET Core shared framework, web publishing, |
|
Razor class libraries |
Razor compilation |
|
Blazor WebAssembly apps |
the WebAssembly build and publish pipeline |
|
worker services |
the |
|
Aspire app hosts |
orchestration of the referenced projects (used as an additive SDK) |
|
MSTest test projects |
the MSTest packages and the Microsoft.Testing.Platform runner; it is delivered through NuGet, so it needs a version
( |
There is no separate SDK for desktop apps any more: a WinForms or WPF project uses Microsoft.NET.Sdk with a Windows
TFM and a switch. The project builds only on Windows (and is not run here):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<!-- or: <UseWindowsForms>true</UseWindowsForms> -->
</PropertyGroup>
</Project>
Default items
An SDK-style project does not list its files. Microsoft.NET.Sdk.DefaultItems.props defines three item types with
globs relative to the project folder:
| Item | Include | Exclude | Remove |
|---|---|---|---|
|
|
|
— |
|
|
same |
— |
|
|
same |
|
bin/ and obj/ ($(BaseOutputPath) and $(BaseIntermediateOutputPath)) are excluded through
DefaultItemExcludes. Querying the evaluated items of src/Ledgerline shows the result — the five .cs files as
Compile, and the files Directory.Build.targets adds (README and icon, for packing) as None:
dotnet msbuild -p:TargetFramework=net10.0 -getProperty:DefaultItemExcludes -getItem:Compile -getItem:None
"DefaultItemExcludes": ";bin\\Debug//**;obj\\Debug//**;bin\\/**;obj\\/**;bin\\Debug/net10.0/publish//**;**/*.user;**/*.*proj;**/*.sln;**/*.slnx;**/*.vssscc;**/.DS_Store"
Compile ['IsExternalInit.cs', 'JournalLine.cs', 'Ledger.cs', 'Ledger.Json.cs', 'LedgerReader.cs']
None ['packages.lock.json', '~/ledgerline/README.md', '~/ledgerline/icon.png']
(Here and below, JSON output of -getItem is condensed to the item identities.)
The properties that switch the defaults off are EnableDefaultItems (all three), EnableDefaultCompileItems,
EnableDefaultEmbeddedResourceItems and EnableDefaultNoneItems. To customise, modify the default items instead of
re-adding them: <Compile Remove="Legacy/**" /> to exclude a folder, <None Update="journal.ledger.csv"
CopyToOutputDirectory="PreserveNewest" /> to change metadata (as Ledgerline.Report does).
Adding a file that the glob already includes — typically when an old project file is converted by hand — is an error:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Copied from an old-style project: the SDK already includes **/*.cs -->
<Compile Include="Ledger.cs" />
</ItemGroup>
</Project>
<sdk>/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.DefaultItems.Shared.targets(190,5): error NETSDK1022: Duplicate 'Compile' items were included. The .NET SDK includes 'Compile' items from your project directory by default. You can either remove these items from your project file, or set the 'EnableDefaultCompileItems' property to 'false' if you want to explicitly include them in your project file. For more information, see https://aka.ms/sdkimplicititems. The duplicate items were: 'Ledger.cs' [~/journal/Journal.csproj]
A related trap: because the globs are recursive, a nested project folder (a test project placed inside the library
folder, or a samples/ folder below it) has its .cs files compiled into the outer project too. Keep projects in sibling
folders, as Ledgerline does, or remove the folder with <Compile Remove="samples/**" />.
Implicit global usings
With <ImplicitUsings>enable</ImplicitUsings> (in every dotnet new template since .NET 6, and in Ledgerline's
Directory.Build.props), the SDK generates a file of global using directives in obj/:
cat obj/Debug/net10.0/Ledgerline.GlobalUsings.g.cs
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
Microsoft.NET.Sdk.Web and .Worker add their hosting namespaces; WinForms adds System.Drawing and
System.Windows.Forms; WPF removes System.IO and System.Net.Http to avoid clashes. Add or remove namespaces with
Using items — <Using Include="Ledgerline" />, <Using Remove="System.Net.Http" />, or an alias
<Using Include="System.Text.Json.JsonSerializer" Alias="Json" />. Since the .NET 8 SDK, System.Net.Http is no longer
added when targeting .NET Framework.
Implicit package and framework references
The project does not reference the framework explicitly either. For net5.0 and later, the SDK adds a
FrameworkReference to Microsoft.NETCore.App (plus Microsoft.AspNetCore.App for the Web SDK). For
netstandard1.0--netstandard2.0 it adds an implicit PackageReference to NETStandard.Library, marked
IsImplicitlyDefined, so it cannot be updated or removed by hand. Querying Ledgerline for its netstandard2.0 target
shows it next to the two real references, whose versions come from Directory.Packages.props (Central Package
Management), not from the project:
dotnet msbuild -p:TargetFramework=netstandard2.0 -getItem:PackageReference
NETStandard.Library 2.0.3 true
Microsoft.Bcl.HashCode None
System.Text.Json None
(columns: identity, Version metadata, IsImplicitlyDefined)
The complete Ledgerline library project
Everything shared between projects lives in the repository’s Directory.Build.props (language version, Nullable,
ImplicitUsings, TreatWarningsAsErrors, authors, licence, repository URL) and Directory.Packages.props (versions),
so the library project is short and states only what is specific to it:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net8.0;netstandard2.0</TargetFrameworks>
<RootNamespace>Ledgerline</RootNamespace>
<IsPackable>true</IsPackable>
<PackageId>Ledgerline</PackageId>
<Version>1.0.0</Version>
<Description>Double-entry bookkeeping ledgers: read journal CSV files, check that every entry balances and export a trial balance as JSON.</Description>
<PackageTags>ledger;accounting;double-entry;trial-balance;csv</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnablePackageValidation>true</EnablePackageValidation>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.HashCode" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
</Project>
| Element | Why |
|---|---|
|
Multi-targeting: one inner build per TFM (Target Frameworks and Runtime Identifiers). |
|
Package metadata for |
|
Writes |
|
Checks the package for API compatibility between TFMs and versions (Package Validation). |
The conditional |
Polyfill packages only for |
Legacy (non-SDK) projects
Before .NET Core, a C# project listed every file and imported the common targets explicitly; Visual Studio rewrote it on
every change. Such projects are still met in .NET Framework code bases, typically together with packages.config. A
fragment of one (not run here: it needs the .NET Framework targeting packs that Visual Studio installs):
<?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" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<ProjectGuid>{6A1F5B0E-0000-4C1D-9C2E-000000000000}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>Ledgerline</RootNamespace>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="Ledger.cs" />
<Compile Include="LedgerReader.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
The differences that matter when converting one: the explicit Compile list (delete it, or NETSDK1022 follows),
Properties\AssemblyInfo.cs (the SDK generates the same attributes — delete the file or set
GenerateAssemblyInfo to false, otherwise the compiler reports duplicate attributes), packages.config (migrate to
PackageReference first, see
nuget.exe, Visual Studio and packages.config)
and TargetFrameworkVersion v4.8, which becomes <TargetFramework>net48</TargetFramework>.
Choosing and Combining the Tools
covers the migration path.
Source: .NET project SDKs.
Common mistakes
-
Re-adding files the SDK already includes —
NETSDK1022. UseUpdate/Remove, notInclude. -
A project inside another project’s folder — its sources are compiled twice.
-
Editing
obj/*.g.csfiles — they are regenerated on every build; set properties instead (AssemblyTitle,Usingitems). -
Giving a NuGet-delivered SDK no version —
MSB4236; put versions inglobal.jsonmsbuild-sdks. -
Keeping
AssemblyInfo.csafter converting — duplicate attribute errors (CS0579). -
Overriding an SDK target in the project body — it is ignored because
Sdk.targetsis imported later and redefines it; use explicit imports or anAfterTargetshook.
Release notes
-
.NET 6: implicit global usings (
ImplicitUsings). -
.NET 8:
System.Net.Httpno longer an implicit using for .NET Framework targets. -
.NET 10:
.slnxbecomes the default solution format fordotnet new sln(Solutions and .slnx).