📌 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):
dotnet --version
dotnet new webapi -n MyApi
cd MyApi
dotnet runDefault 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.
dotnet new mvc -n MyMvc
dotnet new webapi -n MyApi
dotnet new razor -n MyPagesSeç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):
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
| Özellik | Singleton | Scoped | Transient |
|---|---|---|---|
| Yaşam döngüsü | Uygulama boyunca tek instance | HTTP request başına tek instance | Her injection'da yeni instance |
| Ne zaman kullan | Cache, configuration, HttpClient factory | DbContext, UnitOfWork, repository | Hafif, stateless servisler |
| Örnek servis | CacheService, AppSettings | DbContext, UserService | EmailBuilder, GuidGenerator |
| Bellek | En düşük (tek instance) | Orta (request başına) | En yüksek (her çağrıda yeni) |
| Thread safety | Gerekli (concurrent erişim) | Genelde gerekmez (tek request) | Gerekmez |
// 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:
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:
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:
// 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:
// 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):
// 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:
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:
[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.
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:
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:
dotnet ef migrations add Init
dotnet ef database update(Install tools) dotnet tool install --global dotnet-ef
LINQ examples:
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:
// 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)
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)
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)
// 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
| Özellik | Data Annotations | Fluent API |
|---|---|---|
| Konum | Entity sınıfı üzerinde | OnModelCreating içinde |
| Okunabilirlik | Basit senaryolarda iyi | Karmaşık ilişkilerde daha iyi |
| Kapsam | Sınırlı (temel kural) | Tam kontrol (her şeyi yapabilir) |
| Domain temizliği | Entity'ye attribute ekler | Entity temiz kalır |
| Composite key | Desteklemez | HasKey(x => new { x.A, x.B }) |
| Önerilen | Basit validasyon | İlişki ve tablo konfigürasyonu |
// 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:
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
// 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:
// 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:
// 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:
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:
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:
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):
dotnet add package FluentValidation.AspNetCorepublic 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.
dotnet new mvc --auth IndividualConfig:
builder.Services.AddDefaultIdentity<IdentityUser>(o => o.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDb>();
app.UseAuthentication();
app.UseAuthorization();JWT (API'ler için):
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerbuilder.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
[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:
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
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:
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
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:
dotnet new xunit -n MyApp.Tests
dotnet add MyApp.Tests reference MyApi
dotnet testMinimal APIs (hızlı endpoint'ler):
app.MapGet("/api/ping", () => Results.Ok(new { ok = true }));Swagger/OpenAPI:
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önerCustom Middleware Yazma
Class-Based Middleware
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
// 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
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
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
// 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:
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ı:
UseAuthenticationsonraUseRoutingyazarsan: Auth çalışmazUseCorssonraUseStaticFilesyazarsan: Statik dosyalara CORS uygulanmazUseExceptionHandleren 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:
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:
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ığı:
// 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:
// 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) -- 409Minimal API vs Controller Karşılaştırma
| Özellik | Minimal API | Controller |
|---|---|---|
| Kod miktarı | Az, tek dosyada | Daha fazla, ayrı sınıflar |
| Performans | Marjinal olarak daha hızlı | Çok yakın |
| Swagger desteği | TypedResults ile iyi | Otomatik, tam destek |
| Model validation | Manuel veya filter ile | Otomatik (ApiController) |
| Filters | Endpoint filters | Action/Result/Exception filters |
| Routing | Inline tanım | Attribute routing |
| Test edilebilirlik | Handler fonksiyon testi | Controller testi |
| Büyük projeler | Route groups ile yönetilebilir | Doğal organizasyon |
| Önerilen boyut | Mikro servis, küçük API | Orta-büyük projeler |
| Öğrenme eğrisi | Düşük | Orta |
✅ 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:
dotnet publish -c Release -o outLinux service (Kestrel + Nginx reverse proxy):
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):
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):
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)
// 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)
// 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
// 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
// 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
// Razor otomatik HTML encoding yapar -- güvenli
// @Model.Title --> <script>alert('xss')</script>
// @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)
// 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
// 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
// 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)
| Belirti | Sebep | Çözüm |
|---|---|---|
| 404 on MVC route | endpoint map edilmedi | app.MapControllerRoute(...) kontrol |
| CORS error | yanlış origin | WithOrigins() veya cors policy |
| 500 generic | detay kapalı | dev'de ayrıntı, prod'da UseExceptionHandler |
| EF timeout | yavaş sorgu | index ekle, AsNoTracking, Include optimize |
| JWT 401 | clock skew / imza | Issuer/Audience/Key, ClockSkew ayarı |
| SSL behind proxy | wrong scheme | UseForwardedHeaders middleware |
19) Appendix: CLI, Snippets & Templates
CLI (dotnet) en sık:
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 outMinimal API + EF sample:
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:
// 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:
// 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:
// 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:
// 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.slnDI Kayıtları (Program.cs)
// 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)
| Özellik | AutoMapper | Mapster |
|---|---|---|
| Performans | Yavaş (reflection) | Hızlı (code generation) |
| Konfigürasyon | Profile sınıfları ile | Inline veya config ile |
| Popülerlik | Daha yaygın | Hızla büyüyor |
| Karmaşıklık | Orta | Düşük |
| Önerilen | Büyük, karmaşık mapping | Performans kritik projeler |
// 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
// 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)
// 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)
// 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)
// 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
// 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)
// 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)
// 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
// 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)
// 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
// 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
// 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
| Özellik | IOptions | IOptionsSnapshot | IOptionsMonitor |
|---|---|---|---|
| Yaşam döngüsü | Singleton | Scoped | Singleton |
| Yeniden yükleme | Uygulama başında bir kez | Her request'te | Değişiklikte anında |
| Kullanım | Statik config | Request-scoped config | Dinamik config |
| Named options | Desteklemez | Destekler | Destekler |
// 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
// 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)
// 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
// 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
// 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
// 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)
// 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)
// 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)
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
| Özellik | Unit Test (Controller) | Integration Test |
|---|---|---|
| Hız | Cok hızlı | Daha yavaş |
| Kapsam | Tek sınıf | Tüm pipeline |
| Bağımlılıklar | Mock edilir | Gerçek (veya in-memory) |
| Middleware | Test edilmez | Test edilir |
| Routing | Test edilmez | Test edilir |
| Validation | Test edilmez | Test edilir |
| Ne zaman | İş mantığı testi | API davranış testi |
FluentAssertions Kullanımı
// 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)
# 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
- Backend Genel Bakış
- Yazilim Mimarisi
- API Development
- Laravel Rehberi
- Node.js Rehberi
- Python Rehberi
- AI & LLM