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

ASP.NET Core'da Program.cs ve Hosting Modeli: WebApplication ve Environment'lar

Mert Özen 4 Kas 2026 7 dk 2 görüntülenme
ASP.NET Core'da Program.cs ve Hosting Modeli: WebApplication ve Environment'lar

Minimal hosting modelinin iki aşaması: builder ile service, configuration ve logging hazırlığı, Build sonrası app ile pipeline ve endpoint'ler. Environment'lar, app.Run() ve perde arkasındaki Generic Host.

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

Aşağıdaki Program.cs derleniyor, hiçbir uyarı da çıkmıyor. Şablondan tek farkı bir satırın yeri: controller'ları kaydeden satır builder.Build()'in altına kaymış. Uygulamayı çalıştırdığında ne olur? Controller'lar yine çalışır mı, bir uyarı mı görürsün, yoksa uygulama hiç ayağa kalkmaz mı?

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

builder.Services.AddControllers();

app.MapControllers();
app.Run();

İki Aşama: Hazırlık ve Çalışma

Cevap: uygulama ayağa kalkmaz. Çalıştırdığın anda şu exception ile kapanır:

Unhandled exception. System.InvalidOperationException: The service collection cannot be modified because it is read-only.
   at Microsoft.Extensions.DependencyInjection.ServiceCollection.ThrowReadOnlyException()

Sebebi, minimal hosting modelinin iki ayrı aşamadan oluşması. WebApplication.CreateBuilder(args) sana bir WebApplicationBuilder verir; bu hazırlık aşamasıdır. Service'leri kaydeder, configuration'ı ve logging'i ayarlarsın. builder.Build() çağrıldığında bu hazırlık mühürlenir: dependency injection container kurulur ve service listesi salt okunur hâle gelir. Elinde artık bir WebApplication vardır ve onunla sadece pipeline'ı ve endpoint'leri tanımlarsın.

Bir tiyatro sahnesi gibi düşün. Perde açılmadan önce dekor kurulur, oyuncular seçilir, ışıklar ayarlanır. Perde açıldıktan sonra sahneye yeni bir oyuncu sokamazsın; sadece oyunun nasıl akacağını yönetirsin. Build() o perdenin açıldığı an.

builder'ın İçinde Ne Var

Bu yazı boyunca kullanacağımız Program.cs şu:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Logging.ClearProviders();
builder.Logging.AddConsole();

builder.Host.ConfigureHostOptions(options => options.ShutdownTimeout = TimeSpan.FromSeconds(15));

string catalogName = builder.Configuration["Catalog:Name"] ?? "ProductCatalog";

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.Logger.LogInformation("Development mode: detailed error pages are on");
}

app.MapControllers();

app.MapGet("/info", (IWebHostEnvironment env) => new
{
    Catalog = catalogName,
    env.ApplicationName,
    env.EnvironmentName,
    PageSize = app.Configuration.GetValue<int>("Catalog:PageSize")
});

app.Lifetime.ApplicationStarted.Register(() => app.Logger.LogInformation("{Catalog} is ready", catalogName));

app.Run();

builder üzerinde dört property'yi sık kullanacaksın. builder.Services, dependency injection container'a kaydedeceğin service'lerin listesi; AddControllers() MVC'nin controller'lar için ihtiyaç duyduğu her şeyi buraya ekler. builder.Configuration, appsettings dosyalarından, environment variable'lardan ve komut satırından okunan ayarların birleşik görüntüsü. builder.Logging ile log'ların nereye yazılacağını belirlersin; örnekte varsayılan provider'ları temizleyip yalnızca console'u bıraktık. builder.Environment ise uygulamanın hangi ortamda çalıştığını söyler.

Bunların yanında iki kapı daha var: builder.Host ve builder.WebHost. Birincisi Generic Host ayarlarına, ikincisi Kestrel gibi web sunucusu ayarlarına açılır. ConfigureHostOptions satırı uygulama kapanırken çalışan işlerin bitmesi için 15 saniye bekleneceğini söylüyor. Bu iki kapıyı nadiren açarsın; ama eski projelerde ve bazı library'lerin kurulum dokümanlarında karşına çıkarlar.

Dependency injection, configuration ve logging'in her biri için ayrı yazılar var. Burada amacımız onların Program.cs'te nerede durduğunu görmek.

Configuration'a İlk Bakış

