Mert Özen Her Satırda Daha İleri
Backend C# Temelleri — #18

C# Kodu Düzenlemek: Namespace, global using, Solution ve NuGet

Mert Özen 7 Kas 2026 8 dk 1 görüntülenme
C# Kodu Düzenlemek: Namespace, global using, Solution ve NuGet

internal bir member'a başka projeden erişince neden CS0117 alırsın? Proje ve assembly sınırı, file-scoped namespace, global using ve implicit using'lerin üretilen dosyası, .NET 10 SDK'nın .slnx solution'ı, projeler arası referans ve NuGet paketi eklemek.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un fiyat hesaplarını Bookstore.Core adında ayrı bir projeye taşıdık. KDV oranını dışarıya açmak istemediğimiz için internal yaptık:

namespace Bookstore.Core.Pricing;

public static class PriceCalculator
{
    internal const decimal VatRate = 0.10m;

    public static decimal ApplyDiscount(decimal price, int percent)
    {
        return Math.Round(price * (100 - percent) / 100m, 2);
    }

    public static decimal WithVat(decimal price)
    {
        return Math.Round(price * (1 + VatRate), 2);
    }
}

Console uygulaması Bookstore.App bu projeye referans veriyor ve şunu yazıyor:

using Bookstore.Core.Pricing;

Console.WriteLine(PriceCalculator.WithVat(100m));
Console.WriteLine(PriceCalculator.VatRate);

Bu kod derlenir mi? Derlenmezse derleyici sence ne der: "erişim yok" mu, başka bir şey mi?

Proje, Assembly ve internal

Kod derlenmez ve hata mesajı çoğu kişinin beklediği gibi değil:

src/Bookstore.App/Program.cs(4,35): error CS0117: 'PriceCalculator' does not contain a definition for 'VatRate'

Derleyici "erişemezsin" demiyor, "böyle bir şey yok" diyor. Sebebi, internal'ın sınırının proje olması. Her proje derlendiğinde tek bir assembly, yani bir .dll dosyası üretir. internal bir member, kendi assembly'sinin içinden herkese açıktır, dışından ise hiç görünmez. Bookstore.App derlenirken Bookstore.Core.dll'e bakan derleyici VatRate'i görmezden geliyor; sana da onu hiç bulamamış gibi cevap veriyor.

Bu yazının konusu tam olarak bu sınırlar. Namespace'ler kodu isimlendirerek düzenler, projeler ise kodu fiziksel olarak ayırır ve neyin dışarı açılacağını belirler. Program büyüdükçe ikisine de ihtiyacın olacak. public ve internal dışındaki access modifier'ları OOP serisinin encapsulation yazısında ayrıntılı göreceğiz; burada önemli olan, bir class library yazdığında public olan her şeyin onun dış yüzü olduğu.

Namespace: Tiplerin Soyadı

Bir namespace, tiplerin tam adının parçasıdır. PriceCalculator'ın tam adı Bookstore.Core.Pricing.PriceCalculator. Aynı isimde iki class, farklı namespace'lerde olduğu sürece çakışmaz; tıpkı aynı ofiste iki "Ali" olunca soyadıyla ayırt etmen gibi.

Yukarıdaki dosyanın ilk satırı C# 10 ile gelen file-scoped namespace yazımı: noktalı virgülle biten tek satır, dosyadaki her şeyi o namespace'e koyar. Eski yazım ise süslü parantezlerle bir blok açar ve bütün dosyayı bir seviye içeri iter:

namespace Bookstore.Core.Catalog
{
    public static class IsbnValidator
    {
        public static bool IsValid(string isbn)
        {
            string digits = isbn.Replace("-", "");
            if (digits.Length != 13 || !digits.All(char.IsDigit))
            {
                return false;
            }

            int sum = 0;
            for (int i = 0; i < 12; i++)
            {
                int digit = digits[i] - '0';
                sum += i % 2 == 0 ? digit : digit * 3;
            }

            int check = (10 - sum % 10) % 10;
            return check == digits[12] - '0';
        }
    }
}

İkisi aynı sonucu verir. Bir dosyada neredeyse her zaman tek bir namespace olduğu için yeni kodda file-scoped yazımı kullan; bir girinti seviyesi kazanırsın. Eski projelerde blok yazımını göreceksin. İkisini aynı dosyada karıştıramazsın: error CS8955: Source file can not contain both file-scoped and normal namespace declarations.

Namespace ile klasör arasında derleyicinin zorladığı bir bağ yok; Catalog klasöründeki bir dosyaya istediğin namespace'i yazabilirsin. Ama yerleşik gelenek, namespace'in proje adı artı klasör yolu olması: Bookstore.Core/Pricing/PriceCalculator.cs → Bookstore.Core.Pricing. IDE'ler yeni dosyayı bu kurala göre oluşturur ve bu kurala uyan bir projede bir tipi bulmak için arama yapmana gerek kalmaz. Top-level statements kullanan Program.cs'teki kod ise hiçbir namespace'e ait değildir, global namespace'tedir.

using Yönergeleri ve global using

