.NET 10 LTS Production Patterns — Bir Migration Hikâyesi

0 yorum 848 görüntülenme

Yazılım Mühendisliği dotnet

20 dk okuma 3820 kelime

TL;DR

7 Mayıs 2026'da bilalkose.com.tr web uygulamasını ve bağlı 5 paketi .NET 8'den .NET 10 LTS'ye taşıdım. Sonuç: 6 csproj target framework bump, 26 Microsoft paketi güncellemesi, 9 özel paketin major versiyon push'u, AspNetCoreRateLimit sökülüp native AddRateLimiter'a geçiş, AutoMapper 16 constructor adapt ve SqlClient TLS varsayılan değişimi — toplam 0 hata + 18 uyarı build, 10/10 smoke test. Bu yazı; AI-üretimi rehberlerin atlamadığı production gotcha'ları, gerçek migration yolunda karşıma çıkan altı kırılma noktasını ve bunları nasıl çözdüğümü anlatıyor.

Yazar notu: Bu yazıdaki kod parçalarının tamamı bilalkose.com.tr canlı kod tabanından alındı. Migration tarihi 2026-05-07, dosya konumları ve hata mesajları gerçek prod ortamdan.


İçindekiler

  1. Neden .NET 10 LTS — Şu An Ne Değişiyor?
  2. Migration Setup: 6 csproj + 26 Paket + Core Feed'i
  3. Rate Limiter Geçişi: AspNetCoreRateLimit → AddRateLimiter
  4. AutoMapper 16 Constructor Kırılması
  5. SqlClient Yeni Varsayılanları (TLS sürprizi)
  6. Serilog 4.3 + ASP.NET Core 10 Transitive Tuzakları
  7. Docker ve Kubernetes Production Deploy
  8. Production'a Geçmeden Önce: 10 Smoke Test
  9. Observability: Prometheus + OpenTelemetry
  10. Bu Migration'dan 7 Çıkarım
  11. Sıkça Sorulan Sorular — 10 Q&A

Neden .NET 10 LTS — Şu An Ne Değişiyor?

.NET 10, Kasım 2025'te genel kullanıma açıldı ve Long-Term Support sınıfında. Yani Kasım 2028'e kadar resmi güvenlik patch'i alacak. Bunun pratik anlamı: kurumsal sistemler için bir sonraki "huzurlu üç yıl".

Microsoft'un LTS politikası iki yılda bir tekrar ediyor: .NET 6 (Kasım 2021), .NET 8 (Kasım 2023), .NET 10 (Kasım 2025). Aradaki tek sayılı sürümler (.NET 7, .NET 9) "Standard Term Support" — 18 ay destekli. Bu açıkça LTS odağı: kurumsal mühendislik takımları kararlarını çift sayılara göre veriyor.

Hız kararını dayatan iki olgu var. Birincisi: 10 Kasım 2026'da .NET 8 ve .NET 9 birleşik EOL'e (End-of-Life) gidiyor. Yani Kasım 2026'dan itibaren bu sürümlerde resmi CVE patch'i yok. Türk pazarında 2026 başında yapılan sektörel anketlerde .NET kullanan ekiplerin yaklaşık %60'ının hâlâ .NET 8 üstünde olduğu tahmin ediliyor; %25'i .NET 9'da, %15'i daha yeni — yani Q3-Q4 2026'da çok geniş bir geçiş dalgası geliyor. Şimdi başlayan ekipler bu yoğunlaşmadan önce manevra alanı kazanıyor. İkinci olgu: CVE-2026-40372 (DataProtection padding-oracle açığı), sadece .NET 10.0.7+ patch'inde kapatılıyor. .NET 9 üstündeyseniz alternatif yok.

Erken adopter olmanın somut faydaları:

  • Performans: ASP.NET Core 10, yatay yük testlerinde .NET 8'e karşı yaklaşık %10-20 throughput kazancı gösteriyor (Microsoft resmi). Bu özellikle JSON serializer ve middleware allocation profilinde belirgin. Kendi setup'ımda da benchmark çıkarmayı planlıyorum.
  • Native rate limiter: Microsoft.AspNetCore.RateLimiting artık framework içinde. Üçüncü taraf paket bağımlılığı azalıyor (bu yazıda detay var).
  • C# 14: Class düzeyinde primary constructor, field-targeted attribute syntax, partial property, lambda parameter modifier (ref/out) — daha az boilerplate, daha okunaklı domain model. EFCore entity'leri için özellikle değerli.
  • Native AOT olgunluğu: ASP.NET Core 10 ile birlikte native AOT artık standart web API'lerinde production-ready. Cold start 3-4 saniyeden 50-100ms'ye düşüyor (Kubernetes auto-scaling senaryolarında kritik).
  • Daha ufak image: mcr.microsoft.com/dotnet/aspnet:10.0 Docker layer'ı, 8.0'a göre yaklaşık %3 daha küçük.
  • OpenTelemetry maturation: Diagnostic ve tracing API'leri ile geçmişe göre daha iyi entegre — distributed tracing setup'ı yarı yarıya kısaldı.

