Skip to content

Bölüm 2 / 14

Proje yapısı

Solution, csproj, NuGet, proje referansları.

9 dakikalık okuma

4. hattın mimari dersinde katmanları proje referanslarıyla birbirinden ayırmıştın; bu ders o referansların ardındaki dosyayı açıyor. Proje dosyası (.csproj) yalnız derleme ayarı tutmaz — hedef çerçeveyi, paket bağımlılıklarını, uyarı politikasını ve dağıtım biçimini o belirler. Kıdemli geliştiricinin farkı burada da görülür: proje dosyalarını kopyalayarak değil, ortak kuralları tek yerden yöneterek çalışır.

01Ne öğreneceksin

  • Çözüm (solution) ve proje ayrımı; klasör düzeni yerleşik pratikleri
  • SDK biçimli .csproj anatomisi ve en çok kullanılan ayarlar
  • Directory.Build.props ile ortak kuralları tek yerden yönetmek
  • Merkezî paket sürüm yönetimi (Directory.Packages.props)
  • PackageReference ile ProjectReference farkı ve geçişli bağımlılıklar
  • Sürüm çakışmasının nasıl çözüldüğü; packages.lock.json
  • NuGet kaynakları, özel besleme, nuget.config ve kaynak eşleme
  • Anlamsal sürümleme (SemVer), kayan sürüm riski, kendi paketini üretmek

02Çözüm, proje, klasör düzeni

Proje tek bir derleme birimi üretir (.dll ya da .exe). Çözüm (.sln ya da yeni .slnx biçimi) yalnız bir gruplamadır: projeleri bir arada açmayı ve tek komutla derlemeyi sağlar, derlemeye kendi başına bir anlam katmaz. Bir projenin derlenmesi için çözüme ait olması gerekmez.

Yaygın ve savunulabilir bir düzen:

Ticaret.sln
Directory.Build.props          # tüm projelere ortak ayarlar
Directory.Packages.props       # tüm paket sürümleri
.editorconfig                  # kod stili ve analizör kuralları
src/
  Ticaret.Domain/
  Ticaret.Application/
  Ticaret.Infrastructure/
  Ticaret.Api/
tests/
  Ticaret.Domain.Tests/
  Ticaret.Api.IntegrationTests/

src/tests ayrımı zorunlu değildir ama iki işe yarar: kök dizinde ne olduğu bir bakışta görülür ve sürekli tümleştirme betikleri sade kalır. Proje adları ad alanlarıyla hizalanır — Ticaret.Application projesindeki tipler Ticaret.Application.* ad alanındadır.

dotnet new sln -n Ticaret
dotnet new classlib -o src/Ticaret.Domain
dotnet new webapi   -o src/Ticaret.Api
dotnet sln add src/Ticaret.Domain src/Ticaret.Api
dotnet add src/Ticaret.Api reference src/Ticaret.Domain

03SDK biçimli csproj

Modern proje dosyası kısadır, çünkü dosyalar tek tek listelenmez — klasördeki tüm .cs dosyaları kendiliğinden derlemeye girer:

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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <InvariantGlobalization>true</InvariantGlobalization>
    <UserSecretsId>a1b2c3d4-...</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Serilog.AspNetCore" />
    <ProjectReference Include="..\Ticaret.Application\Ticaret.Application.csproj" />
  </ItemGroup>

</Project>

Bilinmesi gereken ayarlar:

  • Sdk — Microsoft.NET.Sdk (kütüphane/konsol), Microsoft.NET.Sdk.Web (ASP.NET Core), Microsoft.NET.Sdk.Worker (arka plan servisi). Seçim, hangi varsayılan hedeflerin ve paketlerin geleceğini belirler.
  • TargetFramework — net8.0. Birden çok hedef gerekiyorsa TargetFrameworks (çoğul) kullanılır ve kod içinde #if NET8_0_OR_GREATER ile ayrışır; bu yalnız kütüphaneler için anlamlıdır.
  • Nullable — 3. hattın null dersindeki uyarıları açar. TreatWarningsAsErrors ile birlikte kalite çıtasını isteğe bağlı olmaktan çıkarır (4. hattın 1. dersi).
  • ImplicitUsings — sık kullanılan using satırlarını otomatik ekler. Kendi ortak using'lerini <Using Include="..." /> ile ya da bir dosyada global using ile tanımlayabilirsin.
  • InvariantGlobalization — kültür verilerini devre dışı bırakır; kapsayıcı boyutunu küçültür ama 3. hattaki kültüre duyarlı biçimlendirme ve karşılaştırmaları etkiler, bilinçli açılmalıdır.
  • LangVersion, AssemblyName, RootNamespace, OutputType — sırasıyla dil sürümü, üretilen dosya adı, kök ad alanı ve çıktı türü (Exe/ Library).

