.NET SDK'da .NET 6'dan 11'e Değişenler: Varsayılan Release Publish, Terminal Logger, .slnx, dnx, File-Based App'ler ve .NET 11 CLI
SDK'yı yükseltirken kırılan şey genellikle kod değil, sürümü tekrar eden yerlerdir: global.json'un errorMessage'ı, container image etiketleri ve CI adımları. .NET 6'dan 11'e SDK değişikliklerini tabloya döküyor, eski bir projeyi okumak için kontrol listesi veriyor, Bookstore'u net10.0'da tutarak SDK 11'e taşıyor ve MSBuild server'ın CI'daki etkisini ölçümlerle gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET SDK 11.0.100 ve 10.0.404 ile derlenip denenmiştir; uygulamalar net10.0 hedefliyor ve .NET 10.0.15 runtime'ında çalışıyor. PublishAot çıktısı mcr.microsoft.com/dotnet/sdk:11.0.100-resolute-aot image'ında alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) global.json'unda tek satırı değiştirdik: "version": "10.0.404" yerine "11.0.100". Uygulamalar yine net10.0 hedefliyor; değişen yalnız build'i yapan SDK. Henüz güncellenmemiş, yalnız SDK 10.0.404 kurulu bir CI agent'ında build şöyle bitti:
The command could not be loaded, possibly because:
* You intended to execute a .NET application:
The application 'build' does not exist or is not a managed .dll or .exe.
* You intended to execute a .NET SDK command:
Bookstore needs .NET SDK 10.0.4xx (10.0.404 or later). Install it from https://dotnet.microsoft.com/download/dotnet/10.0
Agent'ta 10.0.404 kurulu ve mesaj tam olarak onu istiyor. Hata nerede? Mesajı 1. yazıda global.json'un errorMessage alanına biz yazmıştık; sürümü değiştirdik ama mesajı unuttuk. Yanlış yönlendiren bir hata mesajı, hiç mesaj olmamasından daha çok zaman kaybettirir. Serinin bu son yazısında .NET 6'dan 11'e SDK'da nelerin değiştiğine bakıyor, eski bir projeyi okurken neye dikkat edileceğini listeliyor ve Bookstore'u SDK 11'e taşırken gerçekten nelerin kırıldığını gösteriyoruz.
.NET 6'dan 9'a: Bugünkü Varsayılanlar Nereden Geldi?
Bu serinin büyük kısmı, son beş yılda SDK'ya giren varsayılanların üzerine kuruldu. Her maddeyi yazım günü Microsoft Learn'deki "what's new" ve breaking change sayfalarından kontrol ettik:
| Sürüm | SDK'daki değişiklik | Bu seride |
|---|---|---|
| .NET 6 | Template'lerde ImplicitUsings ve Nullable; dotnet workload | 1. ve 2. yazı |
| .NET 7 | Central Package Management; dotnet publish ile container image; dotnet new install (önceki --install) | 3. yazı |
| .NET 8 | publish ve pack'te Release varsayılanı; artifacts output; -getProperty/-getItem/-getTargetResult; Source Link ve InformationalVersion'da commit; NuGetAudit; terminal logger (opt-in) | 2., 3., 6. ve 8. yazı |
| .NET 9 | Terminal logger varsayılan; workload set'leri; 9.0.200 ile .slnx ve dotnet sln migrate; dotnet run -e | 1. ve 3. yazı |
| .NET 10 | dotnet new sln artık .slnx; file-based app'ler; dnx; dotnet test'te Microsoft Testing Platform; noun-first komutlar; net10.0 projelerde NuGetAuditMode=all | 1., 4. ve 7. yazı |
| .NET 11 | Native AOT (Ahead-of-Time; önceden makine koduna derleme) ile derlenmiş CLI host; MSBuild server varsayılan açık; solution filter komutları; minimum publish age; NETSDK1235; UseOptimizedPublish | 1., 3., 4., 6. ve 7. yazı |
Tablonun iki satırı eski bir projeyi yükseltirken sessiz davranış değişikliği yaratır. .NET 8 ile dotnet publish Release'e geçti; script'inde bin/Debug/.../publish yolunu arayan bir pipeline o gün bozuldu. Aynı sürümde RuntimeIdentifier vermek artık self-contained anlamına gelmiyor; eskiden -r win-x64 ile runtime'ı da paketleyen bir komut, .NET 8 hedefli projede framework-dependent çıktı üretir. 6. yazıda --self-contained'ı her komutta açıkça yazmamızın sebebi bu.
.NET 9'da terminal logger varsayılan oldu. Terminalde ilerlemeyi canlı gösteren bu çıktı CI log'larında da kullanılmaya çalışılır; log'u satır satır işleyen bir araç kullanıyorsan MSBUILDTERMINALLOGGER=off ya da --tl:off eski düz çıktıyı geri getirir. Bu serideki build çıktılarının bir kısmını bu yüzden -tl:off ile aldık.
Planımızda NuGetAudit'in transitive paketleri taramasını .NET 9'a yazmıştık; NuGet'in dokümanı bunun .NET 10 SDK'sıyla (NuGet 7.0) geldiğini söylüyor: net10.0 ve sonrasını hedefleyen projelerde NuGetAuditMode'un varsayılanı all, daha eskilerde direct. Bookstore.Contracts gibi net8.0;net10.0 hedefleyen bir projede bir TFM (Target Framework Moniker; hedef framework adı) all seçtiği için ikisi de all ile taranıyor.
.NET 10: .slnx, File-Based App'ler ve dnx
.NET 10'un en görünür değişikliği, yeni solution'ların .slnx olarak gelmesi. Microsoft bunu breaking change olarak listeliyor:
dotnet new sln -n Legacy
The template "Solution File" was created successfully.
Klasörde Legacy.slnx oluştu. Eski biçime ihtiyacın varsa --format sln veriyorsun. Var olan bir .sln'i çevirmek için 9.0.200'den beri dotnet sln migrate var. Denerken bir şeye dikkat ettik: aynı klasörde Legacy.slnx zaten varken komut onu uyarı vermeden ezdi.
dotnet new sln -n Legacy --format sln
dotnet sln Legacy.sln add Bookstore.Legacy
dotnet sln Legacy.sln migrate
Project `Bookstore.Legacy/Bookstore.Legacy.csproj` added to the solution.
.slnx file C:\src\Legacy\Legacy.slnx generated.
Migrate eski dosyayı silmiyor. Klasörde iki solution dosyası kalınca dotnet sln add gibi komutlar 7. yazıda gördüğümüz "Found more than one solution file" hatasını veriyor. Çevirdikten sonra .sln'i sil, CI script'lerindeki dosya adlarını güncelle ve ekipte herkesin IDE'sinin .slnx'i açabildiğinden emin ol.
File-based app'ler, dnx ve tool paketlerinin platforma özel hâlleri 7. yazının konusuydu; Microsoft Testing Platform'u dotnet test'e bağlayan global.json ayarı 1. yazıdan beri Bookstore'da. .NET 10 komutları da noun-first biçime taşıdı: dotnet add package yerine dotnet package add. Eski biçim çalışmaya devam ediyor, ama yeni script'lerde yenisini kullanmak dokümanla aynı dili konuşmak demek.
.NET 11: SDK'da Neler Değişti?
.NET 11'in CLI tarafındaki iki büyük değişikliğini, Native AOT ile derlenmiş dotnet komutunu ve varsayılan açık MSBuild server'ı 1. yazıda ölçmüştük. Diğerleri serinin ilgili yazılarına dağıldı: solution filter komutları 3. yazıda, minPublishAgeHours 4. yazıda, UseOptimizedPublish 6. yazıda, NETSDK1235 ve #:include 7. yazıda. UseOptimizedPublish için bir not: breaking change sayfası Native AOT publish'in tam build'i atladığını yazıyor, "what's new" sayfası ve SDK 11.0.100'ün kendi target dosyası ise ayarın opt-in olduğunu söylüyor. Sayfalar çeliştiğinde kurulu SDK'nın target dosyası son sözü söyler.
Container tarafında üç yenilik var ve bu yazıda hiçbirini çalıştırmadık: SOURCE_DATE_EPOCH ile aynı girdilerden aynı image digest'i (reproducible container), Podman ile multi-arch image ve container runtime seçiminde Windows'ta wslc'nin, macOS'ta container'ın öncelikli olması. Image ve registry konuları Docker serisinde.
Gözden kaçabilecek küçük bir değişiklik: dotnet run'ın "Using launch settings from..." mesajı artık stdout'a değil stderr'e yazılıyor. dotnet run çıktısını bir dosyaya ya da başka bir komuta yönlendiren script'ler bu satırı ayıklamak zorunda kalmıyor.
Eski Bir Projeyi Okumak: Kontrol Listesi
Kurumlarda hâlâ .NET Framework döneminden kalma proje dosyaları var. Bir projeyi açtığında şunlara bak:
packages.configvarsa paketler proje dosyasında değil ayrı bir XML'de vepackages/klasöründe duruyor; transitive bağımlılıklar da elle listelenmiş. Visual Studio'nun "Migrate packages.config to PackageReference" komutu geçişi yapıyor; bu yazıda denemedik.<Project ToolsVersion=...>ile başlayan,Sdkattribute'u olmayan bir csproj, her dosyayıCompile Includeile tek tek listeler. SDK-style'a geçerken bu listeyi sil; 2. yazıdaki glob'lar aynı işi yapıyor.Properties/AssemblyInfo.csSDK-style projede SDK'nın ürettiği attribute'larla çakışır.- Kökte
.slnvarsadotnet sln migrateile.slnx'e çevir ve eski dosyayı sil. - Kökte
global.jsonyoksa ekle; olmadan her makine kendi en yeni SDK'sıyla derler.
Üçüncü maddeyi bir deneme projesiyle gördük. SDK-style'a çevrilmiş bir library'de eski AssemblyInfo.cs kalınca:
C:\src\Legacy\Bookstore.Legacy\obj\Debug\net10.0\Bookstore.Legacy.AssemblyInfo.cs(15,12): error CS0579: Duplicate 'System.Reflection.AssemblyFileVersionAttribute' attribute [C:\src\Legacy\Bookstore.Legacy\Bookstore.Legacy.csproj]
C:\src\Legacy\Bookstore.Legacy\obj\Debug\net10.0\Bookstore.Legacy.AssemblyInfo.cs(18,12): error CS0579: Duplicate 'System.Reflection.AssemblyTitleAttribute' attribute [C:\src\Legacy\Bookstore.Legacy\Bookstore.Legacy.csproj]
C:\src\Legacy\Bookstore.Legacy\obj\Debug\net10.0\Bookstore.Legacy.AssemblyInfo.cs(19,12): error CS0579: Duplicate 'System.Reflection.AssemblyVersionAttribute' attribute [C:\src\Legacy\Bookstore.Legacy\Bookstore.Legacy.csproj]
Hatanın gösterdiği dosya obj altında, yani SDK'nın ürettiği kopya; ama sorun senin dosyanda. Sürüm ve başlık bilgisini Version, AssemblyTitle gibi property'lere taşıyıp dosyayı silmek, GenerateAssemblyInfo'yu kapatmaktan daha iyi; kapatırsan 8. yazıdaki AssemblyMetadata item'ları da çalışmaz. Microsoft'un .NET Upgrade Assistant aracı Learn'deki nota göre artık geliştirilmiyor ve yerine GitHub Copilot'un modernization agent'ı öneriliyor; ikisini de kullanmadık, adımları elle yaptık.
Bookstore'u SDK 11'e Taşımak
Neden taşıyoruz? .NET 11 bir STS (Standard Term Support; 24 ay süren destek) sürümü ve desteği Kasım 2028'de bitiyor; LTS (Long Term Support; uzun süreli destek) sürümü olan .NET 10'un bitiş tarihi de aynı. Uygulamalar net10.0'da, yani LTS runtime'ında kalıyor; SDK 11 bize yeni CLI'ı, NuGet 8'i ve MSBuild 18.12'yi getiriyor. Taşımayı bir repo kopyasında yaptık ve her adımı SDK 11.0.100 ile tekrar çalıştırdık: solution build'i sıfır uyarıyla geçti, iki framework'teki 16 test geçti, win-x64 publish profile'ı, tool ve template paketleri ve check-isbns.cs script'i değişmeden çalıştı. Kırılanlar kodda değil, SDK sürümünü tekrar eden yerlerdeydi.
Birincisi girişteki hata mesajı. global.json'un yeni hâli:
{
"sdk": {
"version": "11.0.100",
"rollForward": "latestPatch",
"errorMessage": "Bookstore needs .NET SDK 11.0.100 or a later 11.0.1xx patch. The apps still target net10.0. Install the SDK from https://dotnet.microsoft.com/download/dotnet/11.0"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
İkincisi 6. yazıdaki Native AOT container'ı. sdk:10.0.404-noble-aot image'ının içinde SDK 11 yok; global.json onu kabul etmedi ve build errorMessage'daki metinle durdu. Doğru image'ı ararken bir sürpriz daha çıktı: yazım günü MCR'de (Microsoft Container Registry; Microsoft'un image'larını yayımladığı registry) sdk:11.0.100-noble-aot diye bir etiket yok; .NET 11 SDK image'ları Ubuntu 26.04 tabanlı ve -resolute son ekiyle geliyor:
docker run --rm -v ${PWD}:/src -w /src mcr.microsoft.com/dotnet/sdk:11.0.100-resolute-aot dotnet publish tools/Bookstore.Admin -r linux-x64 -p:PublishAot=true -o artifacts/aot
Generating native code
Bookstore.Admin -> /src/artifacts/aot/
SDK sürümü bir repoda genellikle dört yerde tekrar eder: global.json, CI'daki SDK kurulum adımı, Dockerfile'lardaki FROM satırları ve dokümantasyon. Bookstore'da README'deki "Gerekenler" satırını da güncelledik. Bir sürüm yükseltmesini bu dört yeri birlikte değiştiren tek bir pull request olarak yap.
Üçüncüsü bir uyarı: dotnet tool restore, dotnet-ef için 11.0.0'ın çıktığını söylüyor. SDK'yı yükseltmek araçları yükseltmeyi gerektirmiyor; Bookstore Entity Framework Core 10 kullanıyor, araç da 10.0.15'te kalıyor.
MSBuild Server ve CI
1. yazıda MSBuild server'ın CI'da neden kapatılabileceğini bu yazıya bırakmıştık. SDK 11 ile bir build'den sonra arka planda komut satırı /nodemode:8 olan bir MSBuild süreci kalıyor; sonraki build'ler projeyi bu sıcak süreçte değerlendirip build ediyor. Faydasını Bookstore'da ölçtük. Yerelde, değişiklik olmadan art arda alınan dotnet build tools/Bookstore.Admin server açıkken medyan 0.28 saniye, DOTNET_CLI_USE_MSBUILD_SERVER=false ile 0.62 saniye sürdü. Ama build server'larını kapatıp her seferinde soğuk başladığımızda fark kayboldu: 0.78 saniyeye karşı 0.85 saniye.
CI job'ı tam olarak bu ikinci durum: her job temiz bir makinede başlar, çoğu zaman tek bir build alır ve biter. Server'ın kazandırdığı yok denecek kadar az; buna karşılık job bittiğinde geride bir süreç kalır. Geçici agent'larda bu süreç makineyle birlikte silinir. Kalıcı (self-hosted) agent'larda ise job'lar arasında yaşar, belleği tutar, yüklediği task assembly'lerini bırakmaz; repoda derlenip build'de kullanılan bir task'ın yeni sürümü, eski süreç kapanana kadar devreye girmeyebilir. Bookstore'un CI'ı için önerimiz server'ı ortam değişkeniyle kapatmak:
DOTNET_CLI_USE_MSBUILD_SERVER=false
Bu değişkenle aldığımız build'den sonra /nodemode:8 süreci oluşmadı. Geliştirici makinelerinde ise server açık kalsın; art arda build alırken kazandırdığı süre gerçek. Kalıcı agent kullanıyorsan job'ın son adımına dotnet build-server shutdown eklemek de bir seçenek. Seçimin, CI'ın geçici mi kalıcı mı olduğuna bağlı.
Üretimde Dikkat
SDK yükseltmesini runtime yükseltmesinden ayır. Bookstore SDK 11 ile derleniyor ama .NET 10 runtime'ında çalışıyor; production'daki risk değişmedi, değişen yalnız build araçları. Runtime'ı net11.0'a taşımak ayrı bir karar ve ayrı bir pull request; .NET 11 STS olduğu için o karar destek süresiyle birlikte verilmeli.
Yükseltmeden önce ve sonra aynı komutları çalıştırıp çıktıları karşılaştır: build uyarıları, test sayısı, publish klasörünün içeriği, paketlerin içeriği. Bizim taşımamızda hepsi aynı çıktı. Aynı çıkmasaydı farkı yükseltme pull request'inde görmek, production'da görmekten çok daha ucuz olurdu.
Breaking change listelerini oku, ama kendi SDK'nda doğrula. Bu seride dotnet run -e'nin, #:include'un ve solution filter komutlarının .NET 11 sürüm notlarında yazdığından daha erken SDK'larda çalıştığını, UseOptimizedPublish'in de iki sayfada farklı anlatıldığını gördük.
Sık Düşülen Tuzaklar
global.json'da sürümü değiştirip errorMessage'ı unutmak. Mesaj, agent'ta kurulu olan sürümü istiyormuş gibi görünür ve sorunu bulmak uzar.
SDK sürümünü yalnız global.json'da değiştirmek. Container image'ları, CI kurulum adımları ve dokümantasyon eski sürümde kalır; bizim AOT container'ımız tam olarak böyle kırıldı.
MSBuild server'ın kalıcı CI agent'larında açık kalması. Geride kalan süreçler bellek tutar ve eski task assembly'lerini yüklü tutabilir; geçici agent'larda sorun yoktur, kalıcılarda kapat ya da job sonunda kapattır.
dotnet sln migrate'ten sonra .sln'i bırakmak. İki solution dosyası olan klasörde CLI hangisini kullanacağını bilemez; aynı adlı bir .slnx varsa migrate onu da uyarısız ezer.
Eski AssemblyInfo.cs'i SDK-style projede tutmak. CS0579 hataları gelir; bilgiyi property'lere taşı ve dosyayı sil.
Küçük Bir Deneme
★☆☆ global.json'daki version'ı makinende kurulu olmayan bir sürüme (örneğin 11.0.200) çevir ve dotnet build çalıştır. Kendi errorMessage'ının nasıl göründüğüne bak, sonra mesajı okuyan birinin ne yapması gerektiğini anlayacağı biçimde yeniden yaz.
★★☆ Bookstore'u SDK 10.0.404 ve 11.0.100 ile ayrı ayrı publish et (dotnet publish src/Bookstore.Api) ve iki publish klasörünü dosya dosya karşılaştır. Fark eden dosya var mı, varsa neden?
★★★ Kendi şirketindeki en eski .NET projesini bu yazıdaki kontrol listesiyle oku: packages.config, SDK-style olmayan csproj, AssemblyInfo.cs, .sln, global.json. Bir branch'te SDK-style'a çevir, build ve testlerin önceki ve sonraki çıktısını karşılaştır.
Bu yazıyla seri bitiyor. dotnet CLI'dan başlayıp global.json, proje dosyası, ortak build ayarları, NuGet restore, multi-targeting, publish seçenekleri, tool ve template'ler ve MSBuild'in kendisine kadar indik; her adımı Bookstore'da çalıştırıp gerçek çıktısıyla gösterdik. Buradan sonra yolun devamı başka serilerde: paketi yayımlamak ve sürüm numarası üretmek Versioning ve Release Management serisinde, build'i bir pipeline'a koymak GitHub Actions serisinde, publish çıktısını image'a çevirmek Docker serisinde.