Skip to content

📌 Ne Zaman Kullanılır?

  • ✅ Enterprise uygulama, performans kritik API, .NET ekosistemi, kurumsal proje
  • ⚠️ Küçük proje için overengineering olabilir (Laravel/Node.js daha hızlı başlangıç)
  • ❌ Hızlı prototip/MVP (Laravel veya Express daha uygun)

Önerilen Kullanım: Enterprise API + MSSQL/PostgreSQL + Azure Alternatifler: Laravel (PHP), Express (Node.js), Django (Python)

ASP.NET Core Mastery Guide (C#) — Beginner → Advanced (EN + TR)

📘 Complete English + Turkish learning guide for ASP.NET Core (C#) covering MVC, Web API, Razor Pages, EF Core, Identity, JWT, Deployment, and best practices.
(📘 ASP.NET Core (C#) için MVC, Web API, Razor Pages, EF Core, Identity, JWT, Dağıtım ve en iyi uygulamaları kapsayan İngilizce + Türkçe tam öğrenme rehberi.)

1) Fundamentals & Setup (Temeller ve Kurulum)

What is ASP.NET Core? Cross-platform, high-performance, open-source framework for modern web apps/APIs.
(ASP.NET Core nedir? Modern web uygulamaları/API'ler için çok platformlu, yüksek performanslı, açık kaynak bir çatı.)

Prerequisites (Gereksinimler):

  • .NET SDK 8+
  • IDE: Visual Studio 2022 / Rider / VS Code (C# extensions)
  • SQL Server / SQLite / PostgreSQL

Create a project (Proje oluşturma):

bash
dotnet --version
dotnet new webapi -n MyApi
cd MyApi
dotnet run

Default URL: https://localhost:7147 (random ports).

✅ Summary: You can create and run an ASP.NET Core app.
(Artık bir ASP.NET Core uygulaması oluşturup çalıştırabiliyorsun.)


2) Project Types (Proje Türleri)

  • MVC: Controllers + Views (server-rendered).
  • Web API: JSON endpoints for SPA/mobile.
  • Razor Pages: Page-based, simpler than MVC.
bash
dotnet new mvc -n MyMvc
dotnet new webapi -n MyApi
dotnet new razor -n MyPages

Seçim: API + React/Vue/Angular için Web API; klasik SSR için MVC/Razor Pages.


3) Anatomy: Program.cs, Middleware, DI

Program.cs (Minimal hosting model):

csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // or AddRazorPages()
builder.Services.AddDbContext<AppDb>(o => o.UseSqlite("Data Source=app.db"));
builder.Services.AddScoped<IMailService, MailService>(); // DI

var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers(); // or MapRazorPages()
app.Run();

Middleware order matters (Sıra önemlidir): HTTPS → StaticFiles → Routing → Auth → Endpoints.
DI (Dependency Injection): AddSingleton, AddScoped, AddTransient yaşam döngüleri.

✅ Summary: Program.cs, pipeline ve DI uygulamanın kalbidir.
(Program.cs, pipeline ve DI uygulamanın kalbidir.)


3.1) Dependency Injection Detayli

ASP.NET Core'un temel taşlarından biri olan Dependency Injection (DI), servislerin yaşam döngüsünü ve bağımlılıklarını yönetir. Framework'ün built-in IoC container'ı çoğu senaryo için yeterlidir.

Scoped vs Transient vs Singleton

ÖzellikSingletonScopedTransient
Yaşam döngüsüUygulama boyunca tek instanceHTTP request başına tek instanceHer injection'da yeni instance
Ne zaman kullanCache, configuration, HttpClient factoryDbContext, UnitOfWork, repositoryHafif, stateless servisler
Örnek servisCacheService, AppSettingsDbContext, UserServiceEmailBuilder, GuidGenerator
BellekEn düşük (tek instance)Orta (request başına)En yüksek (her çağrıda yeni)
Thread safetyGerekli (concurrent erişim)Genelde gerekmez (tek request)Gerekmez
csharp
// Kayıt örnekleri
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailBuilder, EmailBuilder>();

Constructor Injection vs Method Injection

Constructor injection en yaygın ve önerilen yöntemdir. Sınıfın tüm bağımlılıkları constructor'da belirlenir:

csharp
public class OrderService : IOrderService
{
    private readonly IOrderRepository _repo;
    private readonly ILogger<OrderService> _logger;
    private readonly IEmailService _emailService;

    // Constructor injection -- tüm bağımlılıklar burada
    public OrderService(
        IOrderRepository repo,
        ILogger<OrderService> logger,
        IEmailService emailService)
    {
        _repo = repo;
        _logger = logger;
        _emailService = emailService;
    }

    public async Task<Order> CreateAsync(OrderDto dto)
    {
        _logger.LogInformation("Yeni sipariş oluşturuluyor");
        var order = new Order { ProductId = dto.ProductId, Quantity = dto.Quantity };
        await _repo.AddAsync(order);
        await _emailService.SendOrderConfirmationAsync(order);
        return order;
    }
}

Method injection ise nadiren, sadece belirli metotlarda ihtiyaç duyulan servislerde kullanılır:

csharp
public class ReportService : IReportService
{
    // [FromServices] ile method injection (Controller'larda)
    public IActionResult GenerateReport([FromServices] IPdfGenerator pdfGen)
    {
        var pdf = pdfGen.Create("Rapor");
        return File(pdf, "application/pdf");
    }
}

// Minimal API'de method injection doğal olarak desteklenir
app.MapGet("/report", (IPdfGenerator pdfGen) =>
{
    var pdf = pdfGen.Create("Rapor");
    return Results.File(pdf, "application/pdf");
});

Interface ile DI (IService / Service Pattern)

Her servis için bir interface tanımlamak, test edilebilirliği ve gevşek bağlılığı (loose coupling) sağlar:

csharp
// 1. Interface tanımla
public interface IProductService
{
    Task<List<Product>> GetAllAsync();
    Task<Product?> GetByIdAsync(int id);
    Task<Product> CreateAsync(CreateProductDto dto);
    Task<bool> DeleteAsync(int id);
}

// 2. Implementasyon
public class ProductService : IProductService
{
    private readonly AppDbContext _db;

    public ProductService(AppDbContext db) => _db = db;

    public async Task<List<Product>> GetAllAsync()
        => await _db.Products.AsNoTracking().ToListAsync();

    public async Task<Product?> GetByIdAsync(int id)
        => await _db.Products.FindAsync(id);

    public async Task<Product> CreateAsync(CreateProductDto dto)
    {
        var product = new Product { Name = dto.Name, Price = dto.Price };
        _db.Products.Add(product);
        await _db.SaveChangesAsync();
        return product;
    }

    public async Task<bool> DeleteAsync(int id)
    {
        var product = await _db.Products.FindAsync(id);
        if (product is null) return false;
        _db.Products.Remove(product);
        await _db.SaveChangesAsync();
        return true;
    }
}

// 3. DI kaydı
builder.Services.AddScoped<IProductService, ProductService>();

Multiple Implementation (Factory Pattern & Keyed Services)

Aynı interface'in birden fazla implementasyonu olabilir. .NET 8 ile gelen keyed services bunu kolaylaştırır:

csharp
// Interface
public interface INotificationService
{
    Task SendAsync(string to, string message);
}

// Implementasyonlar
public class EmailNotificationService : INotificationService
{
    public async Task SendAsync(string to, string message)
        => await Task.Run(() => Console.WriteLine($"Email -> {to}: {message}"));
}

public class SmsNotificationService : INotificationService
{
    public async Task SendAsync(string to, string message)
        => await Task.Run(() => Console.WriteLine($"SMS -> {to}: {message}"));
}

// .NET 8 Keyed Services ile kayıt
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");

// Kullanım
public class OrderController : ControllerBase
{
    public async Task<IActionResult> Notify(
        [FromKeyedServices("email")] INotificationService emailService,
        [FromKeyedServices("sms")] INotificationService smsService)
    {
        await emailService.SendAsync("user@mail.com", "Sipariş onaylandı");
        await smsService.SendAsync("+905551234567", "Sipariş onaylandı");
        return Ok();
    }
}

// Factory pattern ile alternatif yaklaşım (.NET 7 ve öncesi)
builder.Services.AddScoped<EmailNotificationService>();
builder.Services.AddScoped<SmsNotificationService>();
builder.Services.AddScoped<Func<string, INotificationService>>(sp => key =>
{
    return key switch
    {
        "email" => sp.GetRequiredService<EmailNotificationService>(),
        "sms" => sp.GetRequiredService<SmsNotificationService>(),
        _ => throw new ArgumentException($"Bilinmeyen bildirim tipi: {key}")
    };
});

Captive Dependency Problemi

Bir Singleton servis, Scoped bir servisi inject ederse, Scoped servis de Singleton gibi davranır ve request boyunca paylaşılmaz. Bu ciddi sorunlara yol açar (ör. DbContext thread-safe değildir):

csharp
// YANLIS -- Captive dependency!
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<AppDbContext>();

public class CacheService : ICacheService
{
    private readonly AppDbContext _db; // Scoped servis, Singleton içinde yakalandı!

    public CacheService(AppDbContext db) => _db = db;
    // _db artık tüm request'ler arasında paylaşılır -- veri tutarsızlığı, thread hatası
}

// DOGRU -- Singleton içinde Scoped servise ihtiyaç varsa IServiceScopeFactory kullan
public class CacheService : ICacheService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public CacheService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;

    public async Task<List<Product>> GetProductsAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        return await db.Products.AsNoTracking().ToListAsync();
    }
}

Kural: Yaşam döngüsü hiyerarşisi: Singleton > Scoped > Transient. Uzun ömürlü bir servis, kısa ömürlü bir servisi doğrudan inject etmemeli. Development ortamında ValidateScopes aktif ederek bu hataları erken yakalayabilirsin:

csharp
var builder = WebApplication.CreateBuilder(args);
// Development'ta scope doğrulaması varsayılan olarak açık
// Production'da manuel açmak için:
builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});

✅ Summary: DI yaşam döngülerini doğru seç, captive dependency'den kaçın, interface ile gevşek bağlılık sağla.


4) Controllers, Routing & Model Binding

Attribute routing:

csharp
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
    [HttpGet]                 // GET /api/posts
    public async Task<IEnumerable<Post>> Get() => await _db.Posts.ToListAsync();

    [HttpGet("{id:int}")]     // GET /api/posts/5
    public async Task<ActionResult<Post>> GetById(int id)
    {
        var post = await _db.Posts.FindAsync(id);
        return post is null ? NotFound() : Ok(post);
    }

    [HttpPost]                // POST /api/posts
    public async Task<ActionResult<Post>> Create(PostDto dto)
    {
        var post = new Post { Title = dto.Title, Body = dto.Body };
        _db.Posts.Add(post);
        await _db.SaveChangesAsync();
        return CreatedAtAction(nameof(GetById), new { id = post.Id }, post);
    }
}

Model binding & validation: [FromBody], [FromRoute], [FromQuery], data annotations: [Required], [MaxLength].

✅ Summary: Route attribute'ları ve model binding ile temiz endpoint'ler yaz.
(Route nitelikleri ve model bağlama ile temiz uç noktalar.)


5) Views (Razor), Layout, Tag Helpers

Layout & partials: _Layout.cshtml, _ValidationScriptsPartial.cshtml
Tag Helpers: <form asp-action="Create">, <input asp-for="Title" />
ViewModel pattern: Strongly-typed views for compile-time safety.

csharp
public class PostVm { [Required] public string Title { get; set; } = ""; public string Body { get; set; } = ""; }