Bu yazı yeniliklere genel bakış değil. Microsoft Learn'in sürüm özeti zaten kapsamlı. Burada odak, gerçek bir orta ölçekli prod uygulamasının migration yolunda karşıma çıkan altı kırılma noktası ve nasıl çözdüğüm.


Migration Setup: 6 csproj + 26 Paket + Core Feed'i

Geçirdiğim sistemi tanıyalım, çünkü kararların çoğu setup büyüklüğüne göre değişiyor.

Kod tabanı:

  • 1 web uygulaması (ASP.NET Core, Razor + MVC + bazı minimal API endpoint'ler)
  • 4 Application/Domain/Persistence/Infrastructure katman csproj'u (Clean Architecture'a benzer)
  • 1 test projesi (MSTest)
  • 9 özel paket (Core.Application.Base, Core.CrossCuttingConcerns.Base, Core.Mailing.Base, Core.Test.Base, vb. — kendi feed'imde, nuget.bilalkose.com.tr üzerinde)

Hedef:

  • Target framework net8.0net10.0
  • 26 Microsoft.* paketi (AspNetCore, EFCore, Extensions.*, System.Drawing.Common, System.Text.Json) 8.x → 10.0.x
  • 9 özel paketin tamamı 1.x → 2.0.0 (major bump — breaking change içeriyor, AutoMapper 16 transitive zorunlu)
  • Üçüncü taraf paketler: AspNetCoreRateLimit söküm, Serilog 4.3'e bump, Microsoft.NET.Test.Sdk 18.0.0, MSTest 3.10

Yol planı:

  1. global.json ekle: "sdk.version": "10.0.100", "rollForward": "latestFeature", "allowPrerelease": true — dev makinemde preview SDK var, prod Docker GA pull edecek.
  2. Dockerfile base image bump: sdk:8.0sdk:10.0, aspnet:8.0aspnet:10.0.
  3. 6 csproj target framework bump.
  4. 26 Microsoft paketi <Version> güncelle.
  5. 9 özel paketin tamamı kendi csproj'larında net10.0 + transitive bump (özellikle AutoMapper 13 → 16, IdentityModel.Tokens 7.5.2 → 8.0.0, MimeKit/MailKit 4.4 → 4.14, BouncyCastle drop).
  6. dotnet pack + dotnet nuget push × 9 paket → kendi feed'ime.
  7. Web uygulaması csproj'larında özel paket referansları 1.x → 2.0.0.
  8. AspNetCoreRateLimit söküm + AddRateLimiter migration.
  9. Build + smoke test.

Toplam süre: yaklaşık 6-8 saat odaklı çalışma + 1-2 saat smoke test ve düzeltme. Tipik bir Türk SaaS şirketinde 1-2 takım üyesi için 1-2 günlük iş.

9 özel paketin major bump tablosu (self-hosted feed nuget.bilalkose.com.tr):

# Paket 1.x 2.0.0 Breaking change
1 Core.Application.Base 1.5.x 2.0.0 net10.0 + AutoMapper 16 transitive
2 Core.CrossCuttingConcerns.Base 1.x 2.0.0 net10.0 + Serilog 4.3
3 Core.Mailing.Base 1.x 2.0.0 MimeKit/MailKit 4.4 → 4.14, BouncyCastle drop
4 Core.Persistence.Base 1.x 2.0.0 EFCore 8 → 10, SqlClient 5.x defaults
5 Core.Security.Base 1.x 2.0.0 IdentityModel.Tokens 7.5.2 → 8.0.0
6 Core.Test.Base 1.x 2.0.0 MapperConfiguration ctor adapt + MSTest 3.10
7 Core.ElasticSearch.Base 1.x 2.0.0 NEST → Elastic.Clients.Elasticsearch upgrade path
8 Core.Caching.Base 1.x 2.0.0 Microsoft.Extensions.Caching 8 → 10
9 Core.HealthChecks.Base 1.x 2.0.0 AspNetCore.HealthChecks 8.x → 10.x

Her paket için dotnet pack -c Release + dotnet nuget push --source bilal-nuget --api-key dummy (self-hosted feed anonymous push kabul ediyor). Aynı sürüm overwrite edilemez (409 Conflict) — versiyonu her zaman bump etmek zorundayım.


Rate Limiter Geçişi: AspNetCoreRateLimit → AddRateLimiter

.NET 10'un native rate limiter'ı Microsoft.AspNetCore.RateLimiting namespace altında. AspNetCoreRateLimit (5.0.0) bağımlılığı .NET 10'da hâlâ derleniyor — yani teknik olarak söküm zorunlu değildi. Ama orta vadeli iki neden söküm kararını dayattı:

  1. Vendor bu paket için yeni CVE patch yapmayı taahhüt etmiyor; .NET 10'da test edilmiş değil, sadece "çalışıyor".
  2. Native API daha düşük allocation'lı (zero-allocation hot path) ve appsettings.json'a 124 satır config yığmak yerine kod ile yönetiliyor.

Geçiş kodu — gerçek prod implementasyonu:

İki tasarım kararı dikkate değer. Birincisi: [EnableRateLimiting("policy-name")] attribute yerine GlobalLimiter pattern seçtim. Sebep: 30+ controller'a attribute eklemek yerine tek noktada URL-based routing yapmak deploy farklarını daha okunaklı bırakıyor. İkincisi: FixedWindow yerine SlidingWindow — burst trafiğini daha pürüzsüz absorb ediyor (window border'ında 2× spike problemini önlüyor).

