Mert Özen Her Satırda Daha İleri
Backend .NET Core Web API — #02

İlk ASP.NET Core Web API Projesi: .NET SDK, global.json ve Proje Yapısı

Mert Özen 4 Kas 2026 8 dk 1 görüntülenme
İlk ASP.NET Core Web API Projesi: .NET SDK, global.json ve Proje Yapısı

.NET SDK ile runtime farkı, birden fazla SDK ve global.json, dotnet new webapi şablonunun sürümlere göre değişimi, csproj ve TargetFramework, launchSettings.json ve ilk isteğin neden 307 döndüğü.

Sürüm notu: Bu yazıdaki kodlar .NET 6 ile yazılmıştır ve .NET 10'da da değişmeden çalışır.

Önce Tahmin Et

Bu yazının sonunda elinde çalışan bir API olacak ve terminalde şu satırları göreceksin: uygulama hem https://localhost:7278 hem de http://localhost:5214 adresini dinliyor. Sen de ilk isteği HTTP adresine atıyorsun: curl -i http://localhost:5214/weatherforecast. Hava durumu JSON'unu mu alırsın, yoksa başka bir şey mi? Cevap, ilk isteği attığımız bölümde.

SDK ve Runtime: İki Ayrı Şey

.NET'i kurarken iki parçayla karşılaşırsın. Runtime, derlenmiş uygulamayı çalıştıran motordur; sunucuda yalnızca onun olması yeter. SDK ise geliştirici makinesine kurulan araç kutusu: derleyici, dotnet new, dotnet build, dotnet run ve proje şablonları onun içinde. SDK kurduğunda yanında uygun runtime da gelir.

Bugün yeni bir makine hazırlıyorsan .NET 10 SDK'yı kur. .NET 10 hem güncel LTS sürüm hem de .NET 6'yı hedefleyen projeleri derleyebiliyor. Bu bloktaki bütün örnekleri ben de bu şekilde, aynı kodu hem net6.0 hem net10.0 hedefinde derleyerek doğruluyorum. Elinde .NET 6 ile çalışan bir proje varsa, onun son SDK'sı 6.0.428. Makinende hangi SDK'ların olduğunu şöyle görürsün:

$ dotnet --list-sdks
8.0.419 [/usr/local/share/dotnet/sdk]
10.0.103 [/usr/local/share/dotnet/sdk]
10.0.402 [/usr/local/share/dotnet/sdk]

Benim makinemde üç SDK yan yana duruyor ve birbirini ezmiyor. Runtime'lar için de aynısı geçerli; dotnet --list-runtimes onları listeler.

Birden Fazla SDK ve global.json

Birden fazla SDK kuruluyken dotnet komutu varsayılan olarak en yenisini seçer. Çoğu zaman istediğin budur, ama ekipteki herkesin ve CI sunucusunun aynı SDK ailesiyle derlemesini istiyorsan proje klasörüne bir global.json koyarsın:

$ dotnet new globaljson --sdk-version 10.0.100 --roll-forward latestFeature
The template "global.json file" was created successfully.

$ cat global.json
{
  "sdk": {
    "rollForward": "latestFeature",
    "version": "10.0.100"
  }
}
$ dotnet --version
10.0.402

version alt sınırı söyler, rollForward ise ne kadar yukarı çıkılabileceğini. latestFeature "10.0 ailesinde kalmak şartıyla kurulu olan en yeni SDK'yı kullan" demektir; bu yüzden 10.0.100 istememize rağmen 10.0.402 seçildi. Makinede 8.0 SDK'sı da olduğu hâlde ona inilmedi.

Peki makinede olmayan bir SDK'yı istersen? Aynı klasörde global.json'u 6.0.428'e sabitleyip tekrar denedim:

$ dotnet --version
The command could not be loaded, possibly because:
  * You intended to execute a .NET application:
      The application '--version' does not exist or is not a managed .dll or .exe.
  * You intended to execute a .NET SDK command:
      A compatible .NET SDK was not found.

Requested SDK version: 6.0.428

Hata uzun ama mesaj net: istenen SDK yok. Bir repoyu klonladıktan sonra en basit komut bile böyle patlıyorsa ilk bakacağın yer global.json.

dotnet new webapi

Proje şablonları SDK ile gelir, bu yüzden dotnet new webapi çıktısı hangi SDK'yı kullandığına göre değişir. .NET 6 SDK'sı ile şu komutu çalıştırdığında controller'lı ve Swagger'lı bir proje alırsın:

dotnet new webapi -o ProductCatalog.Api

