Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Coding Conventions ve Ekip Standartları — #02

C# Convention'larını .editorconfig ile Kodlamak: Code-style Option'ları, Naming Rule'lar ve Build'de Zorlamak

Mert Özen 14 Kas 2026 12 dk 3 görüntülenme
C# Convention'larını .editorconfig ile Kodlamak: Code-style Option'ları, Naming Rule'lar ve Build'de Zorlamak

Naming rule'un kendi severity satırı build'de okunmaz; kuralları dotnet build'de zorlamak için EnforceCodeStyleInBuild ile birlikte dotnet_diagnostic.IDE1006.severity gerekir. Yazı .editorconfig hiyerarşisini, code-style option'larının IDE kural ID'lerini, naming rule'ların symbols-style-severity üçlüsünü, test klasörü override'ını, category-Style'ın seçmediğin varsayılanları zorlamasını ve yalnız IDE'de çalışan kuralları gerçek build çıktılarıyla anlatıyor.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (SDK 10.0.402) ile derlenip denenmiştir.

Önce Tahmin Et: Kural Var, Uyarı Yok

Bookstore ekibi ilk yazıda verdiği naming kararlarını .editorconfig'e yazıyor. İlk kural basit: interface'ler I ile başlar. Kural dosyada, severity warning, ve repo kökündeki Directory.Build.props'ta code-style kurallarını build'e taşıyan property açık:

root = true

[*.cs]
indent_style = space
indent_size = 4

# Naming: interface I öneki
dotnet_naming_rule.interfaces_begin_with_i.symbols = interfaces
dotnet_naming_rule.interfaces_begin_with_i.style = begins_with_i
dotnet_naming_rule.interfaces_begin_with_i.severity = warning
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = *
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case
<Project>
  <PropertyGroup>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>

Sipariş modülünde stok kontrolü için yazılmış interface'in adı ise StockChecker; yani kuralı açıkça ihlal ediyor:

namespace Bookstore.Ordering;

public interface StockChecker
{
    Task<bool> IsInStockAsync(string isbn, int quantity);
}

Naming rules dokümanına göre IDE bu ihlali kuralda yazan severity'yle işaretler. Peki komut satırında dotnet build kaç uyarı verir? Bir mi, sıfır mı? Cevabı tahmin etmeden önce kuralın hangi satırının build'e ne söylediğini düşün.

.editorconfig Nasıl Okunur: Hiyerarşi ve Glob'lar

EditorConfig, editörden bağımsız bir format; spec'i editorconfig.org'da yayımlı ve .NET derleyicisi (Roslyn) onu analyzer configuration'ı olarak da okuyor. Bir .cs dosyası derlenirken araç dosyanın bulunduğu klasörden başlayıp yukarı doğru bütün .editorconfig dosyalarını toplar. root = true içeren dosyaya gelince durur. Aynı property birden fazla dosyada varsa dosyaya en yakın olan kazanır; aynı dosyada iki section aynı property'yi ayarlarsa sonra gelen kazanır.

Bunun pratik anlamı: repo köküne tek bir .editorconfig koyup root = true yazarsın, böylece geliştiricinin home klasöründe duran kişisel bir dosya repo'yu etkilemez. Belirli bir klasör için farklı kural istiyorsan o klasöre root satırı olmayan küçük bir dosya koyarsın; yalnız farklı olan satırları yazar, gerisini kökten miras alır.

Section başlıkları glob'dur. [*.cs] her klasördeki C# dosyalarını, [*.{cs,csx}] iki uzantıyı birden, [**/Generated/*.cs] herhangi bir derinlikteki Generated klasörünü yakalar. Başlıkta / varsa desen .editorconfig'in kendi klasörüne göre çözülür; yoksa her klasörde aranır. Bookstore'da yapı şöyle; kökteki .editorconfig root = true ile bütün kuralları taşıyor, tests/ altındaki yalnız test'e özgü farkları:

Bookstore/
├── .editorconfig
├── Directory.Build.props
├── Bookstore.slnx
├── src/
│   └── Bookstore.Ordering/
└── tests/
    ├── .editorconfig
    └── Bookstore.Ordering.Tests/

Code-Style Option'ları ve IDE Kural ID'leri

Bir code-style option'ı iki şey söyler: tercih ve (isteğe bağlı) severity. csharp_style_namespace_declarations = file_scoped:warning satırında tercih file_scoped, severity warning. Her option'ın arkasında bir ya da birkaç IDE kuralı vardır ve build çıktısında, dotnet format raporunda ve suppress ederken gördüğün şey bu ID'dir. Bookstore'un kullandığı başlıca eşleşmeler:

OptionBookstore tercihiKural ID'si
csharp_style_var_for_built_in_types, _when_type_is_apparent, _elsewherefalse (açık tip)IDE0008 (açık tip iste), IDE0007 (var iste)
csharp_style_namespace_declarationsfile_scopedIDE0161 (file-scoped iste), IDE0160 (block iste)
csharp_using_directive_placementoutside_namespaceIDE0065
csharp_style_expression_bodied_methodswhen_on_single_lineIDE0022
csharp_style_expression_bodied_propertiestrueIDE0025
dotnet_style_prefer_collection_expressionwhen_types_loosely_matchIDE0028, IDE0300–IDE0305
csharp_style_prefer_primary_constructorstrue (suggestion)IDE0290

Tablodaki bilgiyi bilmenin iki faydası var. Birincisi, build bir uyarı verdiğinde ID'den option'a gidebilirsin; Microsoft Learn'de her IDE kuralının sayfası hangi option'la yönetildiğini yazar. İkincisi, aşağıda göreceğimiz gibi bazı araçlar option satırının sonundaki severity'yi değil, yalnız ID'ye bağlı severity'yi okur.

Bookstore'un küçük bir StockService'inde bu option'ları :warning son ekiyle yazıp build aldık; dosya block-scoped namespace, klasik constructor, new List<string>() ve tek satırlık bir block-body method içeriyor:

[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_prefer_primary_constructors = true:warning
dotnet_style_prefer_collection_expression = when_types_loosely_match:warning
csharp_style_expression_bodied_methods = when_on_single_line:warning
$ dotnet build --no-incremental
StockService.cs(1,11): warning IDE0161: Convert to file-scoped namespace
StockService.cs(7,16): warning IDE0290: Use primary constructor
StockService.cs(14,35): warning IDE0028: Collection initialization can be simplified
StockService.cs(25,30): warning IDE0022: Use expression body for method

$ dotnet format style --verify-no-changes
StockService.cs(1,11): warning IDE0161: Convert to file-scoped namespace
StockService.cs(25,30): warning IDE0022: Use expression body for method
StockService.cs(7,16): warning IDE0290: Use primary constructor

Build dört kuralı da gördü. Microsoft Learn'deki code analysis sayfası, option = value:severity biçiminin build'de geçerli olmasının .NET 9 ile geldiğini yazıyor ve çıktı bunu doğruluyor. Ama dotnet format IDE0028'i görmedi. IDE'ler serisinin "Refactoring Araçları ve Code Style: Quick Actions, Alt+Enter, .editorconfig ve Code Cleanup" yazısında aynı farkı ölçmüştük; SDK 10.0.402'de hâlâ duruyor. Ders aynı: CI pipeline'ında zorlayacağın her kuralı ayrıca dotnet_diagnostic.IDExxxx.severity satırıyla yaz, iki araç da aynı şeyi görsün.

Naming Rule Anatomisi: Symbols, Style, Severity

Naming rule üç ayrı tanımdan oluşur ve üçü birbirine adla bağlanır. Symbols bir symbol grubunu tarif eder: hangi tür (field, method, interface…), hangi erişim seviyesi, hangi modifier'lar. Style ismin biçimini tarif eder: önek, son ek, büyük-küçük harf düzeni. Rule ikisini birleştirir ve bir severity verir. Satır sırası önemli değil; Microsoft Learn'deki naming rules sayfası da property sırasının anlamsız olduğunu söylüyor. Bookstore'un dört kuralı:

[*.cs]
# Severity: build'de tüm naming rule'lar için tek ayar
dotnet_diagnostic.IDE1006.severity = warning

# Kural 1: private field'lar _camelCase
dotnet_naming_rule.private_fields_underscore.symbols = private_fields
dotnet_naming_rule.private_fields_underscore.style = underscore_camel
dotnet_naming_rule.private_fields_underscore.severity = warning

# Kural 2: const'lar PascalCase
dotnet_naming_rule.constants_pascal.symbols = constants
dotnet_naming_rule.constants_pascal.style = pascal
dotnet_naming_rule.constants_pascal.severity = warning

# Kural 3: async method'lar Async ile biter
dotnet_naming_rule.async_methods_suffix.symbols = async_methods
dotnet_naming_rule.async_methods_suffix.style = ends_with_async
dotnet_naming_rule.async_methods_suffix.severity = warning

# Kural 4: interface'ler I ile başlar
dotnet_naming_rule.interfaces_begin_with_i.symbols = interfaces
dotnet_naming_rule.interfaces_begin_with_i.style = begins_with_i
dotnet_naming_rule.interfaces_begin_with_i.severity = warning

# Symbols
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_symbols.constants.applicable_kinds = field
dotnet_naming_symbols.constants.applicable_accessibilities = *
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_symbols.async_methods.applicable_kinds = method
dotnet_naming_symbols.async_methods.applicable_accessibilities = *
dotnet_naming_symbols.async_methods.required_modifiers = async
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = *

# Styles
dotnet_naming_style.underscore_camel.required_prefix = _
dotnet_naming_style.underscore_camel.capitalization = camel_case
dotnet_naming_style.pascal.capitalization = pascal_case
dotnet_naming_style.ends_with_async.required_suffix = Async
dotnet_naming_style.ends_with_async.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case

Test için kuralların her birini bozan bir OrderService yazdık: bulkDiscountThreshold adında bir private const, defaultCurrency adında bir private static field, items adında bir private field, Task<bool> dönen ama Async son eki olmayan PlaceOrder ve StockChecker interface'i. Build:

$ dotnet build Bookstore.slnx --no-incremental
src/Bookstore.Ordering/OrderService.cs(14,38): warning IDE1006: Naming rule violation: Missing prefix: '_'
src/Bookstore.Ordering/OrderService.cs(13,36): warning IDE1006: Naming rule violation: Missing prefix: '_'
src/Bookstore.Ordering/OrderService.cs(12,23): warning IDE1006: Naming rule violation: These words must begin with upper case characters: bulkDiscountThreshold
src/Bookstore.Ordering/OrderService.cs(18,29): warning IDE1006: Naming rule violation: Missing suffix: 'Async'
src/Bookstore.Ordering/OrderService.cs(3,18): warning IDE1006: Naming rule violation: Missing prefix: 'I'
tests/Bookstore.Ordering.Tests/OrderServiceTests.cs(10,23): warning IDE1006: Naming rule violation: Missing suffix: 'Async'

Önce tahminin cevabı: açılıştaki dosyayla build sıfır uyarı veriyordu. Naming rule'un kendi severity satırı yalnız IDE'de okunuyor; derleyici bu satırı anlamıyor. Microsoft Learn'deki naming rules sayfası bunu bir dipnotta söylüyor ve build'de zorlamak için dotnet_diagnostic.IDE1006.severity satırını gösteriyor. Bütün naming ihlalleri tek bir ID'yle, IDE1006 ile raporlanır. Biz bunu ayrıca denedik: private field kuralının severity'sini suggestion'a, const kuralınınkini error'a çektik, build'deki altı uyarının hiçbiri değişmedi. Build'de seviye her zaman IDE1006'nın seviyesi. Tek istisna none: kuralı tamamen kapatıyor, bunu aşağıda test klasöründe kullanacağız.

Kuralların Sırası ve Çakışma

bulkDiscountThreshold satırına dikkat et. Bu symbol iki gruba birden uyuyor: private bir field (Kural 1, _ önekini ister) ve bir const (Kural 2, PascalCase ister). Build "Missing prefix: '_'" demedi, "These words must begin with upper case characters" dedi; yani Kural 2 kazandı. Sebep dosyadaki sıra değil. Naming rules sayfasına göre kurallar dosyadaki yerlerine göre değil, ne kadar spesifik olduklarına göre sıralanır: erişim seviyesi, modifier ve symbol türü daha dar olan kural önce gelir. Const grubunun required_modifiers = const satırı onu private field grubundan daha dar yapıyor. Doğrulamak için iki kuralın bloklarının yerini değiştirip tekrar build aldık; aynı satır için aynı mesaj geldi.

Visual Studio 2019 öncesinde ilk eşleşen kural uygulanıyordu ve kuralları en spesifikten en genele sıralamak gerekiyordu; internetteki eski örneklerin çoğu bu yüzden sıraya özen gösterir. Bugün sıra önemsiz, ama çakışma hâlâ mümkün. Örneğin "public method'lar PascalCase" ve "async method'lar Async ile biter" kuralları public async bir method'da kesişir. Dokümanın önerisi, kesişim için ayrı ve daha spesifik bir kural yazmak. Doküman ayrıca required_modifiers = static olan bir grubun const'ları da kapsadığını (const'lar örtük olarak static) hatırlatıyor; private static field'lar için runtime'daki gibi s_ önekli bir kural eklersen const kuralını açıkça yazmış olman gerekir. Bizim denememizde const kuralı her iki erişim tanımıyla da kazandı, ama sonucu her SDK güncellemesinde test edemeyeceğin için kesişimi açık bir kuralla kapatmak daha güvenli.

Klasör Bazında Override: Test Projeleri

Test method'larının kendi convention'ları var. Bir test async Task döndüğünde çoğu ekip test adının sonuna Async yazmaz; test runner method'u adıyla raporlar ve son ek gereksiz olur. Yukarıdaki çıktının son satırı tam bu: CalculateTotal_applies_bulk_discount_at_five_books için "Missing suffix: 'Async'". Çözüm tests/ klasörüne küçük bir .editorconfig:

[*.cs]
# Test method'larında Async son eki istemiyoruz
dotnet_naming_rule.async_methods_suffix.severity = none

Bu dosyada root = true yok, yani kökteki her şeyi miras alıyor ve yalnız bir kuralı kapatıyor. Build'de test dosyasındaki uyarı kayboldu, src/ altındaki beş uyarı yerinde kaldı. Aynı yöntemle, örneğin, benchmark projesinde ya da migration klasöründe farklı kurallar uygulanabilir.

Test adındaki alt çizgiler ise uyarı üretmedi, çünkü Bookstore'un method'lar için genel bir PascalCase kuralı yok; .NET'in varsayılan naming stilleri yalnız IDE önerisi düzeyinde. Method adlarına da kural koyacaksan test klasöründe onu da kapatman gerekecek.

Build'de Zorlamak ve Solution Geneline Yaymak

Code-style kuralları komut satırı build'inde varsayılan olarak kapalı; Visual Studio'da öneri olarak görünür ama dotnet build onları çalıştırmaz. Açmanın yolu MSBuild (Microsoft Build Engine) özelliği EnforceCodeStyleInBuild. Bunu her .csproj'a tek tek yazmak yerine repo kökündeki Directory.Build.props'a koyarsın; MSBuild bu dosyayı proje klasöründen yukarı doğru arar ve bulduğu ilkini her projeye dahil eder. Yeni bir proje eklendiğinde kural otomatik olarak gelir.

Kural seviyelerini toptan ayarlamanın bir yolu da kategori severity'si: dotnet_analyzer_diagnostic.category-Style.severity = warning bütün Style kategorisini warning yapar. İlk bakışta pratik görünüyor; Bookstore çözümüne ekleyip build aldık:

$ dotnet build Bookstore.slnx --no-incremental
   5 warning IDE1006: Naming rule violation
   2 warning IDE0160: Convert to block scoped namespace
   1 warning IDE0058: Expression value is never used
   1 warning IDE0022: Use block body for method
   1 warning IDE0008: Use explicit type instead of 'var'

(Tekrarlanan satırları ayıklayıp uyarıları ID'ye göre saydırdık.) İkinci satır öğretici: build bizden file-scoped namespace'leri block-scoped'a çevirmemizi istiyor. Biz namespace option'ını bu deneme dosyasında ayarlamamıştık ve kategori ayarı, option'ın varsayılanı olan block_scoped'u zorladı. IDE0022'de de aynısı: varsayılan block body. Kategori severity'si "seçmediğin bütün varsayılanları da kural yap" demek. Kullanacaksan önce kategorideki her option'ı bilinçli olarak ayarla, istemediğin kuralları dotnet_diagnostic.IDExxxx.severity = none ile tek tek kapat. Bookstore kural kural gitmeyi seçti.

Üretilmiş Kodu Dışarıda Bırakmak

Analyzer'lar üretilmiş kodu varsayılan olarak atlar: dosya adı .g.cs ya da .designer.cs ile bitiyorsa veya dosyanın başında <auto-generated> yorumu varsa. Ama her code generator bu işareti koymuyor. Bookstore'da bir ödeme service'inin istemcisi bir araçla üretilip Generated klasörüne konmuş, başında işaret yok; build onun için de naming uyarıları verdi:

src/Bookstore.Ordering/Generated/PaymentApiClient.cs(5,20): warning IDE1006: Naming rule violation: Missing prefix: '_'
src/Bookstore.Ordering/Generated/PaymentApiClient.cs(7,31): warning IDE1006: Naming rule violation: Missing suffix: 'Async'

Bu dosyayı düzeltmenin anlamı yok, araç bir sonraki üretimde geri yazar. Kök .editorconfig'e bir section ekledik:

# Üretilmiş kod: analyzer'lar dokunmasın
[**/Generated/*.cs]
generated_code = true

Bir sonraki build'de Generated klasöründen tek uyarı gelmedi; kalan beş uyarı OrderService'e ait.

Sık Düşülen Tuzaklar

Yalnız IDE'de çalışan kurallar. Bazı code-style kuralları performans gerekçesiyle build'de hiç çalışmaz; EnforceCodeStyleInBuild ve severity ne olursa olsun. Roslyn kaynak kodundaki EnforceOnBuildValues.cs dosyası bunları "Never" olarak işaretliyor: IDE0001 ve IDE0002 (isim sadeleştirme), IDE0003 ve IDE0009 (this. kaldır/ekle), IDE0049 (Int32 yerine int), IDE0035 (erişilemeyen kod) ve IDE0079 (gereksiz suppression) bunlardan bazıları. İlk yazıda this. satırlarını build'in değil dotnet format'ın yakalaması bu yüzdendi. Bu kurallar senin için önemliyse CI'da dotnet format --verify-no-changes da çalışmalı. IDE0005 (gereksiz using) ise ayrı bir durum: build'de ancak GenerateDocumentationFile açıkken çalışıyor, bunu IDE'ler serisinde görmüştük.

Naming ihlallerini dotnet format'ın düzelteceğini sanmak. Denedik:

$ dotnet format style Bookstore.slnx --diagnostics IDE1006 --severity warn
Unable to fix IDE1006. Code fix NamingStyleCodeFixProvider doesn't support Fix All in Solution.

Rename bir refactoring; çağıran yerleri, reflection'ı ve serialize edilen adları etkileyebileceği için toplu yapılmıyor. Naming ihlalleri IDE'nin rename'iyle, dosya dosya düzeltilir.

.editorconfig ile global AnalyzerConfig'i karıştırmak. .NET bir de .globalconfig dosyasını tanıyor: başında is_global = true olan, dosya yoluna göre section içermeyen ve projedeki bütün dosyalara uygulanan bir configuration. Ortak bir NuGet paketiyle şirket genelinde kural dağıtmak için uygun. İki hata sık: .globalconfig'e [*.cs] başlığı yazmak ve iki dosya çeliştiğinde globalconfig'in kazanacağını sanmak. Birincisini derleyici söylüyor:

CSC : warning InvalidGlobalSectionName: Global analyzer config section name '*.cs' is invalid as it is not an absolute path. Section will be ignored. Section was declared in file: '.globalconfig'

İkincisi için .globalconfig'e dotnet_diagnostic.IDE1006.severity = error ve dotnet_diagnostic.IDE0008.severity = error yazdık. IDE0008'i .editorconfig hiç ayarlamadığı için test dosyasındaki var error oldu; IDE1006 ise .editorconfig'te warning olduğu için warning kaldı. Çelişkide .editorconfig kazanır.

TreatWarningsAsErrors'u hazırlıksız açmak. Test klasörü override'ı ve generated_code ayarından sonra çözümde beş naming uyarısı kalmıştı; aynı çözümü -p:TreatWarningsAsErrors=true ile build ettiğimizde beşi de error'a döndü ve build kırıldı (Build FAILED, 5 Error(s)). Bu property yalnız stil uyarılarını değil derleyicinin nullable uyarıları dahil her şeyini error yapar; eski bir kod tabanında yüzlerce error demek. Önce uyarıları sıfırla ya da tek tek kural severity'sini error'a çek, global anahtarı en son aç. Bu geçişin stratejisi beşinci yazıda.

Küçük Bir Deneme

★☆☆ Kendi projende EnforceCodeStyleInBuild'i açmadan ve açtıktan sonra build al. Uyarı sayısı değişmiyorsa .editorconfig'te severity'si warning ya da error olan hiçbir code-style kuralın yok demektir; hangi option'ların sonunda :warning olduğuna bak.

★★☆ Private static field'lar için s_ önekli bir kural ekle (required_modifiers = static) ve private const'larının hangi kurala düştüğünü build çıktısından kontrol et. Sonucu değiştirmek için hangi kuralı daha spesifik yapman gerekir?

★★★ dotnet_analyzer_diagnostic.category-Style.severity = warning satırını geçici olarak ekle, çıktıyı kural ID'sine göre say ve her ID için karar ver: option'ını ayarla ya da none ile kapat. Sonunda kategori satırını kaldırıp yalnız bilinçli kararların kaldığı bir .editorconfig bırak.

Stil ve naming kurallarını .editorconfig'e yazdık ve build'e bağladık. Girinti, boşluk ve satır kırma ise hâlâ açıkta. Bir sonraki yazıda formatter seçimine bakacağız: dotnet format mı, CSharpier mı ve aynı kuralın iki aracın elinde kalmasının neden "format çatışması" çıkardığı.