Örnekteki Catalog bölümü iki dosyadan geliyor:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Catalog": {
    "Name": "ProductCatalog",
    "PageSize": 20
  }
}
{
  "Catalog": {
    "PageSize": 5
  }
}

İlki appsettings.json, ikincisi appsettings.Development.json. İç içe anahtarlara iki nokta ile ulaşırsın: "Catalog:Name". CreateBuilder configuration kaynaklarını belirli bir sırayla yükler ve sonra gelen öncekini ezer: önce appsettings.json, sonra ortama özel dosya, sonra environment variable'lar, en son komut satırı argümanları. Development ortamında user secrets da ortama özel dosyadan hemen sonra devreye girer. Aynı uygulamayı farklı şekillerde çalıştırıp /info endpoint'ine baktım:

$ ASPNETCORE_ENVIRONMENT=Development dotnet run --no-launch-profile --urls http://localhost:5214
$ curl http://localhost:5214/info
{"catalog":"ProductCatalog","applicationName":"ProductCatalog.Api","environmentName":"Development","pageSize":5}

$ ASPNETCORE_ENVIRONMENT=Production dotnet run --no-launch-profile --urls http://localhost:5214
$ curl http://localhost:5214/info
{"catalog":"ProductCatalog","applicationName":"ProductCatalog.Api","environmentName":"Production","pageSize":20}

$ ASPNETCORE_ENVIRONMENT=Staging Catalog__Name=StagingCatalog dotnet run --no-launch-profile --urls http://localhost:5214
$ curl http://localhost:5214/info
{"catalog":"StagingCatalog","applicationName":"ProductCatalog.Api","environmentName":"Staging","pageSize":20}

$ dotnet run --no-launch-profile --urls http://localhost:5214 --Catalog:PageSize=50
$ curl http://localhost:5214/info
{"catalog":"ProductCatalog","applicationName":"ProductCatalog.Api","environmentName":"Production","pageSize":50}

Development'ta pageSize 5, çünkü ortama özel dosya ana dosyayı ezdi. Production'da öyle bir dosya olmadığı için 20 kaldı. Environment variable'da iki nokta yerine çift alt çizgi kullanılır: Catalog__Name. Komut satırı ise en son sözü söyledi. Son çalıştırmada ortam belirtmediğim için uygulama Production'da açıldı. Bu katmanların tamamını ve options pattern'i configuration yazısında ayrıntılı göreceğiz.

Environment'lar

ASP.NET Core ortam adını ASPNETCORE_ENVIRONMENT environment variable'ından okur. Hiçbir şey verilmezse ortam Production'dır. Bu bilinçli bir seçim: unutulan bir ayar yüzünden canlı sunucuda hata sayfalarının ayrıntılı açılmasındansa geliştirme makinesinde bir şeyin eksik görünmesi çok daha ucuzdur.

Kodda ortama app.Environment üzerinden bakarsın. IsDevelopment(), IsStaging() ve IsProduction() hazır gelir, kendi ortam adın için IsEnvironment("Testing") kullanırsın. Ortam adı aynı zamanda hangi appsettings.{Environment}.json dosyasının yükleneceğini belirler, yukarıdaki çıktılarda bunu gördün.

Önceki yazıda gördüğümüz launchSettings.json, dotnet run sırasında bu değişkeni Development olarak ayarlıyordu. Sunucuda ise değişkeni sen verirsin: Docker'da ENV ile, bir hosting panelinde uygulama ayarı olarak ya da systemd service tanımında.

app: Pipeline ve Endpoint'ler

builder.Build()'in döndürdüğü WebApplication, uygulamanın çalışan hâli. Onun üzerinde de tanıdık property'ler var: app.Environment, app.Configuration, app.Services ve app.Logger. Örnekte app.Logger ile yazdığımız log'un kategorisi uygulamanın adı oluyor:

info: ProductCatalog.Api[0]
      Development mode: detailed error pages are on
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

app'in asıl işi isteklerin geçeceği yolu tarif etmek. app.UseHttpsRedirection() gibi Use... ile başlayan çağrılar pipeline'a middleware ekler. app.MapControllers() ve app.MapGet(...) gibi Map... çağrıları ise endpoint tanımlar. /info endpoint'i bir minimal API; ayrıntılarını kendi yazısında göreceğiz, şimdilik "bu adrese gelen GET isteğine bu fonksiyon cevap verir" demesi yeterli.

