Mert Özen Her Satırda Daha İleri
Backend C# Sürümleri — #06

C# ve .NET Sürüm Yükseltme: Eski Projeyi .NET 10'a Taşımak

Mert Özen 7 Kas 2026 10 dk 9 görüntülenme
C# ve .NET Sürüm Yükseltme: Eski Projeyi .NET 10'a Taşımak

Bir .NET 6 projesini .NET 10'a taşımak: destek takvimi, LangVersion ile TFM ilişkisi ve latest tuzağı, global.json ile SDK sabitleme, gerçek CS8981 ve CS9258 uyarılarını okumak, field keyword'ünün sessiz davranış değişikliği ve breaking change sayfalarını okumak; C# 15 için ayrı yazı.

Sürüm notu: Bu yazı C# 10 / .NET 6 ile yazılmış bir projeyi C# 14 / .NET 10'a taşımayı anlatır; örnekler .NET 10 SDK (10.0.402) ve .NET 8 SDK (8.0.419) ile denenmiştir.

Önce Tahmin Et

Elinde .NET 6 döneminden kalmış küçük bir Bookstore projesi var. Proje dosyası şu:

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <LangVersion>10</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Kod da şu:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

string[] titles = { "Clean Code", "Refactoring", "Domain-Driven Design" };
foreach (string title in titles.Reverse())
    Console.WriteLine(title);

var line = new orderline { Title = "Clean Code", Quantity = 2 };
var shelf = new Shelf();
shelf.Label = "A1";
Console.WriteLine($"{line.Title} x{line.Quantity} on {shelf.Label}");

public class orderline
{
    public string Title { get; set; } = "";
    public int Quantity { get; set; }
}

public class Shelf
{
    private string field = "";

    public string Label
    {
        get { return field; }
        set { field = value; }
    }
}

.NET 6'da tek bir uyarı bile vermeden derleniyor. TargetFramework'ü net10.0 yapıp LangVersion satırını silersen kaç uyarı alırsın? Hangi satırlar? Program aynı çıktıyı verir mi?

Önce Durumu Oku

Hiçbir şeye dokunmadan, makinedeki .NET 10 SDK ile derleyince ilk sinyal geliyor:

warning NETSDK1138: The target framework 'net6.0' is out of support and will not receive security updates in the future. Please refer to https://aka.ms/dotnet-core-support for more information about the support policy.

Derleme başarılı, çünkü SDK eski framework'leri hedefleyebiliyor. Çalıştırmak ise başka bir konu. Makinemde .NET 6 runtime'ı yok, sadece 8 ve 10 var (bu yazıdaki çıktılarda uzun dosya yollarını kısalttım):

You must install or update .NET to run this application.

App: …/Bookstore/bin/Debug/net6.0/Bookstore
Architecture: arm64
Framework: 'Microsoft.NETCore.App', version '6.0.0' (arm64)
.NET location: /usr/local/share/dotnet