✅ Summary: Razor ile güçlü SSR; Tag Helpers ile güvenli formlar.
(Razor SSR; Tag Helpers ile güvenli form oluşturma.)


6) Data: EF Core, Migrations, LINQ

DbContext & entity:

csharp
public class AppDb : DbContext {
  public AppDb(DbContextOptions<AppDb> o) : base(o) {}
  public DbSet<Post> Posts => Set<Post>();
}
public class Post { public int Id { get; set; } public string Title { get; set; } = ""; public string Body { get; set; } = ""; }

Migrations:

bash
dotnet ef migrations add Init
dotnet ef database update

(Install tools) dotnet tool install --global dotnet-ef

LINQ examples:

csharp
var latest = await _db.Posts.OrderByDescending(p=>p.Id).Take(10).ToListAsync();
var filtered = await _db.Posts.Where(p=>p.Title.Contains(q)).ToListAsync();

✅ Summary: EF Core ile hızlı CRUD, LINQ ile güçlü sorgular.
(EF Core CRUD, LINQ sorguları.)


6.1) Entity Framework Core Detayli

DbContext Yaşam Döngüsü

DbContext varsayılan olarak Scoped yaşam döngüsüyle kaydedilir. Bu, her HTTP request için yeni bir DbContext instance'ı oluşturulması anlamına gelir:

csharp
// Scoped kayıt (varsayılan ve önerilen)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// DbContext factory (Singleton servisler veya Blazor için)
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

DbContext'i Singleton olarak kaydetme -- thread-safe değildir ve ciddi hatalara yol açar.

Relationships (İlişkiler)

One-to-One (1-1)

csharp
public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public UserProfile? Profile { get; set; } // Navigation property
}

public class UserProfile
{
    public int Id { get; set; }
    public string Bio { get; set; } = "";
    public string AvatarUrl { get; set; } = "";
    public int UserId { get; set; } // Foreign key
    public User User { get; set; } = null!;
}

// Fluent API ile konfigürasyon
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
        .HasOne(u => u.Profile)
        .WithOne(p => p.User)
        .HasForeignKey<UserProfile>(p => p.UserId);
}

One-to-Many (1-N)

csharp
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public List<Product> Products { get; set; } = new();
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
    public int CategoryId { get; set; } // Foreign key
    public Category Category { get; set; } = null!;
}

// Fluent API
modelBuilder.Entity<Category>()
    .HasMany(c => c.Products)
    .WithOne(p => p.Category)
    .HasForeignKey(p => p.CategoryId)
    .OnDelete(DeleteBehavior.Cascade);

Many-to-Many (N-N)

csharp
// EF Core 5+ skip navigation (join tablosu otomatik)
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public List<Course> Courses { get; set; } = new();
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public List<Student> Students { get; set; } = new();
}

// Fluent API (explicit join table ile)
public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; } = null!;
    public int CourseId { get; set; }
    public Course Course { get; set; } = null!;
    public DateTime EnrolledAt { get; set; } // Ekstra alan
}

modelBuilder.Entity<StudentCourse>()
    .HasKey(sc => new { sc.StudentId, sc.CourseId });

modelBuilder.Entity<StudentCourse>()
    .HasOne(sc => sc.Student)
    .WithMany()
    .HasForeignKey(sc => sc.StudentId);

modelBuilder.Entity<StudentCourse>()
    .HasOne(sc => sc.Course)
    .WithMany()
    .HasForeignKey(sc => sc.CourseId);

Fluent API vs Data Annotations

ÖzellikData AnnotationsFluent API
KonumEntity sınıfı üzerindeOnModelCreating içinde
OkunabilirlikBasit senaryolarda iyiKarmaşık ilişkilerde daha iyi
KapsamSınırlı (temel kural)Tam kontrol (her şeyi yapabilir)
Domain temizliğiEntity'ye attribute eklerEntity temiz kalır
Composite keyDesteklemezHasKey(x => new { x.A, x.B })
ÖnerilenBasit validasyonİlişki ve tablo konfigürasyonu
csharp
// Data Annotations
public class Product
{
    public int Id { get; set; }

    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = "";

    [Column(TypeName = "decimal(18,2)")]
    public decimal Price { get; set; }

    [Required]
    public int CategoryId { get; set; }
}

// Fluent API (aynı konfigürasyon)
modelBuilder.Entity<Product>(entity =>
{
    entity.Property(p => p.Name)
        .IsRequired()
        .HasMaxLength(200);

    entity.Property(p => p.Price)
        .HasColumnType("decimal(18,2)");

    entity.HasIndex(p => p.Name);

    entity.HasOne(p => p.Category)
        .WithMany(c => c.Products)
        .HasForeignKey(p => p.CategoryId);
});

Seeding (HasData)

Veritabanına başlangıç verisi eklemek için HasData kullanılır:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Category>().HasData(
        new Category { Id = 1, Name = "Elektronik" },
        new Category { Id = 2, Name = "Giyim" },
        new Category { Id = 3, Name = "Kitap" }
    );

    modelBuilder.Entity<Product>().HasData(
        new Product { Id = 1, Name = "Laptop", Price = 25000m, CategoryId = 1 },
        new Product { Id = 2, Name = "T-Shirt", Price = 150m, CategoryId = 2 },
        new Product { Id = 3, Name = "C# in Depth", Price = 350m, CategoryId = 3 }
    );
}

Seed verisi migration'a dahil olur: dotnet ef migrations add SeedData

Raw SQL & Stored Procedures

csharp
// FromSqlRaw -- sorgu sonucu entity'ye map edilir
var products = await _db.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
    .ToListAsync();

// FromSqlInterpolated -- string interpolation ile güvenli parametre
var products = await _db.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE CategoryId = {categoryId}")
    .ToListAsync();

// ExecuteSqlRaw -- INSERT, UPDATE, DELETE gibi non-query komutlar
var affected = await _db.Database
    .ExecuteSqlRawAsync("UPDATE Products SET Price = Price * 1.10 WHERE CategoryId = {0}", categoryId);

// Stored Procedure çağırma
var orders = await _db.Orders
    .FromSqlRaw("EXEC GetOrdersByCustomer @CustomerId = {0}", customerId)
    .ToListAsync();

AsNoTracking (Read-Only Performans)

Change tracker'ı devre dışı bırakarak read-only sorgularda belirgin performans kazancı sağlar:

csharp
// Tek sorguda AsNoTracking
var products = await _db.Products
    .AsNoTracking()
    .Where(p => p.Price > 100)
    .ToListAsync();

// DbContext seviyesinde varsayılan olarak no-tracking
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

// Sadece güncelleme yapılacaksa tracking aç
var product = await _db.Products.FirstAsync(p => p.Id == id); // tracking açık
product.Price = 200;
await _db.SaveChangesAsync();

Split Queries (AsSplitQuery -- N+1 Çözümü)

Birden fazla Include kullanıldığında kartezyen patlama (cartesian explosion) oluşabilir. AsSplitQuery bunu ayrı SQL sorgularına bölerek çözer:

csharp
// Tek sorgu (varsayılan) -- büyük veri setlerinde yavaş olabilir
var orders = await _db.Orders
    .Include(o => o.Items)
    .Include(o => o.Customer)
    .ToListAsync();

// Split query -- her Include ayrı sorgu olarak çalışır
var orders = await _db.Orders
    .Include(o => o.Items)
    .Include(o => o.Customer)
    .AsSplitQuery()
    .ToListAsync();

// Global olarak split query varsayılan yapma
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString, o => o.UseQuerySplittingBehavior(
        QuerySplittingBehavior.SplitQuery)));

Global Query Filters (Soft Delete, Multi-Tenancy)

Tüm sorgulara otomatik filtre uygular. Soft delete ve multi-tenancy için idealdir:

csharp
public class BaseEntity
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

public class Product : BaseEntity
{
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
    public int TenantId { get; set; }
}

// Global filter tanımlama
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Soft delete filtresi -- IsDeleted = true olanlar otomatik filtrelenir
    modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);

    // Multi-tenancy filtresi
    modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == _currentTenantId);
}

// Soft delete uygulama (gerçek silme yerine)
public async Task SoftDeleteAsync(int id)
{
    var product = await _db.Products.FindAsync(id);
    if (product is not null)
    {
        product.IsDeleted = true;
        await _db.SaveChangesAsync();
    }
}

// Filtreyi geçici olarak devre dışı bırakma
var allProducts = await _db.Products
    .IgnoreQueryFilters()
    .ToListAsync();

Interceptors (Audit Log)

SaveChanges sırasında otomatik olarak audit bilgisi ekler:

csharp
public class AuditInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        SetAuditFields(eventData.Context);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        SetAuditFields(eventData.Context);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void SetAuditFields(DbContext? context)
    {
        if (context is null) return;

        var entries = context.ChangeTracker.Entries<BaseEntity>();
        foreach (var entry in entries)
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.CreatedAt = DateTime.UtcNow;
                entry.Entity.CreatedBy = "system"; // IHttpContextAccessor ile kullanıcı alınabilir
            }
            if (entry.State == EntityState.Modified)
            {
                entry.Entity.UpdatedAt = DateTime.UtcNow;
                entry.Entity.UpdatedBy = "system";
            }
        }
    }
}

// Kayıt
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
    options.UseSqlServer(connectionString)
           .AddInterceptors(new AuditInterceptor()));

✅ Summary: EF Core ile ilişkiler, performans optimizasyonu, global filtreler ve audit log altyapısı kur.


7) Validation, Filters & FluentValidation

DataAnnotations:

csharp
public class PostDto {
  [Required, MinLength(3), MaxLength(255)]
  public string Title { get; set; } = "";
  [Required] public string Body { get; set; } = "";
}

Filters: Exception, Resource, Action filters for cross-cutting concerns.
FluentValidation (opsiyonel):

bash
dotnet add package FluentValidation.AspNetCore
csharp
public class PostDtoValidator : AbstractValidator<PostDto> {
  public PostDtoValidator(){ RuleFor(x=>x.Title).NotEmpty().MinimumLength(3); }
}

✅ Summary: Doğrulamayı DTO katmanında merkezileştir.
(Validasyonu DTO seviyesinde tut.)


8) Authentication: Identity (Cookies) & JWT

Identity (Cookie-based): Web uygulamalarında login/register/roles.

bash
dotnet new mvc --auth Individual

Config:

csharp
builder.Services.AddDefaultIdentity<IdentityUser>(o => o.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<AppDb>();
app.UseAuthentication();
app.UseAuthorization();

JWT (API'ler için):

bash
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(o => {
    o.TokenValidationParameters = new TokenValidationParameters{
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(cfg["Jwt:Key"])),
      ValidIssuer = cfg["Jwt:Issuer"],
      ValidAudience = cfg["Jwt:Audience"],
      ValidateIssuer = true, ValidateAudience = true
    };
  });
app.UseAuthentication();
app.UseAuthorization();

Generating tokens via a controller on login.

✅ Summary: Web için Identity (cookie), SPA/mobile için JWT tercih et.
(Web'de cookie, istemcilerde JWT.)


9) Authorization: Roles, Policies & Claims

csharp
[Authorize(Roles = "Admin")]
public IActionResult AdminOnly(){ ... }

builder.Services.AddAuthorization(o => {
  o.AddPolicy("CanEdit", p => p.RequireClaim("permission","edit"));
});
[Authorize(Policy="CanEdit")]
public IActionResult Edit(){ ... }

✅ Summary: İnce taneli yetki için policy/claim kullan.
(Rol + policy kombinasyonu esneklik sağlar.)


10) Configuration & Options Pattern

appsettings.json → hierarchical config.
Options pattern:

