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

Publish Seçenekleri: Framework-Dependent, Self-Contained, Single-File, Trimming, ReadyToRun ve Native AOT

Mert Özen 15 Kas 2026 11 dk 6 görüntülenme
Publish Seçenekleri: Framework-Dependent, Self-Contained, Single-File, Trimming, ReadyToRun ve Native AOT

Framework-dependent publish en küçük çıktıyı verir ve runtime yamalarını makineden alır; self-contained, single-file, trimming ve Native AOT dağıtımı kolaylaştırır ama yamayı yeniden publish'e bağlar. bookstore-admin'i yedi modda publish edip boyut, başlangıç süresi ve belleği ölçüyor; trimming'in R2R'sız başlangıcı neden yavaşlattığını, trim uyarılarını build'e taşımayı, AOT'nin cross-OS sınırını ve publish profile'ını gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.404, runtime 10.0.15) ile derlenip denenmiştir; PublishAot çıktısı mcr.microsoft.com/dotnet/sdk:10.0.404-noble-aot image'ında, UseOptimizedPublish denemesi SDK 11.0.100 ile alınmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) küçük yönetim aracı bookstore-admin'i .NET kurulu olmayan Windows makinelerine tek bir exe olarak dağıtmak istiyoruz. Self-contained single-file publish 70.2 MB tutuyor. Üstüne PublishTrimmed ekleyince dosya 12.6 MB'a iniyor. Peki aracın açılış süresi ne olur: kısalır mı, aynı mı kalır, uzar mı?

Ölçtüğümüzde uzadı: 16 ms'den 21 ms'ye. Trimming kullanılmayan kodu atarken framework assembly'lerinin içindeki önceden derlenmiş kodu da atıyor; geri kalan her method'u çalışma anında JIT (Just-In-Time; method'u ilk çağrıldığında makine koduna çeviren derleyici) derliyor. Bu yazıda dotnet publish'in seçeneklerini tek tek açıp aynı aracı yedi şekilde publish ediyor, boyut ve başlangıç süresini yan yana koyuyoruz.

dotnet publish Ne Üretir?

dotnet publish tools/Bookstore.Admin
  Bookstore.Contracts -> C:\src\Bookstore\artifacts\bin\Bookstore.Contracts\release_net10.0\Bookstore.Contracts.dll
  Bookstore.Admin -> C:\src\Bookstore\artifacts\bin\Bookstore.Admin\release\bookstore-admin.dll
  Bookstore.Admin -> C:\src\Bookstore\artifacts\publish\Bookstore.Admin\release\

Klasör adındaki release'e dikkat et: .NET 8'den beri dotnet publish varsayılan olarak Release configuration kullanıyor, -c Release yazmak gerekmiyor. dotnet build ise Debug'da kaldı. 3. yazıda kurduğumuz artifacts düzeni sayesinde publish çıktısı da artifacts/publish altında toplandı.

Bu varsayılan çıktı framework-dependent: makinede .NET runtime'ı kurulu olmalı. Çalıştırılacak runtime sürümünü bookstore-admin.runtimeconfig.json belirliyor:

{
  "runtimeOptions": {
    "tfm": "net10.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "10.0.0"
    },
    ...
  }
}

10.0.0 bir alt sınır. Host, makinedeki en yüksek 10.0.x patch'ini seçiyor; 1. yazıda anlattığımız roll forward kuralı. Yani Microsoft Update ya da paket yöneticisi runtime'ı 10.0.16'ya yükseltince araç yeniden publish edilmeden güvenlik yamasını alır. Framework-dependent publish'in asıl avantajı bu; boyutun küçük olması ikinci planda.

RuntimeIdentifier, Self-Contained ve Single-File

Çıktıyı belirli bir platforma bağlamak için RID (Runtime Identifier; win-x64, linux-x64 gibi işletim sistemi ve işlemci mimarisi adı) verilir. RID verince SDK o platformun apphost'unu (bookstore-admin.exe) koyar ve platforma özel native dosyaları seçer. --self-contained eklenirse runtime'ın kendisi de çıktıya kopyalanır:

dotnet publish tools/Bookstore.Admin -r win-x64 --self-contained false -o artifacts/fd
dotnet publish tools/Bookstore.Admin -r win-x64 --self-contained -o artifacts/sc
dotnet publish tools/Bookstore.Admin -r win-x64 --self-contained -p:PublishSingleFile=true -o artifacts/single