.NET 6 ve 7 şablonları controller'lar ve Swashbuckle ile gelen Swagger UI ile gelir. .NET 8 SDK'sından itibaren şablon varsayılan olarak minimal API üretiyor, .NET 9'dan itibaren de Swashbuckle yerine yerleşik OpenAPI desteği geliyor; controller'lı projeyi --use-controllers ile istersin. Bu farkların ayrıntısı sürüm yazılarında.

.NET 10 SDK ile bu serinin başlangıç iskeletine en yakın projeyi şöyle alırsın:

dotnet new webapi --use-controllers --no-openapi -o ProductCatalog.Api

Bu proje Swagger satırları dışında .NET 6 şablonunun aynısı. Swagger'ı ayrı bir yazıda ele alacağız. Birebir .NET 6 şablonunu görmek istersen onun template paketini de kurabilirsin: dotnet new install Microsoft.DotNet.Web.ProjectTemplates.6.0 ardından dotnet new webapi -f net6.0 -o ProductCatalog.Api. Bu yazıdaki dosyaları ben böyle ürettim.

Proje Yapısına Bakalım

Oluşan klasör şöyle:

ProductCatalog.Api/
  Controllers/
    WeatherForecastController.cs
  Properties/
    launchSettings.json
  appsettings.json
  appsettings.Development.json
  Program.cs
  ProductCatalog.Api.csproj
  WeatherForecast.cs

Program.cs uygulamanın giriş noktası: service'leri kaydeder, pipeline'ı kurar ve sunucuyu başlatır. Onu bir sonraki yazıda satır satır okuyacağız. appsettings.json ve ortama özel appsettings.Development.json configuration dosyaları. WeatherForecast.cs ile controller ise şablonun sana "nereden başlayacağını" göstermek için koyduğu örnek. Controller'a kısaca bakalım:

using Microsoft.AspNetCore.Mvc;

namespace ProductCatalog.Api.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Dosyanın tepesinde tek bir using var, ama ILogger, IEnumerable ve Enumerable sorunsuz kullanılıyor. Bunu birazdan csproj'de göreceğimiz implicit usings sağlıyor. namespace ProductCatalog.Api.Controllers; satırının süslü parantezsiz yazılması da C# 10 ile gelen file-scoped namespace. [Route("[controller]")] sayesinde bu controller /weatherforecast adresinden çağrılıyor. Attribute'ların her birini routing ve controller yazılarında ayrıntılı göreceğiz.

csproj ve TargetFramework

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

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

</Project>

Kısa bir dosya ama her satırı iş görüyor. Microsoft.NET.Sdk.Web, projeyi bir web projesi yapar ve ASP.NET Core shared framework'ünü otomatik olarak referans eder. Kestrel, routing, MVC, logging gibi parçalar için ayrı paket eklemene gerek kalmaz. TargetFramework uygulamanın hangi .NET sürümünde çalışacağını söyler. Nullable derleyicinin null olabilecek referansları sana uyarı olarak göstermesini açar. ImplicitUsings ise System, System.Linq, Microsoft.AspNetCore.Builder gibi sık kullanılan namespace'leri her dosyaya görünmez global using olarak ekler. Tek paket Swashbuckle; son .NET 6 şablonu onu 6.5.0 sürümüyle getiriyor.

Bir proje aynı anda birden fazla hedefe de derlenebilir. Bu bloktaki örnekleri doğrularken kullandığım proje dosyası şöyle:

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

  <PropertyGroup>
    <TargetFrameworks>net6.0;net10.0</TargetFrameworks>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Property'nin adı çoğul oldu: TargetFrameworks. dotnet build artık iki ayrı çıktı üretir: bin/Debug/net6.0 ve bin/Debug/net10.0. .NET 10 SDK'sı net6.0 hedefi için her derlemede NETSDK1138 uyarısını basar: .NET 6'nın desteği bittiği için güvenlik güncellemesi almayacağını hatırlatır. Uyarı derlemeyi durdurmaz.