csharp
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));
public class SmtpOptions{ public string Host {get;set;} = ""; public int Port {get;set;} }
public class MailService : IMailService {
   private readonly SmtpOptions _o;
   public MailService(IOptions<SmtpOptions> options){ _o = options.Value; }
}

✅ Summary: Config'i Options ile strongly-typed yönet.
(Ayarlar için Options kalıbı.)


11) Caching, Response Compression & GZip

csharp
builder.Services.AddResponseCaching();
app.UseResponseCaching();

[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public IActionResult Index() => View();

MemoryCache / DistributedCache (Redis) ile data cache.
ResponseCompression: Gzip/Brotli for static/dynamic content.

✅ Summary: Cache + sıkıştırma performansı artırır.
(Önbellek ve sıkıştırma önemlidir.)


12) Logging, Monitoring & Health Checks

Logging: built-in ILogger<T>, Serilog (opsiyonel).
Health Checks:

csharp
builder.Services.AddHealthChecks().AddDbContextCheck<AppDb>();
app.MapHealthChecks("/health");

✅ Summary: Gözlemleme ve sağlık uç noktaları prod'da kritik.
(Monitoring/health check kritik.)


13) Globalization (i18n) & Localization

csharp
builder.Services.AddLocalization();
app.UseRequestLocalization(new RequestLocalizationOptions{
  SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("tr") },
  SupportedUICultures = new[] { new CultureInfo("en"), new CultureInfo("tr") },
  DefaultRequestCulture = new RequestCulture("en")
});

Resources: Resources/Views.Home.Index.en.resx, Resources/Views.Home.Index.tr.resx.

✅ Summary: Çok dilli destek için RequestLocalization + resx dosyaları.
(Yerelleştirme alt yapısı.)


14) Testing (xUnit), Minimal APIs & Swagger

xUnit test project:

bash
dotnet new xunit -n MyApp.Tests
dotnet add MyApp.Tests reference MyApi
dotnet test

Minimal APIs (hızlı endpoint'ler):

csharp
app.MapGet("/api/ping", () => Results.Ok(new { ok = true }));

Swagger/OpenAPI:

csharp
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseSwagger(); app.UseSwaggerUI();

✅ Summary: Test + Swagger ile güven ve keşfedilebilirlik artar.
(Test ve dokümantasyon önemlidir.)


14.1) Middleware Pipeline Detayli

Request Pipeline Akışı

ASP.NET Core'da her HTTP isteği bir middleware zincirinden geçer. Her middleware isteği işleyip bir sonrakine aktarabilir veya kısa devre yaparak yanıt dönebilir:

İstek (Request)
    |
    v
[1. UseExceptionHandler]   -- Hata yakalama (en dışta olmalı)
    |
    v
[2. UseHsts]               -- HSTS header
    |
    v
[3. UseHttpsRedirection]   -- HTTP -> HTTPS yönlendirme
    |
    v
[4. UseStaticFiles]        -- wwwroot altındaki statik dosyalar
    |
    v
[5. UseRouting]            -- Endpoint eşleştirme başlar
    |
    v
[6. UseCors]               -- Cross-Origin Resource Sharing
    |
    v
[7. UseAuthentication]     -- Kim olduğunu belirle
    |
    v
[8. UseAuthorization]      -- Yetkisi var mı kontrol et
    |
    v
[9. UseRateLimiter]        -- İstek sınırlama
    |
    v
[10. MapControllers /      -- Endpoint çalıştırma
     MapMinimalApis]
    |
    v
Yanıt (Response) -- aynı zincirden geriye döner

Custom Middleware Yazma

Class-Based Middleware

csharp
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        // Request öncesi
        _logger.LogInformation("Request başladı: {Method} {Path}",
            context.Request.Method, context.Request.Path);

        await _next(context); // Sonraki middleware'e aktar

        // Response sonrası
        stopwatch.Stop();
        _logger.LogInformation("Request tamamlandı: {Method} {Path} -> {StatusCode} ({Elapsed}ms)",
            context.Request.Method,
            context.Request.Path,
            context.Response.StatusCode,
            stopwatch.ElapsedMilliseconds);
    }
}

// Extension method ile temiz kayıt
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
        => builder.UseMiddleware<RequestTimingMiddleware>();
}

// Kullanım
app.UseRequestTiming();

Inline Middleware

csharp
// Basit senaryolar için app.Use ile inline middleware
app.Use(async (context, next) =>
{
    // Request header ekle
    context.Response.Headers.Append("X-Request-Id", Guid.NewGuid().ToString());
    await next(context);
});

// Kısa devre middleware (belirli koşulda yanıt dön)
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/maintenance")
    {
        context.Response.StatusCode = 503;
        await context.Response.WriteAsync("Bakım modu aktif");
        return; // Zinciri kır, sonraki middleware'e geçme
    }
    await next(context);
});

Exception Handling Middleware

csharp
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "İşlenmeyen hata: {Message}", ex.Message);
            await HandleExceptionAsync(context, ex);
        }
    }

    private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";

        var (statusCode, message) = exception switch
        {
            KeyNotFoundException => (StatusCodes.Status404NotFound, "Kayıt bulunamadı"),
            UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Yetkisiz erişim"),
            ArgumentException => (StatusCodes.Status400BadRequest, exception.Message),
            _ => (StatusCodes.Status500InternalServerError, "Sunucu hatası oluştu")
        };

        context.Response.StatusCode = statusCode;

        var response = new
        {
            status = statusCode,
            message,
            traceId = context.TraceIdentifier
        };

        await context.Response.WriteAsJsonAsync(response);
    }
}

// Program.cs'de en başa ekle
app.UseMiddleware<GlobalExceptionMiddleware>();

Request Logging Middleware

csharp
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Correlation ID ekle (distributed tracing için)
        var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                            ?? Guid.NewGuid().ToString();
        context.Items["CorrelationId"] = correlationId;
        context.Response.Headers.Append("X-Correlation-Id", correlationId);

        _logger.LogInformation(
            "[{CorrelationId}] {Method} {Path}{Query} - User: {User}",
            correlationId,
            context.Request.Method,
            context.Request.Path,
            context.Request.QueryString,
            context.User.Identity?.Name ?? "anonymous");

        await _next(context);
    }
}

CORS Middleware Konfigürasyonu

csharp
// Named policy ile CORS
builder.Services.AddCors(options =>
{
    // Geliştirme ortamı -- geniş izin
    options.AddPolicy("Development", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });

    // Üretim ortamı -- sadece belirli origin'ler
    options.AddPolicy("Production", policy =>
    {
        policy.WithOrigins(
                "https://myapp.com",
                "https://admin.myapp.com")
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Content-Type", "Authorization")
              .AllowCredentials() // Cookie/Auth header gönderimi için
              .SetPreflightMaxAge(TimeSpan.FromMinutes(10));
    });
});

// Ortama göre policy seç
if (app.Environment.IsDevelopment())
    app.UseCors("Development");
else
    app.UseCors("Production");

// Controller seviyesinde CORS override
[EnableCors("Production")]
[ApiController]
public class PublicApiController : ControllerBase { }

[DisableCors]
[ApiController]
public class InternalApiController : ControllerBase { }

Middleware Sıralaması Neden Önemli

Middleware sırası kritiktir. Yanlış sıralama güvenlik açığı veya beklenmeyen davranışlara yol açar:

csharp
var app = builder.Build();

// 1. Exception handler en dışta -- tüm hataları yakalar
app.UseExceptionHandler("/error");

// 2. HSTS (sadece production)
if (!app.Environment.IsDevelopment())
    app.UseHsts();

// 3. HTTPS yönlendirme
app.UseHttpsRedirection();

// 4. Statik dosyalar (auth gerektirmez, routing öncesi)
app.UseStaticFiles();

// 5. Routing -- hangi endpoint'e gidileceğini belirler
app.UseRouting();

// 6. CORS -- UseRouting'den sonra, UseAuth'dan önce
app.UseCors();

// 7. Authentication -- "Kim bu?"
app.UseAuthentication();

// 8. Authorization -- "Yetkisi var mı?"
app.UseAuthorization();

// 9. Rate limiting
app.UseRateLimiter();

// 10. Custom middleware'ler
app.UseRequestTiming();

// 11. Endpoint'leri map et
app.MapControllers();

app.Run();

Sıralama hataları:

  • UseAuthentication sonra UseRouting yazarsan: Auth çalışmaz
  • UseCors sonra UseStaticFiles yazarsan: Statik dosyalara CORS uygulanmaz
  • UseExceptionHandler en başta değilse: Önceki middleware'lerin hataları yakalanmaz

✅ Summary: Middleware sıralaması güvenlik ve işleyiş için kritiktir. Custom middleware ile cross-cutting concern'leri merkezileştir.


14.2) Minimal API Detayli

Endpoint Mapping

Minimal API, controller kullanmadan doğrudan endpoint tanımlamayı sağlar:

csharp
var app = builder.Build();

// GET
app.MapGet("/api/products", async (AppDbContext db) =>
    await db.Products.AsNoTracking().ToListAsync());

// GET by ID
app.MapGet("/api/products/{id:int}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound(new { message = "Ürün bulunamadı" }));

// POST
app.MapPost("/api/products", async (CreateProductDto dto, AppDbContext db) =>
{
    var product = new Product { Name = dto.Name, Price = dto.Price };
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Results.Created($"/api/products/{product.Id}", product);
});

// PUT
app.MapPut("/api/products/{id:int}", async (int id, UpdateProductDto dto, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product is null) return Results.NotFound();

    product.Name = dto.Name;
    product.Price = dto.Price;
    await db.SaveChangesAsync();
    return Results.Ok(product);
});

// DELETE
app.MapDelete("/api/products/{id:int}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product is null) return Results.NotFound();

    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return Results.NoContent();
});

Route Groups (MapGroup)

Ortak prefix ve filtre paylaşan endpoint'leri grupla:

csharp
var api = app.MapGroup("/api");

// /api/products grubu
var products = api.MapGroup("/products")
    .WithTags("Products") // Swagger gruplandırma
    .RequireAuthorization();

products.MapGet("/", GetAllProducts);
products.MapGet("/{id:int}", GetProductById);
products.MapPost("/", CreateProduct);
products.MapPut("/{id:int}", UpdateProduct);
products.MapDelete("/{id:int}", DeleteProduct);

// /api/admin grubu -- ekstra yetki
var admin = api.MapGroup("/admin")
    .WithTags("Admin")
    .RequireAuthorization("AdminPolicy");

admin.MapGet("/stats", GetStats);
admin.MapPost("/clear-cache", ClearCache);

// Handler metotları ayrı static veya instance metot olarak tanımlanabilir
static async Task<IResult> GetAllProducts(AppDbContext db)
    => Results.Ok(await db.Products.AsNoTracking().ToListAsync());

static async Task<IResult> GetProductById(int id, AppDbContext db)
    => await db.Products.FindAsync(id) is Product p
        ? Results.Ok(p)
        : Results.NotFound();

Endpoint Filters

Controller'daki Action Filter'ların Minimal API karşılığı:

csharp
// Inline filter
app.MapPost("/api/products", CreateProduct)
    .AddEndpointFilter(async (context, next) =>
    {
        var dto = context.GetArgument<CreateProductDto>(0);
        if (string.IsNullOrWhiteSpace(dto.Name))
            return Results.BadRequest(new { error = "Ürün adı boş olamaz" });

        return await next(context);
    });