Dosyaları elle listelemek gerekmez ama dışlamak gerekebilir: <Compile Remove="Eski\**" />. Çıktıya kopyalanacak veri dosyaları ise <None Update="veri.json" CopyToOutputDirectory="PreserveNewest" /> ile işaretlenir — appsettings.json için SDK bunu zaten yapar.

04Ortak ayarları tek yerden yönetmek

Dört projede aynı beş satırı tekrarlamak, DRY ihlalinin proje dosyası hâlidir. MSBuild bunun için iki dosya sunar; ikisi de kök dizine konur ve alt klasörlerdeki tüm projelere kendiliğinden uygulanır.

<!-- Directory.Build.props -->
<Project>
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
</Project>

Merkezî paket sürüm yönetimi sürümleri tek dosyada toplar; proje dosyalarında yalnız paket adı kalır. Böylece "aynı paketin iki projede farklı sürümü" sorunu kökten biter:

<!-- Directory.Packages.props -->
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
    <PackageVersion Include="FluentValidation" Version="11.9.0" />
  </ItemGroup>
</Project>

Üçüncü ortak dosya .editorconfig'dir: kod stili ve analizör kural şiddetlerini (uyarı / hata / sessiz) taşır. dotnet format ve editörler bu dosyayı okur; kural tartışmasını kod incelemesinden çıkarıp yapılandırmaya taşımak, 4. hattaki "biçim gürültüsünü ayrı commit'te temizle" disiplininin önkoşuludur.

05Referanslar ve bağımlılık çözümü

İki tür referans vardır: ProjectReference (aynı çözümdeki projeye) ve PackageReference (NuGet paketine). İkisi de geçişlidir: A projesi B'yi, B de C'yi referans alıyorsa A, C'nin tiplerini görebilir.

Bu geçişlilik, 4. hattaki katman kuralı için önemli bir uyarı taşır: Api projesi Application'ı, Application da Domain'i referans alıyorsa Api, Domain tiplerini de görür. Bağımlılık kuralı bunu yasaklamaz — yasakladığı şey ters yöndür (Domain'in Infrastructure'ı görmesi). Buna karşılık altyapı paketinin (EF Core gibi) Domain'e sızmasını engelleyen şey, o paketin yalnız Infrastructure projesinde referanslanmasıdır.

Sürüm çakışması nasıl çözülür? Aynı paketin farklı sürümleri geçişli olarak gelirse NuGet en yakın kazanır (nearest wins) kuralını uygular: bağımlılık ağacında projeye daha yakın olan sürüm seçilir; aynı uzaklıkta iki farklı sürüm varsa daha yüksek olan alınır ve gerekirse uyarı üretilir. Doğrudan referans her zaman kazanır — bu yüzden çakışmanın kesin çözümü, istenen sürümü projeye açıkça eklemek (merkezî yönetim varsa orada sabitlemek) olur.

dotnet list package --include-transitive     # gerçekte ne geliyor
dotnet list package --outdated              # güncelleme adayları
dotnet list package --vulnerable            # bilinen açıklar (CI'da çalıştırılır)
dotnet nuget why Ticaret.Api Newtonsoft.Json   # bu paket neden geldi

Yeniden üretilebilir derleme gerekiyorsa kilit dosyası kullanılır: <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> ile packages.lock.json üretilir ve CI'da dotnet restore --locked-mode ile zorlanır. Bu, "benim makinemde çalışıyordu" sınıfı hataların en sinsi türünü — sessizce yükselen geçişli bir bağımlılığı — engeller.

06NuGet: kaynaklar, sürümler, kendi paketin

Paketler nuget.config dosyasında tanımlı kaynaklardan indirilir. Kurum içinde tipik düzen, genel nuget.org ile birlikte özel bir besleme (Azure Artifacts, GitHub Packages, Nexus) kullanmaktır:

<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="kurum" value="https://nexus.sirket.local/repository/nuget/" />
  </packageSources>
</configuration>

<clear /> satırı önemlidir: üst klasörlerden ve kullanıcı profilinden gelen kaynakları temizler, böylece derleme ortamı öngörülebilir olur. Birden çok kaynak varken paket adı sahiplenme saldırılarına karşı kaynak eşleme (package source mapping) kullanılır — hangi paket önekinin hangi beslemeden geleceği sabitlenir. Bu, 14. hattaki tedarik zinciri güvenliğinin .NET'teki somut karşılığıdır.

