.NET Debugger İleri Seviye: Conditional, Tracepoint, Data Breakpoint ve Exception Settings
Değeri kimin değiştirdiğini bulmanın en kısa yolu data breakpoint: Visual Studio'da .NET Core 3+ projelerde property değişince debugger yazan satırda durur. Conditional, hit count ve dependent breakpoint, tracepoint, yutulan exception'lar için Exception Settings, Set Next Statement, Hot Reload, DebuggerDisplay ve Source Link'i gerçek çıktılarla; Copilot önerilerini hipotez olarak test etmeyi ve Rider ile Visual Studio Code karşılıklarını görüyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir. Visual Studio 2026 ve Rider 2026.2 bu makinede (macOS) kurulu olmadığı için çalıştırılmadı; bu iki IDE'nin (Integrated Development Environment) özellikleri, menü adları ve kısayolları Microsoft Learn ve JetBrains dokümanından aktarıldı.
Önce Tahmin Et
Bookstore'da sahtecilik kontrolünün iptal ettiği 1042 numaralı sipariş, gün sonunda Paid olarak görünüyor. Order.Status property'sine kod tabanında 30 ayrı yerden yazılıyor. Her birine breakpoint koyup F5'e otuz kez basabilirsin. Ya da debugger'a "bu sipariş nesnesinin Status'u değiştiği an dur" diyebilirsin. Sence Visual Studio bunu .NET 10'da bir property için yapabiliyor mu? Ve aynı sorunun içinde, kargo fiyatı alan bir client her gün yüzlerce exception'ı sessizce yutuyor; log'da tek satır yok. Debugger'da bunları nasıl görürsün?
Breakpoint, step ve call stack'in temelleri C# Temelleri serisinin "C# Hata Ayıklama: Breakpoint, Call Stack, Debug.Assert ve Derleyici Uyarıları" yazısında. Bu yazı oradan devam ediyor: debugger'ı, hipotezini en az adımla test eden bir deney aracı olarak kullanmak.
Koşullu Breakpoint'ler ve Tracepoint: Doğru Anda Durmak ya da Hiç Durmamak
Bir döngü 10.000 sipariş işliyorsa ve yalnız 1042 bozuksa, düz breakpoint işe yaramaz. Visual Studio'da breakpoint'e sağ tıklayıp Conditions seçtiğinde (ya da Alt+F9, C) üç tür koşul var:
- Conditional Expression:
order.Id == 1042gibi bir ifade; "Is true" doğru olduğunda, "When changed" ifadenin değeri değiştiğinde durur. - Hit Count: satır belirli sayıda çalıştıktan sonra, örneğin 500. turda ya da her ikinci turda durur.
- Filter:
ThreadId,ThreadName,ProcessId,MachineNameile durmayı belli bir thread'e ya da process'e sınırlar.
Koşulun sözdizimi geçersizse ya da çalışırken hata verirse debugger durur; breakpoint yalnız koşul geçerli ve false olduğunda atlanır. Bu iyi bir şey: yanlış yazdığın koşul sessizce hiç tetiklenmeyen bir breakpoint'e dönüşmüyor.
Bunların yanında üç yardımcı tür var. Dependent breakpoint ("Only enable when the following breakpoint is hit"), yalnız başka bir breakpoint önce çalıştıysa devreye girer: PriceCalculator.Calculate her istekte çağrılıyor ama sen onu yalnız checkout akışından gelince görmek istiyorsun. Temporary breakpoint bir kez durur ve kendini kapatır. Function breakpoint (Ctrl+K, B) satır yerine method adıyla kurulur; Bookstore.Pricing.PriceCalculator.Calculate yazarsın, overload'ların hepsinde durur, dosyayı bulman bile gerekmez.
Tracepoint, aynı Breakpoint Settings penceresinde Action kutusunu işaretleyince oluşur ve kırmızı daire baklavaya döner. "Show a message in the Output Window" alanına şunu yazarsın:
Order {order.Id} status={order.Status} total={order.Total} thread=$TID caller=$CALLER
Süslü parantez içindeki ifadeler değerlendirilir; $TID, $CALLER, $FUNCTION, $CALLSTACK gibi anahtar kelimeler thread ID'sini, çağıran method'u, call stack'i yazar. "Continue code execution" açık kaldığı sürece program durmaz. Yani kodu değiştirmeden, yeniden derlemeden, commit'e Console.WriteLine kaçırma riski olmadan geçici log eklemiş olursun. Timing'e duyarlı bir bug'da durmak davranışı değiştirebilir; tracepoint bu yüzden çoğu zaman breakpoint'ten daha dürüst bir gözlem aracıdır. Koşulla birleştirince ("yalnız order.Total > 500 iken log'la") gürültü de kalmaz.
Data Breakpoint: Değeri Kim Değiştiriyor?
Önce Tahmin Et'teki soruya dönelim. Visual Studio'da .NET Core 3.0 ve sonrası (.NET 5+ dahil) projelerde data breakpoint var: debugger bir yerde durduğunda Locals, Autos ya da Watch penceresinde order.Status'a sağ tıklayıp Break when value changes seçersin. O nesnenin o property'si hangi kod yolundan değişirse değişsin, debugger değişikliği yapan satırda durur ve call stack sana yazanı gösterir. Sınırları dokümanda net: static field'larda, struct içindeki field'larda ve DebuggerTypeProxy kullanan class'larda çalışmaz; aynı anda kurabileceğin data breakpoint sayısı donanıma bağlı ve işlemci mimarisine göre iki ile dört arasında. Breakpoint yalnız o debug oturumunda yaşar.
IDE'n yoksa ya da bug yalnız bir test ortamında oluyorsa aynı fikri koda taşıyabilirsin. Property setter'ına yalnız Debug build'de derlenen bir iz koyuyoruz:
public OrderStatus Status
{
get => _status;
set
{
if (value != _status)
{
// debugger olmadan "data breakpoint": değeri kim değiştiriyor?
Trace($"Status {_status} -> {value}");
}
_status = value;
}
}
[Conditional("DEBUG")]
private static void Trace(string message)
{
StackFrame caller = new(2);
Console.WriteLine($"{message} by {caller.GetMethod()?.DeclaringType?.Name}.{caller.GetMethod()?.Name}");
}
Debug build'in gerçek çıktısı:
Status Created -> Cancelled by FraudCheck.Review
Status Cancelled -> Paid by PaymentStep.Complete
final status: Paid
Hikâye iki satırda: FraudCheck siparişi doğru şekilde iptal etmiş, ardından PaymentStep.Complete durumu kontrol etmeden Paid yazmış. Bug iptalde değil, ödeme adımında. [Conditional("DEBUG")] sayesinde Release build'de Trace çağrıları hiç derlenmez; aynı program Release'te yalnız final status: Paid yazdı. new StackFrame(2) inlining'e karşı kırılgandır, Release'te güvenme; bu bir teşhis iskelesi, kalıcı kod değil.
Data breakpoint'in olmadığı yerde (Visual Studio'da static field, eski .NET Framework projesi) dokümanın önerdiği yol Object ID: debugger'da durduğunda Locals'ta nesneye sağ tıklayıp Make Object ID dersin, nesne $1 adını alır. Sonra şüpheli satıra order == $1 koşullu breakpoint koyarsın; 10.000 sipariş arasından yalnız o nesne geldiğinde durursun. Object ID weak reference'tır, nesnenin garbage collector tarafından toplanmasını engellemez ve yalnız o oturumda geçerlidir.
Exception Settings: Yutulan Exception'ları Görmek
Kargo fiyatı alan client'a bakalım:
public decimal GetQuoteWithRetry(string postalCode)
{
for (int attempt = 1; attempt <= 3; attempt++)
{
try
{
return CallCarrier(postalCode);
}
catch (TimeoutException)
{
// sessizce yeniden dene
}
}
return 0m;
}
Dışarıdan bakınca her şey yolunda: fiyat geliyor. Ama her çağrı birkaç exception fırlatıp yutuyor ve bu, latency'nin neden yüksek olduğunu açıklayabilir. Debugger varsayılan olarak yalnız yakalanmamış exception'da durur. Exception Settings penceresinde (Debug > Windows > Exception Settings, kısayolu Ctrl+Alt+E) Common Language Runtime Exceptions altında System.TimeoutException'ı işaretlersen debugger exception fırlatıldığı anda, catch'e varmadan durur. Buna first-chance exception denir. Pencere arama kutusuyla System.IO gibi namespace'leri süzebilir, belli modüllere koşul ekleyebilirsin ("yalnız Bookstore.Shipping.dll'de dur").
Aynı bilgiyi debugger olmadan da toplayabilirsin. Runtime her first-chance exception'da bir event yayınlıyor:
int firstChance = 0;
AppDomain.CurrentDomain.FirstChanceException += (_, e) =>
{
firstChance++;
Console.WriteLine($"first-chance #{firstChance}: {e.Exception.GetType().Name}: {e.Exception.Message}");
};
ShippingQuoteClient client = new();
decimal quote = client.GetQuoteWithRetry("34000");
Console.WriteLine($"quote: {quote}, first-chance exceptions: {firstChance}");
first-chance #1: TimeoutException: Carrier did not answer for 34000.
first-chance #2: TimeoutException: Carrier did not answer for 34000.
quote: 79.90, first-chance exceptions: 2
Tek bir fiyat için iki timeout. Kargo firmasının her timeout'u birkaç saniye sürüyorsa kullanıcının beklediği sürenin nereye gittiği belli. Bu handler'ı production'da açık bırakma; her exception'da çalışır ve içinde exception fırlatırsan sonsuz döngüye girebilirsin. Teşhis için kısa süreli aç, say, kapat.
Exception Settings ile ilgili bir ayrıntı daha: Just My Code açıkken ASP.NET Core'un kendi exception handler'ı yüzünden 500'e dönüşen hatalar için "user-unhandled" durumunda durabilirsin; kendi kodundan çıkıp framework'te yakalanan exception'da debugger seni yine kendi kodunda durdurur.
Çalışan Programı Değiştirmek: Set Next Statement, Immediate ve Hot Reload
Hipotezini test etmek için her seferinde programı baştan çalıştırmak zorunda değilsin. Visual Studio'da debugger durmuşken sarı oku sürükleyerek ya da satıra sağ tıklayıp Set Next Statement ile (Ctrl+Shift+F10) bir sonraki çalışacak satırı değiştirirsin. "FraudCheck.Review'ı atlarsam sipariş yine Paid mi olur?" sorusunu bir saniyede denersin. Dokümandaki uyarılar ciddiye alınmalı: atlanan satırlar çalışmaz, geri gidersen çalışmış satırlar geri alınmaz ve managed kodda başka bir method'a atlayamazsın. Yan etkili kodda (veritabanına yazan, mesaj gönderen) bu aracı çok dikkatli kullan.
Immediate Window (Ctrl+Alt+I) ve Watch, durmuş programın içinde ifade çalıştırmanı sağlar: order.Lines.Sum(l => l.Quantity * l.UnitPrice) yazıp Total ile karşılaştırırsın, hatta order.Status = OrderStatus.Cancelled diyerek durumu elle değiştirip devam edersin. Değerlendirme gerçekten kodu çalıştırır; property getter'ı yan etkiliyse Watch penceresine bakmak bile programın durumunu değiştirebilir.
Düzeltmeyi denemek için de yeniden başlatman gerekmeyebilir. Visual Studio'da Hot Reload ve Edit and Continue, Rider'da Hot Reload aynı işi yapar. Aynı mekanizmayı IDE'siz dotnet watch ile gerçekten çalıştırdık. İndirimin iki kez uygulandığı bir endpoint:
static class PriceRules
{
public static decimal Discounted(decimal listPrice) => Math.Round(listPrice * 0.90m * 0.90m, 2);
}
$ curl -s http://localhost:5202/api/books/7/price
{"id":7,"price":283.42}
Uygulama çalışırken * 0.90m'lerden birini silip dosyayı kaydediyoruz:
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch ⌚ Waiting for changes
dotnet watch 🔥 C# and Razor changes applied in 263ms.
$ curl -s http://localhost:5202/api/books/7/price
{"id":7,"price":314.91}
Process yeniden başlamadı, değişiklik 263 ms'de uygulandı ve aynı istek düzeltilmiş fiyatı döndü. Hot Reload her değişikliği kabul etmez (örneğin bazı tip imzası değişiklikleri "rude edit" sayılır ve yeniden başlatma ister); kabul edilmediğinde araç bunu söyler.
Debugger'ın Gördüğünü Düzenlemek: DebuggerDisplay, Just My Code ve Source Link
Watch penceresinde {Bookstore.Debugging.Order} yazan bir satır sana hiçbir şey söylemez. DebuggerDisplay ile nesnenin değer sütununda ne görüneceğini sen seçersin:
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class Order(int id)
{
private OrderStatus _status = OrderStatus.Created;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly Dictionary<string, object> _cache = [];
public int Id { get; } = id;
public List<OrderLine> Lines { get; } = [];
public decimal Total => Lines.Sum(l => l.Quantity * l.UnitPrice);
private string DebuggerDisplay => $"Order {Id}: {Status}, {Lines.Count} lines, total {Total}";
}
Debugger'ın bu nesne için göstereceği metni aynı private property'yi reflection ile okuyarak ürettik:
debugger shows: Order 1042: Paid, 1 lines, total 699.80
Attribute'un içine doğrudan karmaşık ifade yazmak yerine private bir property'ye yönlendirmek dokümanın önerisi: süslü parantezdeki ifadeler her gösterimde, o anki stack frame'in dilindeki evaluator ile değerlendirilir ve büyük koleksiyonlarda step atmayı yavaşlatabilir. ,nq (no quotes) tırnakları kaldırır. DebuggerBrowsable(Never) ise _cache gibi gürültü field'larını görünümden çıkarır. Visual Studio'da "Show raw structure of objects in variables windows" açıksa DebuggerDisplay yok sayılır.
Just My Code varsayılan olarak açıktır ve debugger'ı senin kodunla sınırlar: framework içinde step atmazsın, call stack'te framework frame'leri "External Code" olarak katlanır. Bug'ın bir kütüphanenin içinde olduğundan şüpheleniyorsan Tools > Options > Debugging > General altında Enable Just My Code'u kapatır, symbol server'ları (Microsoft Symbol Servers, NuGet.org Symbol Server) açarsın. Kaynak kodun kendisi Source Link ile gelir: PDB'nin (Program Database, sembol dosyası) içinde, derlemenin yapıldığı commit'e işaret eden bir eşleme durur ve debugger dosyayı oradan indirir. .NET 8 SDK'sından (Software Development Kit) beri Source Link SDK'nın içinde; GitHub remote'u olan bir repo'da düz bir dotnet build bu dosyayı üretiyor:
$ dotnet build -c Release -p:ContinuousIntegrationBuild=true
$ cat obj/Release/net10.0/Bookstore.Pricing.sourcelink.json
{"documents":{"/_/*":"https://raw.githubusercontent.com/example/bookstore-pricing/f500bfabfd7d940fcb0f8adcbaa89b8bcef24dcb/*"}}
Commit hash'i git rev-parse HEAD ile birebir aynı. ContinuousIntegrationBuild, makineye özgü klasör yolunu /_/ ile değiştirir; CI (Continuous Integration) build'lerinde açık olmalı. Visual Studio 2022 17.7'den beri symbol bulunamadığında .NET kodunu otomatik decompile edebiliyor; Rider da harici kaynakları varsayılan olarak decompile ediyor. Decompile edilmiş kod okunur ama orijinal değil: yorumlar ve yerel değişken adları yoktur.
AI Desteği: Öneriyi Hipotez Gibi Test Etmek
Visual Studio'nun AI (Artificial Intelligence, yapay zekâ) asistanı Copilot artık debugger'ın içinde. Dokümana göre exception'da durduğunda Analyze with Copilot düğmesi exception'ı, call stack'i ve değişkenleri bağlam olarak alır ve bir açıklama ile düzeltme önerir. Conditional breakpoint ve tracepoint penceresinde ifade alanına tıkladığında Copilot koda özgü koşul önerileri getirir. Visual Studio 2026'daki Debugger Agent ise bir adım ileri gidiyor: Copilot Chat'te mod listesinden Debugger seçilir ya da Agent modunda @debugger yazılır, bir GitHub veya Azure DevOps issue'su ya da düz cümleyle bug tarif edilir. Agent bug'ı yeniden üretmeye çalışır, hipotez kurar, uygulamaya tracepoint ve conditional breakpoint yerleştirir, debug oturumunu çalıştırıp runtime verisine bakar ve hedefli bir düzeltme önerir; son doğrulama sende.
Bu adımlar serinin ilk yazısındaki gözlem, hipotez, deney döngüsünün ta kendisi. Fark, hipotezi kimin kurduğu. Copilot'un "Status muhtemelen eşzamanlı iki istek yüzünden ezildi" demesi bir hipotezdir, sonuç değil. Onu bu yazıdaki araçlarla test et: data breakpoint gerçekten iki farklı thread'den yazma gösteriyor mu, tracepoint'teki $TID değerleri farklı mı? Öneri bir tahmin üretir; kanıt üreten debugger'dır. Bu özellikleri bu makinede çalıştırmadık; açıklamalar Microsoft Learn'deki "Debug with GitHub Copilot" sayfasından.
Rider ve Visual Studio Code Karşılıkları
Aynı fikirlerin üç araçtaki adları farklı. Tablo Microsoft Learn, JetBrains Rider 2026.2 dokümanı ve Visual Studio Code'un C# debugging sayfasından derlendi; "dokümanda yok" yazan yerlerde özellik olmayabilir ya da belgelenmemiş olabilir. Rider kısayolları seçtiğin keymap'e göre değişir.
| Özellik | Visual Studio 2026 | Rider 2026.2 | Visual Studio Code (C# Dev Kit) |
|---|---|---|---|
| Conditional breakpoint | Conditions > Conditional Expression | Breakpoint özelliklerinde Condition | Edit Condition (Expression) |
| Hit count | Conditions > Hit Count | Breakpoint özelliklerinde hit count (More) | Dokümanda yok; logpoint'te $HITCOUNT |
| Dependent breakpoint | Only enable when the following breakpoint is hit | Seçilen breakpoint çalışana kadar devre dışı | Triggered breakpoint |
| Tracepoint | Action: Show a message in the Output Window | Suspend kapalı, Evaluate and log | Logpoint |
| Data breakpoint | Break when value changes (.NET Core 3+) | Data breakpoint (yalnız Windows, .NET Core 3+) | Dokümanda yok |
| Exception'da durmak | Exception Settings (Ctrl+Alt+E) | Exception breakpoint | All Exceptions / User-Unhandled Exceptions |
| Sonraki satırı değiştirmek | Set Next Statement (Ctrl+Shift+F10) | Execution pointer'ı taşımak | Dokümanda yok |
| Nesneyi işaretlemek | Make Object ID ($1) | Mark Object (label) | Dokümanda yok |
| İfade çalıştırmak | Immediate Window, Watch | Immediate window, Evaluate | Debug Console, Watch |
| Hot Reload | Hot Reload, Edit and Continue | Hot Reload | csharp.experimental.debug.hotReload |
| Kendi kodunla sınırlamak | Enable Just My Code | External source debugging (decompile) ayarı | csharp.debug.justMyCode |
Üretimde Dikkat
Production'daki bir process'e interaktif debugger bağlamak, o process'i breakpoint'te dondurmak demektir: o sırada gelen bütün istekler bekler, health check düşer, orchestrator process'i yeniden başlatabilir. Production'da teşhis için durdurmayan araçları kullan: log, dump ve trace. Bunlar serinin dokuzuncu ve onuncu yazısının konusu.
Koşullu breakpoint'in bedelini bil. Debugger satır her çalıştığında durur, koşulu değerlendirir ve devam eder; sıcak bir döngüde bu programı belirgin şekilde yavaşlatabilir ve timing'e bağlı bug'ı ortadan kaldırabilir. Çok sık çalışan bir satırda, debugger altında koşulu koda koymak (if (order.Id == 1042) Debugger.Break();) daha hızlıdır; işin bitince silmeyi unutma.
Release build'i debug ederken optimizasyonlar yüzünden değişkenler "optimized away" görünür, bazı satırlar atlanır. Bu bir debugger hatası değil; derleyici kodu yeniden düzenlemiş. Gerekirse Visual Studio Code'da csharp.debug.suppressJITOptimizations gibi ayarlarla JIT (Just-In-Time) optimizasyonlarını debug sırasında kapatabilirsin, ama gördüğün davranış production'dakinden farklı olabilir.
DebuggerDisplay ve tracepoint mesajlarına müşteri e-postası, kart numarası gibi PII (Personally Identifiable Information) koyma. Tracepoint çıktısı Output penceresinde kalsa da ekran paylaşımlarında, kayıtlarda ve kopyalanan log'larda dolaşır.
Sık Düşülen Tuzaklar
Watch penceresinde yan etkili ifade çalıştırmak. queue.Dequeue() ya da lazy yüklenen bir property'yi Watch'a eklemek, programın durumunu sen bakarken değiştirir; "debugger'la bakınca bug kayboluyor" vakalarının bir kısmı budur.
Set Next Statement ile atlanan kodun yan etkilerini unutmak. Bir using bloğunun başını atlarsan connection hiç açılmaz; geri giderek bir satırı ikinci kez çalıştırırsan veritabanına iki kayıt yazabilirsin.
Exception Settings'te "bütün CLR (Common Language Runtime) exception'larında dur"u açık unutmak. Framework kendi içinde çok sayıda exception fırlatıp yakalar; debugger her birinde durur ve asıl aradığın noktaya hiç gelemezsin. Yalnız şüphelendiğin tipi işaretle.
AI önerisini test etmeden uygulamak. Önerilen düzeltme belirtiyi kapatıp sebebi bırakabilir; yukarıdaki sipariş örneğinde PaymentStep'e "Cancelled ise dokunma" eklemek doğru, ama FraudCheck'i kaldırmak da testleri yeşile çevirir.
Data breakpoint'in struct field'larında ve static field'larda çalışmadığını bilmemek. Breakpoint kurulmuyorsa önce bu sınırları kontrol et, sonra Object ID ya da kod içi iz kullan.
Küçük Bir Deneme
★☆☆ FirstChanceException handler'ını kendi bir projende 30 saniye açık tut ve sayacı yazdır. Kaç exception fırlatılıp yakalanıyor, hangileri senin kodundan, hangileri framework'ten?
★★☆ Order'ı class yerine struct yap ve Status izini tekrar çalıştır. Çıktı değişiyor mu? Visual Studio'da data breakpoint bu durumda neden kurulamaz; Object ID neden de işe yaramaz?
★★★ dotnet watch ile çalışan endpoint'te PriceRules.Discounted'ın imzasını değiştir (örneğin ikinci bir parametre ekle). Hot Reload ne diyor? Hangi değişiklikleri sorunsuz uygulayabiliyor, hangilerinde yeniden başlatma istiyor; bir liste çıkar.
Tek thread'lik bir programda debugger güçlü bir mikroskop. Ama Bookstore'un checkout'u async ve çok thread'li; orada durmak, bazen bug'ı kendi elinle saklamak demek. Bir sonraki yazıda Parallel Stacks ve Tasks penceresiyle async call stack'leri okuyup deadlock ve race condition avına çıkıyoruz.