// Class-based filter
public class ValidationFilter<T> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var validator = context.HttpContext.RequestServices
            .GetService<IValidator<T>>();

        if (validator is not null)
        {
            var dto = context.GetArgument<T>(0);
            if (dto is not null)
            {
                var result = await validator.ValidateAsync(dto);
                if (!result.IsValid)
                    return Results.BadRequest(result.Errors
                        .Select(e => new { e.PropertyName, e.ErrorMessage }));
            }
        }

        return await next(context);
    }
}

// Filter kullanımı
app.MapPost("/api/products", CreateProduct)
    .AddEndpointFilter<ValidationFilter<CreateProductDto>>();

// Logging filter
public class LoggingFilter : IEndpointFilter
{
    private readonly ILogger<LoggingFilter> _logger;

    public LoggingFilter(ILogger<LoggingFilter> logger) => _logger = logger;

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        _logger.LogInformation("Endpoint çağrıldı: {Method} {Path}",
            context.HttpContext.Request.Method,
            context.HttpContext.Request.Path);

        var result = await next(context);

        _logger.LogInformation("Endpoint tamamlandı: {StatusCode}",
            context.HttpContext.Response.StatusCode);

        return result;
    }
}

TypedResults

TypedResults ile derleme zamanında tip güvenliği ve Swagger/OpenAPI desteği sağlanır:

csharp
// Results (runtime) vs TypedResults (compile-time)
// TypedResults Swagger'da dönüş tipini otomatik belirler

app.MapGet("/api/products/{id:int}", async Task<Results<Ok<Product>, NotFound>> (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    return product is not null
        ? TypedResults.Ok(product)
        : TypedResults.NotFound();
});

app.MapPost("/api/products", async Task<Results<Created<Product>, BadRequest<string>>> (
    CreateProductDto dto, AppDbContext db) =>
{
    if (string.IsNullOrWhiteSpace(dto.Name))
        return TypedResults.BadRequest("Ürün adı zorunludur");

    var product = new Product { Name = dto.Name, Price = dto.Price };
    db.Products.Add(product);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/api/products/{product.Id}", product);
});

// Yaygın TypedResults metotları:
// TypedResults.Ok(value)           -- 200
// TypedResults.Created(uri, value) -- 201
// TypedResults.NoContent()         -- 204
// TypedResults.BadRequest(error)   -- 400
// TypedResults.NotFound()          -- 404
// TypedResults.Unauthorized()      -- 401
// TypedResults.Forbid()            -- 403
// TypedResults.Conflict(error)     -- 409

Minimal API vs Controller Karşılaştırma

ÖzellikMinimal APIController
Kod miktarıAz, tek dosyadaDaha fazla, ayrı sınıflar
PerformansMarjinal olarak daha hızlıÇok yakın
Swagger desteğiTypedResults ile iyiOtomatik, tam destek
Model validationManuel veya filter ileOtomatik (ApiController)
FiltersEndpoint filtersAction/Result/Exception filters
RoutingInline tanımAttribute routing
Test edilebilirlikHandler fonksiyon testiController testi
Büyük projelerRoute groups ile yönetilebilirDoğal organizasyon
Önerilen boyutMikro servis, küçük APIOrta-büyük projeler
Öğrenme eğrisiDüşükOrta

✅ Summary: Minimal API küçük/orta projeler için hızlı başlangıç sağlar. Büyük projelerde Controller daha iyi organize edilir.


15) Deployment: Kestrel, Nginx/Apache, IIS

Build & publish:

bash
dotnet publish -c Release -o out

Linux service (Kestrel + Nginx reverse proxy):

nginx
server {
  listen 80;
  server_name example.com;
  location / {
    proxy_pass         http://localhost:5000;
    proxy_http_version 1.1;
    proxy_set_header   Upgrade $http_upgrade;
    proxy_set_header   Connection keep-alive;
    proxy_set_header   Host $host;
    proxy_cache_bypass $http_upgrade;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
  }
}

Windows IIS: Web Deploy veya dotnet MyApp.dll ile IIS reverse proxy.
ENV: ASPNETCORE_ENVIRONMENT=Production

✅ Summary: Kestrel tek başına değil, reverse proxy ile servis et.
(Nginx/IIS önerilir.)


16) Docker & CI/CD (GitHub Actions)

Dockerfile (basit):

dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore && dotnet publish -c Release -o /out
FROM base AS final
WORKDIR /app
COPY --from=build /out .
ENTRYPOINT ["dotnet","MyApp.dll"]

GitHub Actions (örnek):

yaml
name: Build & Test
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with: { dotnet-version: '8.0.x' }
      - run: dotnet restore
      - run: dotnet build --no-restore -c Release
      - run: dotnet test --no-build --verbosity normal

✅ Summary: Container + CI ile tekrar üretilebilir ve güvenilir dağıtım.
(Tekrarlanabilir build ve otomatize test.)


17) Security & Performance Checklist (Güvenlik ve Performans)

  • [ ] HTTPS + HSTS, reverse proxy doğru
  • [ ] ASPNETCORE_ENVIRONMENT=Production
  • [ ] Detailed errors off (prod), exception handler page on
  • [ ] Rate limiting & input validation (DataAnnotations/Fluent)
  • [ ] AuthN (Identity/JWT) + AuthZ (Policies/Roles/Claims)
  • [ ] Anti-forgery tokens for forms (MVC/Razor)
  • [ ] CORS: allow only required origins
  • [ ] EF Core: No tracking for read-only, AsSplitQuery for includes
  • [ ] Response caching + compression on
  • [ ] Logging levels tuned, PII masked
  • [ ] Secrets in Azure KeyVault/Environment, not in repo
  • [ ] Health checks + monitoring (App Insights/Seq/Serilog)
  • [ ] GC Server mode (prod), thread pool tuned (advanced)
  • [ ] Load test before launch

17.1) Güvenlik Detayli

Identity Detayli (ApplicationUser, Role, Manager)

csharp
// Custom ApplicationUser -- IdentityUser'ı genişlet
public class ApplicationUser : IdentityUser
{
    public string FullName { get; set; } = "";
    public DateTime BirthDate { get; set; }
    public string? AvatarUrl { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

// Identity konfigürasyonu
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    // Şifre kuralları
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 8;

    // Hesap kilitleme
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // Kullanıcı ayarları
    options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

// UserManager ve RoleManager kullanımı
public class AccountService
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountService(
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager,
        SignInManager<ApplicationUser> signInManager)
    {
        _userManager = userManager;
        _roleManager = roleManager;
        _signInManager = signInManager;
    }

    public async Task<IdentityResult> RegisterAsync(RegisterDto dto)
    {
        var user = new ApplicationUser
        {
            UserName = dto.Email,
            Email = dto.Email,
            FullName = dto.FullName
        };

        var result = await _userManager.CreateAsync(user, dto.Password);
        if (result.Succeeded)
        {
            await _userManager.AddToRoleAsync(user, "User");
        }
        return result;
    }

    public async Task SeedRolesAsync()
    {
        string[] roles = { "Admin", "User", "Editor" };
        foreach (var role in roles)
        {
            if (!await _roleManager.RoleExistsAsync(role))
                await _roleManager.CreateAsync(new IdentityRole(role));
        }
    }
}

JWT Detayli (Token Service, Refresh Token, Revocation)

csharp
// Token servisi
public interface ITokenService
{
    string GenerateAccessToken(ApplicationUser user, IList<string> roles);
    RefreshToken GenerateRefreshToken();
}

public class TokenService : ITokenService
{
    private readonly IConfiguration _config;

    public TokenService(IConfiguration config) => _config = config;

    public string GenerateAccessToken(ApplicationUser user, IList<string> roles)
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id),
            new(ClaimTypes.Email, user.Email!),
            new(ClaimTypes.Name, user.FullName),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        // Rolleri claim olarak ekle
        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(15), // Kısa ömürlü
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public RefreshToken GenerateRefreshToken()
    {
        return new RefreshToken
        {
            Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)),
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            CreatedAt = DateTime.UtcNow
        };
    }
}

// Refresh token entity
public class RefreshToken
{
    public int Id { get; set; }
    public string Token { get; set; } = "";
    public DateTime ExpiresAt { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? RevokedAt { get; set; }
    public bool IsRevoked => RevokedAt.HasValue;
    public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
    public bool IsActive => !IsRevoked && !IsExpired;

    public string UserId { get; set; } = "";
    public ApplicationUser User { get; set; } = null!;
}

// Login endpoint
app.MapPost("/api/auth/login", async (
    LoginDto dto,
    UserManager<ApplicationUser> userManager,
    ITokenService tokenService,
    AppDbContext db) =>
{
    var user = await userManager.FindByEmailAsync(dto.Email);
    if (user is null || !await userManager.CheckPasswordAsync(user, dto.Password))
        return Results.Unauthorized();

    var roles = await userManager.GetRolesAsync(user);
    var accessToken = tokenService.GenerateAccessToken(user, roles);
    var refreshToken = tokenService.GenerateRefreshToken();

    // Refresh token'ı DB'ye kaydet
    refreshToken.UserId = user.Id;
    db.RefreshTokens.Add(refreshToken);
    await db.SaveChangesAsync();

    return Results.Ok(new
    {
        accessToken,
        refreshToken = refreshToken.Token,
        expiresIn = 900 // 15 dakika (saniye)
    });
});

// Refresh endpoint
app.MapPost("/api/auth/refresh", async (
    RefreshRequestDto dto,
    AppDbContext db,
    UserManager<ApplicationUser> userManager,
    ITokenService tokenService) =>
{
    var storedToken = await db.RefreshTokens
        .Include(r => r.User)
        .FirstOrDefaultAsync(r => r.Token == dto.RefreshToken);

    if (storedToken is null || !storedToken.IsActive)
        return Results.Unauthorized();

    // Eski token'ı revoke et
    storedToken.RevokedAt = DateTime.UtcNow;

    // Yeni token'lar üret
    var roles = await userManager.GetRolesAsync(storedToken.User);
    var newAccessToken = tokenService.GenerateAccessToken(storedToken.User, roles);
    var newRefreshToken = tokenService.GenerateRefreshToken();
    newRefreshToken.UserId = storedToken.UserId;
    db.RefreshTokens.Add(newRefreshToken);

    await db.SaveChangesAsync();

    return Results.Ok(new
    {
        accessToken = newAccessToken,
        refreshToken = newRefreshToken.Token
    });
});

CSRF/XSRF Korunma

csharp
// MVC/Razor Pages -- otomatik anti-forgery token
// Form Tag Helper otomatik ekler:
// <form asp-action="Create" method="post">
//   <!-- __RequestVerificationToken otomatik eklenir -->
// </form>

// Global olarak tüm POST/PUT/DELETE'lere uygula
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

// API'lerde (SPA ile çalışırken) -- Cookie-to-header pattern
builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-XSRF-TOKEN"; // SPA bu header'ı gönderir
    options.Cookie.Name = "XSRF-TOKEN";
    options.Cookie.HttpOnly = false; // JS'nin okuyabilmesi için
});

app.Use(async (context, next) =>
{
    var antiforgery = context.RequestServices.GetRequiredService<IAntiforgery>();
    var tokens = antiforgery.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
        new CookieOptions { HttpOnly = false, Secure = true, SameSite = SameSiteMode.Strict });
    await next(context);
});

SQL Injection Korunma

csharp
// EF Core parametreli sorgu kullanır -- güvenli
var products = await _db.Products
    .Where(p => p.Name.Contains(searchTerm))
    .ToListAsync();

// FromSqlInterpolated -- parametreler otomatik escape edilir -- güvenli
var products = await _db.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Name LIKE {'%' + searchTerm + '%'}")
    .ToListAsync();

