Mert Özen Her Satırda Daha İleri
Backend .NET SDK, CLI ve MSBuild — #03

Çok Projeli Solution Düzeni: Directory.Build.props/targets, Central Package Management ve Ortak Ayarlar

Mert Özen 15 Kas 2026 12 dk 6 görüntülenme
Çok Projeli Solution Düzeni: Directory.Build.props/targets, Central Package Management ve Ortak Ayarlar

Directory.Build.props, Sdk.props'un içinden csproj'dan önce okunduğu için OutputType ya da IsPackable gibi değerleri göremez; projeye bağlı kararlar Directory.Build.targets'a gider, ama SDK'nın daha önce türettiği değerler orada da değişmez. Bookstore'da iç içe props zincirini, artifacts output'u, Central Package Management'ı, transitive pinning'i, VersionOverride'ı ve GlobalPackageReference ile eklenen analyzer'ı gerçek build çıktılarıyla kuruyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.404, MSBuild 18.9) ile derlenip denenmiştir; Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0 kullanılmıştır.

Önce Tahmin Et

Bookstore'da paket olarak yayımlanacak her library XML doküman dosyası üretsin istiyoruz. Bunu her csproj'a yazmak yerine repo kökündeki Directory.Build.props'a bir kural ekliyoruz:

<PropertyGroup Condition="'$(IsPackable)' == 'true' and '$(OutputType)' == 'Library'">
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Bookstore.Contracts bir library ve paketlenebilir. Kural bu projeye uygulanır mı? Çoğu kişi "evet" der. Proje dosyasından GenerateDocumentationFile satırını silip soruyoruz:

dotnet msbuild src/Bookstore.Contracts -getProperty:GenerateDocumentationFile -getProperty:OutputType -getProperty:IsPackable
{
  "Properties": {
    "GenerateDocumentationFile": "false",
    "OutputType": "Library",
    "IsPackable": "true"
  }
}

Projenin son hâlinde OutputType Library, IsPackable true; koşul doğru görünüyor, ama kural uygulanmamış. Çünkü Directory.Build.props okunduğu anda bu iki property'nin değeri henüz boştu. Bu yazıda, sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) üzerinden ortak ayarların hangi dosyada ve hangi sırayla değerlendirildiğini, iç içe Directory.Build.props zincirini, CPM'i (Central Package Management; paket sürümlerini tek dosyada toplayan NuGet özelliği) ve artifacts output düzenini anlatıyoruz.

Directory.Build.props Ne Zaman Okunur?

MSBuild bir projeyi değerlendirirken önce Sdk.props'u, sonra csproj'un gövdesini, en son Sdk.targets'ı okur. Directory.Build.props bu sıranın en başında, Sdk.props'un içinden import edilir; Directory.Build.targets ise csproj'dan sonra gelir. Bir önceki yazıdaki -preprocess çıktısında dosyaların sırası şöyle:

03-msbuild-import-order-tr.png

Property'ler yukarıdan aşağıya tek geçişte değerlendirilir. Bir dosya okunduğunda, yalnız ondan önce set edilmiş property'leri görür. Bunu doğrudan görmek için iki dosyaya da o anki değerleri başka property'lere kopyalayan birer satır ekledik (<SeenOutputType>$(OutputType)</SeenOutputType> gibi) ve -getProperty ile okuduk:

PropertyDirectory.Build.props'ta görülenDirectory.Build.targets'ta görülenSon değer
OutputType (Contracts)boşLibraryLibrary
OutputType (bookstore-admin)boşExeExe
TargetFrameworkIdentifierboş.NETCoreApp.NETCoreApp
IsPackable (Contracts)boşboştrue

OutputType csproj'da yazılı (ya da SDK'nın varsayılanı) olduğu için Directory.Build.props onu göremez. TargetFrameworkIdentifier, SDK'nın TargetFramework'ten türettiği bir değer ve Sdk.targets'ın başında hesaplanıyor. IsPackable'ın varsayılanı ise pack target'larıyla birlikte, Directory.Build.targets'tan da sonra geliyor. Kural basit: Directory.Build.props'ta yalnız kendi set ettiğin değerlere ve ortam değişkenlerine dayanan koşullar yaz. Projeye ya da SDK'ya bağlı kararlar Directory.Build.targets'a gider; o da her değeri görmez.

