Mert Özen Her Satırda Daha İleri
Backend .NET SDK, CLI ve MSBuild — #02

csproj'un Anatomisi: SDK-Style Proje, Sık Kullanılan Property'ler, Item'lar ve Implicit Davranışlar

Mert Özen 15 Kas 2026 11 dk 6 görüntülenme
csproj'un Anatomisi: SDK-Style Proje, Sık Kullanılan Property'ler, Item'lar ve Implicit Davranışlar

SDK-style bir csproj, Sdk attribute'unun eklediği import'lar, Directory.Build.props ve **/*.cs gibi varsayılan glob'lar sayesinde kısa kalır; property'nin gerçek değerini dotnet msbuild -getProperty, item'ları -getItem, dosyanın tamamını -preprocess gösterir. Bookstore'un proje dosyalarını okuyup template tekrarlarını temizliyor; console projede appsettings.json'un neden kopyalanmadığını, InvariantGlobalization, PrivateAssets ve InternalsVisibleTo'yu gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.404, MSBuild 18.9) ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un bookstore-admin aracının bir önceki yazıda CLI ile oluşturduğumuz proje dosyasına bir appsettings.json ekledik. Proje dosyası şu, klasörde Program.cs ve appsettings.json var:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <AssemblyName>bookstore-admin</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Bookstore.Contracts\Bookstore.Contracts.csproj" />
  </ItemGroup>

</Project>

Üç soru: Bu proje hangi framework'ü hedefler? Hangi .cs dosyalarını derler? dotnet run -- info ayar dosyasındaki API adresini okuyabilir mi? Dosyada TargetFramework yok, ama proje net10.0 hedefliyor. Dosya listesi yok, ama klasördeki bütün .cs dosyaları derleniyor. Üçüncü sorunun cevabı ise çoğu kişiyi şaşırtıyor:

Runtime:     .NET 10.0.15
Environment: (not set)
API:         (appsettings.json not found)

Dosya klasörde duruyor ama bin/'e kopyalanmıyor. Aynı dosya Bookstore.Api'de sorunsuz kopyalanıyor. Bu yazıda, sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) proje dosyalarını satır satır okuyup bu tür örtük davranışların nereden geldiğini, sık kullanılan property ve item'ları ve bir proje dosyasının son hâlini nasıl göreceğini anlatıyoruz.

Sdk Attribute'u ve Görünmeyen Import'lar

SDK-style proje dosyasının ilk satırındaki Sdk attribute'u, dosyanın başına ve sonuna birer import ekler. Yukarıdaki proje, MSBuild'in gözünde aşağıdakiyle aynıdır:

<Project>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

  <PropertyGroup>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>

Bu açık hâli de derlenir ve çalışır. Sdk.props senin property'lerinden önce gelir ve varsayılanları kurar; Sdk.targets sonra gelir ve senin değerlerini kullanarak build adımlarını tanımlar. Proje türüne göre farklı SDK'lar var: class library ve console için Microsoft.NET.Sdk, ASP.NET Core için Microsoft.NET.Sdk.Web, background service için Microsoft.NET.Sdk.Worker, Razor class library için Microsoft.NET.Sdk.Razor. Web SDK, temel SDK'nın üzerine Razor, static web asset ve publish desteğini ekler.

Bu iki satırın ne kadar şey getirdiğini -preprocess gösterir. Bütün import'ları tek bir dosyada açar:

dotnet msbuild tools/Bookstore.Admin -preprocess:admin.pp.xml

16 satırlık Bookstore.Admin.csproj, SDK 10.0.404'te 115 ayrı .props ve .targets dosyasından oluşan 17.416 satırlık bir projeye dönüşüyor; Web SDK kullanan Bookstore.Api'de bu 169 dosya ve 23.258 satır. Bu dosyayı baştan sona okumazsın ama bir property'nin nerede tanımlandığını aramak için işe yarar. Örneğin Directory.Build.props'u ararsan 94. satırda, yani senin csproj içeriğinden çok önce, Sdk.props'un içinden import edildiğini görürsün. Bu sıranın önemini bir sonraki yazıda göreceğiz.

Bookstore.Api.csproj Satır Satır

Bookstore'un API projesi kısa:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <RootNamespace>Bookstore</RootNamespace>
    <UserSecretsId>bookstore-api</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>

RootNamespace, yeni eklenen dosyalarda IDE'nin önereceği namespace'in ve gömülü kaynak (embedded resource) adlarının başıdır. Varsayılanı proje adıdır, yani Bookstore.Api. API'deki kod Bookstore.Catalog, Bookstore.Ordering gibi namespace'ler kullandığı için kök Bookstore yapılmış. UserSecretsId, dotnet user-secrets'ın connection string'i hangi klasörde tutacağını belirler; repo'ya girmemesi gereken değerler orada durur. Paket satırlarında sürüm yok, çünkü Bookstore CPM (Central Package Management; paket sürümlerinin tek dosyada toplandığı düzen) kullanıyor. Peki TargetFramework, Nullable, ImplicitUsings nerede? Repo kökündeki Directory.Build.props'ta:

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

MSBuild bu dosyayı klasör ağacında yukarı doğru arayıp her projeye kendiliğinden ekler. Sık kullanılan property'lerin ne işe yaradığı:

PropertyNe yaparVarsayılan
TargetFrameworkHedeflenen .NET sürümü; birden fazlası için TargetFrameworksYok, zorunlu
OutputTypeExe ya da LibraryLibrary (Web ve Worker SDK'da Exe)
NullableNullable reference type analiziKapalı; template'ler açar
ImplicitUsingsSDK'ya göre global using'ler üretirKapalı; template'ler açar
LangVersionC# dil sürümüHedef framework'e göre; net10.0 için 14.0
AssemblyName, RootNamespaceÇıktı dosyasının adı, varsayılan namespaceİkisi de proje adı
TreatWarningsAsErrorsDerleyici uyarılarını hataya çevirirfalse
GenerateDocumentationFileXML doküman dosyası üretirfalse
InvariantGlobalizationICU (International Components for Unicode; culture verisini sağlayan library) olmadan çalışırfalse
UserSecretsIduser-secrets klasörünün kimliğiYok

LangVersion'ı elle yazmana genelde gerek yok. TFM (Target Framework Moniker; net10.0 gibi hedef framework adı) hangi C# sürümünün desteklendiğini zaten belirliyor. latest ya da preview yazmak, eski bir TFM'de runtime'ın desteklemediği dil özelliklerini açabilir; bu yüzden yalnız bilerek yap.

Projenin Son Hâlini Görmek: -getProperty, -getItem, -preprocess

Bir property'nin değerini tahmin etmek yerine sormak mümkün. dotnet msbuild'in -getProperty parametresi projeyi değerlendirir ve sonucu JSON olarak döner, build yapmaz:

dotnet msbuild src/Bookstore.Api -getProperty:TargetFramework -getProperty:OutputType -getProperty:LangVersion -getProperty:RootNamespace -getProperty:AssemblyName -getProperty:TreatWarningsAsErrors
{
  "Properties": {
    "TargetFramework": "net10.0",
    "OutputType": "Exe",
    "LangVersion": "14.0",
    "RootNamespace": "Bookstore",
    "AssemblyName": "Bookstore.Api",
    "TreatWarningsAsErrors": "true"
  }
}

Hiçbiri csproj'da yazmıyor ama hepsinin değeri belli. OutputType Web SDK'dan, LangVersion TFM'den, TargetFramework ve TreatWarningsAsErrors Directory.Build.props'tan geliyor. Item'lar için -getItem kullanılır. API projesinde appsettings dosyalarının nasıl işaretlendiğine bakalım (çıktının ilgili kısmı):

dotnet msbuild src/Bookstore.Api -getItem:Content
{
  "Items": {
    "Content": [
      {
        "Identity": "appsettings.Development.json",
        "ExcludeFromSingleFile": "true",
        "CopyToOutputDirectory": "PreserveNewest",
        "CopyToPublishDirectory": "PreserveNewest",
        ...
        "DefiningProjectName": "Sdk.StaticWebAssets.StaticAssets.ProjectSystem",
        "DefiningProjectExtension": ".props"
      },
      {
        "Identity": "appsettings.json",
        ...

Cevap burada: Web SDK, proje klasöründeki .json dosyalarını Content item'ı yapıyor ve CopyToOutputDirectory metadata'sını kendisi koyuyor. DefiningProjectName da bu kuralın hangi SDK dosyasından geldiğini söylüyor. Aynı komut Microsoft.NET.Sdk kullanan bookstore-admin'de Content item'ı döndürmüyor; orada appsettings.json metadata'sız bir None item'ı. Bu yöntemi bir sonraki yazılarda sık kullanacağız; "bu property gerçekte ne değerde?" sorusunun en kısa cevabı bu. Property'nin hangi dosyanın hangi satırında set edildiğini görmek için ise binary log gerekir; onu 8. yazıda göreceğiz.

Template'lerin Bıraktığı Tekrarları Temizlemek

dotnet new console ve dotnet new classlib, repo'da Directory.Build.props olup olmadığına bakmaz. Bir önceki yazıda oluşturduğumuz üç projenin her biri şunu taşıyordu:

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net10.0</TargetFramework>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
</PropertyGroup>

Bugün zararsız görünüyor, çünkü değerler aynı. Ama yarın Directory.Build.props'ta net11.0'a geçtiğinde bu üç proje net10.0'da kalır ve kimse fark etmez; csproj'daki değer Directory.Build.props'takini ezer. Tekrarlanan satırları sildik, bookstore-admin'e çıktı adını ekledik. Silmeden önce ve sonra aynı soruyu soruyoruz:

dotnet msbuild tools/Bookstore.Admin -getProperty:TargetFramework -getProperty:Nullable -getProperty:LangVersion -getProperty:AssemblyName -getProperty:RootNamespace
{
  "Properties": {
    "TargetFramework": "net10.0",
    "Nullable": "enable",
    "LangVersion": "14.0",
    "AssemblyName": "bookstore-admin",
    "RootNamespace": "Bookstore.Admin"
  }
}

Değerler aynı kaldı; değişen tek şey AssemblyName. RootNamespace'in bookstore-admin'den türetilmediğine dikkat et: varsayılanı proje adından alır, bu yüzden kod Bookstore.Admin namespace'inde kalır, çıktı dosyası ise Windows'ta bookstore-admin.exe ve bookstore-admin.dll olur. Komut satırı aracı için kullanıcının yazacağı ad bu olduğundan doğru tercih.

Davranış Değiştiren Property'ler

Bazı property'ler yalnız çıktıyı değil, build'in başarılı olup olmamasını ya da uygulamanın çalışma şeklini değiştirir. Bookstore.Contracts 4. yazıda paket olarak yayımlanacak; paketi kullanan geliştiriciler IDE'de method açıklamalarını görsün diye XML doküman dosyası üretmesini istiyoruz:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
C:\src\Bookstore\src\Bookstore.Contracts\Isbn.cs(3,31): error CS1591: Missing XML comment for publicly visible type or member 'Isbn' [C:\src\Bookstore\src\Bookstore.Contracts\Bookstore.Contracts.csproj]
C:\src\Bookstore\src\Bookstore.Contracts\Isbn.cs(7,19): error CS1591: Missing XML comment for publicly visible type or member 'Isbn.Value' [C:\src\Bookstore\src\Bookstore.Contracts\Bookstore.Contracts.csproj]
C:\src\Bookstore\src\Bookstore.Contracts\BookSummary.cs(3,22): error CS1591: Missing XML comment for publicly visible type or member 'BookSummary' [C:\src\Bookstore\src\Bookstore.Contracts\Bookstore.Contracts.csproj]
...
    11 Error(s)

GenerateDocumentationFile açılınca derleyici dokümanı olmayan her public üye için CS1591 uyarısı verir. Directory.Build.props'taki TreatWarningsAsErrors bu uyarıları hataya çeviriyor ve build kırılıyor. İki yol var: dokümanı yazmak ya da bu projede <NoWarn>$(NoWarn);CS1591</NoWarn> ile uyarıyı kapatmak. Paket olacak bir library için ilki doğru; public tipler az olduğu için açıklamaları yazdık:

/// <summary>A validated 13-digit International Standard Book Number.</summary>
public readonly record struct Isbn
{
    private Isbn(string value) => Value = value;

    /// <summary>The 13 digits without hyphens or spaces.</summary>
    public string Value { get; }

Build geçti ve bin/Debug/net10.0/ altında Bookstore.Contracts.xml oluştu. NoWarn yazarken $(NoWarn); önekini unutma; unutursan SDK'nın ve Directory.Build.props'un daha önce eklediği değerleri silersin.

İkinci örnek InvariantGlobalization. Açınca uygulama ICU yüklemeden çalışır, çıktı küçülür ve Linux container image'ında ICU paketine gerek kalmaz; 6. yazıda Native AOT (Ahead-of-Time; uygulamayı çalışmadan önce makine koduna derleme) için bunu ölçeceğiz. Ama Bookstore fiyatları Türk lirası olarak gösteriyor ve bookstore-admin'in price komutu tr-TR culture'ını kullanıyor. Property'yi komut satırından geçici olarak açıp deneyelim:

dotnet run --project tools/Bookstore.Admin -- price 1249.5
dotnet run --project tools/Bookstore.Admin -p:InvariantGlobalization=true -- price 1249.5
₺1.249,50
Unhandled exception. System.Globalization.CultureNotFoundException: Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information. (Parameter 'name')
tr-tr is an invalid culture identifier.
   at System.Globalization.CultureInfo.GetCultureInfo(String name)

Invariant modda yalnız invariant culture var ve GetCultureInfo("tr-TR") exception fırlatıyor. Bu yüzden bookstore-admin'de property'yi açmadık. -p:Name=Value sözdizimi her dotnet komutunda çalışır ve bir property'yi dosyayı değiştirmeden denemenin en hızlı yoludur.

Item'lar: Compile, None, Content ve Glob'lar

SDK-style projede dosyaları tek tek listelemezsin. SDK, proje klasöründeki **/*.cs dosyalarını Compile, kalan dosyaları None item'ı olarak ekler; bin/ ve obj/ hariç tutulur. Web SDK buna wwwroot/** ve .json dosyaları için Content item'larını ekler. Eski .NET Framework projelerinden kalan alışkanlıkla bir dosyayı ayrıca listelersen:

<ItemGroup>
  <Compile Include="Isbn.cs" />
</ItemGroup>
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: 'Isbn.cs' [C:\src\Bookstore\src\Bookstore.Contracts\Bookstore.Contracts.csproj]

Bir dosyayı derlemeden çıkarmak için <Compile Remove="Legacy/**" />, var olan bir item'ın metadata'sını değiştirmek için Update kullanılır. Include yeni item ekler, Update glob'un zaten eklediği item'ı değiştirir. Baştaki appsettings.json sorununun çözümü de bu:

<ItemGroup>
  <None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Runtime:     .NET 10.0.15
Environment: (not set)
API:         http://localhost:5080

PreserveNewest dosyayı yalnız değiştiğinde kopyalar; Always her build'de kopyalar ve incremental build'i gereksiz yere yavaşlatır. None item'ı için CopyToOutputDirectory verildiğinde dosya publish çıktısına da gider. dotnet publish tools/Bookstore.Admin çıktısında artık appsettings.json, bookstore-admin.exe, bookstore-admin.dll ve Bookstore.Contracts.dll yan yana duruyor.

PackageReference, ProjectReference ve InternalsVisibleTo

PackageReference item'ının metadata'sı, paketin hangi parçalarının projene ve projene referans veren projelere geçeceğini belirler. Bookstore.Api'deki Microsoft.EntityFrameworkCore.Design bunun tipik örneği: migration üretirken dotnet ef aracının ihtiyaç duyduğu, ama uygulamanın çalışırken kullanmadığı bir paket. PrivateAssets değeri all olduğu için paket, API'ye referans veren test projesine geçmiyor:

dotnet package list --project tests/Bookstore.Api.Tests --include-transitive
   Transitive Package                                             Resolved
   ...
   > Microsoft.EntityFrameworkCore                                10.0.15
   > Microsoft.EntityFrameworkCore.Abstractions                   10.0.15
   > Microsoft.EntityFrameworkCore.Analyzers                      10.0.15
   > Microsoft.EntityFrameworkCore.Relational                     10.0.15
   > Microsoft.EntityFrameworkCore.SqlServer                      10.0.15
   ...

SqlServer paketi transitive olarak geldi, Design gelmedi. IncludeAssets satırı ise listede compile olmadığı için API kodunun bu paketin tiplerini yanlışlıkla kullanmasını engelliyor. Analyzer, source generator ve build aracı paketlerinde de aynı ikiliyi görürsün.

ProjectReference başka bir projeyi build'e ve referanslara ekler; paketlerin aksine sürümü yoktur, her build'de kaynak koddan derlenir. Testlerde sık karşılaşılan bir ihtiyaç da internal tiplere erişmek. Bookstore.Contracts'ta ISBN'in (International Standard Book Number; kitaplara verilen uluslararası numara) kontrol hanesini hesaplayan kodu internal bir IsbnChecksum sınıfına taşıdık ve test projesinden doğrudan test etmek istedik:

C:\src\Bookstore\tests\Bookstore.Contracts.Tests\IsbnChecksumTests.cs(13,32): error CS0122: 'IsbnChecksum' is inaccessible due to its protection level [C:\src\Bookstore\tests\Bookstore.Contracts.Tests\Bookstore.Contracts.Tests.csproj]

Eskiden bunun için AssemblyInfo.cs'e attribute yazılırdı. SDK artık bunu bir item olarak alıyor ve attribute'u build sırasında kendisi üretiyor:

<ItemGroup>
  <InternalsVisibleTo Include="Bookstore.Contracts.Tests" />
</ItemGroup>

Üretilen dosyada (obj/Debug/net10.0/Bookstore.Contracts.AssemblyInfo.cs) karşılığı olan satır görünüyor ve testler geçiyor:

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Bookstore.Contracts.Tests")]
Test run summary: Passed!
  total: 7
  failed: 0
  succeeded: 7
  skipped: 0

Test projesi de temizlendi: OutputType, PackageReference ve ProjectReference dışında satırı kalmadı. Xunit namespace'i için Bookstore.Api.Tests'teki gibi <Using Include="Xunit" /> item'ı eklenebilir; ImplicitUsings'in ürettiği global using listesine bu item'la kendi namespace'lerini ekleyebilirsin.