Sürüm ifadeleri ve riskleri: Version="8.0.1" "en az bu sürüm" anlamına gelir ve çözümde daha yükseğe çıkabilir; Version="[8.0.1]" tam olarak o sürümü sabitler; Version="8.0.*" kayan sürümdür ve her restore ile değişebilir — üretim projelerinde kaçınılır, çünkü derlemeyi yeniden üretilemez kılar.

Kendi paketini üretmek kurum içinde ortak kütüphane paylaşmanın standart yoludur:

dotnet pack -c Release
dotnet nuget push bin/Release/Ticaret.Ortak.1.2.0.nupkg -s kurum -k $API_KEY

Burada anlamsal sürümleme (SemVer) sözleşmesi geçerlidir: ANA.İKİNCİL.YAMA — yamada davranış korunur, ikincil sürümde geriye dönük uyumlu ekleme yapılır, ana sürümde kırıcı değişiklik olur. Kütüphane yazarken kırıcı değişikliğin ne olduğunu bilmek gerekir: genel bir imzayı değiştirmek, arayüze varsayılan uygulama vermeden üye eklemek, bir tipi başka ad alanına taşımak. 6. hattaki API versiyonlama dersinde aynı sözleşme HTTP tarafında karşına çıkacak.

07Mini alıştırma — Çözümü kur

Yaklaşık 20 dakika · dotnet komut satırı

  1. İskelet: bir çözüm ve dört proje oluştur (Domain, Application, Infrastructure, Api); referansları yalnız içeri bakacak biçimde ekle. Domain'den Api'ye referans eklemeyi dene ve döngüsel referans hatasını gör.
  2. Ortak ayar: Directory.Build.props yaz; dört projeden TargetFramework ve Nullable satırlarını sil. dotnet build ile hâlâ derlendiğini doğrula.
  3. Kalite çıtası: ortak dosyaya TreatWarningsAsErrors ekle. Kullanılmayan bir değişken yazıp derlemenin kırıldığını gör.
  4. Merkezî sürüm: Directory.Packages.props kur, iki projeye aynı paketi sürümsüz ekle. Sonra bir projede sürüm belirtmeyi dene — MSBuild ne diyor?
  5. Geçişli bağımlılık: Infrastructure'a bir paket ekle ve Api'de dotnet list package --include-transitive ile bunu gör. Api'nin bu paketin tiplerini kullanabilmesi mimari olarak sorun mu? Bir cümleyle yaz.
  6. Kilit dosyası: RestorePackagesWithLockFile aç, dotnet restore çalıştır, üretilen packages.lock.json dosyasına bak. Sonra bir paket sürümünü elle değiştirip dotnet restore --locked-mode çalıştır — ne oluyor?
  7. Paket üret: Domain projesini dotnet pack ile paketle; .nupkg dosyasını bir arşiv programıyla açıp içinde ne olduğunu gör.

08Kendini sına

SDK biçimli .csproj neden bu kadar kısa? En çok kullanılan ayarlar neler?

Çünkü dosyalar tek tek listelenmez: klasördeki tüm .cs dosyaları kendiliğinden derlemeye girer, ayrıca seçilen SDK (Microsoft.NET.Sdk, .Web, .Worker) varsayılan hedefleri ve paketleri getirir. Sık kullanılan ayarlar: TargetFramework (kütüphanelerde çoklu hedef için TargetFrameworks), Nullable ve TreatWarningsAsErrors — ikisi birlikte kalite çıtasını isteğe bağlı olmaktan çıkarır —, ImplicitUsings, LangVersion, AssemblyName/RootNamespace, OutputType ve InvariantGlobalization; sonuncusu kapsayıcı boyutunu küçültür ama kültüre duyarlı biçimlendirme ve karşılaştırmaları etkilediği için bilinçli açılır. Dosya listelemek gerekmez ama dışlamak gerekebilir (Compile Remove) ve çıktıya kopyalanacak veri dosyaları None Update ... CopyToOutputDirectory ile işaretlenir. Çözüm dosyası derlemeye anlam katmaz; yalnız projeleri gruplar.

Directory.Build.props ve Directory.Packages.props ne işe yarar?

İkisi de kök dizine konur ve alt klasörlerdeki tüm projelere kendiliğinden uygulanır. Directory.Build.props ortak MSBuild ayarlarını tek yerde toplar — hedef çerçeve, nullable, uyarı politikası, analizör düzeyi — yani proje dosyalarındaki tekrarı kaldırır; bu, DRY'ın derleme yapılandırmasındaki karşılığıdır. Directory.Packages.props merkezî paket sürüm yönetimidir: ManagePackageVersionsCentrally açıldığında sürümler tek dosyada PackageVersion ile tanımlanır, proje dosyalarında yalnız paket adı kalır; böylece aynı paketin iki projede farklı sürümde olması sorunu kökten çözülür. Üçüncü ortak dosya .editorconfig'dir: kod stili ve analizör kural şiddetlerini taşır, dotnet format ve editörler onu okur.