services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
    {
        string ip = ResolveClientIp(ctx);
        if (IsLocalhost(ip))
            return RateLimitPartition.GetNoLimiter("local:" + ip);

        // GET path: tek global sliding window
        if (ctx.Request.Method != HttpMethods.Post)
        {
            return RateLimitPartition.GetSlidingWindowLimiter(
                "global-get:" + ip,
                _ => new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = 200,
                    Window = TimeSpan.FromMinutes(1),
                    SegmentsPerWindow = 4,
                    QueueLimit = 0
                });
        }

        // POST path: URL pattern → policy mapping
        string path = ctx.Request.Path.Value?.ToLowerInvariant() ?? "";
        (string policy, int limit, TimeSpan window, int segments) = path switch
        {
            var p when p.StartsWith("/api/mailgonder")
                    || p.StartsWith("/api/donation/create")
                => ("mail", 12, TimeSpan.FromMinutes(1), 4),

            var p when p.StartsWith("/api/donation/verify")
                => ("donation-verify", 20, TimeSpan.FromMinutes(1), 4),

            var p when p.Contains("/contact")
                => ("contact", 20, TimeSpan.FromHours(1), 6),

            var p when p.Contains("/blog/details")
                    || p.Contains("/projects/addrating")
                => ("comment", 20, TimeSpan.FromHours(1), 6),

            var p when p.StartsWith("/api/")
                => ("api-default", 12, TimeSpan.FromMinutes(1), 4),

            _ => ("global-post", 60, TimeSpan.FromMinutes(1), 4)
        };

        return RateLimitPartition.GetSlidingWindowLimiter(
            $"{policy}:{ip}",
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = limit,
                Window = window,
                SegmentsPerWindow = segments,
                QueueLimit = 0
            });
    });

    options.OnRejected = async (context, token) =>
    {
        var http = context.HttpContext;
        http.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        http.Response.ContentType = "text/html; charset=utf-8";

        int retryAfter = 60;
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan ra))
            retryAfter = (int)Math.Ceiling(ra.TotalSeconds);
        http.Response.Headers["Retry-After"] = retryAfter.ToString(CultureInfo.InvariantCulture);

        // rateLimit429Html template dosyadan okunuyor (3 dil + emoji destekli)
        await http.Response.WriteAsync(string.Format(rateLimit429Html,
            "—", "—", retryAfter), token);
    };
});

app.UseRateLimiter();

ResolveClientIp helper'ı CF-Connecting-IP > X-Real-IP > RemoteIpAddress zincirini takip ediyor — Cloudflare arkasında olduğunuzda zorunlu, aksi tüm trafik tek IP partition'a düşer ve limiter işe yaramaz. IsLocalhost ise ::1 ve 127.0.0.1'i bypass ediyor (dev test akışı için).

URL pattern → policy haritası:

Policy Limit Pencere URL pattern
mail 12 1 dakika /api/mailgonder*, /api/donation/create*
donation-verify 20 1 dakika /api/donation/verify*
contact 20 1 saat herhangi yerde /contact
comment 20 1 saat /blog/details*, /projects/addrating*
api-default 12 1 dakika diğer tüm /api/*
global-post 60 1 dakika POST tarafı son fallback
global-get 200 1 dakika tüm GET istekleri

429 HTML template: Pipeline başlangıcında RateLimitQuotaExceededHtml.GetPhysicalPath(env.ContentRootPath) ile bir HTML şablonu okuyorum (3 dil + emoji + retry timer). Dosya yoksa FallbackTemplate devreye giriyor. Bu pattern büyük cache benefit sağlıyor — her 429 response'unda template tekrar render edilmiyor, sadece string.Format ile 3 placeholder dolduruluyor.

Retry-After: 60 header sayesinde well-behaved client'lar (özellikle SDK'lar ve mobil app) backoff hesabını otomatik yapıyor.

Sonuç: appsettings.json'dan 124 satır IpRateLimiting ve IpRateLimitPolicies bloğu silindi. Yedi pattern artık tek SecurityServiceCollectionExtensions.cs dosyasında — deploy farkları açık, code review hızlı.

Geçiş süresi: ~1 saat. Dört dosyada using AspNetCoreRateLimit; temizliği, iki csproj'da paket ref kaldırma, yedi pattern'in mapping yazımı, OnRejected + HTML template entegrasyonu, smoke test.

Söküm anının küçük zaferi: appsettings.json'dan IpRateLimiting ve IpRateLimitPolicies bloklarını sildim — 124 satır gitti, yerine boşluk kalmadı. JSON config'in azalması her zaman iyi sinyaldir: deploy farkları daha okunaklı, sırlar config dosyasına sızma riski yok, dev/prod davranış farkı kod incelemesiyle anlaşılabilir. Bu küçük "diff azalması" anı .NET 10'a geçişin ergonomik tarafının en somut göstergesi oldu — daha az config, daha çok kod.

Smoke test örneği:

# /api/mailgonder hızlı rate limit kontrolü (mail policy: 12/1m)
for i in {1..15}; do
  curl -s -o /dev/null -w "%{http_code} " https://yourapp.com/api/mailgonder
done
# Beklenen: 12 × 200/202, 3 × 429
# Son 429'da Retry-After header set ediliyor (curl -I ile gör)

AutoMapper 16 Constructor Kırılması

AutoMapper 13 → 16.1.1 transitive zorunluluğunu özel paket bump'ı tetikledi (BouncyCastle CVE'leri Core.Mailing.Base'den temizlerken paralel olarak transitive AutoMapper'ı da çekti). Geçiş sırasında derleyici hatası:

error CS7036: There is no argument given that corresponds to the required parameter 'loggerFactory'
  of 'MapperConfiguration.MapperConfiguration(Action<IMapperConfigurationExpression>, ILoggerFactory)'

AutoMapper 14'ten itibaren MapperConfiguration constructor'ı ILoggerFactory parametresini zorunlu hale getirdi. Önceki sürümlerde optional ya da hiç yoktu.

Etki noktası: Test setup, çünkü production DI register tarafında services.AddAutoMapper(typeof(SomeProfile)) extension method'u zaten ILoggerFactory'yi container'dan inject ediyor. Sorun tipik olarak test base sınıflarında veya integration test factory'lerinde patlıyor.

Fix:

// ÖNCE (AutoMapper 13):
var config = new MapperConfiguration(cfg =>
{
    cfg.AddProfile<SomeProfile>();
});
var mapper = config.CreateMapper();

// SONRA (AutoMapper 16):
var config = new MapperConfiguration(
    cfg => cfg.AddProfile<SomeProfile>(),
    NullLoggerFactory.Instance);  // <-- zorunlu parametre
var mapper = config.CreateMapper();

Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance test bağlamında log üretmiyor; production'da gerçek ILoggerFactory zaten DI container'dan geliyor. Bu fix benim Core.Test.Base paketimde tek dosyada (BaseMockRepository.cs) toplandı, web-app ana koduna yansımadı.

Diğer breaking change yok — Profile API, ProjectTo, dynamic mapping hepsi 13 ile geriye uyumlu.

Pasaj — ilk derleme şoku: Core paketlerini 2.0.0'a bump edip dotnet build çalıştırdığımda derleyici 7 dosyada CS7036 hatası fırlattı. İlk refleks "ne kadar yaygın breaking change!" oldu, sonra hata izlerine baktım: hepsi aynı MapperConfiguration ctor noktasına işaret ediyordu, ama 5 tanesi production DI'da, 2 tanesi test base'inde. Production DI tarafında services.AddAutoMapper(...) extension method'u zaten ILoggerFactory'yi container'dan inject ediyor — derleyici hatası using direktifi sırasıyla ya da incremental compilation cache'inden kaynaklanıyordu. dotnet clean + dotnet restore --force sonrasında prod hataları 7'den 2'ye düştü, sadece test base sınıfında düzeltme kaldı. Toplam fix süresi: 10 dakika, ama "geri çağırma" anı yaşatan tipik bir migration tuzağı.


SqlClient Yeni Varsayılanları (TLS sürprizi)

Bu, migration'ın en sinir bozucu kısımı oldu — çünkü hata mesajı yanıltıcı.

EFCore 8 → 10 geçişi transitive olarak Microsoft.Data.SqlClient 5.x'i çekiyor. SqlClient 5.0'dan itibaren bağlantı dizesi varsayılanları değişti:

  • Encrypt default değer: FalseMandatory (TLS zorunlu)
  • TrustServerCertificate default: False

Yani önceden çalışan basit bir bağlantı dizesi (Server=...;Database=...;User Id=...;Password=...;) artık şu hatayı veriyor:

A connection was successfully established with the server,
but then an error occurred during the login process.
(provider: SSL Provider, error: 0 — The handshake failed
due to an unexpected packet format.)

İlk okuyuşta SQL Server, ağ veya credential sorunu gibi görünüyor — ama gerçek sebep TLS handshake. SQL Server self-signed sertifika kullanıyorsa veya prod'da forced encryption açıksa eski bağlantı dizesi handshake'i geçemiyor.

Çözüm — production-grade bağlantı dizesi:

Server=...;Database=...;User Id=...;Password=...;
Encrypt=True;
TrustServerCertificate=True;
Connect Timeout=60;
MultipleActiveResultSets=True;

Üç ana parametre:

  • Encrypt=True (Mandatory ile aynı, TLS açık) — bağlantı şifrelenmiş kalır
  • TrustServerCertificate=True self-signed sertifika kabul et — kurumsal Public CA imzalı sertifikanız yoksa zorunlu (cluster-internal SQL Server için yaygın)
  • Connect Timeout=60 (saniye) Kubernetes cold start + TLS handshake = ~15-30 saniye toplam, default 15 saniye yetmeyebilir

Production konfigürasyon stratejim — iki katmanlı:

appsettings.json'da temel parametreleri tutuyorum (TrustServerCertificate=True;MultipleActiveResultSets=True;) ama gerçek password ve hassas tarafı REPLACE_ME placeholder olarak bırakıyorum. Pod start anında HashiCorp Vault secret/bilal-website path'inden ConnectionStrings__BaseDb patch'i çekiliyor (AddVaultSecrets("bilal-website") extension method). Vault'taki tam dize Encrypt=True;Connect Timeout=60 ek parametrelerini içeriyor — yani production'da konfigürasyon Vault'tan komple geliyor, repo placeholder fingerprint amaçlı.

Vault secret patch akışı:

kubectl exec -n vault vault-0 -- vault kv patch bilal-website   ConnectionStrings__BaseDb="Server=...;...;Encrypt=True;TrustServerCertificate=True;Connect Timeout=60;..."

Pod yeniden başladığında (kubectl rollout restart deployment/myapp) yeni dize devreye giriyor. Vault sidecar reloader cluster'da çalışıyorsa restart bile gerekmez, secret değişikliği otomatik yansır.

Bu sorunun bana 30 dakika kaybettirme hikâyesi: 7 Mayıs 2026 sabahı dotnet run ile lokal başlattığımda ilk hata "Login failed" geldi. Önce SQL Server konfigürasyonunu kontrol ettim (server logs temiz), ardından K8s network policy'yi (iptables temiz). Ama hata mesajının ilk satırı — SSL Provider, error: 0 — The handshake failed due to an unexpected packet format — TLS handshake olduğunu söylüyordu, ben "login failed" diye genelleyip atlamıştım. Migration yolunda Encrypt + TrustServerCertificate eklemek 5 saniyelik iş, sebebi anlamak yarım saat.

Test edilen ortamlar: SQL Server 2019, SQL Server 2022, Azure SQL Database. Üçünde de aynı dize çalıştı. Azure SQL'de TrustServerCertificate aslında gereksiz (Microsoft public CA imzalı), ama bağlantı dizesini ortamlar arası tek bırakmak deploy hatasını önlüyor.


Serilog 4.3 + ASP.NET Core 10 Transitive Tuzakları

Logging tarafı .NET 10 için iki paket güncellemesi gerektiriyor:

  • Serilog 4.0.1 → 4.3.0 (4.4 NuGet'te henüz yok, Mayıs 2026 itibariyle)
  • Serilog.AspNetCore 8.0.2 → 10.0.0

Tuzak: Serilog.AspNetCore 10 yüklendiğinde transitive olarak yeni minimum versiyonlar talep ediyor:

error NU1605: Detected package downgrade:
  Serilog.Sinks.Console from 6.1.1 to 6.0.0
  Serilog.Sinks.File from 7.0.0 to 6.0.0

Yani projeniz Sinks.Console 6.0.0 kullanıyorsa explicit olarak >= 6.1.1'e bump etmek zorundasınız. Aynı şekilde Sinks.File 7.0.0. Aksi NU1605 derlemeyi durduruyor.

Fix:

<ItemGroup>
  <PackageReference Include="Serilog" Version="4.3.0" />
  <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
  <PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
  <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>

Sink uyumluluk testi: Console, File, MSSQL, Graylog, MongoDB sink'lerimi 4.3'te denedim — hepsi geriye uyumlu. Davranış aynı, log format değişmedi.


Docker ve Kubernetes Production Deploy

Multi-stage Dockerfile için temel değişiklik base image bump:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:80
ENTRYPOINT ["dotnet", "MyApp.dll"]

Image boyutu: aspnet:10.0 yaklaşık 213 MB (8.0'a göre %3 küçük). Bu marjinal kazanç çok ama çok deploy yapıyorsanız Jenkins agent disk + registry bandwidth tarafında birikiyor.

Liveness/Readiness probe: /health/live endpoint pipeline'ın en başında, HostFiltering ve diğer middleware'lerden önce çalışıyor. Pod cold start anında bile 200 dönüyor; Kubernetes "Container failed to start" false alarm'larını engelliyor. Önerim:

livenessProbe:
  httpGet:
    path: /health/live
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

Rollout strategy: RollingUpdate, maxSurge: 1, maxUnavailable: 0 → sıfır downtime. .NET 10 startup zamanı dev makinemde ~2-3 saniye: yeni pod liveness'a hazır olduktan sonra eski pod kapanıyor.

global.json + preview SDK uyumu: Dev makinemde 10.0.300 preview SDK var, prod Docker GA pull ediyor. global.json'da "rollForward": "latestFeature" + "allowPrerelease": true — pod build sırasında SDK uyumsuzluk olmuyor. CVE-2026-40372 patch'i 10.0.7'de geldi; runtime GA otomatik yansıyor.

Probe ayrımı: ASP.NET Core'da üç tür probe var ve karıştırılmaması önemli.

  • livenessProbe (/health/live): "Pod hâlâ canlı mı?" Sadece process'in deadlock'a girip girmediğini kontrol eder. Genellikle 200 sabit cevap.
  • readinessProbe (/health/ready): "Trafik almaya hazır mı?" Veritabanı, cache, dış API bağlantıları içerir; başarısızsa pod service'ten otomatik çıkarılır.
  • startupProbe (/health/startup): Cold start uzunsa (örn. EFCore migration kontrol, cache warm-up), liveness'tan önce çalışır ve daha uzun timeout'a izin verir.

Bizim setup'ta sadece livenessProbe + /health/live var çünkü startup hızlı (~2-3 sn). EFCore migration runtime'da değil deploy job'unda çalışıyor (init container) — bu pattern .NET 10'da da değişmedi.

Multi-arch image: ARM64 desteği prod'da yaygınlaşıyor (özellikle AWS Graviton ve son nesil Azure Ampere instance'ları). Buildx ile:

docker buildx create --use --name multibuilder
docker buildx build   --platform linux/amd64,linux/arm64   --push -t registry.example.com/myapp:10.0 .

mcr.microsoft.com/dotnet/aspnet:10.0 zaten multi-arch — ek konfigürasyon gerekmez. ARM64 worker node'larında %20-30 maliyet avantajı, performans neredeyse eş. Node sayısını, paketleme verimliliğini ve tahmini aylık maliyeti pod kaynak isteklerinden hesaplamak için Kubernetes kaynak bütçesi hesaplayıcısını kullanabilirsiniz.

Image security scanning: Production'a push etmeden önce Trivy veya Grype ile CVE taraması yararlı:

trivy image registry.example.com/myapp:10.0 --severity HIGH,CRITICAL

Migration sonrası ilk taramamda 2 transitive CVE çıktı: MimeKit/MailKit 4.14 (vendor design issue, kabul edilebilir) ve System.Security.Cryptography.Xml 10.0.0 (Microsoft latest patch zaten 10.0.0). İkisi de "false positive" sayılabilir.


Production'a Geçmeden Önce: 10 Smoke Test

7 Mayıs 2026 deploy öncesi cluster'da çalıştırdığım smoke test listesi. Her madde otomatize edilebilir — bir bash script + curl yeterli. Yeşil light için 10/10 geçer kapı:

  • 1) Health livenessGET /health/live → 200 (HostFiltering öncesi pipeline başı)
  • 2) Health readinessGET /health/ready → 200 (DB + cache + dış API'ler hazır)
  • 3) Locale switcher TRGET /?lang=tr → 200, <html lang="tr"> doğrula
  • 4) Locale switcher ENGET /?lang=en → 200, <html lang="en"> doğrula
  • 5) Locale switcher ARGET /?lang=ar → 200, <html lang="ar" dir="rtl"> doğrula
  • 6) Rate limiter trigger — 15 ardışık POST /api/mailgonder → 12×{200,202} + 3×429 + son cevapta Retry-After: 60 header
  • 7) Attack probe /.envGET /.env → 404 (dotfile block middleware)
  • 8) Attack probe /.git/configGET /.git/config → 404
  • 9) Attack probe /web.configGET /web.config → 404 (klasik IIS path; ASP.NET Core'da yok)
  • 10) Prometheus scrapeGET /metrics → 200, en az process_runtime_dotnet_gc_heap_size_bytes ve http_server_request_duration_seconds_bucket metrikleri görünür

Smoke test bash one-liner:

BASE="https://yourapp.com"
echo "1) /health/live"; curl -sI $BASE/health/live | head -1
echo "2) /health/ready"; curl -sI $BASE/health/ready | head -1
for lang in tr en ar; do
  echo "3-5) /?lang=$lang"; curl -sI "$BASE/?lang=$lang" | head -1
done
echo "6) rate-limit 15x"; for i in {1..15}; do curl -s -o /dev/null -w "%{http_code} " "$BASE/api/mailgonder"; done; echo
for path in .env .git/config web.config; do
  echo "7-9) /$path"; curl -sI "$BASE/$path" | head -1
done
echo "10) /metrics"; curl -s $BASE/metrics | head -5

Sonuç: 10/10 yeşil, build sonrası 8 dakika içinde. Bu listeyi tek bir smoke-test.sh olarak versiyon kontrolüne almak deploy sonrası 5 dakikalık manuel doğrulama yükünü 30 saniyeye indirir.


Observability: Prometheus + OpenTelemetry

.NET 10, diagnostic API tarafında daha temiz bir yüzey sunuyor — özellikle System.Diagnostics.Metrics ve OpenTelemetry.Exporter.Prometheus.AspNetCore paketi ile.

Standart kurulum:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddMeter("MyApp.Custom")
        .AddPrometheusExporter())
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter());

app.MapPrometheusScrapingEndpoint(); // /metrics

Custom counter örneği:

private static readonly Meter Meter = new("MyApp.Custom");
private static readonly Counter<long> AttackProbes =
    Meter.CreateCounter<long>("bilal_attack_probe_blocks_total");

AttackProbes.Add(1, new KeyValuePair<string, object?>("category", "encoded_or_dotfile"));

Prometheus scrape config bu metriği /metrics endpoint'inden çekiyor; Grafana dashboard'da time-series olarak görüntüleniyor. Bu, security telemetry için kullandığım pattern.

Sink eşleştirmesi: Serilog yapısal log'ları, OpenTelemetry trace ID'leri, Prometheus counter'ları + Grafana dashboard tek pencereden okunabiliyor. Trace ID, log enrichment'la otomatik akıyor:

Log.Logger = new LoggerConfiguration()
    .Enrich.WithProperty("Application", "MyApp")
    .Enrich.With<TraceIdEnricher>()
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] [{TraceId}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

Grafana tarafında bir log satırı + TraceId tıklandığında Tempo / Jaeger üstünden distributed trace açılıyor — özellikle çoklu mikroservis veya OAuth callback gibi dış API bağımlılığı olan flow'larda saatler kurtarıyor.


Bu Migration'dan 7 Çıkarım

Bir günlük yoğun çalışmadan sonra defterimde kalan 7 madde — bir sonraki .NET 10 geçişi yapan herkesin önceden bilmesi gereken şeyler:

  1. TLS hata mesajını okumayı öğren. SqlClient 5.x defaults değişikliği "Login failed" gibi görünen TLS handshake hatası fırlatıyor. Hata satırının ilk kelimesine değil tamamına bak: SSL Provider, error: 0 görüyorsan credential değil sertifika sorunu. Encrypt=True;TrustServerCertificate=True;Connect Timeout=60 üçlüsü standart.
  2. Üçüncü taraf rate limiter söküm fırsatı. AspNetCoreRateLimit hâlâ derleniyor ama vendor patch taahhüdü yok. Native AddRateLimiter + SlidingWindow + URL-pattern routing yarım gün iş, 124 satır JSON config kaldırıyor. Borç bu kadar ucuz kapanmaz.
  3. AutoMapper 16 = ctor breaking değil test setup breaking. Production DI services.AddAutoMapper(...) zaten ILoggerFactory'yi inject ediyor. CS7036 hatası %95 test base sınıflarında — NullLoggerFactory.Instance ile 1 satır fix.
  4. Serilog 4.3'ün NU1605 tuzağı transitive. Serilog.AspNetCore 10 yüklediğinde Sinks.Console >= 6.1.1 ve Sinks.File >= 7.0.0 explicit bump yapmalısın, yoksa downgrade hatası derlemeyi durdurur.
  5. Kubernetes startup hızlı, probe ayrımı önemli. .NET 10 cold start ~2-3 saniye — livenessProbe + /health/live yeterli, startupProbe'a ihtiyaç yok. EFCore migration deploy job'unda init container olarak kalsın, runtime'da değil.
  6. Self-hosted feed major bump zinciri okunaklı yap. 9 paket 1.x → 2.0.0 atlandığında her birinde transitive değişiklik liste halinde dursun. Aynı sürüm overwrite edilemez (HTTP 409), her hata sonrası versiyonu yine bump etmen gerekir.
  7. Smoke test 10 endpoint = 30 saniyelik güven. Deploy sonrası manuel "şu URL'yi açıp bakayım" yerine versiyon kontrolündeki bash script otomatik 10 endpoint test ediyor. Geri dönüş kararını saatlere değil saniyelere bağlıyor.

Sıkça Sorulan Sorular

.NET 10'a şimdi mi geçmeliyim, .NET 8 LTS hâlâ 2026 sonuna kadar destekli değil mi?
.NET 8 desteği 10 Kasım 2026'da bitiyor (.NET 9 da aynı tarihte EOL). Şimdiden başlayan ekipler 5-7 ay manevra alanı kazanıyor; geçişi Eylül-Ekim'e bırakanlar EOL haftalarında patch + migration aynı anda baskıya giriyor.

AspNetCoreRateLimit kullanmaya devam edebilir miyim?
Evet, paket .NET 10'da hâlâ derleniyor. Ama vendor patch taahhüdü yok. Native AddRateLimiter'a geçmeniz kısa vadede yarım gün, uzun vadede CVE riski azaltıyor.

AutoMapper 16'ya yükseltmek zorunlu mu?
Hayır. AutoMapper 13/14/15 hâlâ .NET 10 ile uyumlu. Ama IdentityModel.Tokens 8 veya MimeKit 4.14 gibi paketlere bağımlıysanız transitive olarak çekilebilir. Test setup'ta MapperConfiguration ctor değişikliğine dikkat.

SqlClient bağlantı hatası alıyorum, ne yapmalıyım?
Encrypt=True;TrustServerCertificate=True;Connect Timeout=60 üçlüsünü bağlantı dizesine ekleyin. Self-signed sertifika veya forced encryption olan SQL Server'da bu zorunlu.

Serilog hangi sürümü kullanmalıyım?
Mayıs 2026 itibariyle en güncel kararlı sürüm Serilog 4.3.0 + Serilog.AspNetCore 10.0.0. Sinks.Console >= 6.1.1, Sinks.File >= 7.0.0 explicit bump zorunlu (NU1605 hatası).

Production'a deploy etmeden önce hangi smoke test'leri yapayım?
Minimum: /health/live 200, dil değiştirici (?lang=tr|en|ar) 200, rate limiter 429 deneme (curl ile 13 istek POST), attack probe path'leri (/.env, /.git, /web.config) 404 dönüyor mu. Otomasyonlaştırılabilir 10 endpoint testi yeterli.

.NET 10 SDK preview kullanıyorum, prod image'da sorun olur mu?
global.json ile "rollForward": "latestFeature" ve "allowPrerelease": true ayarladığınızda dev makineniz preview, prod Docker GA pull eder — uyumsuzluk olmaz.

Migration süresi tipik ne kadar?
Tek geliştirici için, orta ölçek bir uygulama (5-7 csproj, ~50 paket): 4-8 saat odaklı geçiş + 1-2 saat smoke. Test ekibi ile birlikte 1-2 gün. Büyük monorepo'larda haftalar uzayabilir.

Geri dönüş (rollback) planım ne olmalı?
Kubernetes kubectl rollout undo deployment/my-app. Docker image tag'lerini en az 3 sürüm tut. DB migration'lar varsa idempotent yapın (down migration), .NET 10 EFCore 10 bağlanır ama eski sürüm bağlantı dizesi farklı parametrelerle döner.


Kaynaklar ve Referanslar

Resmi belgeler:

Topluluk yazıları:

Migration bağlamı (bu yazının kaynak verisi):

  • bilalkose.com.tr web uygulaması — 6 csproj, .NET 8 → 10 LTS migration, 7 Mayıs 2026
  • 9 özel paket — nuget.bilalkose.com.tr kendi NuGet feed'inden major bump (1.x → 2.0.0)
  • Build sonucu: 0 hata, 18 uyarı, ~25 sn build, 10/10 smoke test endpoint

Yorumlar (0)

Yorum ve puan bırakın

Henüz yorum yapılmamış. İlk yorumu sen bırak.