// TEHLIKELI -- string concatenation ile raw SQL kullanma
// var products = await _db.Products
//     .FromSqlRaw($"SELECT * FROM Products WHERE Name = '{userInput}'") // SQL INJECTION!
//     .ToListAsync();

// GUVENLI -- parametre ile raw SQL
var products = await _db.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Name = {0}", userInput)
    .ToListAsync();

XSS Korunma

csharp
// Razor otomatik HTML encoding yapar -- güvenli
// @Model.Title  -->  &lt;script&gt;alert('xss')&lt;/script&gt;

// @Html.Raw() kullanma -- XSS riski!
// @Html.Raw(Model.Content) // Tehlikeli

// Manuel encoding gerektiğinde
public class CommentService
{
    private readonly HtmlEncoder _htmlEncoder;

    public CommentService(HtmlEncoder htmlEncoder) => _htmlEncoder = htmlEncoder;

    public string SanitizeInput(string input)
        => _htmlEncoder.Encode(input);
}

// Content Security Policy header ekle
app.Use(async (context, next) =>
{
    context.Response.Headers.Append(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
    await next(context);
});

Rate Limiting (.NET 7+ Built-in)

csharp
// Rate limiting konfigürasyonu
builder.Services.AddRateLimiter(options =>
{
    // Fixed window -- sabit zaman penceresinde istek sınırı
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 100;        // Pencere başına max istek
        opt.Window = TimeSpan.FromMinutes(1);
        opt.QueueLimit = 0;           // Kuyruk yok, direkt reddet
    });

    // Sliding window -- kayan pencere
    options.AddSlidingWindowLimiter("sliding", opt =>
    {
        opt.PermitLimit = 60;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.SegmentsPerWindow = 6;    // 10 saniyelik segment'ler
    });

    // Token bucket -- yavaş dolum, burst izni
    options.AddTokenBucketLimiter("token", opt =>
    {
        opt.TokenLimit = 100;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        opt.TokensPerPeriod = 10;
    });

    // Global rate limit aşıldığında
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            message = "İstek limiti aşıldı. Lütfen bekleyin.",
            retryAfter = context.Lease.TryGetMetadata(
                MetadataName.RetryAfter, out var retryAfter)
                ? retryAfter.TotalSeconds : 60
        }, token);
    };
});

app.UseRateLimiter();

// Endpoint seviyesinde kullanım
app.MapGet("/api/products", GetProducts)
    .RequireRateLimiting("fixed");

// Controller seviyesinde
[EnableRateLimiting("sliding")]
[ApiController]
public class ApiController : ControllerBase { }

// Belirli endpoint'i rate limiting'den muaf tut
[DisableRateLimiting]
public IActionResult HealthCheck() => Ok();

Data Protection API

csharp
// Data Protection kayıt
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo("/keys"))
    .SetApplicationName("MyApp")
    .SetDefaultKeyLifetime(TimeSpan.FromDays(90));

// Kullanım -- hassas veriyi şifrele/çöz
public class SecureService
{
    private readonly IDataProtector _protector;

    public SecureService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("SecureService.v1");
    }

    public string Encrypt(string plainText) => _protector.Protect(plainText);
    public string Decrypt(string cipherText) => _protector.Unprotect(cipherText);
}

// Time-limited protection (süreli token)
public class TokenService
{
    private readonly ITimeLimitedDataProtector _protector;

    public TokenService(IDataProtectionProvider provider)
    {
        _protector = provider
            .CreateProtector("TokenService")
            .ToTimeLimitedDataProtector();
    }

    public string CreateToken(string userId)
        => _protector.Protect(userId, TimeSpan.FromHours(24));

    public string? ValidateToken(string token)
    {
        try { return _protector.Unprotect(token); }
        catch { return null; }
    }
}

Secret Management

csharp
// Development -- User Secrets
// dotnet user-secrets init
// dotnet user-secrets set "Jwt:Key" "super-secret-key-12345"
// dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost;..."

// appsettings.json'da placeholder
// {
//   "Jwt": {
//     "Key": "" // user-secrets veya env variable'dan gelir
//   }
// }

// Production -- Environment Variables
// ASPNETCORE_ENVIRONMENT=Production
// ConnectionStrings__Default=Server=prod-server;...
// Jwt__Key=production-secret-key

// Azure Key Vault entegrasyonu
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVault:Name"]}.vault.azure.net/"),
    new DefaultAzureCredential());

// Konfigürasyon öncelik sırası (sonraki öncekini override eder):
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User secrets (Development)
// 4. Environment variables
// 5. Command-line arguments
// 6. Azure Key Vault

✅ Summary: Katmanlı güvenlik stratejisi uygula -- Identity, JWT, CORS, rate limiting, input validation ve secret management birlikte kullan.


18) Troubleshooting & Recipes (Sorun Giderme)

BelirtiSebepÇözüm
404 on MVC routeendpoint map edilmediapp.MapControllerRoute(...) kontrol
CORS erroryanlış originWithOrigins() veya cors policy
500 genericdetay kapalıdev'de ayrıntı, prod'da UseExceptionHandler
EF timeoutyavaş sorguindex ekle, AsNoTracking, Include optimize
JWT 401clock skew / imzaIssuer/Audience/Key, ClockSkew ayarı
SSL behind proxywrong schemeUseForwardedHeaders middleware

19) Appendix: CLI, Snippets & Templates

CLI (dotnet) en sık:

bash
dotnet new mvc -n WebApp
dotnet new webapi -n Api
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet ef migrations add Init && dotnet ef database update
dotnet user-secrets init && dotnet user-secrets set "Smtp:Key" "value"
dotnet publish -c Release -o out

Minimal API + EF sample:

csharp
app.MapGet("/api/posts", async (AppDb db) => await db.Posts.ToListAsync());
app.MapPost("/api/posts", async (AppDb db, PostDto dto) => {
  var p = new Post { Title = dto.Title, Body = dto.Body };
  db.Posts.Add(p); await db.SaveChangesAsync(); return Results.Created($"/api/posts/{p.Id}", p);
});

Global using (C# 10+): GlobalUsings.cs ile sık using'leri tek yerde topla.
Records & DTOs: public record PostDto(string Title, string Body);


20) Clean Architecture / Proje Yapısı

Katmanli Mimari

Clean Architecture, bağımlılıkların içeriden dışarıya doğru aktığı bir yapıdır. Dış katmanlar iç katmanlara bağımlıdır, iç katmanlar dış katmanları bilmez.

┌─────────────────────────────────────────────────────┐
│  Presentation (Controllers, Middleware, Filters)    │
│  ┌─────────────────────────────────────────────┐    │
│  │  Infrastructure (DbContext, Repos, External) │   │
│  │  ┌─────────────────────────────────────┐     │   │
│  │  │  Application (DTOs, Services, CQRS) │     │   │
│  │  │  ┌─────────────────────────────┐    │     │   │
│  │  │  │  Domain (Entities, Enums)   │    │     │   │
│  │  │  └─────────────────────────────┘    │     │   │
│  │  └─────────────────────────────────────┘     │   │
│  └─────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────┘

Domain Katmani

İş kurallarını ve entity'leri içerir. Hiçbir dış bağımlılığı yoktur:

csharp
// Entity
public class Product
{
    public int Id { get; set; }
    public string Name { get; private set; } = "";
    public decimal Price { get; private set; }
    public int Stock { get; private set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; } = null!;
    public bool IsActive { get; set; } = true;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    // İş kuralları entity içinde
    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice <= 0)
            throw new ArgumentException("Fiyat sıfırdan büyük olmalı");
        Price = newPrice;
    }

    public void DecreaseStock(int quantity)
    {
        if (quantity > Stock)
            throw new InvalidOperationException("Yetersiz stok");
        Stock -= quantity;
    }
}

// Value Object
public record Address(string Street, string City, string ZipCode, string Country);

public record Money(decimal Amount, string Currency)
{
    public static Money TRY(decimal amount) => new(amount, "TRY");
    public static Money USD(decimal amount) => new(amount, "USD");
}

// Enum
public enum OrderStatus
{
    Pending = 0,
    Confirmed = 1,
    Shipped = 2,
    Delivered = 3,
    Cancelled = 4
}

Application Katmani

DTO'lar, interface'ler, servis tanımları ve validasyon kurallarını içerir:

csharp
// DTO'lar
public record CreateProductDto(string Name, decimal Price, int Stock, int CategoryId);
public record UpdateProductDto(string Name, decimal Price);
public record ProductResponseDto(int Id, string Name, decimal Price, int Stock, string CategoryName);

// Interface
public interface IProductRepository
{
    Task<List<Product>> GetAllAsync(CancellationToken ct = default);
    Task<Product?> GetByIdAsync(int id, CancellationToken ct = default);
    Task AddAsync(Product product, CancellationToken ct = default);
    Task UpdateAsync(Product product, CancellationToken ct = default);
    Task DeleteAsync(int id, CancellationToken ct = default);
}

public interface IProductService
{
    Task<List<ProductResponseDto>> GetAllAsync(CancellationToken ct = default);
    Task<ProductResponseDto?> GetByIdAsync(int id, CancellationToken ct = default);
    Task<ProductResponseDto> CreateAsync(CreateProductDto dto, CancellationToken ct = default);
    Task<ProductResponseDto?> UpdateAsync(int id, UpdateProductDto dto, CancellationToken ct = default);
    Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}

// Validator
public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
    public CreateProductDtoValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Ürün adı zorunludur")
            .MaximumLength(200).WithMessage("Ürün adı en fazla 200 karakter");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Fiyat sıfırdan büyük olmalı");

        RuleFor(x => x.Stock)
            .GreaterThanOrEqualTo(0).WithMessage("Stok negatif olamaz");

        RuleFor(x => x.CategoryId)
            .GreaterThan(0).WithMessage("Geçerli bir kategori seçiniz");
    }
}

Infrastructure Katmani

Veritabanı, external servisler ve repository implementasyonlarını içerir:

csharp
// DbContext
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}

// Repository implementasyonu
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _db;

    public ProductRepository(AppDbContext db) => _db = db;

    public async Task<List<Product>> GetAllAsync(CancellationToken ct)
        => await _db.Products
            .Include(p => p.Category)
            .AsNoTracking()
            .ToListAsync(ct);

    public async Task<Product?> GetByIdAsync(int id, CancellationToken ct)
        => await _db.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id, ct);

    public async Task AddAsync(Product product, CancellationToken ct)
    {
        await _db.Products.AddAsync(product, ct);
        await _db.SaveChangesAsync(ct);
    }

    public async Task UpdateAsync(Product product, CancellationToken ct)
    {
        _db.Products.Update(product);
        await _db.SaveChangesAsync(ct);
    }

    public async Task DeleteAsync(int id, CancellationToken ct)
    {
        var product = await _db.Products.FindAsync(new object[] { id }, ct);
        if (product is not null)
        {
            _db.Products.Remove(product);
            await _db.SaveChangesAsync(ct);
        }
    }
}

Presentation Katmani

Controller'lar ve HTTP ile ilgili kodları içerir:

csharp
// Service implementasyonu (Application katmanında olabilir)
public class ProductService : IProductService
{
    private readonly IProductRepository _repo;

    public ProductService(IProductRepository repo) => _repo = repo;

    public async Task<List<ProductResponseDto>> GetAllAsync(CancellationToken ct)
    {
        var products = await _repo.GetAllAsync(ct);
        return products.Select(p => new ProductResponseDto(
            p.Id, p.Name, p.Price, p.Stock, p.Category.Name)).ToList();
    }