.NET 6 ile gelen küçük bir kolaylık da var: Development ortamında developer exception page'i WebApplication kendisi ekler. Şablonda app.UseDeveloperExceptionPage() satırı görmemenin sebebi bu.

app.Run() Ne Yapar

app.Run(), Kestrel'i başlatır, configuration'daki adresleri dinlemeye başlar ve uygulama kapatılana kadar geri dönmez. Altındaki satırlar ancak uygulama kapanırken çalışır. Bunu app.Run("http://localhost:5050"); satırının altına bir Console.WriteLine("After Run"); koyarak denedim: yazı, uygulamaya kapatma sinyali gönderip "Application is shutting down..." log'u düştükten sonra ekrana geldi.

Uygulamanın hayat döngüsüne bağlanmak istersen app.Lifetime var. Örnekteki ApplicationStarted.Register, sunucu dinlemeye başladıktan sonra çalışır; log'larda "ProductCatalog is ready" satırını "Application started" mesajından sonra görürsün. ApplicationStopping ve ApplicationStopped da kapanış anları için.

Perde Arkasında Generic Host

WebApplication sıfırdan yazılmış ayrı bir dünya değil. Altında .NET'in Generic Host'u çalışıyor: aynı host, worker service'lerde ve konsol uygulamalarında da dependency injection, configuration, logging ve hayat döngüsünü yönetir. WebApplication bunun üstüne üç rolü tek bir object'te toplar:

IHost host = app;
IApplicationBuilder pipeline = app;
IEndpointRouteBuilder endpoints = app;

Üç atama da derlenir, çünkü WebApplication bu üç interface'i de uygular. Host olarak başlatılır ve durdurulur, pipeline olarak middleware alır, endpoint builder olarak route tanımlarını tutar. .NET 6 öncesinde bu roller Program.cs ve Startup.cs arasında bölünmüştü; Host.CreateDefaultBuilder, ConfigureServices ve Configure method'ları ile kurulan o yapıyı eski projeleri okumayı anlattığımız yazıda göreceğiz.

Sık Düşülen Tuzaklar

Build()'den sonra service kaydetmek. Yazının başında gördüğün gibi uygulama başlarken patlar. Bütün builder.Services satırları Build()'in üstünde durmalı.

Ortamı builder.WebHost üzerinden değiştirmeye çalışmak. builder.WebHost.UseEnvironment("Staging") satırı da derlenir ama çalışma anında şunu fırlatır: System.NotSupportedException: The environment changed from "Development" to "Staging". Changing the host configuration using WebApplicationBuilder.WebHost is not supported. Use WebApplication.CreateBuilder(WebApplicationOptions) instead. Mesaj çözümü de söylüyor:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    EnvironmentName = Environments.Staging
});

Ortamı koda gömmek yerine çoğu zaman environment variable ile vermek daha doğru; bu kalıbı test projelerinde ve özel araçlarda kullanırsın.

app.Run()'dan sonra kod yazıp çalışmasını beklemek. O satırlar uygulama kapanana kadar sıraya girmez.

Production'ın varsayılan olduğunu unutmak. Sunucuda ortam değişkenini vermeyi unutursan uygulama Production ayarlarıyla açılır; Development'a özel bir ayar bekliyorsan bulamazsın.

Küçük Bir Deneme

★☆☆ Örnek Program.cs'i kendi projene koy, appsettings.Development.json'a farklı bir Catalog:Name yaz ve /info çıktısının değiştiğini gör.

★★☆ Uygulamayı ASPNETCORE_ENVIRONMENT=Testing ile çalıştır, appsettings.Testing.json dosyası oluştur ve app.Environment.IsEnvironment("Testing") doğruysa log'a bir satır yazdır.

★★★ app.Lifetime.ApplicationStopping'e bir log ekle, uygulamayı Ctrl+C ile kapat ve log sırasını "Application is shutting down..." mesajıyla karşılaştır.

app ile tanımladığımız Use... satırlarının arkasında her isteğin sırayla içinden geçtiği bir boru hattı var. Bir sonraki yazıda middleware pipeline'ını açıyor; Use, Run ve Map'i, sıralamanın neden bu kadar önemli olduğunu ve kendi middleware'imizi yazmayı görüyoruz.