Directory.Build.targets: Geç Gelen Kararlar

Koşuldan IsPackable'ı çıkarıp kuralı Directory.Build.targets'a taşıyalım; artık OutputType görünüyor:

<Project>
  <PropertyGroup Condition="'$(OutputType)' == 'Library'">
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
</Project>
dotnet msbuild src/Bookstore.Contracts -getProperty:GenerateDocumentationFile -getProperty:DocumentationFile
{
  "Properties": {
    "GenerateDocumentationFile": "true",
    "DocumentationFile": ""
  }
}

Property true oldu ama build sonrası Bookstore.Contracts.xml oluşmadı ve dokümanı olmayan bir tipte CS1591 uyarısı da gelmedi. Sebebi DocumentationFile'ın boş olması. SDK, GenerateDocumentationFile'a bakıp XML dosyasının yolunu Microsoft.NET.Sdk.BeforeCommon.targets'ta, yani şekildeki 5. adımda hesaplıyor. Directory.Build.targets 6. adımda geldiği için girdiyi değiştirdik ama ondan türetilen değer çoktan hesaplanmıştı. -getProperty'nin true demesi ayarın çalıştığını göstermez; asıl etkiyi build çıktısında doğrula.

Bookstore'da bu ayarı Bookstore.Contracts.csproj'da bıraktık. Directory.Build.targets'a ise sırası gerçekten önemli olan bir kural koyduk. Bir önceki yazıda bookstore-admin'in appsettings.json'unu çıktıya kopyalamak için csproj'a None Update satırı eklemiştik. Bookstore'a eklenecek her console aracında aynı satırı tekrar yazmak yerine:

<Project>
  <!-- OutputType csproj'dan sonra belli olur, SDK'nın None glob'u da Sdk.props'tan sonra vardır. -->
  <ItemGroup Condition="'$(OutputType)' == 'Exe' and '$(UsingMicrosoftNETSdkWeb)' != 'true'">
    <None Update="appsettings*.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
</Project>

Bu kural Directory.Build.props'ta çalışmaz. Koşul sorunu bir yana, Update yalnız kendisinden önce eklenmiş item'ları değiştirir; SDK'nın None glob'u ise 3. adımda, Directory.Build.props'tan sonra ekleniyor. Denediğimizde appsettings.json item'ı metadata'sız kaldı. Directory.Build.targets'ta ise item'lar artık var ve OutputType belli. Web SDK zaten Content olarak kopyaladığı için Bookstore.Api kuralın dışında bırakıldı; test projeleri Exe ama appsettings.json'ları olmadığı için kural onlarda bir şey değiştirmiyor.

İç İçe Dosyalar ve Zinciri Bilerek Kurmak

MSBuild, projenin klasöründen başlayıp yukarı doğru ilk bulduğu Directory.Build.props'u import eder ve aramayı orada bırakır. İki test projesinin ortak ayarlarını (xUnit v3 için OutputType, paket referansı, global using) tests/Directory.Build.props'a toplamak istedik. İlk deneme:

<Project>
  <PropertyGroup>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
</Project>
C:\Program Files\dotnet\sdk\10.0.404\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(109,5): error NETSDK1013: The TargetFramework value '' was not recognized. It may be misspelled. If not, then the TargetFrameworkIdentifier and/or TargetFrameworkVersion properties must be specified explicitly. [C:\src\Bookstore\tests\Bookstore.Contracts.Tests\Bookstore.Contracts.Tests.csproj]

Yeni dosya, kökteki dosyanın yerini aldı; TargetFramework, Nullable ve TreatWarningsAsErrors test projelerinden kayboldu. Zinciri elle kurmak gerekiyor:

<Project>
  <!-- İç içe Directory.Build.props aramayı durdurur; kökteki dosyayı açıkça import et. -->
  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

  <PropertyGroup>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="xunit.v3" />
    <Using Include="Xunit" />
  </ItemGroup>
</Project>

GetPathOfFileAbove, verilen klasörden başlayıp yukarı doğru dosyayı arar. İkinci parametrede ../ önemli: aramaya kendi klasöründen başlarsa kendini bulur ve sonsuz import döngüsüne girer. Import satırı dosyanın başında, çünkü kökteki değerlerin önce gelmesini ve test klasörünün onları gerektiğinde ezebilmesini istiyoruz. Artık iki test csproj'unda yalnız proje ve test projesine özel paket referansları kaldı:

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

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