    public async Task<ProductResponseDto?> GetByIdAsync(int id, CancellationToken ct)
    {
        var product = await _repo.GetByIdAsync(id, ct);
        if (product is null) return null;
        return new ProductResponseDto(
            product.Id, product.Name, product.Price, product.Stock, product.Category.Name);
    }

    public async Task<ProductResponseDto> CreateAsync(CreateProductDto dto, CancellationToken ct)
    {
        var product = new Product
        {
            Name = dto.Name,
            Price = dto.Price,
            Stock = dto.Stock,
            CategoryId = dto.CategoryId
        };
        await _repo.AddAsync(product, ct);
        var created = await _repo.GetByIdAsync(product.Id, ct);
        return new ProductResponseDto(
            created!.Id, created.Name, created.Price, created.Stock, created.Category.Name);
    }

    public async Task<ProductResponseDto?> UpdateAsync(int id, UpdateProductDto dto, CancellationToken ct)
    {
        var product = await _repo.GetByIdAsync(id, ct);
        if (product is null) return null;
        product.UpdatePrice(dto.Price);
        await _repo.UpdateAsync(product, ct);
        return new ProductResponseDto(
            product.Id, product.Name, product.Price, product.Stock, product.Category.Name);
    }

    public async Task<bool> DeleteAsync(int id, CancellationToken ct)
    {
        var product = await _repo.GetByIdAsync(id, ct);
        if (product is null) return false;
        await _repo.DeleteAsync(id, ct);
        return true;
    }
}

// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;

    public ProductsController(IProductService service) => _service = service;

    [HttpGet]
    public async Task<IActionResult> GetAll(CancellationToken ct)
        => Ok(await _service.GetAllAsync(ct));

    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id, CancellationToken ct)
    {
        var product = await _service.GetByIdAsync(id, ct);
        return product is not null ? Ok(product) : NotFound();
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductDto dto, CancellationToken ct)
    {
        var product = await _service.CreateAsync(dto, ct);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }

    [HttpPut("{id:int}")]
    public async Task<IActionResult> Update(int id, UpdateProductDto dto, CancellationToken ct)
    {
        var product = await _service.UpdateAsync(id, dto, ct);
        return product is not null ? Ok(product) : NotFound();
    }

    [HttpDelete("{id:int}")]
    public async Task<IActionResult> Delete(int id, CancellationToken ct)
    {
        var result = await _service.DeleteAsync(id, ct);
        return result ? NoContent() : NotFound();
    }
}

Klasör Yapısı

MyApp/
├── src/
│   ├── MyApp.Domain/                   # Domain katmanı
│   │   ├── Entities/
│   │   │   ├── Product.cs
│   │   │   ├── Category.cs
│   │   │   └── Order.cs
│   │   ├── ValueObjects/
│   │   │   ├── Address.cs
│   │   │   └── Money.cs
│   │   ├── Enums/
│   │   │   └── OrderStatus.cs
│   │   └── MyApp.Domain.csproj
│   │
│   ├── MyApp.Application/              # Application katmanı
│   │   ├── DTOs/
│   │   │   ├── CreateProductDto.cs
│   │   │   ├── UpdateProductDto.cs
│   │   │   └── ProductResponseDto.cs
│   │   ├── Interfaces/
│   │   │   ├── IProductRepository.cs
│   │   │   └── IProductService.cs
│   │   ├── Services/
│   │   │   └── ProductService.cs
│   │   ├── Validators/
│   │   │   └── CreateProductDtoValidator.cs
│   │   └── MyApp.Application.csproj
│   │
│   ├── MyApp.Infrastructure/           # Infrastructure katmanı
│   │   ├── Data/
│   │   │   ├── AppDbContext.cs
│   │   │   ├── Configurations/
│   │   │   │   └── ProductConfiguration.cs
│   │   │   └── Migrations/
│   │   ├── Repositories/
│   │   │   └── ProductRepository.cs
│   │   ├── ExternalServices/
│   │   │   └── EmailService.cs
│   │   └── MyApp.Infrastructure.csproj
│   │
│   └── MyApp.Api/                      # Presentation katmanı
│       ├── Controllers/
│       │   └── ProductsController.cs
│       ├── Filters/
│       │   └── ValidationFilter.cs
│       ├── Middleware/
│       │   └── GlobalExceptionMiddleware.cs
│       ├── Program.cs
│       ├── appsettings.json
│       └── MyApp.Api.csproj

├── tests/
│   ├── MyApp.UnitTests/
│   │   └── MyApp.UnitTests.csproj
│   └── MyApp.IntegrationTests/
│       └── MyApp.IntegrationTests.csproj

└── MyApp.sln

DI Kayıtları (Program.cs)

csharp
// Program.cs -- tüm katmanların DI kaydı
var builder = WebApplication.CreateBuilder(args);

// Infrastructure
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// Application
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductDtoValidator>();

// Presentation
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

✅ Summary: Clean Architecture ile katmanlar arası bağımlılığı tersine çevir, test edilebilir ve bakımı kolay yapı kur.


21) C#/.NET Ekosistem Paketleri

AutoMapper / Mapster (DTO Mapping)

ÖzellikAutoMapperMapster
PerformansYavaş (reflection)Hızlı (code generation)
KonfigürasyonProfile sınıfları ileInline veya config ile
PopülerlikDaha yaygınHızla büyüyor
KarmaşıklıkOrtaDüşük
ÖnerilenBüyük, karmaşık mappingPerformans kritik projeler
csharp
// AutoMapper
// dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
public class ProductProfile : Profile
{
    public ProductProfile()
    {
        CreateMap<Product, ProductResponseDto>()
            .ForMember(dest => dest.CategoryName,
                       opt => opt.MapFrom(src => src.Category.Name));
        CreateMap<CreateProductDto, Product>();
    }
}

builder.Services.AddAutoMapper(typeof(ProductProfile));

// Kullanım
public class ProductService
{
    private readonly IMapper _mapper;
    public ProductService(IMapper mapper) => _mapper = mapper;

    public ProductResponseDto Map(Product product)
        => _mapper.Map<ProductResponseDto>(product);
}

// Mapster
// dotnet add package Mapster
// dotnet add package Mapster.DependencyInjection
var dto = product.Adapt<ProductResponseDto>(); // Tek satır mapping

// Mapster konfigürasyon
TypeAdapterConfig<Product, ProductResponseDto>.NewConfig()
    .Map(dest => dest.CategoryName, src => src.Category.Name);

FluentValidation Detayli

csharp
// dotnet add package FluentValidation.AspNetCore
public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
    private readonly AppDbContext _db;

    public CreateProductDtoValidator(AppDbContext db)
    {
        _db = db;

        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Ürün adı zorunludur")
            .MaximumLength(200).WithMessage("En fazla 200 karakter")
            .MustAsync(BeUniqueName).WithMessage("Bu isimde ürün zaten var");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Fiyat sıfırdan büyük olmalı")
            .LessThan(1_000_000).WithMessage("Fiyat çok yüksek");

        RuleFor(x => x.Stock)
            .InclusiveBetween(0, 99999);

        RuleFor(x => x.CategoryId)
            .MustAsync(CategoryExists).WithMessage("Kategori bulunamadı");
    }

    private async Task<bool> BeUniqueName(string name, CancellationToken ct)
        => !await _db.Products.AnyAsync(p => p.Name == name, ct);

    private async Task<bool> CategoryExists(int categoryId, CancellationToken ct)
        => await _db.Categories.AnyAsync(c => c.Id == categoryId, ct);
}

// Otomatik validasyon kaydı
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductDtoValidator>();

MediatR (CQRS, Mediator Pattern)

csharp
// dotnet add package MediatR
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

// Query (okuma)
public record GetProductByIdQuery(int Id) : IRequest<ProductResponseDto?>;

public class GetProductByIdHandler : IRequestHandler<GetProductByIdQuery, ProductResponseDto?>
{
    private readonly AppDbContext _db;

    public GetProductByIdHandler(AppDbContext db) => _db = db;

    public async Task<ProductResponseDto?> Handle(
        GetProductByIdQuery request, CancellationToken ct)
    {
        var product = await _db.Products
            .Include(p => p.Category)
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == request.Id, ct);

        if (product is null) return null;

        return new ProductResponseDto(
            product.Id, product.Name, product.Price, product.Stock, product.Category.Name);
    }
}

// Command (yazma)
public record CreateProductCommand(string Name, decimal Price, int Stock, int CategoryId)
    : IRequest<ProductResponseDto>;

public class CreateProductHandler : IRequestHandler<CreateProductCommand, ProductResponseDto>
{
    private readonly AppDbContext _db;

    public CreateProductHandler(AppDbContext db) => _db = db;

    public async Task<ProductResponseDto> Handle(
        CreateProductCommand request, CancellationToken ct)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            Stock = request.Stock,
            CategoryId = request.CategoryId
        };

        _db.Products.Add(product);
        await _db.SaveChangesAsync(ct);

        await _db.Entry(product).Reference(p => p.Category).LoadAsync(ct);

        return new ProductResponseDto(
            product.Id, product.Name, product.Price, product.Stock, product.Category.Name);
    }
}

// Notification (event)
public record ProductCreatedNotification(int ProductId, string ProductName) : INotification;

public class ProductCreatedEmailHandler : INotificationHandler<ProductCreatedNotification>
{
    private readonly IEmailService _email;

    public ProductCreatedEmailHandler(IEmailService email) => _email = email;

    public async Task Handle(ProductCreatedNotification notification, CancellationToken ct)
    {
        await _email.SendAsync("admin@app.com",
            $"Yeni ürün eklendi: {notification.ProductName}");
    }
}

// Controller'da kullanım
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProductsController(IMediator mediator) => _mediator = mediator;

    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id, CancellationToken ct)
    {
        var result = await _mediator.Send(new GetProductByIdQuery(id), ct);
        return result is not null ? Ok(result) : NotFound();
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductCommand command, CancellationToken ct)
    {
        var result = await _mediator.Send(command, ct);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
}

Hangfire (Background Jobs)

csharp
// dotnet add package Hangfire
// dotnet add package Hangfire.SqlServer (veya Hangfire.PostgreSql)
builder.Services.AddHangfire(config =>
    config.UseSqlServerStorage(builder.Configuration.GetConnectionString("Hangfire")));
builder.Services.AddHangfireServer();

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new HangfireAuthFilter() }
});

// Fire-and-forget job
BackgroundJob.Enqueue(() => Console.WriteLine("Tek seferlik iş"));

// Delayed job
BackgroundJob.Schedule(() => SendReminderEmail(userId), TimeSpan.FromHours(24));

// Recurring job (cron)
RecurringJob.AddOrUpdate("daily-report",
    () => reportService.GenerateDailyReport(), Cron.Daily);

RecurringJob.AddOrUpdate("cleanup",
    () => cleanupService.RemoveExpiredTokens(), "0 3 * * *"); // Her gece saat 3

// Continuation job
var parentId = BackgroundJob.Enqueue(() => BuildReport());
BackgroundJob.ContinueJobWith(parentId, () => SendReport());

// Dashboard auth filter
public class HangfireAuthFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();
        return httpContext.User.IsInRole("Admin");
    }
}

SignalR (Real-Time)

csharp
// dotnet add package Microsoft.AspNetCore.SignalR
// Hub tanımı
public class NotificationHub : Hub
{
    public async Task SendMessage(string user, string message)
        => await Clients.All.SendAsync("ReceiveMessage", user, message);

    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).SendAsync("UserJoined", Context.User?.Identity?.Name);
    }

    public async Task SendToGroup(string groupName, string message)
        => await Clients.Group(groupName).SendAsync("ReceiveMessage", message);

    public override async Task OnConnectedAsync()
    {
        await Clients.Caller.SendAsync("Connected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await base.OnDisconnectedAsync(exception);
    }
}