Geçişli bağımlılık nedir, sürüm çakışması nasıl çözülür?

A projesi B'yi, B de C'yi referans alıyorsa C, A için geçişli bağımlılıktır ve A onun tiplerini görebilir; bu hem ProjectReference hem PackageReference için geçerlidir. Aynı paketin farklı sürümleri geçişli olarak gelirse NuGet "en yakın kazanır" kuralını uygular: ağaçta projeye daha yakın olan sürüm seçilir, aynı uzaklıkta iki sürüm varsa daha yüksek olan alınır. Doğrudan referans her zaman kazandığı için çakışmanın kesin çözümü istenen sürümü projeye açıkça eklemek ya da merkezî sürüm dosyasında sabitlemektir. Teşhis için dotnet list package --include-transitive ve dotnet nuget why kullanılır. Yeniden üretilebilir derleme için RestorePackagesWithLockFile ile kilit dosyası üretilir ve CI'da --locked-mode ile zorlanır; bu, sessizce yükselen bir geçişli bağımlılığın yol açtığı hataları engeller. Mimari açıdan geçişlilik ters yön bağımlılığı meşrulaştırmaz: altyapı paketinin alan katmanına sızmaması, o paketin yalnız altyapı projesinde referanslanmasıyla sağlanır.

NuGet sürüm ifadeleri ve besleme yapılandırmasında nelere dikkat edersin?

Version="8.0.1" "en az bu sürüm" anlamına gelir ve çözümde daha yükseğe çıkabilir; köşeli parantezli [8.0.1] tam sabitler; 8.0.* kayan sürümdür ve her restore ile değişebileceği için üretimde kaçınılır — derlemeyi yeniden üretilemez kılar. Beslemede nuget.config içinde <clear /> kullanmak önemlidir: üst klasörlerden ve kullanıcı profilinden gelen kaynakları temizler, ortamı öngörülebilir yapar. Birden çok kaynak varken paket adı sahiplenme saldırılarına karşı kaynak eşleme ile hangi önekin hangi beslemeden geleceği sabitlenir. CI'da dotnet list package --vulnerable ve --outdated çalıştırılır. Kendi paketini üretirken SemVer sözleşmesi geçerlidir: yamada davranış korunur, ikincil sürümde geriye dönük uyumlu ekleme yapılır, ana sürümde kırıcı değişiklik olur — genel bir imzayı değiştirmek, arayüze varsayılan uygulama vermeden üye eklemek ve tipi başka ad alanına taşımak kırıcı değişikliktir.

09Sınav köşesi

Yazılıda nasıl sorulur

(1) Kavram: solution ile project farkı; assembly ile package farkı. (2) Dosya: .csproj içindeki TargetFramework, PackageReference, ProjectReference öğelerinin görevi. (3) Karşılaştırma: packages.config ile PackageReference farkı (eski/yeni proje biçimi). (4) Bağımlılık: geçişli bağımlılık nedir, sürüm çakışması nasıl çözülür ("en yakın kazanır"). (5) Araç: dotnet new, build, restore, publish, pack, add reference, add package komutlarının işlevi. (6) SemVer: ANA.İKİNCİL.YAMA anlamları ve kırıcı değişiklik örnekleri. (7) Yapılandırma: Directory.Build.props ve merkezî paket yönetimi ne işe yarar. (8) nuget.config: özel besleme tanımı ve <clear />'ın anlamı.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Yapı — "çözüm bir gruplamadır, asıl birim projedir; her proje bir assembly üretir. Katmanları ayrı projeler yapar, referansları yalnız içeri bakacak biçimde kurarım — mimari kural böylece derleyici tarafından zorlanır." (2) Proje dosyası — "SDK biçimli csproj kısadır çünkü dosyalar örtük gelir; ben orada Nullable, TreatWarningsAsErrors ve analizör düzeyini açarım." (3) Ortak yönetim — "bu ayarları her projeye kopyalamam; Directory.Build.props ile tek yerden veririm, paket sürümlerini de Directory.Packages.props ile merkezîleştiririm." (4) Bağımlılık — "geçişli bağımlılıkları dotnet list package --include-transitive ile denetlerim; çakışmada 'en yakın kazanır' kuralı işler, kesin çözüm sürümü doğrudan sabitlemektir." (5) Kıdem vuruşu — "üretimde kayan sürüm kullanmam ve CI'da kilit dosyasıyla --locked-mode çalıştırırım; ayrıca --vulnerable taramasını boru hattına koyarım. Özel besleme varsa <clear /> ve kaynak eşleme ile paket adı sahiplenme riskini kaparım."