</Project>

Aynı arama kuralı repo'nun dışına da uzanır. Bir ekip arkadaşı küçük bir araç reposunu C:\src\bookstore-tools'a clone'luyor; repo'da Directory.Build.props yok, ama C:\src'de yıllar önce başka bir proje için bırakılmış bir dosya var. Template'in yazdığı TargetFramework satırını, bir önceki yazıda yaptığımız gibi, "zaten ortak dosyadan geliyor" diye silince:

dotnet msbuild Seeder -getProperty:TargetFramework -getProperty:DirectoryBuildPropsPath
{
  "Properties": {
    "TargetFramework": "net8.0",
    "DirectoryBuildPropsPath": "C:\src\Directory.Build.props"
  }
}

Proje sessizce .NET 8'e döndü. DirectoryBuildPropsPath property'si hangi dosyanın import edildiğini doğrudan söyler; şüphelendiğinde ilk bakılacak yer burası. Bookstore'un kökünde kendi Directory.Build.props'u olduğu için arama orada durur ve dışarıdaki bir dosya araya giremez. Repo kökünde, içi boş olsa bile, bir Directory.Build.props bulundurmak bu yüzden iyi bir alışkanlık.

Artifacts Output: Bütün Çıktılar Tek Klasörde

.NET 8 ile gelen artifacts output düzeni, her projenin kendi bin/ ve obj/ klasörleri yerine bütün çıktıları repo kökündeki artifacts/ altında toplar. CI'da hangi dosyanın nerede olduğunu bilmek, cache'lemek ve temizlemek kolaylaşır. Açmak için UseArtifactsOutput'u Directory.Build.props'a yazdık. SDK çıktı yollarını Sdk.props içinde hesapladığı için csproj'a yazılan değer geç kalır ve SDK bunu açıkça reddeder:

C:\Program Files\dotnet\sdk\10.0.404\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.DefaultOutputPaths.targets(161,5): error NETSDK1199: The ArtifactsPath and UseArtifactsOutput properties cannot be set in a project file, due to MSBuild ordering constraints. They must be set in a Directory.Build.props file or from the command line. See https://aka.ms/netsdk1199 for more information. [C:\src\Bookstore\tools\Bookstore.Admin\Bookstore.Admin.csproj]

Directory.Build.props'taki ayarla ilk build'in sonucu:

  Bookstore.Contracts net10.0 succeeded (0.1s) → artifacts\bin\Bookstore.Contracts\debug\Bookstore.Contracts.dll
  Bookstore.Admin net10.0 succeeded (0.1s) → artifacts\bin\Bookstore.Admin\debug\bookstore-admin.dll
  Bookstore.Contracts.Tests net10.0 succeeded (0.1s) → tests\artifacts\bin\Bookstore.Contracts.Tests\debug\Bookstore.Contracts.Tests.dll
  Bookstore.Api net10.0 succeeded (0.4s) → artifacts\bin\Bookstore.Api\debug\Bookstore.Api.dll
  Bookstore.Api.Tests net10.0 succeeded (0.1s) → tests\artifacts\bin\Bookstore.Api.Tests\debug\Bookstore.Api.Tests.dll

Test projeleri tests\artifacts'a yazdı. Varsayılan konum, en yakındaki Directory.Build.props'un yanı; test projeleri için bu artık tests/ klasöründeki dosya. Yolu kökteki dosyanın klasörüne sabitledik:

<PropertyGroup>
  <!-- SDK props'tan önce set edilmeli: bütün build çıktısı repo kökündeki artifacts/'a gider. -->
  <UseArtifactsOutput>true</UseArtifactsOutput>
  <!-- Bu satır olmadan iç içe Directory.Build.props altındaki projeler kendi artifacts/ klasörünü açar. -->
  <ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>

$(MSBuildThisFileDirectory), içinde yazıldığı dosyanın klasörüdür; projeye göre değişmez. Klasör düzeni artifacts/<type>/<project>/<pivot>: type bin, obj, publish ya da package; pivot ise configuration'ın küçük harfli hâli, gerekirse TFM (Target Framework Moniker; net10.0 gibi hedef framework adı) ve RID (Runtime Identifier; linux-x64 gibi hedef platform kimliği) ile birleşik (debug, release_linux-x64). Pack ve publish de aynı düzene uyuyor:

  Successfully created package 'C:\src\Bookstore\artifacts\package\release\Bookstore.Contracts.1.0.0.nupkg'.
  Bookstore.Admin -> C:\src\Bookstore\artifacts\publish\Bookstore.Admin\release\

