.NET ve Angular'da Sürüm Bilgisi: AssemblyVersion, FileVersion, InformationalVersion, Source Link ve /version Endpoint'i
.NET'te sürümü Version ile tek yerden ver, kütüphanede AssemblyVersion'ı major'a sabitle, FileVersion'a CI build numarasını koy ve InformationalVersion'ı .NET 8'den beri commit SHA'sını ekleyen SDK'ya bırak. Bookstore API'sinde /version endpoint'i, log satırı ve OpenTelemetry service.version ile sürümü görünür kılıyor; Angular'da define ile build'e sürüm gömüyor ve service worker ile yeni sürüm bildirimini Playwright'la deniyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), OpenTelemetry .NET 1.21, Angular 22.2, Node.js 24, Playwright 1.65 ve actionlint 1.7.12 ile denenmiştir. Workflow actionlint ile doğrulandı; GitHub'da koşulmadı.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API projesine, kendi sürüm bilgisini JSON olarak döndüren küçük bir /version endpoint'i ekledik. Proje dosyasında sürümle ilgili tek bir satır yok:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
Repository bir Git deposu ve son commit af4ea8a. dotnet build ile derleyip endpoint'i çağırdığında version alanında ne görürsün: boş bir değer mi, 0.0.0 mı, 1.0.0 mı? Commit bilgisi bir yerde görünür mü?
$ dotnet build src/Bookstore.Api
$ curl -s http://localhost:5000/version
{"version":"1.0.0","commit":"af4ea8ae3c61bdfbc44fb46410c535c6f0508f41","assemblyVersion":"1.0.0.0","fileVersion":"1.0.0.0","runtime":".NET 10.0.13"}
SDK hiçbir şey söylemediğinde sürümü 1.0.0 kabul ediyor ve .NET 8'den beri commit'in tam SHA'sını (Git'in commit'i tanımlayan 40 karakterlik hash değeri) da kendiliğinden ekliyor. Yani her Bookstore build'i, sen bir şey yapmasan da "1.0.0" olarak etiketleniyor. Bu yazıda bu varsayılanları anlamlı değerlerle değiştiriyoruz: hangi MSBuild property'si neyi belirliyor, kütüphanede AssemblyVersion neden sabit kalmalı, Source Link ne ekliyor ve sürüm çalışan uygulamada, loglarda, telemetride ve Angular arayüzünde nasıl görünür hâle geliyor.
Bir Assembly'nin Dört Sürümü
Bir .NET projesinde "sürüm" dediğimiz şey aslında birkaç ayrı property. Hepsi aynı kaynaktan türüyor, ama farklı yerlere yazılıyor ve farklı işler görüyor:
| Property | Varsayılan | Nereye yazılır | Ne için |
|---|---|---|---|
VersionPrefix / VersionSuffix | 1.0.0 / boş | Version'ı oluşturur | Sabit kısım projede, ek CI'da |
Version | Prefix + "-" + Suffix | Diğer hepsinin kaynağı | Tek giriş noktası |
PackageVersion | Version | .nupkg | NuGet'in gördüğü sürüm |
AssemblyVersion | Suffix'siz, 4 bölüm | Assembly kimliği | Runtime'ın assembly yüklerken baktığı sürüm |
FileVersion | Suffix'siz, 4 bölüm | Win32 dosya kaynağı | Windows'ta dosya özelliklerinde görünür |
InformationalVersion | Version + "+" + commit | Attribute | İnsanın okuyacağı tam sürüm |
Microsoft'un library sürümleme rehberi bu dört sürümden yalnız AssemblyVersion'ın runtime davranışını etkilediğini, FileVersion ve InformationalVersion'ın bilgi amaçlı olduğunu söylüyor. FileVersion için önerisi CI build numarasını dördüncü bölüme koymak; InformationalVersion'ı ise elle set etmemek, SDK'nın ve Source Link'in üretmesine bırakmak.
Directory.Build.props ile Merkezileştirmek
Bookstore'da iki proje var ve sürüm kuralları farklı: API CalVer (sürüm numarasının bir kısmını yayın tarihinden alan yaklaşım) kullanıyor, Bookstore.Contracts paketi SemVer (Semantic Versioning; uyumluluk anlamı taşıyan sürüm kuralları). Ortak ayarlar repository kökündeki Directory.Build.props'ta duruyor; MSBuild bu dosyayı her projeden önce otomatik içeri alıyor:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Company>Bookstore</Company>
<Product>Bookstore</Product>
</PropertyGroup>
<!-- CI'da deterministic path'ler: pdb'deki yollar /_/ ile başlar -->
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true' Or '$(TF_BUILD)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
</Project>
Sürüm numarası ise projeye özel. Kütüphane, SemVer'in sabit kısmını kendi dosyasında tutuyor:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<VersionPrefix>1.1.0</VersionPrefix>
<!-- Kütüphane: AssemblyVersion yalnız major değişince değişir -->
<AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>
</Project>
API'de yerel build'ler 0.0.0-dev olarak çıkıyor; gerçek sürümü yalnız CI veriyor. Böylece bir geliştiricinin makinesinde derlenen bir şeyin production sürümüyle karışma ihtimali kalmıyor:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- CalVer: CI bunu -p:Version=YYYY.M.BUILD ile ezer -->
<VersionPrefix>0.0.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.21.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.21.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.21.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Bookstore.Contracts\Bookstore.Contracts.csproj" />
</ItemGroup>
</Project>
VersionPrefix/VersionSuffix ayrımı, kütüphanelerde pre-release üretmenin en kısa yolu: dotnet pack -p:VersionSuffix=beta.1, 1.1.0-beta.1 paketini üretir ve projedeki 1.1.0'a dokunmaz. CI'da tam sürümü vermek istersen -p:Version= diğer ikisini ezer. Bir sonraki yazıda bu sayıları elle yazmak yerine Git'ten üreteceğiz; buradaki property'ler o araçların da doldurduğu yerler.
AssemblyVersion ve Binding: Neden Major'a Sabitlenir?
Bookstore.Contracts.csproj'taki AssemblyVersion satırı bilinçli bir tercih. Rehberin önerisi kütüphanede AssemblyVersion'a yalnız major'ı yazmak: 1.0 ve 1.0.1 aynı 1.0.0.0'ı taşır, 2.0 2.0.0.0'a geçer. Sebebini görmek için tam tersini yapalım ve AssemblyVersion'ı her build'de değiştiren bir pipeline'ı taklit edelim. Contracts'ı build 57 ile, API'yi build 58 ile derleyip build 57'nin dll'ini API'nin yanına koyuyoruz; aynı kaynak koddan, yalnız bir build farkıyla:
$ dotnet build src/Bookstore.Contracts -c Release -p:AssemblyVersion=1.1.0.57 -o out/c57
$ dotnet build src/Bookstore.Api -c Release -p:AssemblyVersion=1.1.0.58 -o out/app
$ cp out/c57/Bookstore.Contracts.dll out/app/
$ dotnet out/app/Bookstore.Api.dll
Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'Bookstore.Contracts, Version=1.1.0.58, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
File name: 'Bookstore.Contracts, Version=1.1.0.58, Culture=neutral, PublicKeyToken=null'
at Program.<Main>$(String[] args)
Uygulama hiç ayağa kalkmıyor. .NET Framework, strong-name'li bir assembly için birebir aynı sürümü istiyordu ve farkı app.config'teki binding redirect'lerle kapatmak gerekiyordu. .NET (Core) daha esnek: rehbere göre runtime, derlendiği sürümden yüksek bir assembly'yi yükleyebiliyor. Ama daha düşük olanı yüklemiyor ve hata mesajı da yanıltıcı: dosya orada, eksik olan istenen sürüm. Plugin klasörü, iki servisin aynı dll'i paylaştığı bir deploy ya da yalnız tek dll'i değiştiren bir hotfix bu duruma kolayca düşer.
AssemblyVersion'ı major'a sabitlemek bu riski major değişimine bağlıyor. Major zaten kırılma demek, orada binding hatası beklenen bir sonuç. Rehberin bir uyarısı daha var: AssemblyVersion'ı hiç değiştirmemek de yanlış, çünkü 2.0 gibi gerçekten kırıcı bir sürüm eski 1.x'in yerine sessizce geçebilir. Uygulama projeleri için (API gibi) bu sorun çok daha küçük, çünkü onlara kimse referans vermiyor; orada AssemblyVersion'ın CalVer'i takip etmesi sorun değil.
InformationalVersion, Commit Hash ve Source Link
Tahmin bölümündeki commit hash'i nereden geldi? .NET 8 SDK'sı ile birlikte Source Link SDK'ya dahil edildi. Yan etki olarak InformationalVersion, Version'ın arkasına SourceRevisionId property'sini, yani commit SHA'sını ekliyor. Bu değişiklik .NET 8 SDK'sının breaking change listesinde yer alıyor, çünkü InformationalVersion'ı parse eden ya da ekranda olduğu gibi gösteren kodu etkiliyor. İstemiyorsan kapatabilirsin:
$ dotnet build src/Bookstore.Api -c Release -p:IncludeSourceRevisionInInformationalVersion=false
$ curl -s http://localhost:5000/version
{"version":"0.0.0-dev","commit":null,"assemblyVersion":"0.0.0.0","fileVersion":"0.0.0.0","runtime":".NET 10.0.13"}
Biz kapatmıyoruz; AppVersion class'ı değeri + işaretinden bölüp sürümü ve commit'i ayrı alanlara koyuyor. Hash'in build metadata olarak durması, bir önceki yazıda gördüğümüz gibi sıralamayı etkilemiyor.
Source Link'in asıl işi debugging. Derleme sırasında pdb dosyasına, kaynak dosyaların yolunu repository'deki ham dosya adresine eşleyen küçük bir JSON gömüyor. Repository'nin origin remote'u GitHub'daki bir adrese işaret ettiğinde, Contracts pdb'sine gömülen JSON şöyle görünüyor:
// yerel build
{"documents":{"/home/user/src/bookstore/*":"https://raw.githubusercontent.com/example/bookstore/5e856fb6aadbd1ee1c7845696e1b57f5858f35fa/*"}}
// CI build'i (ContinuousIntegrationBuild=true)
{"documents":{"/_/*":"https://raw.githubusercontent.com/example/bookstore/5e856fb6aadbd1ee1c7845696e1b57f5858f35fa/*"}}
İki satır arasındaki fark ContinuousIntegrationBuild. Bu property açıkken derleyici pdb'deki yolları makineden bağımsız /_/ önekine çeviriyor; build, hangi klasörde yapılırsa yapılsın aynı çıktıyı üretiyor. .NET SDK projelerinde Deterministic zaten varsayılan olarak açık; tam deterministic build için yanına ContinuousIntegrationBuild da gerekiyor. Bu property'yi yalnız CI'da açıyoruz, çünkü yerelde açarsan debugger kaynak dosyaları kendi diskinde bulamıyor. Directory.Build.props'taki koşul bu yüzden GITHUB_ACTIONS ve Azure Pipelines'ın TF_BUILD değişkenine bakıyor. Source Link'in SDK'daki sürümü GitHub, Azure Repos, GitLab ve Bitbucket'ı tanıyor; başka bir Git sunucusu kullanıyorsan ilgili Source Link paketini eklemen gerekiyor.
Çalışan Uygulamada Sürüm: /version, Log ve OpenTelemetry
Production'da bir hata araştırırken ilk soru "hangi kod koşuyor?" olur. CI/CD Temelleri serisinin "Build Once, Deploy Many" yazısında bu soruya küçük bir endpoint'le cevap vermiştik; burada aynı fikri tüm sürüm property'lerini gösterecek şekilde genişletiyoruz. Değerleri assembly'nin attribute'larından okuyan record:
using System.Reflection;
using System.Runtime.InteropServices;
namespace Bookstore.Api;
public sealed record AppVersion(string Version, string? Commit, string AssemblyVersion, string FileVersion, string Runtime)
{
public static AppVersion FromAssembly(Assembly assembly)
{
var informational = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
var fileVersion = assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "unknown";
var assemblyVersion = assembly.GetName().Version?.ToString() ?? "unknown";
// "2026.11.3+5114f85..." -> sürüm ve commit
var parts = informational.Split('+', 2);
return new(parts[0], parts.Length == 2 ? parts[1] : null, assemblyVersion, fileVersion, RuntimeInformation.FrameworkDescription);
}
}
API'nin Program.cs'i bu değeri üç yerde kullanıyor: /version endpoint'inde, açılıştaki log satırında ve OpenTelemetry resource'unda:
using Bookstore.Api;
using Bookstore.Contracts;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
var appVersion = AppVersion.FromAssembly(typeof(Program).Assembly);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("bookstore-api", serviceVersion: appVersion.Version))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddConsoleExporter());
var app = builder.Build();
app.Logger.LogInformation("Bookstore.Api {Version} ({Commit}) starting", appVersion.Version, appVersion.Commit);
BookDto[] books =
[
new("9780441172719", "Dune", "Frank Herbert", 420.00m),
new("9780141439587", "Emma", "Jane Austen", 185.50m),
new("9780547928227", "The Hobbit", "J. R. R. Tolkien", 310.00m),
];
app.MapGet("/api/books", () => books);
app.MapGet("/version", () => appVersion);
app.Run();
CI'ın yapacağı gibi sürümü ve build numarasını vererek derleyip endpoint'i çağıralım:
$ dotnet build src/Bookstore.Api -c Release -p:Version=2026.11.3 -p:FileVersion=2026.11.3.57
$ curl -s http://localhost:5000/version
{"version":"2026.11.3","commit":"5e856fb6aadbd1ee1c7845696e1b57f5858f35fa","assemblyVersion":"2026.11.3.0","fileVersion":"2026.11.3.57","runtime":".NET 10.0.13"}
Dört değer de beklediğimiz yerde: CalVer sürüm, tam commit, 2026.11.3.0 olarak doldurulmuş AssemblyVersion ve dördüncü bölümünde CI build numarası taşıyan FileVersion. Uygulama açılırken yazdığı satır, log'larda sürüme göre filtrelemeyi mümkün kılıyor:
info: Bookstore.Api[0]
Bookstore.Api 2026.11.3 (5e856fb6aadbd1ee1c7845696e1b57f5858f35fa) starting
Asıl değerli olan ise OpenTelemetry'deki karşılığı. AddService'e verilen serviceVersion, semantic convention'larda tanımlı service.version resource attribute'una yazılıyor ve bu servisten çıkan her trace, metric ve log kaydına ekleniyor. Console exporter'ın çıktısında görülüyor:
Activity.DisplayName: GET /version
Activity.Kind: Server
...
Resource associated with Activity:
service.name: bookstore-api
service.version: 2026.11.3
service.instance.id: e08ea85a-6493-4382-84dd-26203f689fbc
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.21.0
Bir dashboard'da hata oranını service.version'a göre bölmek, "hata 2026.11.3 ile mi başladı?" sorusunu tek grafikle cevaplıyor. service.version'a commit hash'ini koymadık; sürüm numarası gruplamak için, commit ise /version ve log satırında arama yapmak için. OpenTelemetry'nin kurulumunu ve exporter'larını ilerideki Observability serisinde ayrıntılı ele alacağız.
Angular: Build Sırasında Sürüm Enjekte Etmek
bookstore-web'de de aynı soruya cevap vermek istiyoruz: kullanıcı bir hata bildirdiğinde ekranının altında hangi build'in yazdığını görmek. Angular'ın application builder'ı bunun için define seçeneği sunuyor: bir global identifier'ı build sırasında sabit bir değerle değiştiriyor. Yerel build'ler için angular.json'a bir varsayılan koyuyoruz:
"options": {
"browser": "src/main.ts",
...
"define": {
"APP_VERSION": "'0.0.0-dev'"
}
}
Değer olduğu gibi koda yazıldığı için string'ler tırnak içinde verilmeli; '0.0.0-dev''deki tek tırnaklar bu yüzden orada. TypeScript'in identifier'ı tanıması için küçük bir dosya:
declare const APP_VERSION: string;
// Build sırasında "define" seçeneği ya da --define ile değiştirilir
export const appVersion: string = APP_VERSION;
CI aynı değeri komut satırından eziyor. Sonucu üretilen bundle'da kontrol edelim:
$ npx ng build --define "APP_VERSION='2026.11.3+5114f85'"
$ grep -o "2026.11.3+5114f85" dist/bookstore-web/browser/main-*.js
2026.11.3+5114f85
Sürüm App component'inin footer'ında görünüyor. Bu yaklaşımın bir sınırı var: define build sırasında çalıştığı için, aynı dist klasörünü farklı ortamlara deploy ettiğinde sürüm doğru kalır (aynı build, aynı sürüm), ama ortama özel config'i bu yolla vermemelisin. CI/CD Temelleri serisinin "Build Once, Deploy Many" yazısında config'in neden runtime'da okunan bir config.json'a taşınması gerektiğini anlatmıştık; sürüm build'in parçası, config ortamın parçası.
Service Worker ile Yeni Sürüm Bildirimi
Sürümü göstermek yetmiyor; kullanıcının tarayıcısı eski sürümde kalabiliyor. Angular service worker'ı uygulamanın dosyalarını cache'liyor ve her açılışta sunucudaki ngsw.json manifest'ini kontrol ediyor. Manifest değiştiyse yeni sürümü arka planda indiriyor ve hazır olduğunda SwUpdate.versionUpdates üzerinden VERSION_READY event'i yayınlıyor. Service worker'ı ng generate service-worker ile ekledikten sonra bu event'i bir signal'e çeviren küçük bir service:
import { Injectable, inject, signal } from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class VersionUpdate {
private readonly swUpdate = inject(SwUpdate);
readonly updateReady = signal(false);
constructor() {
if (!this.swUpdate.isEnabled) {
return;
}
this.swUpdate.versionUpdates
.pipe(filter((event): event is VersionReadyEvent => event.type === 'VERSION_READY'))
.subscribe((event) => {
console.log(`New version ready: ${event.latestVersion.hash}`);
this.updateReady.set(true);
});
}
async reload(): Promise<void> {
await this.swUpdate.activateUpdate();
document.location.reload();
}
}
Template'te signal'e bağlı bir banner ve footer:
<header>
<h1>Bookstore</h1>
</header>
@if (update.updateReady()) {
<div class="update-banner" role="status">
A new version is available.
<button type="button" (click)="update.reload()">Reload</button>
</div>
}
<main>
<router-outlet />
</main>
<footer>bookstore-web {{ version }}</footer>
Bu akışı Playwright ile uçtan uca denedik: production build'i statik bir sunucudan yayınladık, sayfayı açıp service worker'ın kontrolü almasını bekledik, sonra sunucudaki dosyaları 2026.11.4 build'iyle değiştirip sayfayı yeniledik:
v1 footer: bookstore-web 2026.11.3+5114f85
v2 deployed
after reload footer: bookstore-web 2026.11.3+5114f85
[console] New version ready: 56c18610c53fde206bf4eb00c67461dfb755e871
banner: A new version is available. Reload
after click footer: bookstore-web 2026.11.4+9c41d2e
Yenilemeden sonra kullanıcı hâlâ eski sürümü görüyor; service worker cache'ten servis ediyor ve arka planda yeni sürümü indiriyor. İndirme bitince banner çıkıyor, butona basınca activateUpdate yeni sürümü etkinleştiriyor ve sayfa yeni footer'la açılıyor. Konsoldaki hash'e dikkat: bu, Bookstore sürümü değil, service worker manifest'inin hash'i. Kullanıcıya "2026.11.4 hazır" demek istersen sürümü ngsw-config.json'daki appData alanıyla manifest'e koyabilirsin; event'teki latestVersion.appData o değeri taşır.
Üretimde Dikkat
Sürümü tek yerden ver. Git tag'inden sürümü çıkarıp hem API'ye hem web'e aynı değeri geçiren bir release workflow'u:
name: release-app
on:
push:
tags: ['app/v*']
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: web/package-lock.json
# app/v2026.11.3 -> 2026.11.3
- name: Compute version
id: version
run: echo "version=${GITHUB_REF_NAME#app/v}" >> "$GITHUB_OUTPUT"
- name: Publish API
env:
VERSION: ${{ steps.version.outputs.version }}
run: >
dotnet publish src/Bookstore.Api -c Release -o out/api
-p:Version="$VERSION"
-p:FileVersion="$VERSION.$GITHUB_RUN_NUMBER"
- name: Build web
working-directory: web
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
npm ci
npx ng build --define "APP_VERSION='$VERSION+${GITHUB_SHA::7}'"
Tag'den gelen değer önce bir env değişkenine alınıyor, sonra shell'de kullanılıyor; ${{ }} ifadesini doğrudan run satırına yazmak, tag adının shell komutu olarak yorumlanmasına kapı açar. Bu workflow actionlint ile doğrulandı, komutları yerelde aynı değişkenlerle koşturduk; GitHub'da koşulmadı. Sürümü tag'den otomatik üretmenin daha sağlam yollarını bir sonraki yazıda göreceğiz.
/version'ı herkese açma. Tam sürüm ve framework bilgisi, bilinen bir zafiyeti hedefleyen biri için kolay bir ipucu. Endpoint'i iç ağa, sağlık kontrolü yapan sisteme ya da yetkili kullanıcılara sınırla; dış dünyaya yalnız sürüm numarasını veren bir header bile çoğu zaman gereksiz.
Telemetride sürüm kardinalitesini düşük tut. service.version'a her build için farklı bir değer (commit hash'i, build numarası) koymak, metric backend'inde sürüm başına ayrı seriler üretir. Sürüm numarası yeterli; commit'i log ve trace'te arama yaparken kullan.
Release build'lerinde ContinuousIntegrationBuild'in gerçekten açık olduğunu kontrol et. Koşul yanlış yazılmışsa hata vermez, sadece pdb'lerde build makinesinin yolları kalır.
Sık Düşülen Tuzaklar
-p:Version= ile her build'de AssemblyVersion'ı değiştirmek. Kütüphanede AssemblyVersion'ı sabitlemezsen Version onu da sürükler ve yukarıdaki binding hatası her patch'te mümkün hâle gelir. Kütüphanede AssemblyVersion'ı major'a sabitle ya da bir sonraki yazıdaki araçlardan birine bırak.
Sürümü elle güncellemek. Release öncesi biri csproj'daki sayıyı değiştirmeyi unutur ve iki farklı build aynı sürümle çıkar. Sürüm ya tag'den ya da CI'dan gelmeli; repository'de yazılı olan sayı en fazla bir başlangıç değeri olmalı.
InformationalVersion'ı ekranda olduğu gibi göstermek. .NET 8'den beri bu değer 40 karakterlik bir commit SHA'sı taşıyor; "Hakkında" ekranında ya da bir User-Agent header'ında taşabilir. Göstermeden önce +'dan böl.
Angular'da define değerini tırnaksız vermek. --define APP_VERSION=2026.11.3 ile build, Invalid define value (must be an entity name or JS literal): 2026.11.3 hatasıyla duruyor; çünkü değer koda olduğu gibi yazılıyor ve 2026.11.3 geçerli bir JavaScript ifadesi değil. String olarak tırnak içinde vermelisin. Daha kötüsü, değer tesadüfen geçerli bir ifade olursa (2026.10 gibi) build başarılı olur, değer bir sayıya dönüşür ve footer'da 2026.1 görünür.
Service worker'ı eklemek ama güncelleme akışını düşünmemek. Kullanıcı sekmeyi kapatmadan eski sürümde kalır; özellikle API contract'ı değiştiyse eski istemci yeni API'ye uymayan istekler atar. Banner, ya da en azından VERSION_READY'de bir sonraki navigasyonda yenileme, release sürecinin parçası olmalı.
Küçük Bir Deneme
★☆☆ Bookstore.Contracts'ı dotnet pack -p:VersionSuffix=beta.1 ile paketle ve .nupkg'nin adını, içindeki dll'in AssemblyVersion'ını ve InformationalVersion'ını tahmin et. Sonra paketi açıp kontrol et.
★★☆ Bağlama denemesini tersine çevir: API'yi AssemblyVersion=1.1.0.57, Contracts'ı 1.1.0.58 ile derleyip yüksek sürümlü dll'i API'nin yanına koy. Uygulama açılıyor mu? Sonuç rehberdeki ".NET (Core) yüksek sürümü yükler" cümlesiyle uyuşuyor mu?
★★★ ngsw-config.json'a appData ile sürüm ekleyen bir build adımı yaz ve banner'ın "2026.11.4 is available" demesini sağla. Değeri tek kaynaktan (tag) aldığından emin ol.
Bu yazıda sürüm numarasını hep dışarıdan verdik: csproj'da, komut satırında ya da tag'den kesip. Bir sonraki yazıda sayıyı doğrudan Git geçmişinden hesaplayan üç aracı, MinVer, Nerdbank.GitVersioning ve GitVersion'ı, Bookstore.Contracts üzerinde karşılaştırıyoruz.