The following frameworks were found:
  8.0.25 at [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  10.0.3 at [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
  10.0.13 at [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

SDK ile runtime'ın ayrı şeyler olduğunu C# Temelleri serisinin ilk yazısında görmüştük; burada o farkın pratikteki karşılığı var. Bu serideki deneyleri DOTNET_ROLL_FORWARD=Major ile yaptım; bu ayar uygulamaya "bulduğun bir sonraki major runtime'da çalış" der ve net6.0 uygulaması .NET 8.0.25 üzerinde açıldı. Deney için işe yarar, ama bir çözüm değil: uygulamayı hiç test edilmediği bir runtime'da çalıştırıyorsun ve desteği bitmiş bir hedefte kalmaya devam ediyorsun. Bu yazının geri kalanında projeyi gerçekten taşıyoruz. Önce Bookstore'un kendi çıktısı, yükseltmeden sonra karşılaştırmak için:

Domain-Driven Design
Refactoring
Clean Code
Clean Code x2 on A1

Destek Takvimi

06-dotnet-support-tr.png

Neden şimdi taşıyoruz? Microsoft'un resmi destek tablosuna göre bugünkü (7 Kasım 2026) durum şöyle. .NET 6 bir LTS sürümüydü ve desteği 12 Kasım 2024'te bitti. .NET 7 bir STS sürümüydü ve ondan da önce, 14 Mayıs 2024'te bitti. .NET 8 (LTS) ve .NET 9 (STS) ise aynı gün, 10 Kasım 2026'da, yani üç gün sonra destek dışı kalıyor. İkisinin aynı güne denk gelmesinin sebebi, STS desteğinin bugünkü politikada 24 aya çıkmış olması. 11 Kasım 2025'te çıkan .NET 10 bir LTS sürümü ve 14 Kasım 2028'e kadar destekleniyor.

Pratikte bu, birkaç gün sonra desteklenen tek LTS sürümün .NET 10 olacağı anlamına geliyor. Taşıma hedefi olarak onu seçiyoruz. Bir de yeni bir sürüm kapıda: C# 15 ile gelecek .NET 11'in GA tarihi 10 Kasım 2026 olarak planlanıyor. .NET 11 bir STS sürümü; C# 15'i, çıktıktan sonra ayrı bir yazıda ele alacağız.

LangVersion ve TFM Birlikte Değişir

Bu serinin ilk yazısında gördüğümüz kuralı hatırla: LangVersion yazmazsan dil sürümünü TargetFramework belirler. net6.0 için C# 10, net10.0 için C# 14. Eski projelerde LangVersion çoğu zaman ya o günün sürümüne sabitlenmiş ya da latest yapılmıştır. İkisi de yükseltme sırasında sorun çıkarır. Sabit kalırsa yeni framework'e geçtiğin hâlde eski dilde kalırsın. latest ise dili SDK'ya bağlar ve framework'ten bağımsız hâle getirir.

İkinci durumun neden tehlikeli olduğunu gördüm. Projeyi yarı yolda bırakıp net9.0 hedefine aldım ve LangVersion'ı latest yaptım. .NET 10 SDK'da latest C# 14 demek; yani .NET 9 kütüphaneleriyle C# 14 kurallarını birleştirmiş oldum. Sonuç bir derleme hatası:

Program.cs(6,26): error CS1579: foreach statement cannot operate on variables of type 'void' because 'void' does not contain a public instance or extension definition for 'GetEnumerator'

Altıncı satır, hiç dokunmadığımız titles.Reverse(). C# 14'ün yeni span dönüşümleri sayesinde compiler artık array'i Span<T>'ye çevirip span'in yerinde ters çeviren ve void dönen Reverse method'unu seçebiliyor. .NET 10 bunu Enumerable.Reverse(T[]) overload'unu ekleyerek çözdü; .NET 9'un kütüphanesinde bu overload yok. Microsoft framework'ün varsayılanından yeni bir dil sürümü kullanmayı desteklemiyor ve bu hata o uyarının somut hâli. Kural basit: yükseltirken LangVersion satırını sil, dil sürümünü framework'e bırak.

Adım Adım Yükseltme

Birinci adım, hangi SDK ile çalıştığını sabitlemek. Ekipte herkesin ve CI makinesinin aynı SDK'yı kullanması için kök klasöre bir global.json koyarsın. Eski bir projede şöyle bir dosya bulman da muhtemel; bunu dotnet new globaljson --sdk-version 8.0.419 --roll-forward latestFeature ile oluşturdum:

{
  "sdk": {
    "rollForward": "latestFeature",
    "version": "8.0.419"
  }
}

Bu klasörde dotnet --version artık 8.0.419 diyor. Projeyi net10.0'a alıp bu SDK ile derlemeye çalışınca:

error NETSDK1045: The current .NET SDK does not support targeting .NET 10.0.  Either target .NET 8.0 or lower, or use a version of the .NET SDK that supports .NET 10.0. Download the .NET SDK from https://aka.ms/dotnet/download

Yani TFM'i yükseltmeden önce global.json'daki sürümü, CI'daki SDK kurulumunu ve Dockerfile'daki base image etiketlerini de güncellemen gerekiyor.

İkinci adım proje dosyası. TargetFramework'ü net10.0 yap ve LangVersion satırını sil:

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

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

</Project>

Üçüncü adım paketler. Gerçek bir projede Microsoft.Extensions.*, EF Core ya da ASP.NET Core paketleri framework sürümüyle birlikte yürür. .NET 10 SDK'da dotnet package list --outdated komutu (eski yazımıyla dotnet list package --outdated) güncel olmayan paketleri listeler. Bizim örneğimizde paket olmadığı için cevap kısa: The given project `Bookstore` has no updates given the current sources. Dördüncü adım derlemek ve uyarıları okumak; asıl iş burada.

Yeni Uyarıları Okumak

Tahmin sorusunun cevabı beş uyarı:

Program.cs(14,14): warning CS8981: The type name 'orderline' only contains lower-cased ascii characters. Such names may become reserved for the language.
Program.cs(26,22): warning CS9258: In language version 14.0, the 'field' keyword binds to a synthesized backing field for the property. To avoid generating a synthesized backing field, and to refer to the existing member, use 'this.field' or '@field' instead.
Program.cs(27,15): warning CS9258: In language version 14.0, the 'field' keyword binds to a synthesized backing field for the property. To avoid generating a synthesized backing field, and to refer to the existing member, use 'this.field' or '@field' instead.
Program.cs(24,19): warning CS9264: Non-nullable property 'Label' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier, or declaring the property as nullable, or safely handling the case where 'field' is null in the 'get' accessor.
Program.cs(22,20): warning CS0414: The field 'Shelf.field' is assigned but its value is never used

CS8981, C# 11 ile gelen warning wave 7'nin uyarısı (-p:WarningLevel=7 ile görünüyor, 6 ile görünmüyor): tamamen küçük harfli tip isimleri ileride dilin keyword'ü olabilir. İlginç olan, bu uyarıyı dil sürümünün değil framework'ün açması. dotnet msbuild -getProperty:WarningLevel net6.0 projesinde 6, net10.0 projesinde 10 döndü. Ayrı bir denemede net10.0 hedefinde LangVersion'ı 10'a çeksem bile CS8981 geldi; -p:WarningLevel=6 verince kayboldu. Yani yeni uyarı dalgaları TFM ile birlikte gelir.

Diğer dört uyarı aynı soruna işaret ediyor ve bu, yükseltmenin en sinsi kısmı. C# 10'da field sıradan bir isimdi ve Shelf'teki private field'ı kastediyordu. C# 14'te property accessor'ı içindeki field bir keyword ve compiler'ın ürettiği backing field'ı temsil ediyor. Kod derleniyor ve çıktı yine aynı:

Domain-Driven Design
Refactoring
Clean Code
Clean Code x2 on A1

Ama artık Label başka bir field'a yazıp okuyor. CS0414 bunu söylüyor: senin tanımladığın field artık hiç okunmuyor. Bu örnekte fark görünmüyor, çünkü class'ta o field'ı kullanan başka bir member yok. Aynı class'ta field'ı doğrudan okuyan bir method olsaydı ne olurdu? Bunu ayrıca denedim:

var shelf = new Shelf();
shelf.Label = "A1";
Console.WriteLine($"[{shelf.Label}] [{shelf.Describe()}]");

public class Shelf
{
    private string field = "";

    public string Label
    {
        get { return field; }
        set { field = value; }
    }

    public string Describe() => $"shelf {field}";
}

C# 13 ile [A1] [shelf A1], C# 14 ile [A1] [shelf ] yazdı. Method'un içindeki field hâlâ private field'ı kastediyor, accessor'lardaki ise compiler'ın ürettiği backing field'ı. İkisi birbirinden koptu ve bunu hiçbir derleme hatası yakalamadı; yalnızca uyarılar haber verdi. Uyarının önerdiği iki çözüm var: eski anlamı korumak için this.field ya da @field yazmak, ya da field'ı yeniden adlandırmak. Ben kodu sadeleştirmeyi seçtim:

public class OrderLine
{
    public string Title { get; set; } = "";
    public int Quantity { get; set; }
}

public class Shelf
{
    public string Label { get; set; } = "";
}

orderline'ı OrderLine yapıp çağıran satırı da güncelledim. Proje artık uyarısız derleniyor ve aynı dört satırı yazıyor. titles.Reverse() ise net10.0'da sorun çıkarmadı; .NET 10'un yeni overload'u devreye girdi.

Breaking Change'leri Okumak

Küçük bir console uygulamasında çıkan sorunlar bunlar. Gerçek bir projede daha fazlası olacak ve hepsini derleme uyarısı olarak görmeyeceksin; bazıları yalnızca runtime'da davranış değişikliği olarak ortaya çıkar. Microsoft her sürüm için ayrı bir "Breaking changes in .NET N" sayfası yayınlıyor; Roslyn ekibi de compiler tarafındaki breaking change'leri ayrıca belgeliyor. .NET 6'dan 10'a atlıyorsan arada kalan 7, 8, 9 ve 10'un sayfalarının hepsine bakman gerekir, çünkü her biri yalnızca bir önceki sürüme göre değişeni anlatır.

Bu sayfaları okurken kendine üç soru sor: bu değişiklik benim kullandığım bir API'ye dokunuyor mu, compile time'da mı yoksa runtime'da mı ortaya çıkıyor, önerilen düzeltme ne? Bu yazıdaki Reverse örneği, .NET 10'un compiler breaking change belgesinde anlatılan maddelerden biri. Runtime'daki değişiklikler için en iyi güvence testlerin; yükseltmeden önce testlerin yeşil olsun, yükseltmeden sonra da aynı testler koşsun.

Büyük bir kod tabanında tek seferde hedef değiştirmek yerine projeleri sırayla taşımak da bir seçenek. Önce bağımlılığı en az olan library projeleri, sonra uygulamayı taşırsın. Her adımda derleme uyarılarını ve testleri temiz tutarsan son adım küçük kalır.

Sık Düşülen Tuzaklar

LangVersion'ı latest bırakmak. Yukarıdaki Reverse hatası tam olarak bu ayarın ürünü. Dil sürümünü framework'e bırak.

Yeni uyarıları "sonra bakarım" diye geçmek. CS9258 gibi bir uyarı, derlenen ama farklı çalışan bir kodun tek habercisi olabilir. Projende TreatWarningsAsErrors açıksa zaten bu uyarılar build'i durdurur; kapalıysa yükseltme sırasında uyarı listesini sıfırlamayı hedefle.

Yalnızca proje dosyasını güncellemek. global.json, CI'daki SDK kurulumu, Dockerfile'daki base image etiketleri ve sunucudaki runtime da aynı yükseltmenin parçası. NETSDK1045 ve "You must install or update .NET" mesajları bunların unutulduğunu gösterir.

Roll-forward'ı kalıcı çözüm sanmak. DOTNET_ROLL_FORWARD=Major eski bir uygulamayı yeni runtime'da açar ama ne test edilmiş bir kombinasyondur ne de desteği bitmiş hedefi kurtarır.

Destek bitiş tarihini takip etmemek. STS sürümleri 24 ay destek alıyor; .NET 9 örneğinde gördüğün gibi bu süre bir önceki LTS'in kalan süresiyle aynı güne denk gelebilir. Hangi sürüme geçersen geç, destek bitiş tarihini takvimine yaz.

Küçük Bir Deneme

★☆☆ Tahmin sorusundaki projeyi kendi makinende net10.0'a taşı ve beş uyarıyı aynı sırayla gördüğünü doğrula.

★★☆ Yukarıdaki Describe() örneğinde accessor'larda field yerine @field yaz. C# 14'te çıktı yeniden [A1] [shelf A1] oluyor mu, CS9258 uyarıları kayboluyor mu?

★★★ Kendi işindeki bir .NET 6 ya da .NET 8 projesini bir dalda net10.0'a al. Kaç uyarı çıkıyor, hangileri yeni uyarı dalgasından, hangileri dil değişikliğinden geliyor?

C# Sürümleri serisinin şimdilik son yazısı bu. C# 1.0'dan C# 14'e dilin nasıl büyüdüğünü, her sürümde neyin neden geldiğini ve eski bir projeyi nasıl taşıyacağını gördük. Seri yeni sürümlerle devam edecek: .NET 11 ve C# 15'in 10 Kasım 2026'da çıkması planlanıyor; union type'lar ve collection expression argümanları gibi yenilikleri, sürüm çıktıktan sonra kendi yazısında aynı yöntemle, gerçek compiler çıktılarıyla ele alacağız.