Referans uygulamanın .gitignore'unda artifacts/ satırı zaten vardı; artık bin ve obj klasörleri projelerin içinde oluşmuyor.

Central Package Management: Sürümler Tek Dosyada

Bookstore, referans uygulamadan beri CPM kullanıyor. Directory.Packages.props'ta ManagePackageVersionsCentrally açık ve her paketin sürümü bir PackageVersion item'ında; csproj'lar yalnız sürümsüz PackageReference yazıyor. Bu düzende bir projeye sürümle paket eklemek hata verir. 1. yazıda test projesini neden dotnet new xunit ile açmadığımızı burada görebilirsin:

dotnet new xunit -o tests/Bookstore.Catalog.Tests
C:\src\Bookstore\tests\Bookstore.Catalog.Tests\Bookstore.Catalog.Tests.csproj : error NU1008: The following PackageReference items cannot define a value for Version: coverlet.collector, Microsoft.NET.Test.Sdk, xunit, xunit.runner.visualstudio. Projects using Central Package Management must define a Version value on a PackageVersion item. For more information, visit https://aka.ms/nuget/cpm/gettingstarted

Template'ler CPM'den habersiz, sürümleri csproj'a yazıyor. CPM'e geçmemiş bir solution'ı geçirmenin yolu da bu hatanın tersi: her projedeki Version attribute'larını toplayıp Directory.Packages.props'a PackageVersion olarak yazmak, csproj'lardan silmek ve aynı paketin farklı sürümlerini tek sürümde birleştirmek. Bu son adım işin asıl kısmı, çünkü iki proje aynı paketin farklı sürümlerini kullanıyorsa hangisinin kalacağına karar vermek gerekir. dotnet package add ise CPM'i tanır: sürümü Directory.Packages.props'a, referansı csproj'a yazar.

Directory.Packages.props da Directory.Build.props gibi yukarı doğru aranır ve ilk bulunan kullanılır. Alt klasörde ikinci bir dosya açarsan üsttekini aynı GetPathOfFileAbove kalıbıyla import etmen gerekir. Modular Monolith serisinde module'ler ayrı projelere bölündüğünde proje sayısı artıyor, ama düzen değişmiyor: sürümler tek dosyada kalıyor.

Transitive Pinning, VersionOverride ve GlobalPackageReference

CPM yalnız doğrudan referans verdiğin paketlerin sürümünü yönetir. API'nin kullandığı Microsoft.EntityFrameworkCore.SqlServer, Microsoft.Data.SqlClient üzerinden Azure.Identity 1.17.1'i transitive olarak getiriyordu; dotnet package list --include-transitive --outdated en güncel sürümün 1.21.0 olduğunu gösterdi. Transitive bir paketi yükseltmek için ona doğrudan referans vermek yerine transitive pinning'i açtık:

<PropertyGroup>
  <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
  <!-- Transitive pin: Microsoft.Data.SqlClient 1.17.1 getiriyor. -->
  <PackageVersion Include="Azure.Identity" Version="1.21.0" />
</ItemGroup>
dotnet package list --project src/Bookstore.Api --include-transitive
   Transitive Package                                     Resolved
   ...
   > Azure.Identity                                       1.21.0
   ...

Pinning yalnız yükseltir; bağımlılığın istediğinden düşük bir sürüm yazarsan restore NU1109 hatası verir. Bu projeden paket üretirsen pinlenen sürüm paketin bağımlılık listesine açıkça yazılır; bu yüzden paket olarak yayımlanan library'lerde pinning'i dikkatle kullan. Hangi transitive paketin güvenlik açığı olduğunu bulmak NuGetAudit'in işi; onu DevSecOps serisinde görüyoruz.

Bir projenin geçici olarak farklı bir sürüme ihtiyacı varsa VersionOverride kullanılır. Test projelerinin xunit.v3 referansı artık tests/Directory.Build.props'tan geldiği için csproj'da Include değil Update yazıyoruz:

<PackageReference Update="xunit.v3" VersionOverride="4.0.2" />
   Top-level Package                                Requested   Resolved
   > xunit.v3                                       4.0.2       4.0.2

Override'lar zamanla birikir ve CPM'in amacını boşa çıkarır. Ekipte buna izin vermek istemiyorsan CentralPackageVersionOverrideEnabled'ı false yap; o zaman aynı satır restore'da hata verir:

C:\src\Bookstore\tests\Bookstore.Contracts.Tests\Bookstore.Contracts.Tests.csproj : error NU1013: The following PackageReference items cannot specify a value for VersionOverride: xunit.v3.  Projects using Central Package Management are currently configured to disable this functionality. For more information, visit https://aka.ms/nuget/cpm/versionoverride

Son araç GlobalPackageReference: her projeye eklenmesi gereken analyzer, sürüm üretici ya da build eklentisi paketleri için. NuGet bu referansı PrivateAssets="All" ve derleme referansı olmadan ekler; paket koda girmez, başka projelere de geçmez. Bookstore'da zaman bilgisinin her yerde TimeProvider ile alınmasını istiyoruz, çünkü API onu DI (dependency injection; bağımlılıkların dışarıdan verilmesi) container'ına kaydediyor ve testler sahte saat verebiliyor. Banned API analyzer'ını bütün projelere ekledik ve yasaklı üyeleri kökteki BannedSymbols.txt'ye yazdık:

<ItemGroup>
  <!-- Her projeye analyzer olarak, private ve derleme referansı olmadan eklenir. -->
  <GlobalPackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
</ItemGroup>
P:System.DateTime.Now;Inject TimeProvider instead
P:System.DateTime.UtcNow;Inject TimeProvider instead
P:System.DateTimeOffset.Now;Inject TimeProvider instead
P:System.DateTimeOffset.UtcNow;Inject TimeProvider instead

Analyzer'ın dosyayı okuyabilmesi için kökteki Directory.Build.props'a <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" /> satırını ekledik. bookstore-admin'e deneme amaçlı DateTime.UtcNow yazınca:

C:\src\Bookstore\tools\Bookstore.Admin\Program.cs(16,39): error RS0030: The symbol 'DateTime.UtcNow' is banned in this project: Inject TimeProvider instead (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md) [C:\src\Bookstore\tools\Bookstore.Admin\Bookstore.Admin.csproj]

Kural, TreatWarningsAsErrors sayesinde hata olarak geldi. Hangi analyzer kuralının hangi projede ne seviyede çalışacağını yönetmek Statik Analiz serisinin konusu; burada önemli olan, tek satırla bütün solution'a analyzer eklenebilmesi.

Solution Filter ile Solution'ın Bir Parçası

1. yazıda oluşturduğumuz Bookstore.Tests.slnf, solution'ın yalnız test projelerini ve onların bağımlılıklarını içeren bir görünüm:

dotnet sln Bookstore.Tests.slnf list
dotnet build Bookstore.Tests.slnf
Project(s)
----------
tests/Bookstore.Api.Tests/Bookstore.Api.Tests.csproj
tests/Bookstore.Contracts.Tests/Bookstore.Contracts.Tests.csproj
  Bookstore.Contracts -> C:\src\Bookstore\artifacts\bin\Bookstore.Contracts\debug\Bookstore.Contracts.dll
  Bookstore.Contracts.Tests -> C:\src\Bookstore\artifacts\bin\Bookstore.Contracts.Tests\debug\Bookstore.Contracts.Tests.dll
  Bookstore.Api -> C:\src\Bookstore\artifacts\bin\Bookstore.Api\debug\Bookstore.Api.dll
  Bookstore.Api.Tests -> C:\src\Bookstore\artifacts\bin\Bookstore.Api.Tests\debug\Bookstore.Api.Tests.dll

bookstore-admin build'e girmedi, çünkü hiçbir test projesi ona referans vermiyor. Filtre dosyası projeleri listeler; bağımlılıklar ise ProjectReference'lardan gelir. Büyük solution'larda CI'ın test job'ı ya da IDE'de yalnız bir module üzerinde çalışan geliştirici için bu, gereksiz projeleri yüklememek demek. .slnf dosyasını CLI ile oluşturmak ve düzenlemek .NET 11 SDK'nın yenilikleri arasında sayılıyor; 1. yazıda gördüğümüz gibi 10.0.4xx band'inde de çalışıyor, 10.0.1xx'te çalışmıyor.