Her seferinde tam adı yazmamak için dosyanın başına using eklersin: using Bookstore.Core.Pricing; ile o namespace'teki tipleri kısa adlarıyla kullanırsın. Bir dosyadaki using sadece o dosyayı etkiler. Proje büyüdükçe aynı using'leri her dosyanın başında tekrarlamaya başlarsın.

C# 10 bunun için global using getirdi. Projede bir kez yazılan global using bütün dosyalar için geçerli olur. Yaygın kullanım, bunları ayrı bir dosyada toplamak:

global using System.Globalization;
global using Bookstore.Core.Catalog;
global using Bookstore.Core.Pricing;

Aynı şeyi csproj dosyasından da yapabilirsin. Using item'ı bir global using ekler, Remove ise var olanı çıkarır:

<ItemGroup>
  <Using Include="Humanizer" />
  <Using Remove="System.Net.Http" />
</ItemGroup>

Peki Console, File, List<T> şimdiye kadar hiç using yazmadan nasıl çalıştı? Cevap, csproj'daki <ImplicitUsings>enable</ImplicitUsings> satırı. SDK, derleme sırasında obj klasörüne global using'lerden oluşan bir dosya üretir. Yukarıdaki iki satırı ekledikten sonra obj/Debug/net6.0/Bookstore.App.GlobalUsings.g.cs şöyle oldu:

// <auto-generated/>
global using Humanizer;
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;

Humanizer listeye girdi, System.Net.Http çıktı. System.Globalization ise burada yok, çünkü onu kendi GlobalUsings.cs dosyamızda yazdık. Bir tipin "nereden geldiğini" bulamadığında ilk bakacağın yer bu dosya.

Ölçülü kullan. Global using, bir dosyayı okurken bağımlılıklarını görmeyi zorlaştırır. Projenin hemen her dosyasında kullanılan birkaç namespace için iyidir; tek bir dosyada lazım olan bir namespace o dosyanın kendi using'inde kalsın. Bir de alias yazımı var: using Pricing = Bookstore.Core.Pricing; ile uzun bir namespace'e kısa bir ad verebilir, aynı isimli iki tipi ayırt edebilirsin.

Solution: Projeleri Bir Arada Tutmak

Bir solution, birlikte geliştirilen projelerin listesidir. Kendi başına kod içermez; IDE'ye ve dotnet build'e "bu projeler bir arada" der. Bookstore'u iki projeye ayıralım: kullanıcıyla konuşan console uygulaması ve iş kurallarını tutan class library.

dotnet new sln -n Bookstore
dotnet new console -o src/Bookstore.App
dotnet new classlib -o src/Bookstore.Core
dotnet sln add src/Bookstore.App src/Bookstore.Core

.NET 10 SDK'da dotnet new sln, eski .sln yerine XML tabanlı yeni .slnx formatında bir dosya üretir. Eski formatı istersen --format sln verirsin; .NET 6 ya da 8 SDK'lı bir ekipte çalışıyorsan bunu yapman gerekebilir, çünkü eski SDK'lar .slnx'i tanımaz. Oluşan dosya okunaklı:

<Solution>
  <Folder Name="/src/">
    <Project Path="src/Bookstore.App/Bookstore.App.csproj" />
    <Project Path="src/Bookstore.Core/Bookstore.Core.csproj" />
  </Folder>
</Solution>

Bir ayrıntı daha: .NET 10 SDK template'leri net10.0 hedefler ve -f seçeneği net6.0 kabul etmiyor. Bu serinin kurgusuna uymak için iki csproj'da da TargetFramework'ü net6.0 yaptım ve <LangVersion>10</LangVersion> ekledim. Classlib template'inin ürettiği boş Class1.cs'i silip yerine yukarıdaki iki dosyayı Pricing ve Catalog klasörlerine koydum.

Klasör yapısını src altında toplamak bir gelenek; ileride tests klasörüne test projeleri eklediğinde düzen bozulmaz. Solution'ın kök klasöründe dotnet build dediğinde içindeki bütün projeler doğru sırayla derlenir.

Projeler Arası Referans

İki proje aynı solution'da olsa da birbirini görmez. Console uygulamasının library'yi kullanabilmesi için ona referans vermesi gerekir:

$ dotnet add src/Bookstore.App reference src/Bookstore.Core
Reference `..\Bookstore.Core\Bookstore.Core.csproj` added to the project.

Bu komut Bookstore.App.csproj'a bir ProjectReference ekler. Referansı unutursan ya da silersen, using satırında şu hatayı alırsın:

src/Bookstore.App/Program.cs(2,7): error CS0246: The type or namespace name 'Bookstore' could not be found (are you missing a using directive or an assembly reference?)

Mesajın sonundaki iki ihtimali ezberle: ya using eksik ya da referans. Using doğruysa sorun neredeyse her zaman csproj'dadır.

Referansın yönü önemli. Bookstore.App, Bookstore.Core'a bağlıdır; tersi değil. Core, onu kimin kullandığını bilmez; yarın aynı library'yi bir Web API projesine de bağlayabilirsin. İki projenin birbirine referans vermesine izin yok. Denediğimde build şu hatayla durdu: error MSB4006: There is a circular dependency in the target dependency graph. Böyle bir ihtiyaç doğduysa, ortak kodu üçüncü bir projeye çıkarman gerektiğinin işaretidir.