Üretimde Dikkat

Bir property'yi değiştirmeden önce ve sonra dotnet msbuild -getProperty ile değerini kontrol et. Özellikle çok projeli solution'larda bir değerin csproj'dan mı, Directory.Build.props'tan mı, SDK'dan mı geldiğini tahmin etmek hata üretir. CI'da kritik property'leri (TFM, TreatWarningsAsErrors, Nullable) bu komutla doğrulayan küçük bir adım, yanlışlıkla kapatılan bir ayarı erken yakalar.

InvariantGlobalization, PublishTrimmed gibi çalışma anı davranışını değiştiren property'leri ilk kez açtığında uygulamayı gerçekten çalıştırarak test et. Derleme geçer; hata ancak ilgili kod yolunda, production'da çıkar.

Uyarıları toptan susturmak yerine NoWarn'ı proje bazında ve gerekçesiyle kullan. Hangi analyzer kuralının ne zaman hata olacağını ekip olarak belirlemek Statik Analiz serisinin konusu.

Sık Düşülen Tuzaklar

appsettings.json'un çıktıya ya da publish klasörüne gitmemesi. Web SDK bu dosyaları kendiliğinden Content yapar, console ve class library projelerinde ise metadata'sız None kalırlar. Worker'dan console'a geçen ya da console'da konfigürasyon okuyan her proje bu hatayı en az bir kez yaşar; None Update satırını ekle.

Eski .NET Framework csproj alışkanlıklarını taşımak. Dosyaları tek tek listelemek NETSDK1022 verir; EnableDefaultCompileItems'ı kapatmak ise yeni eklenen dosyaların sessizce derlenmemesine yol açar. Glob'lara güven, istisnaları Remove ile yaz.

Template'ten gelen property'leri olduğu gibi bırakmak. Directory.Build.props'taki ortak ayar değiştiğinde csproj'daki eski değer onu ezer ve projeler sessizce farklı TFM'lerde kalır.

NoWarn'ı $(NoWarn); olmadan yazmak. SDK'nın ve üst dosyaların eklediği değerleri silersin; daha önce görünmeyen uyarılar bir anda hataya dönüşür.

LangVersion'ı latest yapmak. Eski TFM'lerde runtime'ın desteklemediği özellikler açılabilir, SDK güncellenince de dil sürümü kendiliğinden değişir. Varsayılan TFM'ye göre seçilir ve genelde doğrusu odur.

Küçük Bir Deneme

★☆☆ dotnet msbuild src/Bookstore.Api -getItem:Using ile Web SDK'nın ürettiği global using listesini al ve tools/Bookstore.Admin için aynı komutun çıktısıyla karşılaştır. Web SDK hangi namespace'leri fazladan ekliyor?

★★☆ bookstore-admin'e data/ klasöründe birkaç örnek JSON dosyası ekle ve bunları tek bir None Update="data/**/*.json" satırıyla çıktıya kopyala. Sonra bu dosyaların publish'e gitmesini engelle ama build çıktısında kalmasını sağla (ipucu: CopyToPublishDirectory).

★★★ dotnet msbuild tools/Bookstore.Admin -preprocess:admin.pp.xml ile açılmış dosyada AssemblyName ve RootNamespace'in varsayılan değerlerinin hangi SDK dosyasında ve hangi koşulla set edildiğini bul. AssemblyName'i değiştirmenin RootNamespace'i neden etkilemediğini bu satırlarla açıkla.

Bu yazıda tek bir proje dosyasının içine baktık ve TargetFramework'ün aslında Directory.Build.props'tan geldiğini gördük. Bir sonraki yazıda bu dosyayı ve kardeşi Directory.Build.targets'ı, CPM'i ve çok projeli bir solution'da ortak ayarların hangi sırayla değerlendirildiğini ayrıntılı ele alacağız.