Framework-dependent klasörde pdb'ler hariç yedi dosya var:

   9216 Bookstore.Contracts.dll
   2990 Bookstore.Contracts.xml
     67 appsettings.json
    869 bookstore-admin.deps.json
   6656 bookstore-admin.dll
 162304 bookstore-admin.exe
    330 bookstore-admin.runtimeconfig.json

Self-contained klasörde dosya sayısı 194'e, boyut 76.9 MB'a çıkıyor; System.*.dll'ler, coreclr.dll ve diğer runtime dosyaları artık aracın parçası. runtimeconfig.json da değişiyor:

    "includedFrameworks": [
      {
        "name": "Microsoft.NETCore.App",
        "version": "10.0.15"
      }
    ],

Burada bir alt sınır yok, sabit bir sürüm var. Araç, publish edildiği gün SDK'nın getirdiği 10.0.15 runtime'ıyla yaşar; makinedeki .NET güncellemeleri ona dokunmaz. 10.0.16 bir güvenlik açığını kapattığında self-contained araca o düzeltmeyi götürmenin tek yolu yeni bir SDK ile tekrar publish edip tekrar dağıtmak.

Single-file, self-contained klasörü tek bir exe'de topluyor: 70.2 MB'lık bookstore-admin.exe, yanında yalnız appsettings.json ve Bookstore.Contracts.xml var. appsettings.json içeride değil, çünkü content dosyaları ayrı kalıyor; aracın dosyayı AppContext.BaseDirectory'den okuması bu yüzden doğru. Assembly.Location single-file'da boş string döner; dosya yolunu ondan türeten kod burada bozulur, SDK'nın single-file analyzer'ı da bunu uyarıyla gösterir. EnableCompressionInSingleFile ile exe 35.9 MB'a iniyor ama her açılışta açılması gereken bir arşive dönüşüyor; bizim ölçümümüzde başlangıç süresi 16 ms'den 43 ms'ye çıktı.

XML dosyası nereden geldi? 2. yazıda Bookstore.Contracts'ta GenerateDocumentationFile'ı açmıştık; SDK referans verilen projelerin XML dokümantasyonunu da publish klasörüne kopyalıyor. Bir CLI aracı için gereksiz; PublishReferencesDocumentationFiles property'si bunu kapatıyor.

Trimming ve Trim Uyarıları

Trimming, publish sırasında ILLink aracıyla uygulamanın ve framework'ün kullanılmayan kodunu siliyor. Yalnız self-contained çıktıda anlamlı, çünkü framework'ün kendisi de trim ediliyor:

dotnet publish tools/Bookstore.Admin -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -o artifacts/trim

Exe 12.6 MB'a indi. Trimming statik analizle çalışır: kod hangi tipe nasıl ulaşıyorsa ILLink onu görmek zorunda. Reflection ile çalışan kod bu analizi atlatır. Bunu görmek için aracın appsettings.json okuyan kısmını, JsonDocument yerine reflection kullanan JsonSerializer.Deserialize<AdminSettings> ile yazdık. dotnet build sessizce geçti; uyarı ancak trimmed publish'te geldi. Bunu build'e taşımak için projeye tek property ekledik:

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <AssemblyName>bookstore-admin</AssemblyName>
  <!-- Trim, single-file ve AOT analyzer'ları yalnız publish'te değil her build'de çalışsın. -->
  <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