Üretimde Dikkat

Ortak dosyaya kural ekledikten sonra etkisini her proje türünde ayrı ayrı doğrula. Bu yazıdaki iki deneme, -getProperty'nin beklenen değeri gösterdiği ama kuralın ya hiç uygulanmadığı ya da bir şey değiştirmediği durumlardı. Library, console, web ve test projesinden birer örnek için build çıktısına bakmak birkaç dakika sürer.

Directory.Build.props ve Directory.Packages.props değiştiğinde bütün projeler yeniden değerlendirilir ve incremental build'in avantajı kaybolur. Bu dosyalara geçici deneme satırları eklemek yerine -p: ile komut satırından dene.

Transitive pinning ile yükselttiğin paketleri kayıt altına al. Pin'in sebebi kaybolunca (üst paket yeni sürümünde zaten yükseltir) satır gereksiz kalır ve bir gün ters yönde, düşürme denemesi olarak NU1109 hatası verir. Directory.Packages.props'taki pin'leri ayrı bir ItemGroup'ta ve yorum satırıyla tutmak bunu kolaylaştırır.

Sık Düşülen Tuzaklar

Directory.Build.props'un repo dışındaki bir üst klasörden okunması. Repo'da kendi dosyan yoksa, C:\src ya da kullanıcı klasöründe unutulmuş bir dosya bütün projelerine uygulanır. Kökte bir Directory.Build.props bulundur ve şüphelendiğinde DirectoryBuildPropsPath'e bak.

İç içe dosyada üst dosyayı import etmeyi unutmak. Alt klasördeki dosya zinciri kırar; ortak ayarlar o klasördeki projelerden sessizce kaybolur. NETSDK1013 gibi bir hata alırsan şanslısın; TreatWarningsAsErrors gibi bir ayarın kaybolması hiçbir hata üretmez.

Test projelerine production kurallarını olduğu gibi uygulamak. Paketlenen library için XML doküman zorunluluğu mantıklı, test projesi için değil. Ortak dosyaya genel bir kural yazıp istisnaları sonradan aramak yerine, test klasörüne kendi Directory.Build.props'unu ver ve farkları orada tut.

Projeye bağlı koşulları Directory.Build.props'a yazmak. OutputType, IsPackable, TargetFrameworkIdentifier gibi değerler orada henüz boştur; koşul hiçbir projede doğru çıkmaz ve hata da vermez.

CPM'li repoda template'i olduğu gibi kullanmak. NU1008 hatası alırsın; sürümleri Directory.Packages.props'a taşı ve csproj'daki Version attribute'larını sil.

Küçük Bir Deneme

★☆☆ dotnet msbuild tests/Bookstore.Contracts.Tests -getProperty:DirectoryBuildPropsPath -getProperty:ArtifactsPath çalıştır. Sonra tests/Directory.Build.props'taki import satırını yorum satırına al ve aynı komutu tekrarla; hangi değerler değişti?

★★☆ Directory.Build.targets'a, değerlendirme sırasında görünen değerleri kaydeden bir PropertyGroup ekle (<SeenTfm>$(TargetFramework)</SeenTfm> gibi) ve aynı satırı Directory.Build.props'a da koy. TargetFramework, AssemblyName, OutputPath ve IntermediateOutputPath için iki dosyada görülen değerleri karşılaştır.

★★★ Directory.Packages.props'ta Azure.Identity'yi 1.17.1'in altındaki bir sürüme pinle ve restore'un verdiği NU1109 hatasını oku. Sonra pin'i kaldır ve dotnet package list --project src/Bookstore.Api --include-transitive ile Azure.Identity'nin sürümünü karşılaştır.

Bu yazıda paket sürümlerini tek dosyaya topladık, ama paketlerin nereden indirildiğine hiç karışmadık: Bookstore şu an yalnız nuget.org'u kullanıyor. Bir sonraki yazıda Bookstore.Contracts'ı şirket içi bir feed'e koyup nuget.org ile birlikte güvenli şekilde kullanacağız: nuget.config hiyerarşisi, package source mapping, lock file'lar ve .NET 11'in minimum publish age ayarı.