// Program.cs
builder.Services.AddSignalR();
app.MapHub<NotificationHub>("/hubs/notification");

// Controller'dan hub'a mesaj gönderme
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly IHubContext<NotificationHub> _hub;

    public OrdersController(IHubContext<NotificationHub> hub) => _hub = hub;

    [HttpPost]
    public async Task<IActionResult> Create(OrderDto dto)
    {
        // Sipariş oluştur...
        await _hub.Clients.All.SendAsync("OrderCreated", new { orderId = 1 });
        return Ok();
    }
}

Swagger / Swashbuckle Detayli

csharp
// dotnet add package Swashbuckle.AspNetCore
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "MyApp API",
        Version = "v1",
        Description = "Ürün yönetim API dokümantasyonu",
        Contact = new OpenApiContact
        {
            Name = "Geliştirici",
            Email = "dev@myapp.com"
        }
    });

    // JWT auth için Swagger UI
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT token giriniz: Bearer {token}",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });

    // XML comments
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    if (File.Exists(xmlPath))
        options.IncludeXmlComments(xmlPath);
});

Serilog + Seq (Structured Logging)

csharp
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Seq
// dotnet add package Serilog.Enrichers.Environment
// dotnet add package Serilog.Enrichers.Thread

// Program.cs
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithEnvironmentName()
    .Enrich.WithThreadId()
    .Enrich.WithProperty("Application", "MyApp")
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}")
    .WriteTo.Seq("http://localhost:5341") // Seq server
    .WriteTo.File("logs/log-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30)
    .CreateLogger();

builder.Host.UseSerilog();

// Kullanım
public class OrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger) => _logger = logger;

    public async Task<Order> CreateOrder(OrderDto dto)
    {
        // Structured logging -- alanlar ayrı property olarak kaydedilir
        _logger.LogInformation(
            "Sipariş oluşturuluyor: {@OrderDto}, Kullanıcı: {UserId}",
            dto, dto.UserId);

        try
        {
            var order = new Order { /* ... */ };
            _logger.LogInformation("Sipariş oluşturuldu: {OrderId}", order.Id);
            return order;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Sipariş oluşturma hatası: {@OrderDto}", dto);
            throw;
        }
    }
}

// Request logging middleware (Serilog built-in)
app.UseSerilogRequestLogging(options =>
{
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("UserId",
            httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous");
    };
});

Polly (Retry, Circuit Breaker, Timeout)

csharp
// dotnet add package Microsoft.Extensions.Http.Polly
// HttpClient ile Polly entegrasyonu
builder.Services.AddHttpClient("ExternalApi", client =>
{
    client.BaseAddress = new Uri("https://api.external.com");
    client.Timeout = TimeSpan.FromSeconds(30);
})
// Retry policy -- geçici hatalarda yeniden dene
.AddTransientHttpErrorPolicy(policy =>
    policy.WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
        onRetry: (outcome, timespan, retryCount, context) =>
        {
            Console.WriteLine($"Retry {retryCount} - {timespan.TotalSeconds}s bekleniyor");
        }))
// Circuit breaker -- sürekli hata varsa devre dışı bırak
.AddTransientHttpErrorPolicy(policy =>
    policy.CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 5,
        durationOfBreak: TimeSpan.FromSeconds(30)));

// Manuel Polly policy tanımı
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));

var circuitBreaker = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(
        exceptionsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromMinutes(1));

// Bulkhead (eşzamanlı istek sınırlama)
var bulkhead = Policy
    .BulkheadAsync<HttpResponseMessage>(
        maxParallelization: 10,
        maxQueuingActions: 20);

HealthChecks Detayli + UI

csharp
// dotnet add package AspNetCore.HealthChecks.SqlServer
// dotnet add package AspNetCore.HealthChecks.Redis
// dotnet add package AspNetCore.HealthChecks.UI
// dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database", tags: new[] { "db" })
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default")!,
        name: "sqlserver",
        tags: new[] { "db" })
    .AddRedis(
        builder.Configuration.GetConnectionString("Redis")!,
        name: "redis",
        tags: new[] { "cache" })
    .AddUrlGroup(new Uri("https://api.external.com/health"),
        name: "external-api",
        tags: new[] { "external" })
    .AddCheck("custom", () =>
    {
        var isHealthy = true; // Özel kontrol mantığı
        return isHealthy
            ? HealthCheckResult.Healthy("Sistem sağlıklı")
            : HealthCheckResult.Unhealthy("Sorun tespit edildi");
    });

// Health Check UI
builder.Services.AddHealthChecksUI(options =>
{
    options.SetEvaluationTimeInSeconds(30);
    options.MaximumHistoryEntriesPerEndpoint(50);
    options.AddHealthCheckEndpoint("MyApp", "/health");
})
.AddInMemoryStorage();

// Endpoint mapping
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecks("/health/db", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("db")
});

app.MapHealthChecksUI(options => options.UIPath = "/health-ui");

Ardalis.Result / ErrorOr (Result Pattern)

csharp
// dotnet add package ErrorOr
// Hata ve başarıyı tek tip ile döndürme

public class ProductService
{
    public async Task<ErrorOr<ProductResponseDto>> GetByIdAsync(int id)
    {
        var product = await _db.Products.FindAsync(id);
        if (product is null)
            return Error.NotFound("Product.NotFound", $"ID={id} ürün bulunamadı");

        return new ProductResponseDto(product.Id, product.Name, product.Price,
            product.Stock, product.Category?.Name ?? "");
    }

    public async Task<ErrorOr<ProductResponseDto>> CreateAsync(CreateProductDto dto)
    {
        if (await _db.Products.AnyAsync(p => p.Name == dto.Name))
            return Error.Conflict("Product.Duplicate", "Bu isimde ürün zaten var");

        if (dto.Price <= 0)
            return Error.Validation("Product.InvalidPrice", "Fiyat sıfırdan büyük olmalı");

        var product = new Product { Name = dto.Name, Price = dto.Price };
        _db.Products.Add(product);
        await _db.SaveChangesAsync();

        return new ProductResponseDto(product.Id, product.Name, product.Price,
            product.Stock, "");
    }
}

// Controller'da kullanım
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
    var result = await _service.GetByIdAsync(id);

    return result.Match(
        product => Ok(product),
        errors => errors.First().Type switch
        {
            ErrorType.NotFound => NotFound(errors),
            ErrorType.Validation => BadRequest(errors),
            ErrorType.Conflict => Conflict(errors),
            _ => Problem()
        });
}

✅ Summary: .NET ekosistemi zengin paket desteği sunar. Projenin ihtiyacına göre doğru paketleri seç ve entegre et.


22) Tips & Best Practices

async/await Her Yerde Kullan

csharp
// DOGRU -- tamamen asenkron zincir
public async Task<List<Product>> GetProductsAsync(CancellationToken ct)
    => await _db.Products.AsNoTracking().ToListAsync(ct);

// YANLIS -- senkron bloklama (deadlock riski)
public List<Product> GetProducts()
    => _db.Products.AsNoTracking().ToListAsync().Result; // Deadlock!

// Library kodunda ConfigureAwait(false) kullan
public async Task<byte[]> DownloadAsync(string url)
{
    var response = await _httpClient.GetAsync(url).ConfigureAwait(false);
    return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}

DTO Kullan -- Entity Direkt Dönme

csharp
// YANLIS -- entity direkt dönülür (circular reference, gereksiz veri, güvenlik riski)
[HttpGet]
public async Task<List<User>> GetUsers()
    => await _db.Users.ToListAsync(); // Şifre hash'i de gider!

// DOGRU -- DTO ile sadece gerekli alanları dön
public record UserDto(int Id, string Name, string Email);

[HttpGet]
public async Task<List<UserDto>> GetUsers()
    => await _db.Users
        .Select(u => new UserDto(u.Id, u.Name, u.Email))
        .ToListAsync();

IOptions vs IOptionsSnapshot vs IOptionsMonitor

ÖzellikIOptionsIOptionsSnapshotIOptionsMonitor
Yaşam döngüsüSingletonScopedSingleton
Yeniden yüklemeUygulama başında bir kezHer request'teDeğişiklikte anında
KullanımStatik configRequest-scoped configDinamik config
Named optionsDesteklemezDesteklerDestekler
csharp
// IOptions -- uygulama boyunca sabit
public class MyService
{
    private readonly MyOptions _options;
    public MyService(IOptions<MyOptions> options) => _options = options.Value;
}

// IOptionsSnapshot -- her request'te güncel (Scoped servislerde)
public class MyScopedService
{
    private readonly MyOptions _options;
    public MyScopedService(IOptionsSnapshot<MyOptions> options) => _options = options.Value;
}

// IOptionsMonitor -- değişiklikte callback (Singleton servislerde)
public class MySingletonService
{
    private MyOptions _options;

    public MySingletonService(IOptionsMonitor<MyOptions> monitor)
    {
        _options = monitor.CurrentValue;
        monitor.OnChange(newOptions => _options = newOptions);
    }
}

CancellationToken Propagation

csharp
// Her async metotta CancellationToken parametresi olmalı
[HttpGet]
public async Task<IActionResult> GetProducts(CancellationToken ct)
{
    var products = await _service.GetAllAsync(ct);
    return Ok(products);
}

public async Task<List<ProductDto>> GetAllAsync(CancellationToken ct)
    => await _db.Products.AsNoTracking().ToListAsync(ct);

// Uzun süren işlemlerde kontrol
public async Task ProcessBatchAsync(IEnumerable<Item> items, CancellationToken ct)
{
    foreach (var item in items)
    {
        ct.ThrowIfCancellationRequested(); // İptal edilmişse exception fırlat
        await ProcessItemAsync(item, ct);
    }
}

Records vs Classes (DTO için Record Tercih Et)

csharp
// Record -- immutable, value equality, kısa syntax (DTO için ideal)
public record CreateProductDto(string Name, decimal Price, int Stock);
public record ProductResponseDto(int Id, string Name, decimal Price, int Stock, string Category);

// Record with yapısı ile kopyalama
var updated = original with { Price = 200 };

// Class -- mutable state gerektiğinde (entity, servis)
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}

Structured Logging ile Correlation ID

csharp
// Her request'e benzersiz ID atayarak logları ilişkilendir
app.Use(async (context, next) =>
{
    var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                        ?? Guid.NewGuid().ToString();

    using (LogContext.PushProperty("CorrelationId", correlationId))
    {
        context.Response.Headers.Append("X-Correlation-Id", correlationId);
        await next(context);
    }
});

// Log çıktısı: [14:30:22 INF] Sipariş oluşturuldu {OrderId=42, CorrelationId="abc-123"}

Global Exception Middleware ile Hata Yönetimi

csharp
// Tek noktadan tüm hataları yakala ve tutarlı format dön
// (17.1 Güvenlik Detaylı bölümündeki GlobalExceptionMiddleware'e bakınız)

// .NET 8+ ProblemDetails factory
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
    };
});

app.UseExceptionHandler();
app.UseStatusCodePages();

Minimal API vs Controller Seçimi

  • Minimal API: Mikro servisler, basit CRUD API'ler, hızlı prototip, az endpoint
  • Controller: Büyük enterprise projeler, karmaşık iş mantığı, çok sayıda endpoint, MVC pattern

Her iki yaklaşım aynı projede birlikte kullanılabilir.

✅ Summary: async/await tutarlı kullan, DTO ile entity'yi ayır, CancellationToken'ı propag et, record'ları DTO için tercih et.


23) Test

xUnit Temelleri