C:\src\Bookstore\tools\Bookstore.Admin\Program.cs(30,31): error IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize<TValue>(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [C:\src\Bookstore\tools\Bookstore.Admin\Bookstore.Admin.csproj]
C:\src\Bookstore\tools\Bookstore.Admin\Program.cs(30,31): error IL3050: Using member 'System.Text.Json.JsonSerializer.Deserialize<TValue>(String, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications. [C:\src\Bookstore\tools\Bookstore.Admin\Bookstore.Admin.csproj]

IsAotCompatible, trim, single-file ve AOT (Ahead-of-Time; uygulamayı publish sırasında tamamen makine koduna derlemek) analyzer'larını birlikte açıyor; TreatWarningsAsErrors da uyarıları hataya çeviriyor. Artık bu tür bir değişiklik pull request'te, publish'ten önce yakalanır. Aynı property'yi paket olarak dağıttığımız Bookstore.Contracts'a da ekledik; library'yi kullanan uygulama trim ya da AOT ile publish ederken sürpriz yaşamasın. Kodun düzeltilmesi (System.Text.Json source generator'ı ve JsonSerializerContext) ASP.NET Core Web API serisinin Native AOT yazısında anlatıldı; biz denemeyi geri alıp JsonDocument'te kaldık, çünkü o zaten trim-safe.

ReadyToRun

ReadyToRun (R2R), IL (Intermediate Language; C# derleyicisinin ürettiği, platformdan bağımsız ara kod) yanına önceden derlenmiş makine kodu koyar. Uygulama başlarken JIT'in yapacağı işin büyük kısmı hazır olur. .NET'in kendi framework assembly'leri zaten R2R olarak gelir. Bu yüzden framework-dependent bir uygulamada R2R yalnız senin assembly'lerini etkiler: bookstore-admin.dll 6.5 KB'tan 20 KB'a büyüdü, başlangıç süresi değişmedi. Bu kadar küçük bir araçta derlenecek kod zaten az.

Girişteki sorunun cevabı da burada. ILLink assembly'leri yeniden yazarken R2R kodunu korumuyor; trimmed framework saf IL'e dönüyor ve her şeyi JIT derliyor. PublishReadyToRun'ı trimming'le birlikte açınca SDK trim edilmiş assembly'leri yeniden R2R derliyor:

dotnet publish tools/Bookstore.Admin -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -p:PublishReadyToRun=true -o artifacts/trim-r2r

Exe 12.6 MB'tan 16.6 MB'a çıktı, başlangıç 21 ms'den 12 ms'ye indi. R2R cross-compile edebiliyor; win-x64 için başka bir işletim sisteminde de derleyebilirsin. Native AOT için bu geçerli değil.

Native AOT

Native AOT, uygulamanın tamamını publish sırasında makine koduna çevirir ve JIT'i tamamen kaldırır. Çıktı tek bir native executable; runtime, trim edilmiş kütüphanelerle birlikte içine gömülü. Trimming her zaman açıktır ve reflection'a, çalışma anında kod üretmeye dayanan kod ya çalışmaz ya da uyarı verir; IsAotCompatible'ı bu yüzden önceden açtık.

AOT derlemesi hedef işletim sisteminin native linker'ını kullanır, bu yüzden cross-OS derleme yapmaz: farklı bir işletim sistemi için denediğinde Cross-OS native compilation is not supported. hatası alırsın. Windows'ta win-x64 için publish etmek Visual Studio'nun C++ araçlarını ("Desktop development with C++" workload'u) ister; bu kurulumu ve Windows çıktısını bu yazıda denemedik. Bookstore'da AOT'nin kullanılacağı yer Linux container'ları, o yüzden Microsoft'un AOT için gereken araçlarla gelen SDK image'ında linux-x64 için publish ettik:

docker run --rm -v ${PWD}:/src -w /src mcr.microsoft.com/dotnet/sdk:10.0.404-noble-aot dotnet publish tools/Bookstore.Admin -r linux-x64 -p:PublishAot=true -o artifacts/aot
  Bookstore.Admin -> /src/artifacts/bin/Bookstore.Admin/release_linux-x64/bookstore-admin.dll
  Generating native code
  Bookstore.Admin -> /src/artifacts/aot/
-rw-r--r-- 1 root root   11828 Nov 15 06:15 Bookstore.Contracts.pdb
-rw-r--r-- 1 root root    2990 Nov 15 06:15 Bookstore.Contracts.xml
-rw-r--r-- 1 root root      67 Nov 15 06:14 appsettings.json
-rwxr-xr-x 1 root root 1649048 Nov 15 06:15 bookstore-admin
-rwxr-xr-x 1 root root 2810728 Nov 15 06:15 bookstore-admin.dbg

Executable 1.6 MB; .dbg dosyası ayrı tutulan debug sembolleri, dağıtıma girmez. bookstore-admin info container'da .NET 10.0.15 yazdı.

AOT projelerinde sık önerilen bir ayar InvariantGlobalization: ICU (International Components for Unicode; işletim sisteminin dil ve bölge kuralları kütüphanesi) üzerinden gelen kültür verisini devre dışı bırakır. Boyut bizde 1.65 MB'tan 1.53 MB'a indi, ama price komutu çöktü:

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) + 0x184
   at Program.<<Main>$>g__FormatPrice|0_3(String) + 0x74
   at Program.<Main>$(String[] args) + 0xdd

Araç fiyatı tr-TR kültürüyle biçimlendiriyor; invariant modda bu kültür yok. Yaklaşık 120 KB için davranış değiştirmeye değmez, ayarı kapalı bıraktık. Bu ayarın Linux container'larında asıl faydası boyut değil, image'da ICU kütüphanesine gerek kalmaması; o kararı Docker serisinde image seçerken vermek daha doğru.

Yedi Modun Karşılaştırması

06-publish-size-startup-tr.png

Boyutlar win-x64 publish'lerinden (AOT hariç, o linux-x64), pdb ve .dbg dosyaları sayılmadan. Başlangıç süresi ve bellek, aynı modları yazının hazırlandığı makinenin kendi RID'iyle publish edip bookstore-admin isbn 978-0-441-17271-9 komutunu üç kez boşa çalıştırdıktan sonra 40 kez çalıştırarak ölçüldü; değerler medyan. Bellek, işlemin ulaştığı en yüksek RSS (Resident Set Size; işlemin fiziksel bellekte tuttuğu alan). Mutlak değerler senin Windows makinende farklı çıkar; bakılacak yer modlar arasındaki fark.

ModDosyaBoyutBaşlangıçBellek
Framework-dependent70.2 MB18 ms40 MB
Framework-dependent + R2R70.2 MB18 ms40 MB
Self-contained19476.9 MB18 ms40 MB
Single-file370.2 MB16 ms37 MB
Single-file + trim312.6 MB21 ms18 MB
Single-file + trim + R2R316.6 MB12 ms23 MB
Native AOT (linux-x64)31.6 MB3 ms7 MB

Tablodan çıkan sonuçlar: framework-dependent ile self-contained aynı runtime'ı çalıştırdığı için hızları aynı, fark yalnız dağıtımda. Trimming boyutu ve belleği yarıdan fazla düşürüyor, R2R olmadan başlangıcı yavaşlatıyor. Native AOT başlangıçta ve bellekte açık ara önde, ama derleme hedef platformda yapılmalı ve kodun AOT uyumlu olması gerekiyor. 18 ms'lik bir CLI aracı için bunların hiçbiri kullanıcıya hissettirmez; seçimi hız değil dağıtım belirler. Aynı tablo, saniyede binlerce kez başlatılan bir serverless function'da ya da yüzlerce replica'sı olan bir serviste çok farklı bir anlam taşır.

Publish Profile ile Seçimi Kalıcı Yapmak

Uzun komut satırını her seferinde yazmak yerine seçimi bir publish profile'ına koyduk. Mağaza personelinin .NET kurulu olmayan Windows makineleri için trimmed ve R2R single-file:

<Project>
  <!-- .NET runtime'ı olmayan Windows personel makineleri için tek exe: dotnet publish -p:PublishProfile=win-x64 -->
  <PropertyGroup>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <SelfContained>true</SelfContained>
    <PublishSingleFile>true</PublishSingleFile>
    <PublishTrimmed>true</PublishTrimmed>
    <PublishReadyToRun>true</PublishReadyToRun>
    <PublishReferencesDocumentationFiles>false</PublishReferencesDocumentationFiles>
  </PropertyGroup>
</Project>
dotnet publish tools/Bookstore.Admin -p:PublishProfile=win-x64
  Bookstore.Admin -> C:\src\Bookstore\artifacts\publish\Bookstore.Admin\release_win-x64\

Dosya tools/Bookstore.Admin/Properties/PublishProfiles/win-x64.pubxml'da, ve dotnet publish profile adını bu klasörde arıyor. Çıktıda exe, appsettings.json ve pdb'ler kaldı; XML dosyası gitti. Commit'ten önce git status profili göstermedi:

git check-ignore -v tools/Bookstore.Admin/Properties/PublishProfiles/win-x64.pubxml
.gitignore:195:*.pubxml	tools/Bookstore.Admin/Properties/PublishProfiles/win-x64.pubxml

dotnet new gitignore'un ürettiği dosya *.pubxml'i yok sayıyor, çünkü Visual Studio'nun web deploy profilleri bağlantı bilgisi ve şifre taşıyabiliyor. Bizim profilimizde secret yok, .gitignore'a bir istisna satırı ekledik: !**/PublishProfiles/*.pubxml. Profile secret koymak gerekiyorsa o bilgi profile değil, CI'ın secret store'una ait.

Container image'ı da publish'in bir hedefi: .NET 10'dan beri console uygulamaları da ek property olmadan dotnet publish -t:PublishContainer ile image üretebiliyor. Image seçimi, katmanlar ve registry konusu Docker serisinde; burada çalıştırmadık. .NET 11 bu alanda iki şey getirdi: SOURCE_DATE_EPOCH ortam değişkeniyle aynı girdilerden aynı image digest'inin çıkması (reproducible container) ve Native AOT publish'te tam Build target'ını atlayan UseOptimizedPublish. İkincisi SDK 11.0.100'de opt-in; SDK'nın Microsoft.NET.Publish.targets dosyası varsayılanı açıkça false yapıyor. Açtığımızda bin klasörüne yaklaşık 200 dosyalık managed çıktı yazılmadı, ama AfterTargets="Build" ile bağlanan custom target'lar da çalışmaz; 8. yazıda target yazarken bunu hatırlayacağız.

Üretimde Dikkat

Self-contained ve AOT çıktıları için bir yeniden publish takvimi tut. Framework-dependent bir araç makinedeki runtime güncellemesiyle yamalanır; self-contained olan yalnız sen tekrar publish edince yamalanır. .NET'in aylık servis sürümü çıktığında CI'da SDK'yı güncelleyip bu çıktıları yeniden üretmek, takvime bağlı bir iş olmalı. Kimin hangi sürümü çalıştırdığını görmek için bookstore-admin info'nun yazdığı runtime satırı yeterli.

Ölçümü kendi ortamında yap. Bizim tablomuz küçük bir CLI aracının, tek bir makinedeki değerleri. Bir web API'de başlangıç süresine bağımlılık yükleme, configuration ve ilk istekler de girer; R2R ve AOT'nin etkisi orada farklı çıkar. Karar vermeden önce kendi uygulamanı aynı modlarla publish edip ölç.

Trimming ya da AOT'yi açtığın gün tüm testleri o çıktı üzerinde de çalıştır. Analyzer'lar statik analizle bulabildiklerini bulur; reflection'la yüklenen bir plugin, configuration'dan okunan bir tip adı ya da serialization'a giden bir tip çalışma anına kadar görünmeyebilir. Uyarısız bir trimmed publish iyi bir işaret, kanıt değil.

Sık Düşülen Tuzaklar

Trim uyarılarını bastırmak. SuppressTrimAnalysisWarnings ya da NoWarn ile IL2026'yı susturmak publish'i yeşile çevirir, ama silinen tip çalışma anında MissingMethodException ya da sessizce boş kalan bir nesne olarak geri gelir. Uyarıyı kodu değiştirerek kapat.

Self-contained uygulamada güvenlik yamasını unutmak. Makinede .NET 10.0.16 kurulu olması self-contained araca bir şey kazandırmaz; araç kendi içindeki 10.0.15 ile çalışmaya devam eder.

AOT'yi her platform için tek makinede derlemeye çalışmak. Native AOT cross-OS derleme yapmaz; CI'da her hedef işletim sistemi için o sistemde çalışan bir job ya da container gerekir. R2R ve trimming'de bu kısıt yok.

Single-file'da dosya yolunu Assembly.Location'dan türetmek. Single-file'da bu değer boş; yanındaki dosyalar için AppContext.BaseDirectory kullan.

Publish profile'ının .gitignore yüzünden commit'e girmediğini fark etmemek. Ekip arkadaşın aynı komutu çalıştırdığında profil bulunamadığı için hata alır.

Küçük Bir Deneme

★☆☆ dotnet publish tools/Bookstore.Admin -p:PublishProfile=win-x64 ile exe'yi üret ve info, isbn, price komutlarını çalıştır. Sonra profile EnableCompressionInSingleFile ekle; boyutu ve PowerShell'de Measure-Command ile ölçtüğün açılış süresini not et.

★★☆ Aynı aracı framework-dependent ve self-contained olarak publish et, iki çıktının runtimeconfig.json dosyalarını karşılaştır. Makinene bir sonraki 10.0.x runtime'ı geldiğinde iki aracın info çıktısında hangi sürümü yazdığını kontrol et.

★★★ ReadApiBaseUrl'ü JsonSerializer.Deserialize<AdminSettings> ile yaz, IL2026 ve IL3050 hatalarını gör, sonra bir JsonSerializerContext ile hataları kaldır. AOT çıktısını container'da üretip info'nun hâlâ doğru adresi yazdığını doğrula.

Bu yazıda bookstore-admin'i dağıtmanın yollarını ölçtük. Sıradaki yazıda onu başka bir yoldan dağıtacağız: bir .NET tool olarak paketleyip ekibin local tool manifest'ine ekleyecek, dnx ile kurmadan çalıştıracak ve yeni module'ler için bir dotnet new template'i yazacağız.