launchSettings.json

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:49146",
      "sslPort": 44310
    }
  },
  "profiles": {
    "ProductCatalog.Api": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:7278;http://localhost:5214",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Bu dosya yalnızca yerel geliştirme içindir; dotnet run, Visual Studio ve Rider onu okur. Port numaraları şablon tarafından rastgele seçilir, senin projende farklı olacak. Asıl önemli satır ASPNETCORE_ENVIRONMENT: uygulamaya Development ortamında olduğunu o söylüyor. Swagger UI da Program.cs'te yalnızca Development ortamında açılıyor. dotnet run --no-launch-profile ile bu dosyayı devre dışı bırakırsan uygulama Production ortamında ve http://localhost:5000 adresinde açılır, /swagger/index.html de 404 döner. Publish edilip sunucuya giden uygulama da bu dosyayı hiç görmez.

dotnet run ve İlk İstek

Proje klasöründe dotnet run çalıştır. Uygulamanın kendi log'ları şöyle:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7278
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5214
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development

Şimdi tahminine dönelim ve HTTP adresine istek atalım:

$ curl -i http://localhost:5214/weatherforecast
HTTP/1.1 307 Temporary Redirect
Content-Length: 0
Date: Tue, 03 Nov 2026 21:12:32 GMT
Server: Kestrel
Location: https://localhost:7278/weatherforecast

JSON değil, 307 Temporary Redirect geldi. Sebebi Program.cs'teki app.UseHttpsRedirection() satırı: HTTP'den gelen her isteği aynı adresin HTTPS karşılığına yönlendiriyor. Tarayıcı bu yönlendirmeyi kendiliğinden takip eder, curl ise sen -L demedikçe takip etmez. HTTPS adresine doğrudan gidelim:

$ curl -k https://localhost:7278/weatherforecast
[{"date":"2026-11-05T00:12:48.604094+03:00","temperatureC":42,"temperatureF":107,"summary":"Warm"},{"date":"2026-11-06T00:12:48.604176+03:00","temperatureC":45,"temperatureF":112,"summary":"Sweltering"},{"date":"2026-11-07T00:12:48.604176+03:00","temperatureC":40,"temperatureF":103,"summary":"Bracing"},{"date":"2026-11-08T00:12:48.604176+03:00","temperatureC":-5,"temperatureF":24,"summary":"Scorching"},{"date":"2026-11-09T00:12:48.604177+03:00","temperatureC":52,"temperatureF":125,"summary":"Cool"}]

-k, curl'e geliştirme sertifikasını doğrulamadan kabul etmesini söyler. Sertifikaya kalıcı olarak güvenmek için bir kez dotnet dev-certs https --trust çalıştırman yeterli. Tarayıcıda https://localhost:7278/swagger adresini açarsan aynı endpoint'i Swagger UI üzerinden de deneyebilirsin.

Kod yazarken uygulamayı her seferinde durdurup başlatmak yerine dotnet watch kullan. Dosyayı kaydettiğin anda değişikliği algılar; .NET 6 ile gelen Hot Reload sayesinde çoğu değişikliği uygulamayı yeniden başlatmadan içeri alır. Hot Reload'un uygulayamadığı değişikliklerde ise uygulamanın yeniden başlatılması gerekir.

Sık Düşülen Tuzaklar

.NET 10 şablonunu üretip TargetFramework'ü net6.0'a çekmek. Yeni şablonun controller'ı collection expression kullanıyor ve .NET 6'nın varsayılan dili C# 10 olduğu için derleme şu hatayla durur: error CS8936: Feature 'collection expressions' is not available in C# 10.0. Please use language version 12.0 or greater. Hedefi düşürüyorsan sürüme ait C# özelliklerini de geri almalısın.

Repoda global.json olduğunu unutmak. Klonladığın projede hiçbir komut çalışmıyorsa önce o dosyaya ve dotnet --list-sdks çıktısına bak.

launchSettings.json'a güvenip production ayarı yapmak. Ortam adı, port ve environment variable'lar bu dosyada sadece senin makinende geçerli. Sunucuda bunları gerçek environment variable'larla ya da configuration ile vermen gerekir.

HTTP portuna istek atıp boş cevap görünce şaşırmak. curl 307'yi takip etmez; ya HTTPS adresini kullan ya da -L ekle.

Küçük Bir Deneme

★☆☆ Kendi makinende dotnet --list-sdks ve dotnet --list-runtimes çalıştır. Ardından boş bir klasörde global.json ile sahip olduğun en eski SDK'yı sabitle ve dotnet --version çıktısının değiştiğini gör.

★★☆ ProductCatalog.Api projesini oluştur, dotnet run ile çalıştır ve curl -i -L http://localhost:PORT/weatherforecast ile yönlendirmenin takip edildiğini gör. Sonra dotnet run --no-launch-profile ile çalıştırıp hangi adreste ve hangi ortamda açıldığını log'lardan oku.

★★★ csproj'deki hedefi TargetFrameworks ile iki sürüme çıkar, dotnet build sonrasında bin/Debug altında oluşan klasörleri incele.

Projeyi ayağa kaldırdık ama Program.cs'teki o birkaç satırın arkasında ne olduğunu henüz konuşmadık. Bir sonraki yazıda WebApplication.CreateBuilder ile başlayan hosting modelini açıyor; builder ile app arasındaki ayrımı, environment'ları ve app.Run()'ın tam olarak ne yaptığını görüyoruz.