csharp
// dotnet new xunit -n MyApp.Tests
// dotnet add MyApp.Tests reference MyApp.Api

public class ProductServiceTests
{
    // Fact -- parametresiz test
    [Fact]
    public void Product_UpdatePrice_ShouldUpdateCorrectly()
    {
        // Arrange
        var product = new Product { Name = "Test", Price = 100 };

        // Act
        product.UpdatePrice(200);

        // Assert
        Assert.Equal(200, product.Price);
    }

    // Fact -- exception testi
    [Fact]
    public void Product_UpdatePrice_NegativePrice_ShouldThrow()
    {
        var product = new Product { Name = "Test", Price = 100 };

        var exception = Assert.Throws<ArgumentException>(
            () => product.UpdatePrice(-50));

        Assert.Contains("sıfırdan büyük", exception.Message);
    }

    // Theory -- parametreli test
    [Theory]
    [InlineData(100, 10, 90)]
    [InlineData(50, 50, 0)]
    [InlineData(200, 1, 199)]
    public void Product_DecreaseStock_ShouldDecreaseCorrectly(
        int initialStock, int decrease, int expected)
    {
        var product = new Product { Name = "Test", Stock = initialStock };

        product.DecreaseStock(decrease);

        Assert.Equal(expected, product.Stock);
    }

    // Theory -- hatalı veri ile
    [Theory]
    [InlineData(10, 11)]  // Stoktan fazla
    [InlineData(0, 1)]    // Sıfır stok
    public void Product_DecreaseStock_InsufficientStock_ShouldThrow(
        int initialStock, int decrease)
    {
        var product = new Product { Name = "Test", Stock = initialStock };

        Assert.Throws<InvalidOperationException>(
            () => product.DecreaseStock(decrease));
    }
}

Moq / NSubstitute (Mock)

csharp
// dotnet add package Moq
// veya
// dotnet add package NSubstitute

// Moq ile service mock
public class ProductServiceTests
{
    private readonly Mock<IProductRepository> _mockRepo;
    private readonly Mock<ILogger<ProductService>> _mockLogger;
    private readonly ProductService _service;

    public ProductServiceTests()
    {
        _mockRepo = new Mock<IProductRepository>();
        _mockLogger = new Mock<ILogger<ProductService>>();
        _service = new ProductService(_mockRepo.Object, _mockLogger.Object);
    }

    [Fact]
    public async Task GetByIdAsync_ExistingId_ShouldReturnProduct()
    {
        // Arrange
        var product = new Product { Id = 1, Name = "Laptop", Price = 25000 };
        product.Category = new Category { Name = "Elektronik" };

        _mockRepo.Setup(r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()))
                 .ReturnsAsync(product);

        // Act
        var result = await _service.GetByIdAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Laptop", result.Name);
        Assert.Equal(25000, result.Price);
        _mockRepo.Verify(r => r.GetByIdAsync(1, It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task GetByIdAsync_NonExistingId_ShouldReturnNull()
    {
        _mockRepo.Setup(r => r.GetByIdAsync(999, It.IsAny<CancellationToken>()))
                 .ReturnsAsync((Product?)null);

        var result = await _service.GetByIdAsync(999);

        Assert.Null(result);
    }

    [Fact]
    public async Task CreateAsync_ValidDto_ShouldCallRepository()
    {
        var dto = new CreateProductDto("Yeni Ürün", 500, 10, 1);

        _mockRepo.Setup(r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()))
                 .Returns(Task.CompletedTask);
        _mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
                 .ReturnsAsync(new Product
                 {
                     Id = 1, Name = "Yeni Ürün", Price = 500,
                     Category = new Category { Name = "Genel" }
                 });

        var result = await _service.CreateAsync(dto);

        Assert.Equal("Yeni Ürün", result.Name);
        _mockRepo.Verify(r => r.AddAsync(
            It.Is<Product>(p => p.Name == "Yeni Ürün"),
            It.IsAny<CancellationToken>()), Times.Once);
    }
}

// NSubstitute ile aynı test
public class ProductServiceNSubTests
{
    private readonly IProductRepository _repo;
    private readonly ProductService _service;

    public ProductServiceNSubTests()
    {
        _repo = Substitute.For<IProductRepository>();
        var logger = Substitute.For<ILogger<ProductService>>();
        _service = new ProductService(_repo, logger);
    }

    [Fact]
    public async Task GetByIdAsync_ExistingId_ShouldReturnProduct()
    {
        var product = new Product { Id = 1, Name = "Laptop", Price = 25000 };
        product.Category = new Category { Name = "Elektronik" };

        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(product);

        var result = await _service.GetByIdAsync(1);

        Assert.NotNull(result);
        Assert.Equal("Laptop", result.Name);
        await _repo.Received(1).GetByIdAsync(1, Arg.Any<CancellationToken>());
    }
}

WebApplicationFactory (Integration Test)

csharp
// dotnet add package Microsoft.AspNetCore.Mvc.Testing

public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Gerçek DB yerine in-memory DB kullan
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                if (descriptor is not null)
                    services.Remove(descriptor);

                services.AddDbContext<AppDbContext>(options =>
                    options.UseInMemoryDatabase("TestDb"));

                // Test verisi ekle
                var sp = services.BuildServiceProvider();
                using var scope = sp.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                db.Database.EnsureCreated();
                SeedTestData(db);
            });
        }).CreateClient();
    }

    private static void SeedTestData(AppDbContext db)
    {
        db.Categories.Add(new Category { Id = 1, Name = "Elektronik" });
        db.Products.Add(new Product
        {
            Id = 1, Name = "Laptop", Price = 25000, Stock = 10, CategoryId = 1
        });
        db.SaveChanges();
    }

    [Fact]
    public async Task GetAll_ShouldReturnProducts()
    {
        var response = await _client.GetAsync("/api/products");

        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains("Laptop", content);
    }

    [Fact]
    public async Task GetById_ExistingId_ShouldReturn200()
    {
        var response = await _client.GetAsync("/api/products/1");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }

    [Fact]
    public async Task GetById_NonExistingId_ShouldReturn404()
    {
        var response = await _client.GetAsync("/api/products/999");

        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

    [Fact]
    public async Task Create_ValidProduct_ShouldReturn201()
    {
        var dto = new { Name = "Telefon", Price = 15000, Stock = 5, CategoryId = 1 };
        var content = new StringContent(
            JsonSerializer.Serialize(dto), Encoding.UTF8, "application/json");

        var response = await _client.PostAsync("/api/products", content);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Assert.Contains("/api/products/", response.Headers.Location?.ToString());
    }

    [Fact]
    public async Task Delete_ExistingProduct_ShouldReturn204()
    {
        var response = await _client.DeleteAsync("/api/products/1");

        Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
    }
}

In-Memory Database (EF Core Test)

csharp
public class ProductRepositoryTests : IDisposable
{
    private readonly AppDbContext _db;
    private readonly ProductRepository _repo;

    public ProductRepositoryTests()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        _db = new AppDbContext(options);
        _repo = new ProductRepository(_db);

        // Test verisi
        _db.Categories.Add(new Category { Id = 1, Name = "Test Kategori" });
        _db.Products.AddRange(
            new Product { Id = 1, Name = "Ürün A", Price = 100, CategoryId = 1 },
            new Product { Id = 2, Name = "Ürün B", Price = 200, CategoryId = 1 }
        );
        _db.SaveChanges();
    }

    [Fact]
    public async Task GetAllAsync_ShouldReturnAllProducts()
    {
        var products = await _repo.GetAllAsync();
        Assert.Equal(2, products.Count);
    }

    [Fact]
    public async Task AddAsync_ShouldAddProduct()
    {
        var newProduct = new Product { Name = "Yeni", Price = 300, CategoryId = 1 };

        await _repo.AddAsync(newProduct);

        var all = await _repo.GetAllAsync();
        Assert.Equal(3, all.Count);
    }

    [Fact]
    public async Task DeleteAsync_ShouldRemoveProduct()
    {
        await _repo.DeleteAsync(1);

        var product = await _repo.GetByIdAsync(1);
        Assert.Null(product);
    }

    public void Dispose() => _db.Dispose();
}

Controller Test vs Integration Test

ÖzellikUnit Test (Controller)Integration Test
HızCok hızlıDaha yavaş
KapsamTek sınıfTüm pipeline
BağımlılıklarMock edilirGerçek (veya in-memory)
MiddlewareTest edilmezTest edilir
RoutingTest edilmezTest edilir
ValidationTest edilmezTest edilir
Ne zamanİş mantığı testiAPI davranış testi

FluentAssertions Kullanımı

csharp
// dotnet add package FluentAssertions
using FluentAssertions;

[Fact]
public async Task GetByIdAsync_ShouldReturnCorrectProduct()
{
    var result = await _service.GetByIdAsync(1);

    result.Should().NotBeNull();
    result!.Name.Should().Be("Laptop");
    result.Price.Should().BeGreaterThan(0);
    result.Price.Should().BeInRange(20000, 30000);
}

[Fact]
public async Task GetAllAsync_ShouldReturnNonEmptyList()
{
    var result = await _service.GetAllAsync();

    result.Should().NotBeEmpty()
          .And.HaveCountGreaterThan(0)
          .And.OnlyContain(p => p.Price > 0);
}

[Fact]
public void Product_UpdatePrice_ShouldThrowForNegativePrice()
{
    var product = new Product { Name = "Test", Price = 100 };

    var act = () => product.UpdatePrice(-50);

    act.Should().Throw<ArgumentException>()
       .WithMessage("*sıfırdan büyük*");
}

[Fact]
public async Task CreateAsync_ShouldReturnCreatedProduct()
{
    var dto = new CreateProductDto("Yeni Ürün", 500, 10, 1);

    var result = await _service.CreateAsync(dto);

    result.Should().BeEquivalentTo(new
    {
        Name = "Yeni Ürün",
        Price = 500m
    }, options => options.ExcludingMissingMembers());
}

Test Coverage (Coverlet)

bash
# Coverlet kurulumu (genelde xUnit şablonunda dahil)
dotnet add MyApp.Tests package coverlet.collector

# Coverage ile test çalıştır
dotnet test --collect:"XPlat Code Coverage"

# ReportGenerator ile HTML rapor
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage-report" -reporttypes:Html

# CI/CD'de minimum coverage zorunluluğu
dotnet test --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute

Önerilen coverage hedefleri:

  • Domain katmanı: %90+
  • Application katmanı: %80+
  • Infrastructure katmanı: %60+
  • Presentation katmanı: %50+

✅ Summary: Unit test (Moq/NSubstitute), integration test (WebApplicationFactory), in-memory DB ve FluentAssertions ile kapsamlı test altyapısı kur.


Final Summary (Kapanış Özeti)

  • ASP.NET Core ile MVC/Web API/Razor Pages temellerinden üretime kadar yol haritası.
  • EF Core, Identity/JWT, Policy-based authorization ve Options ile güçlü mimari.
  • Clean Architecture ile katmanlı, test edilebilir ve bakımı kolay proje yapısı.
  • MediatR, Hangfire, SignalR, Serilog, Polly gibi ekosistem paketleri ile zengin altyapı.
  • Caching/Compression, Health Checks, Logging ve CI/CD pratikleri dahil.
  • Linux Nginx + Kestrel veya IIS üzerinde dağıtım reçetesi.
  • xUnit, Moq, WebApplicationFactory ile kapsamlı test stratejisi.

Tebrikler -- bu rehberle ASP.NET Core projelerini sıfırdan üretim seviyesine taşıyacak donanıma sahipsin!


Ilgili Rehberler

Backend

Diger Kategoriler

Developer Guides & Technical References