NuGet Paketi Eklemek

Başkasının yazdığı kodu projeye NuGet paketi olarak eklersin. Örnek olarak, metinleri insan diline çeviren Humanizer'ı ekleyelim:

dotnet add src/Bookstore.App package Humanizer.Core --version 2.14.1

Komut paketi indirir ve csproj'a bir PackageReference yazar. İki komuttan sonra Bookstore.App.csproj'a şu iki blok eklenmişti:

<ItemGroup>
  <ProjectReference Include="..\Bookstore.Core\Bookstore.Core.csproj" />
</ItemGroup>

<ItemGroup>
  <PackageReference Include="Humanizer.Core" Version="2.14.1" />
</ItemGroup>

Artık her şeyi birleştirebiliriz. Global using'ler sayesinde Program.cs'te tek bir using satırı yok:

decimal price = PriceCalculator.ApplyDiscount(450m, 15);
Console.WriteLine(price.ToString(CultureInfo.InvariantCulture));                          // 382.5
Console.WriteLine(PriceCalculator.WithVat(price).ToString(CultureInfo.InvariantCulture)); // 420.75
Console.WriteLine(IsbnValidator.IsValid("978-0441172719")); // True
Console.WriteLine(IsbnValidator.IsValid("978-0441172710")); // False
Console.WriteLine("book".ToQuantity(3));                    // 3 books
Console.WriteLine("OrderStatusPending".Humanize());         // Order status pending

Derledikten sonra bin/Debug/net6.0 klasöründe üç dll vardı: Bookstore.App.dll, Bookstore.Core.dll ve Humanizer.dll. Paketin kendisi her projeye kopyalanmaz; makinedeki ortak NuGet önbelleğine (~/.nuget/packages) bir kez iner, derleme oradan alır. Projede hangi paketlerin olduğunu dotnet list package ile görürsün.

18-solution-structure-tr.png

Paket seçerken acele etme. Her paket, güncel tutman gereken bir bağımlılık ve potansiyel bir güvenlik açığı demek. nuget.org'da son güncelleme tarihine, indirme sayısına, lisansa ve hedeflediği framework'lere bak. Versiyonu her zaman açıkça yaz; "en son ne varsa" ile kurulan bir proje, aynı kodla iki ay sonra farklı davranabilir.

Sık Düşülen Tuzaklar

Bir library'de yardımcı her şeyi public yapmak. Public olan her şey, o library'yi kullanan herkesin bağımlı olabileceği bir contract'tır. Varsayılan tercihin internal olsun, gerçekten dışarıya lazım olanı açıkça public yap.

CS0117 ya da CS0246 görünce kodu değiştirmeye başlamak. Önce kontrol et: tip public mi, using doğru mu, projeye referans verilmiş mi? Bu üçü hataların büyük kısmını açıklar.

Bir namespace'e ve içindeki bir class'a aynı adı vermek. Bookstore.Core.Pricing namespace'inde Pricing adlı bir class olursa, onu kullanan kodda Pricing adı bazen namespace, bazen class olarak çözülür ve çoğu zaman tam adı yazmak zorunda kalırsın. Namespace'lere çoğul ya da domain adı ver (Pricing, Catalog), class'lara iş yapan isimler (PriceCalculator).

Her şeyi global using'e taşımak. Dosyanın neye bağlı olduğunu okumak zorlaşır, aynı isimli tipler çakışmaya başlar. Global using'i her dosyada gerçekten kullanılan birkaç namespace ile sınırla.

.NET 10 SDK ile .slnx oluşturup eski SDK kullanan bir ekip arkadaşına göndermek. Ekipte herkes aynı SDK'ya geçene kadar --format sln kullan ya da kök klasöre SDK sürümünü sabitleyen bir global.json koy.

Küçük Bir Deneme

★☆☆ IsbnValidator.cs'i file-scoped namespace yazımına çevir ve girintinin nasıl değiştiğini gör. Sonra aynı dosyaya ikinci bir blok namespace ekleyip CS8955 hatasını kendin üret.

★★☆ Bookstore.Core'a Catalog namespace'inde, ISBN'i tirelerden temizleyip döndüren public bir Normalize fonksiyonu ekle. Fonksiyonu Bookstore.App'ten çağır; sonra internal yapıp hata mesajının ne olduğunu not et.

★★★ Solution'a src/Bookstore.Reports adında ikinci bir console projesi ekle. O da Bookstore.Core'a referans versin ve PriceCalculator ile bir fiyat listesi yazdırsın. Solution kökünde tek bir dotnet build ile üç projenin de derlendiğini gör.

Kodu projelere ve namespace'lere ayırdık, derleyicinin hatalarını okumayı da biraz öğrendik. Ama derlenen her kod doğru çalışmıyor. Bir sonraki yazıda hata ayıklamaya geçiyoruz: breakpoint, watch, call stack, Debug.WriteLine ve Debug.Assert, ve dotnet build'in verdiği uyarıları okumak.