Belirttiğiniz
kurallar ve kısıtlamalar doğrultusunda, paylaştığınız müfredat dosyasını satır satır inceleyerek tasarladım. Dakika/saniye bilgilerini,
"Önizleme" ve "ders" ibarelerini
ile anlamsız satırları tamamen temizledim.
İçeriğin
derinliği ve detay seviyesi nedeniyle, konularda eksiklik kalmaması adına bu
süreci parça parça ele alacağız. Bu ilk bölümde, kursun temel vaatlerini
içeren "Öğrenecekleriniz" kısmını derinlemesine açıklıyor,
ardından müfredatın ilk büyük adımlarını detaylı uygulamalar ve kod
örnekleriyle başlatıyorum.
BÖLÜM 1: "ÖĞRENECEKLERİNİZ" PARAGRAFININ DETAYLI
ANALİZİ
Bu
kurs, modern bir web geliştiricinin kurumsal projelerde ihtiyaç duyacağı tüm
arka uç (backend) ve ön yüz (frontend)
entegrasyon yeteneklerini kazandırmayı hedefler. Maddeler halinde listelenen
kazanımların teknik karşılıkları ve projelerdeki önemi şu şekildedir:
BÖLÜM 2: MÜFREDATIN SATIR SATIR
ELE ALINMASI VE DETAYLI UYGULAMALAR
KISIM 1: ASP.NET Core MVC Web
Geliştirmeye Giriş & Kurulumlar
Konu: Giriş ve Uygulama Demosu
Bir
projenin geliştirme sürecine başlamadan önce, projenin bitmiş halini analiz
etmek mimariyi zihinde oturtmak için şarttır. Bu aşamada, kurs boyunca inşa
edilecek olan gelişmiş Blog Projesi incelenir. Projenin nihai hedefi;
admin paneli olan, dinamik içerik eklenebilen, kullanıcıların yorum yapıp
beğenebildiği bir platformdur.
Konu: Geliştirme Ortamının Kurulması (Setting
Up Development Environment)
Bir
.NET projesi geliştirmek için bilgisayarın bir derleyiciye ve veri tabanına
ihtiyacı vardır.
Konu: ASP.NET Core Nedir ve
Avantajları Nelerdir?
ASP.NET
Core; açık kaynaklı, platform bağımsız (Windows,
Linux, macOS üzerinde çalışabilen), yüksek
performanslı modern bir web framework'üdür. En büyük
avantajı, eski .NET Framework mimarilerine göre çok daha hızlı çalışması ve
modüler (bulut tabanlı sistemlere uyumlu) olmasıdır.
KISIM 2: Sıfırdan ASP.NET MVC Web Uygulaması Oluşturma
Konu: Yeni Bir ASP.NET MVC Web Uygulaması Oluşturma ve Dosya
Yapısı
Visual
Studio 2022 üzerinden boş bir şablonla veya temel MVC
şablonuyla proje başlatılır. Proje oluşturulduğunda karşımıza çıkan klasör
yapısının mantığı şudur:
Konu: Etki Alanı Modellerinin Oluşturulması (Domain Models: Blog Post and Tag)
Uygulamamızın
veri tabanındaki iskeletini oluşturmak için Etki Alanı Modelleri (Domain Models) tasarlanır. Bir blog sitesinde en temel iki
unsur Blog Yazısı (BlogPost) ve bu yazıya ait Etiketler
(Tag) yapısıdır. Bu iki model arasında "Çoka Çok" (Many-to-Many) bir
ilişki olacaktır (Bir bloğun birden fazla etiketi olabilir, bir etiket birden
fazla blokta kullanılabilir).
🛠️ Uygulama: Sınıfların
Tanımlanması
Models/Domain klasörü altında şu iki
sınıfı oluştururuz:
// Models/Domain/Tag.cs
public class Tag
{
public Guid Id { get; set; }
public string Name { get; set; }
public string DisplayName { get;
set; }
//
İlişki için Navigasyon Özelliği
public ICollection<BlogPost> BlogPosts { get; set; }
}
// Models/Domain/BlogPost.cs
public class BlogPost
{
public Guid Id { get; set; }
public string Heading { get;
set; }
public string PageTitle { get;
set; }
public string Content { get; set; }
public string ShortDescription { get; set; }
public string FeaturedImageUrl { get; set; }
public string UrlHandle { get;
set; }
public DateTime PublishedDate { get;
set; }
public string Author { get; set; }
public bool Visible { get;
set; }
//
İlişki için Navigasyon Özelliği
public ICollection<Tag> Tags { get; set; }
}
Konu: Entity Framework Core Paketlerinin Eklenmesi ve DBContext
Oluşturma
Modellerimizi
SQL Server'a tablolara dönüştürmek için projemize NuGet
Paket Yöneticisi üzerinden şu paketleri ekleriz:
Ardından,
veritabanı ile kodlarımız arasında köprü görevi
görecek olan DbContext sınıfını yazarız.
🛠️ Uygulama: DbContext Sınıfı Yazımı
Data
isminde bir klasör açıp içine ana veri bağlam sınıfımızı ekleriz:
// Data/BlogDbContext.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Models.Domain;
public class BlogDbContext : DbContext
{
public BlogDbContext(DbContextOptions<BlogDbContext>
options) : base(options)
{
}
//
SQL'de tablolara dönüşecek koleksiyonlar
public DbSet<BlogPost> BlogPosts { get; set; }
public DbSet<Tag> Tags { get; set; }
}
Konu: Bağlantı Metninin (ConnectionString)
Eklenmesi ve Enjeksiyonu
Veritabanının nerede kurulacağını uygulamaya bildirmek için appsettings.json dosyasına bağlantı adresini
tanımlarız.
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"BlogDbConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=BlogDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
Bu
bağlantıyı uygulamanın başlangıcında (Program.cs) sisteme dahil ederiz (Dependency Injection).
// Program.cs
içerisine eklenecek satır
builder.Services.AddDbContext<BlogDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("BlogDbConnectionString")));
Konu: EF Core Migrations
(Veritabanı Göçleri) Çalıştırma
Yazdığımız
C# kodlarının SQL Server tarafında gerçek birer veritabanı
ve tablo haline gelmesi için Visual Studio'da Package Manager Console açılır ve sırasıyla
şu komutlar yürütülür:
Add-Migration "Initial Migration"
Update-Database
Bu
komutlar çalıştıktan sonra SQL Server üzerinde BlogDb adında bir veritabanı, içinde de BlogPosts, Tags ve aralarındaki ilişkiyi
sağlayan ara tablo otomatik olarak kurulmuş olur.
KISIM 3: ASP.NET MVC'de Etiket (Tag) CRUD İşlemlerini Oluşturma
Konu: Controller Yapısı ve GET Metodu ile Sayfa Gösterme
Kullanıcının
etiket ekleyebileceği bir arayüz tasarlamak için bir kontrolör (Controller) ve
buna bağlı bir sayfa (View) oluşturmamız gerekir.
Yönetimsel işlemler olacağı için AdminTagsController ismini tercih ederiz.
🛠️ Uygulama: Controller ve View Tanımlama
// Controllers/AdminTagsController.cs
using Microsoft.AspNetCore.Mvc;
public class AdminTagsController : Controller
{
//
Ekleme sayfasını kullanıcıya gösterecek GET metodu
[HttpGet]
public IActionResult Add()
{
return View();
}
}
Views/AdminTags/Add.cshtml
yolunda bir görünüm (View) dosyası oluşturup Bootstrap 5 form yapısını entegre ederiz.
<!-- Views/AdminTags/Add.cshtml -->
<div class="bg-secondary bg-opacity-10
py-2">
<div
class="container">
<h1>Yeni Etiket Ekle - Admin Panel</h1>
</div>
</div>
<div class="container py-5">
<form method="post">
<div
class="mb-3">
<label class="form-label">Etiket Adı</label>
<input type="text" class="form-control" name="name" />
</div>
<div
class="mb-3">
<label class="form-label">Görünen İsim</label>
<input type="text" class="form-control" name="displayName" />
</div>
<button type="submit" class="btn btn-dark">Kaydet</button>
</form>
</div>
Konu: Form Gönderimi, Veri Bağlama (Data Binding)
ve Veritabanına Kaydetme
Kullanıcı
formdaki "Kaydet" butonuna bastığında veriler HTTP POST isteğiyle
sunucuya gönderilir. Gelen verileri yakalamak için Controller tarafında bir Model
veya DTO (Data Transfer Object) kullanmak mimari açıdan en doğrusudur.
🛠️ Uygulama: HTTP POST ile
Veri Tabanına Kayıt İşlemi
Öncelikle
gelen verileri temiz karşılamak için bir Model oluşturuyoruz:
// Models/ViewModels/AddTagRequest.cs
public class AddTagRequest
{
public string Name { get; set; }
public string DisplayName { get;
set; }
}
Ardından
AdminTagsController içerisine veritabanı bağlamını (BlogDbContext) enjekte ederek POST metodunu yazarız:
// Controllers/AdminTagsController.cs güncellenmiş hali
public class AdminTagsController : Controller
{
private readonly BlogDbContext _blogDbContext;
//
DbContext constructor
aracılığıyla enjekte ediliyor
public AdminTagsController(BlogDbContext blogDbContext)
{
_blogDbContext
= blogDbContext;
}
[HttpGet]
public IActionResult Add() => View();
[HttpPost]
public IActionResult Add(AddTagRequest addTagRequest)
{
//
Gelen DTO verisini gerçek Domain Model nesnesine dönüştürüyoruz
var
tag = new
Tag
{
Name = addTagRequest.Name,
DisplayName
= addTagRequest.DisplayName
};
//
Veritabanına ekleme sırası
_blogDbContext.Tags.Add(tag);
_blogDbContext.SaveChanges();
// Değişiklikleri SQL'e kaydeder
//
İşlem bitince etiket listeleme sayfasına yönlendir
return RedirectToAction("List");
}
}
İstediğiniz
doğrultusunda, senkron kodlama aşamasını tamamen geride bırakıp kurumsal
mimarilerin standardı olan Asenkron Programlama (Async/Await) ve Repository
Tasarım Kalıbı (Repository Pattern)
modeline doğrudan geçiş yapıyoruz.
Kod
derinliğini ve mimari seviyeyi en üst düzeye çıkararak, gevşek bağlı (loosely coupled),
genişletilebilir ve temiz kod (Clean Code) prensiplerine uygun kurumsal bir yapı inşa edeceğiz.
BÖLÜM 3: KURUMSAL MİMARİYE GEÇİŞ (ASENKRON & REPOSITORY
PATTERN)
KISIM 1: Asenkron Programlama ve Mimari Altyapı
Konu: Neden Asenkron Programlama (Async/Await)?
Geleneksel
senkron metotlarda, veritabanına bir sorgu
gönderildiğinde C# tarafındaki mevcut iş parçacığı (Thread),
veritabanından cevap gelene kadar bloklanır
(bekletilir). Yoğun trafik alan uygulamalarda bu durum Thread
Starvation (İş Parçacığı Kıtlığı) yaratarak
uygulamanın kilitlenmesine neden olur.
Konu: Repository Tasarım Kalıbı (Repository Pattern)
Controller
sınıflarımızın doğrudan DbContext (veritabanı)
ile konuşması kurumsal projelerde istenmeyen bir durumdur (Tight
Coupling). Veritabanı
teknolojisi değiştiğinde (örneğin SQL Server'dan PostgreSQL
veya MongoDB'ye geçildiğinde) tüm Controller
kodlarını değiştirmek zorunda kalırız.
KISIM 2: İleri Seviye Repository
ve Katman Tasarımı
Kod
seviyesini yükselterek, hem Tag
hem de BlogPost süreçlerini yönetecek asenkron
arayüzleri ve sınıfları en baştan profesyonel biçimde tasarlıyoruz.
1. Adım: Repository Arayüzlerinin
(Interfaces) Tanımlanması
Repositories adında yeni bir klasör
oluşturup, sistemin ihtiyaç duyacağı tüm sözleşmeleri asenkron (Task) olarak beyan ediyoruz.
// Repositories/ITagRepository.cs
using WebApplication.Models.Domain;
public interface ITagRepository
{
Task<IEnumerable<Tag>> GetAllAsync();
Task<Tag?> GetAsync(Guid id);
Task<Tag> AddAsync(Tag tag);
Task<Tag?> UpdateAsync(Tag tag);
Task<Tag?> DeleteAsync(Guid id);
}
// Repositories/IBlogPostRepository.cs
using WebApplication.Models.Domain;
public interface IBlogPostRepository
{
Task<IEnumerable<BlogPost>> GetAllAsync();
Task<BlogPost?> GetAsync(Guid id);
Task<BlogPost> AddAsync(BlogPost blogPost);
Task<BlogPost?> UpdateAsync(BlogPost blogPost);
Task<BlogPost?> DeleteAsync(Guid id);
}
2. Adım: Somut Sınıfların (Concrete
Classes) SQL Server İçin Yazılması
BlogDbContext nesnesini bu sınıflara
enjekte ederek, Entity Framework Core'un
asenkron metotlarını (ToListAsync, FirstOrDefaultAsync vb.) kullanıyoruz.
// Repositories/SqlTagRepository.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Data;
using WebApplication.Models.Domain;
public class SqlTagRepository : ITagRepository
{
private readonly BlogDbContext _dbContext;
public SqlTagRepository(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Tag>
AddAsync(Tag tag)
{
await _dbContext.Tags.AddAsync(tag);
await _dbContext.SaveChangesAsync();
return tag;
}
public async Task<Tag?>
DeleteAsync(Guid id)
{
var
existingTag = await _dbContext.Tags.FindAsync(id);
if (existingTag
!= null)
{
_dbContext.Tags.Remove(existingTag);
await _dbContext.SaveChangesAsync();
return existingTag;
}
return null;
}
public async Task<IEnumerable<Tag>> GetAllAsync()
{
return await _dbContext.Tags.ToListAsync();
}
public async Task<Tag?>
GetAsync(Guid id)
{
return await _dbContext.Tags.FirstOrDefaultAsync(x
=> x.Id == id);
}
public async Task<Tag?>
UpdateAsync(Tag tag)
{
var
existingTag = await _dbContext.Tags.FindAsync(tag.Id);
if (existingTag
!= null)
{
existingTag.Name
= tag.Name;
existingTag.DisplayName
= tag.DisplayName;
await _dbContext.SaveChangesAsync();
return existingTag;
}
return null;
}
}
Müfredattaki
"BlogPost CRUD Operations -
Dropdowns, Checkbox"
başlığına hazırlık olması açısından, BlogPost kayıt aşamasında etiket
ilişkilerini (Many-to-Many) de çözen gelişmiş SqlBlogPostRepository yapısını kurguluyoruz.
// Repositories/SqlBlogPostRepository.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Data;
using WebApplication.Models.Domain;
public class SqlBlogPostRepository : IBlogPostRepository
{
private readonly BlogDbContext _dbContext;
public SqlBlogPostRepository(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<BlogPost>
AddAsync(BlogPost blogPost)
{
await _dbContext.BlogPosts.AddAsync(blogPost);
await _dbContext.SaveChangesAsync();
return blogPost;
}
public async Task<IEnumerable<BlogPost>> GetAllAsync()
{
//
Performans ve ilişkisel veri yüklemesi (Eager Loading) için Include
kullanıyoruz
return await _dbContext.BlogPosts.Include(x => x.Tags).ToListAsync();
}
public async Task<BlogPost?>
GetAsync(Guid id)
{
return await _dbContext.BlogPosts.Include(x => x.Tags).FirstOrDefaultAsync(x
=> x.Id == id);
}
public async Task<BlogPost?>
UpdateAsync(BlogPost blogPost)
{
//
Mevcut blog yazısını ilişkili etiketleriyle birlikte çekiyoruz
var
existingBlog = await _dbContext.BlogPosts
.Include(x => x.Tags)
.FirstOrDefaultAsync(x => x.Id
== blogPost.Id);
if (existingBlog
!= null)
{
existingBlog.Heading
= blogPost.Heading;
existingBlog.PageTitle
= blogPost.PageTitle;
existingBlog.Content
= blogPost.Content;
existingBlog.ShortDescription
= blogPost.ShortDescription;
existingBlog.FeaturedImageUrl
= blogPost.FeaturedImageUrl;
existingBlog.UrlHandle
= blogPost.UrlHandle;
existingBlog.PublishedDate
= blogPost.PublishedDate;
existingBlog.Author
= blogPost.Author;
existingBlog.Visible
= blogPost.Visible;
// Çoka çok ilişki optimizasyonu: Eski
etiket bağlarını koparıp yenilerini atıyoruz
existingBlog.Tags
= blogPost.Tags;
await _dbContext.SaveChangesAsync();
return existingBlog;
}
return null;
}
public async Task<BlogPost?>
DeleteAsync(Guid id)
{
var
existingBlog = await _dbContext.BlogPosts.FindAsync(id);
if (existingBlog
!= null)
{
_dbContext.BlogPosts.Remove(existingBlog);
await _dbContext.SaveChangesAsync();
return existingBlog;
}
return null;
}
}
3. Adım: Sınıfların IoC Container Veritabanına
Kaydedilmesi
Yazdığımız
soyutlama ve somutlaştırma yapısını .NET Core
mekanizmasına tanıtmak için Program.cs dosyasına bağımlılık
enjeksiyonlarını (Dependency Injection)
tanımlıyoruz. Projede her HTTP isteğinde yeni bir instance
üretilmesi için AddScoped yaşam döngüsünü (Lifecycle) seçiyoruz.
// Program.cs
içerisine eklenecek kurumsal bağımlılık tanımları
builder.Services.AddScoped<ITagRepository,
SqlTagRepository>();
builder.Services.AddScoped<IBlogPostRepository,
SqlBlogPostRepository>();
KISIM 3: Asenkron Mimaride Tag
CRUD Operasyonlarının Tamamlanması
Artık
Controller katmanı DbContext nesnesini doğrudan görmez;
bağımlılık enjeksiyonu ile sadece ITagRepository arayüzünü talep eder.
1. Gelişmiş Görünüm Modelleri (ViewModels
/ DTOs)
İstemci
ve sunucu arasındaki veri trafiğini yönetmek için gelişmiş ViewModeller
tanımlıyoruz.
// Models/ViewModels/EditTagRequest.cs
public class EditTagRequest
{
public Guid Id { get; set; }
public string Name { get; set; }
public string DisplayName { get;
set; }
}
2. Tamamen Asenkron Kurumsal Kontrolör (AdminTagsController)
// Controllers/AdminTagsController.cs
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models.Domain;
using WebApplication.Models.ViewModels;
public class AdminTagsController : Controller
{
private readonly ITagRepository _tagRepository;
//
Sadece soyutlama olan Arayüze (Interface) bağımlıyız
public AdminTagsController(ITagRepository tagRepository)
{
_tagRepository
= tagRepository;
}
[HttpGet]
public async Task<IActionResult>
List()
{
var
tags = await _tagRepository.GetAllAsync();
return View(tags);
}
[HttpGet]
public IActionResult Add() => View();
[HttpPost]
public async Task<IActionResult>
Add(AddTagRequest addTagRequest)
{
var
tag = new
Tag
{
Name = addTagRequest.Name,
DisplayName
= addTagRequest.DisplayName
};
await _tagRepository.AddAsync(tag);
return RedirectToAction("List");
}
[HttpGet]
public async Task<IActionResult>
Edit(Guid id)
{
var
tag = await _tagRepository.GetAsync(id);
if (tag == null) return NotFound();
var
editTagRequest = new
EditTagRequest
{
Id = tag.Id,
Name = tag.Name,
DisplayName
= tag.DisplayName
};
return View(editTagRequest);
}
[HttpPost]
public async Task<IActionResult>
Edit(EditTagRequest editTagRequest)
{
var
tag = new
Tag
{
Id = editTagRequest.Id,
Name = editTagRequest.Name,
DisplayName
= editTagRequest.DisplayName
};
var
updatedTag = await _tagRepository.UpdateAsync(tag);
if (updatedTag ==
null) return RedirectToAction("Edit", new
{ id = editTagRequest.Id });
return RedirectToAction("List");
}
[HttpPost]
public async Task<IActionResult>
Delete(Guid id)
{
var
deletedTag = await _tagRepository.DeleteAsync(id);
if (deletedTag !=
null)
{
// İleride Bootstrap bildirimleri
buraya entegre edilecek
return RedirectToAction("List");
}
return RedirectToAction("List");
}
}
3. Gelişmiş Arayüz Katmanı (Razor Views)
🛠️ Uygulama: Liste Sayfası (Views/AdminTags/List.cshtml)
@model
IEnumerable<WebApplication.Models.Domain.Tag>
<div class="container py-4">
<div
class="d-flex justify-content-between align-items-center mb-4">
<h2>Tüm Etiketler</h2>
<a
asp-action="Add" class="btn btn-primary">Yeni Etiket Ekle</a>
</div>
<table class="table table-striped
table-hover border">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Etiket Adı</th>
<th>Görünen İsim</th>
<th class="text-center">İşlemler</th>
</tr>
</thead>
<tbody>
@if (Model !=
null && Model.Any())
{
foreach (var tag in Model)
{
<tr>
<td>@tag.Id</td>
<td>@tag.Name</td>
<td>@tag.DisplayName</td>
<td class="text-center">
<a asp-action="Edit" asp-route-id="@tag.Id" class="btn btn-sm
btn-warning me-2">Düzenle</a>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="4" class="text-center text-muted">Kayıtlı etiket bulunamadı.</td>
</tr>
}
</tbody>
</table>
</div>
🛠️ Uygulama: Düzenleme ve
Silme Sayfası (Views/AdminTags/Edit.cshtml)
Düzenleme
ve silme süreçlerini tek form mimarisinde güvenli şekilde yönetmek için HTML5 formaction özelliğini entegre ediyoruz.
@model
WebApplication.Models.ViewModels.EditTagRequest
<div class="container py-4">
<h2 class="mb-4">Etiketi Düzenle</h2>
<form method="post" class="card p-4 shadow-sm">
<input type="hidden" asp-for="Id" />
<div
class="mb-3">
<label class="form-label" asp-for="Name">Etiket Adı</label>
<input type="text" class="form-control" asp-for="Name" required />
</div>
<div
class="mb-3">
<label class="form-label" asp-for="DisplayName">Görünen İsim</label>
<input type="text" class="form-control" asp-for="DisplayName" required />
</div>
<div
class="d-flex justify-content-between
mt-4">
<div>
<button type="submit" class="btn btn-success" asp-action="Edit">Değişiklikleri Kaydet</button>
<a
asp-action="List" class="btn btn-secondary
ms-2">İptal</a>
</div>
<button type="submit" class="btn btn-danger" asp-action="Delete" asp-route-id="@Model.Id"
onclick="return confirm('Bu etiketi silmek
istediğinize emin misiniz?');">
Etiketi Sil
</button>
</div>
</form>
</div>
İstediğiniz
doğrultusunda, kod boyutundan veya satır sayısından tamamen bağımsız olarak,
kurumsal bir üretim (production) projesinde
uygulanabilecek en üst düzey mimari yaklaşımlarla devam ediyoruz.
Bu
bölümde, müfredatın en karmaşık lojiklerinden biri olan BlogPost
CRUD İşlemlerini, ilişkisel veri modellerini (Many-to-Many),
Multi-Select Dropdown mimarisini ve HTML5
entegrasyonlarını en ince detayına kadar kurgulayacağız.
BÖLÜM 4: GELİŞMİŞ BLOGPOST CRUD OPERASYONLARI VE İLİŞKİSEL
VERİ YÖNETİMİ
Bir
blog yazısı eklerken veya güncellerken, o yazıya ait sistemde var olan
etiketlerin (Tags) dinamik olarak listelenmesi ve
seçilen etiketlerin veri tutarlılığı bozulmadan asenkron olarak çoka-çok (Many-to-Many) ilişki
tablosuna işlenmesi gerekir.
KISIM 1: BlogPost Gelişmiş Görünüm
Modelleri (ViewModels)
Kurumsal
mimaride, arayüzden (View) gelen veya arayüze
gönderilen veri modelleri doğrudan Domain Entity (Veritabanı Nesnesi) olamaz. Arayüzün ihtiyaç duyduğu
validasyon, seçili eleman listeleri gibi ekstra property'leri
barındıran ViewModel nesneleri tasarlanır.
1. Yeni Blog Ekleme İstek Modeli (AddBlogPostRequest)
Kullanıcı
formu doldurduğunda seçilen etiketlerin ID değerlerini dizi (string[]) halinde yakalayabilmek ve
sistemdeki tüm etiketleri select-box içerisinde
gösterebilmek için şu yapıyı kuruyoruz:
// Models/ViewModels/AddBlogPostRequest.cs
using Microsoft.AspNetCore.Mvc.Rendering;
using System.ComponentModel.DataAnnotations;
public class AddBlogPostRequest
{
[Required(ErrorMessage = "Blog başlığı zorunludur.")]
[StringLength(100, ErrorMessage
= "Başlık en fazla 100 karakter
olabilir.")]
public string Heading { get;
set; }
[Required(ErrorMessage = "Sayfa
başlığı (SEO) zorunludur.")]
public string PageTitle { get;
set; }
[Required(ErrorMessage = "İçerik alanı boş bırakılamaz.")]
public string Content { get; set; }
[Required(ErrorMessage = "Kısa açıklama zorunludur.")]
public string ShortDescription { get; set; }
public string FeaturedImageUrl { get; set; }
[Required(ErrorMessage = "URL yönlendirici (Slug)
zorunludur.")]
public string UrlHandle { get;
set; }
[DataType(DataType.Date)]
public DateTime PublishedDate { get;
set; } = DateTime.UtcNow;
[Required(ErrorMessage = "Yazar bilgisi zorunludur.")]
public string Author { get; set; }
public bool Visible { get;
set; }
//
UI tarafında Multi-Select Dropdown'dan seçilen Tag ID'lerini yakalar
public string[] SelectedTags { get; set; } = Array.Empty<string>();
//
UI tarafında Select elementini dolduracak olan tüm etiketlerin listesi
public IEnumerable<SelectListItem> Tags { get; set; } = new
List<SelectListItem>();
}
2. Blog Düzenleme İstek Modeli (EditBlogPostRequest)
Düzenleme
işleminde, bloğun mevcut verilerinin yanı sıra daha önce seçilmiş olan
etiketlerin de arayüze "seçili" (selected)
olarak aktarılması gerekir.
// Models/ViewModels/EditBlogPostRequest.cs
using Microsoft.AspNetCore.Mvc.Rendering;
public class EditBlogPostRequest
{
public Guid Id { get; set; }
public string Heading { get;
set; }
public string PageTitle { get;
set; }
public string Content { get; set; }
public string ShortDescription { get; set; }
public string FeaturedImageUrl { get; set; }
public string UrlHandle { get;
set; }
public DateTime PublishedDate { get;
set; }
public string Author { get; set; }
public bool Visible { get;
set; }
//
Form gönderildiğinde seçilen yeni veya mevcut etiket ID'leri
public string[] SelectedTags { get; set; } = Array.Empty<string>();
//
Arayüzdeki çoklu seçim kutusunu dolduracak tüm etiket havuzu
public IEnumerable<SelectListItem> Tags { get; set; } = new
List<SelectListItem>();
}
KISIM 2: Kurumsal BlogPost
Kontrolörü (AdminBlogsController)
Bu kontrolör, hem IBlogPostRepository hem de etiketleri listelemek
için ITagRepository soyutlamalarına bağımlıdır. Çoka-çok
ilişkilerin çözümlenmesi tamamen asenkron lojiklerle bu katmanda birleştirilir.
// Controllers/AdminBlogsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using WebApplication.Models.Domain;
using WebApplication.Models.ViewModels;
public class AdminBlogsController : Controller
{
private readonly IBlogPostRepository _blogPostRepository;
private readonly ITagRepository _tagRepository;
public AdminBlogsController(IBlogPostRepository blogPostRepository,
ITagRepository tagRepository)
{
_blogPostRepository
= blogPostRepository;
_tagRepository
= tagRepository;
}
[HttpGet]
public async Task<IActionResult>
List()
{
//
İlişkili Tag'leri ile birlikte tüm blogları çeker (Eager Loading)
var
blogPosts = await _blogPostRepository.GetAllAsync();
return View(blogPosts);
}
[HttpGet]
public async Task<IActionResult>
Add()
{
//
Sistemdeki tüm etiketleri çekip SelectListItem
modeline dönüştürüyoruz
var
tags = await _tagRepository.GetAllAsync();
var
model = new AddBlogPostRequest
{
Tags = tags.Select(x => new SelectListItem
{
Text
= x.DisplayName,
Value = x.Id.ToString()
})
};
return View(model);
}
[HttpPost]
public async Task<IActionResult>
Add(AddBlogPostRequest addBlogPostRequest)
{
if (!ModelState.IsValid)
{
// Validasyon hatası varsa select
listesini yeniden doldurup sayfayı geri döndür
var
tags = await _tagRepository.GetAllAsync();
addBlogPostRequest.Tags
= tags.Select(x => new SelectListItem
{ Text = x.DisplayName,
Value = x.Id.ToString() });
return View(addBlogPostRequest);
}
//
Seçilen Tag ID'lerine göre veritabanından ilgili nesneleri bulup listeye ekleme lojiği
var
selectedTagsDomain = new
List<Tag>();
foreach (var selectedTagId in
addBlogPostRequest.SelectedTags)
{
var
selectedTagIdAsGuid = Guid.Parse(selectedTagId);
var
existingTag = await _tagRepository.GetAsync(selectedTagIdAsGuid);
if (existingTag
!= null)
{
selectedTagsDomain.Add(existingTag);
}
}
//
ViewModel
-> Domain Model Eşlemesi (Mapping)
var
blogPost = new
BlogPost
{
Heading =
addBlogPostRequest.Heading,
PageTitle
= addBlogPostRequest.PageTitle,
Content = addBlogPostRequest.Content,
ShortDescription
= addBlogPostRequest.ShortDescription,
FeaturedImageUrl
= addBlogPostRequest.FeaturedImageUrl,
UrlHandle
= addBlogPostRequest.UrlHandle,
PublishedDate
= addBlogPostRequest.PublishedDate,
Author = addBlogPostRequest.Author,
Visible =
addBlogPostRequest.Visible,
Tags = selectedTagsDomain //
İlişkisel tabloya kayıt için domain listesi atanıyor
};
await _blogPostRepository.AddAsync(blogPost);
return RedirectToAction("List");
}
[HttpGet]
public async Task<IActionResult>
Edit(Guid id)
{
var
blogPost = await _blogPostRepository.GetAsync(id);
if (blogPost == null) return NotFound();
//
Sistemdeki tüm etiket havuzu
var
tagsDomain = await _tagRepository.GetAllAsync();
var
model = new EditBlogPostRequest
{
Id = blogPost.Id,
Heading =
blogPost.Heading,
PageTitle
= blogPost.PageTitle,
Content = blogPost.Content,
ShortDescription
= blogPost.ShortDescription,
FeaturedImageUrl
= blogPost.FeaturedImageUrl,
UrlHandle
= blogPost.UrlHandle,
PublishedDate
= blogPost.PublishedDate,
Author = blogPost.Author,
Visible =
blogPost.Visible,
// Mevcut bloğa ait zaten seçilmiş olan etiketlerin ID dizisi
SelectedTags
= blogPost.Tags.Select(x
=> x.Id.ToString()).ToArray(),
// Tüm etiket havuzunu SelectListItem
formatına çevirme
Tags = tagsDomain.Select(x => new
SelectListItem
{
Text
= x.DisplayName,
Value = x.Id.ToString()
})
};
return View(model);
}
[HttpPost]
public async Task<IActionResult>
Edit(EditBlogPostRequest editBlogPostRequest)
{
//
Yeni seçilen etiketleri toplayıp domain listesine dönüştürme
var
selectedTagsDomain = new
List<Tag>();
foreach (var selectedTagId in
editBlogPostRequest.SelectedTags)
{
if (Guid.TryParse(selectedTagId, out
var tagId))
{
var
foundTag = await _tagRepository.GetAsync(tagId);
if (foundTag != null)
{
selectedTagsDomain.Add(foundTag);
}
}
}
var
blogPostDomain = new
BlogPost
{
Id = editBlogPostRequest.Id,
Heading =
editBlogPostRequest.Heading,
PageTitle
= editBlogPostRequest.PageTitle,
Content = editBlogPostRequest.Content,
ShortDescription
= editBlogPostRequest.ShortDescription,
FeaturedImageUrl
= editBlogPostRequest.FeaturedImageUrl,
UrlHandle
= editBlogPostRequest.UrlHandle,
PublishedDate
= editBlogPostRequest.PublishedDate,
Author = editBlogPostRequest.Author,
Visible =
editBlogPostRequest.Visible,
Tags = selectedTagsDomain
};
var
updatedBlog = await _blogPostRepository.UpdateAsync(blogPostDomain);
if (updatedBlog
== null) return NotFound();
return RedirectToAction("List");
}
[HttpPost]
public async Task<IActionResult>
Delete(Guid id)
{
var
deletedBlog = await _blogPostRepository.DeleteAsync(id);
if (deletedBlog
== null) return NotFound();
return RedirectToAction("List");
}
}
KISIM 3: Gelişmiş Arayüz Katmanı (Razor
Views)
1. Blog Yazıları Listeleme Sayfası (Views/AdminBlogs/List.cshtml)
Bu
görünümde, her bloğun altındaki etiketleri Bootstrap
rozetleri (badge) şeklinde döngüye alarak ilişkisel veriyi ekranda efektif
biçimde sergiliyoruz.
@model
IEnumerable<WebApplication.Models.Domain.BlogPost>
<div class="container py-4">
<div
class="d-flex justify-content-between align-items-center mb-4">
<h2>Blog Yazıları Yönetimi</h2>
<a
asp-action="Add" class="btn btn-success">Yeni Blog Ekle</a>
</div>
<div
class="card shadow-sm">
<div
class="table-responsive">
<table class="table table-hover
align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Başlık</th>
<th>Yazar</th>
<th>Yayın Tarihi</th>
<th>Etiketler</th>
<th>Durum</th>
<th class="text-center">Aksiyon</th>
</tr>
</thead>
<tbody>
@if (Model
!= null && Model.Any())
{
foreach (var blog in Model)
{
<tr>
<td>
<div class="fw-bold">@blog.Heading</div>
<small class="text-muted">@blog.UrlHandle</small>
</td>
<td>@blog.Author</td>
<td>@blog.PublishedDate.ToString("dd MMM yyyy")</td>
<td>
@foreach
(var tag in blog.Tags)
{
<span class="badge bg-info
text-dark me-1">@tag.DisplayName</span>
}
</td>
<td>
@if (blog.Visible)
{
<span class="badge bg-success">Aktif</span>
}
else
{
<span class="badge bg-danger">Taslak</span>
}
</td>
<td class="text-center">
<a asp-action="Edit" asp-route-id="@blog.Id" class="btn btn-sm
btn-outline-warning">Düzenle</a>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="6" class="text-center text-muted
py-4">Henüz
hiç blog yazısı eklenmemiş.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
2. Gelişmiş Çoklu Etiket Seçimli Ekleme Sayfası (Views/AdminBlogs/Add.cshtml)
Arayüzde
standart select elementini multiple="multiple" parametresiyle kurumsal
çoklu seçim bileşenine dönüştürüyoruz.
@model
WebApplication.Models.ViewModels.AddBlogPostRequest
<div class="container py-4">
<h2 class="mb-4">Yeni Blog Yazısı Oluştur</h2>
<form method="post" class="row g-3 card
p-4 shadow-sm mx-auto">
<div
class="col-md-6">
<label class="form-label" asp-for="Heading">Başlık (Heading)</label>
<input type="text" class="form-control" asp-for="Heading" />
<span asp-validation-for="Heading" class="text-danger small"></span>
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="PageTitle">Sayfa Başlığı (SEO - Page Title)</label>
<input type="text" class="form-control" asp-for="PageTitle" />
<span asp-validation-for="PageTitle" class="text-danger small"></span>
</div>
<div
class="col-12">
<label class="form-label" asp-for="Content">İçerik (HTML Metni)</label>
<textarea class="form-control" asp-for="Content" rows="6"></textarea>
<span asp-validation-for="Content" class="text-danger small"></span>
</div>
<div
class="col-12">
<label class="form-label" asp-for="ShortDescription">Kısa Açıklama</label>
<input type="text" class="form-control" asp-for="ShortDescription" />
<span asp-validation-for="ShortDescription" class="text-danger small"></span>
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="UrlHandle">URL Tanımlayıcı (Slug
/ Url Handle)</label>
<input type="text" class="form-control" asp-for="UrlHandle" />
<span asp-validation-for="UrlHandle" class="text-danger small"></span>
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="Author">Yazar</label>
<input type="text" class="form-control" asp-for="Author" />
<span asp-validation-for="Author" class="text-danger small"></span>
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="PublishedDate">Yayınlanma Tarihi</label>
<input type="date" class="form-control" asp-for="PublishedDate" />
</div>
<!--
KURUMSAL MULTI-SELECT DROPDOWN BİLEŞENİ -->
<div
class="col-md-6">
<label class="form-label" asp-for="SelectedTags">Etiket Atama (Çoklu Seçim)</label>
<select class="form-select" asp-for="SelectedTags" asp-items="@Model.Tags" multiple="multiple" style="height: 120px;">
</select>
<small class="text-muted d-block
mt-1">Birden
fazla etiket seçmek için Ctrl tuşuna basılı
tutun.</small>
</div>
<div
class="col-12">
<div
class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" asp-for="Visible">
<label class="form-check-label" asp-for="Visible">Sitede Herkese Açık Görünsün Mü?</label>
</div>
</div>
<div
class="col-12
mt-4">
<button type="submit" class="btn btn-primary
px-4">Bloğu
Kaydet ve Yayınla</button>
<a
asp-action="List" class="btn btn-outline-secondary
ms-2">Listeye
Dön</a>
</div>
</form>
</div>
3. Blog Düzenleme ve İlişki Güncelleme Sayfası (Views/AdminBlogs/Edit.cshtml)
@model
WebApplication.Models.ViewModels.EditBlogPostRequest
<div class="container py-4">
<h2 class="mb-4">Blog Yazısını Düzenle</h2>
<form method="post" class="row g-3 card
p-4 shadow-sm mx-auto">
<input type="hidden" asp-for="Id" />
<div
class="col-md-6">
<label class="form-label" asp-for="Heading">Başlık</label>
<input type="text" class="form-control" asp-for="Heading" required />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="PageTitle">Sayfa Başlığı</label>
<input type="text" class="form-control" asp-for="PageTitle" required />
</div>
<div
class="col-12">
<label class="form-label" asp-for="Content">İçerik</label>
<textarea class="form-control" asp-for="Content" rows="6" required></textarea>
</div>
<div
class="col-12">
<label class="form-label" asp-for="ShortDescription">Kısa Açıklama</label>
<input type="text" class="form-control" asp-for="ShortDescription" required />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="UrlHandle">URL Tanımlayıcı (Slug)</label>
<input type="text" class="form-control" asp-for="UrlHandle" required />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="Author">Yazar</label>
<input type="text" class="form-control" asp-for="Author" required />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="PublishedDate">Yayınlanma Tarihi</label>
<input type="date" class="form-control" asp-for="PublishedDate" />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="SelectedTags">Etiketleri Güncelle</label>
<select class="form-select" asp-for="SelectedTags" asp-items="@Model.Tags" multiple="multiple" style="height: 120px;">
</select>
</div>
<div
class="col-12">
<div
class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" asp-for="Visible">
<label class="form-check-label" asp-for="Visible">Yayında Tut</label>
</div>
</div>
<div
class="col-12
d-flex justify-content-between
mt-4">
<div>
<button type="submit" class="btn btn-success
px-4"
asp-action="Edit">Güncellemeleri Kaydet</button>
<a
asp-action="List" class="btn btn-secondary
ms-2">Vazgeç</a>
</div>
<button type="submit" class="btn btn-danger" asp-action="Delete" asp-route-id="@Model.Id"
onclick="return confirm('Bu blog yazısını
kalıcı olarak silmek istediğinize emin misiniz?');">
Yazıyı Tamamen Sil
</button>
</div>
</form>
</div>
Müfredatın
en kritik entegrasyon adımlarından biri olan Zengin Metin Editörü (WYSIWYG)
ve Görsel Yükleme Altyapısını en üst düzey mimari yaklaşımlarla inşa
ediyoruz.
İstediğiniz
doğrultusunda sistemi önce Yerel Depolama (wwwroot) protokolüyle ayağa
kaldıracak, hemen ardından esnek tasarım (Open-Closed
Principle) prensibine sadık kalarak sistemi Cloudinary Cloud SDK altyapısına
bağlayacağız. Ayrıca Froala Editor'ün
arka planda çalışacağı asenkron API Controller katmanını JavaScript
entegrasyonuyla birlikte uçtan uca kuracağız.
BÖLÜM 5: GÖRSEL YÜKLEME ALTYAPISI VE WYSIWYG METİN EDİTÖRÜ
ENTEGRASYONU
KISIM 1: Görsel Yükleme Soyutlama Katmanı (Abstraction)
Kurumsal
projelerde görselin nereye yükleneceği (Yerel Sunucu, Cloudinary,
AWS S3 vb.) her an değişebilir. Bu yüzden veri tabanında yaptığımız gibi görsel
yönetimini de bir arayüz (Interface) arkasına
gizliyoruz.
// Repositories/IImageRepository.cs
public interface IImageRepository
{
//
Gelen dosya akışını işleyip, yüklenen görselin URL adresini döner
Task<string> UploadAsync(IFormFile file);
}
KISIM 2: Alternatif Kurumsal Yaklaşım – Yerel Depolama (Local Storage)
Bulut
servislerinin kesilmesi veya test ortamlarının internete kapalı olması
durumunda alternatif olarak kullanılacak olan, dosyaları doğrudan projenin wwwroot/uploads dizinine kaydeden somut
yapıyı tasarlıyoruz.
// Repositories/LocalImageRepository.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
public class LocalImageRepository : IImageRepository
{
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
public LocalImageRepository(IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor)
{
_webHostEnvironment
= webHostEnvironment;
_httpContextAccessor
= httpContextAccessor;
}
public async Task<string> UploadAsync(IFormFile
file)
{
//
wwwroot/uploads klasör
yolunu belirleme
var
uploadsFolder = Path.Combine(_webHostEnvironment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsFolder))
{
Directory.CreateDirectory(uploadsFolder);
}
//
Dosya isminin çakışmaması için benzersiz bir isim (Guid)
türetme
var
uniqueFileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
var
filePath = Path.Combine(uploadsFolder, uniqueFileName);
//
Dosyayı sunucu diskine asenkron olarak yazma
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(fileStream);
}
//
İstemcinin tarayıcıdan erişebileceği tam URL adresini oluşturma
var
request = _httpContextAccessor.HttpContext.Request;
var
urlPath = $"{request.Scheme}://{request.Host}/uploads/{uniqueFileName}";
return urlPath;
}
}
KISIM 3: Asıl Altyapı – Cloudinary
SDK Bulut Entegrasyonu
Görselleri
sunucuyu yormadan tamamen bulutta depolamak amacıyla, NuGet
üzerinden CloudinaryDotNet paketini projeye dahil
ettikten sonra konfigürasyonu ve implementasyonu gerçekleştiriyoruz.
1. Ayar Dosyasının Yapılandırılması
Cloudinary panelinden aldığımız kimlik bilgilerini appsettings.json içerisine ekliyoruz:
// appsettings.json içerisine eklenecek alan
"Cloudinary": {
"CloudName": "hasan_cloud_name",
"ApiKey": "123456789012345",
"ApiSecret": "abcde_your_api_secret_key"
}
2. Cloudinary Servis Sınıfının
Yazılması
// Repositories/CloudinaryImageRepository.cs
using CloudinaryDotNet;
using CloudinaryDotNet.Actions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
public class CloudinaryImageRepository : IImageRepository
{
private readonly Cloudinary _cloudinary;
public CloudinaryImageRepository(IConfiguration configuration)
{
//
appsettings.json'dan kimlik
bilgilerini okuyarak Cloudinary hesabını başlatıyoruz
var
account = new
Account(
configuration["Cloudinary:CloudName"],
configuration["Cloudinary:ApiKey"],
configuration["Cloudinary:ApiSecret"]
);
_cloudinary =
new Cloudinary(account);
}
public async Task<string> UploadAsync(IFormFile
file)
{
if (file == null || file.Length == 0)
return string.Empty;
//
Dosyayı bellek akışına (Stream) alarak Cloudinary API'sine gönderme
hazırlığı
using var stream = file.OpenReadStream();
var
uploadParams = new
ImageUploadParams
{
File = new
FileDescription(file.FileName, stream),
Folder = "blog_images", //
Buluttaki klasör adı
DisplayName
= Path.GetFileNameWithoutExtension(file.FileName)
};
//
Asenkron olarak resmi buluta yüklüyoruz
var
uploadResult = await _cloudinary.UploadAsync(uploadParams);
if (uploadResult
!= null && uploadResult.StatusCode
== System.Net.HttpStatusCode.OK)
{
// Yüklenen resmin buluttaki güvenli URL (https)
adresini dönüyoruz
return uploadResult.SecureUrl.ToString();
}
return string.Empty;
}
}
3. Program.cs Bağımlılık Tercihi
Sistemi
Local'den Cloudinary'ye
geçirmek için tek yapmamız gereken Program.cs içerisindeki enjeksiyon
satırını değiştirmektir. Dosya akışını yönetebilmek için IHttpContextAccessor servisini de eklemeyi unutmuyoruz.
// Program.cs
builder.Services.AddHttpContextAccessor();
// Local
depolama istendiğinde:
// builder.Services.AddScoped<IImageRepository,
LocalImageRepository>();
// Müfredat gereği Cloudinary entegrasyonu aktif edildiğinde:
builder.Services.AddScoped<IImageRepository,
CloudinaryImageRepository>();
KISIM 4: Froala Editor İçin
Asenkron Image API Kontrolörü
Froala Editor, metin alanına bir görsel sürüklendiğinde veya
seçildiğinde arka planda asenkron bir POST isteği yapar. Gelen bu görseli yakalayıp JSON formatında
editöre yanıt dönen API mimarimiz şu şekildedir:
// Controllers/API/ImagesController.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Net;
[Route("api/[controller]")]
[ApiController]
public class ImagesController : ControllerBase
{
private readonly IImageRepository _imageRepository;
public ImagesController(IImageRepository imageRepository)
{
_imageRepository
= _imageRepository;
}
[HttpPost]
public async Task<IActionResult>
UploadAsync(IFormFile file)
{
if (file == null)
{
return BadRequest(new { message = "Dosya seçilmedi." });
}
//
Seçilen ImageRepository nesnesi (Cloudinary
veya Local) üzerinden asenkron yükleme tetiklenir
var
imageUrl = await _imageRepository.UploadAsync(file);
if (string.IsNullOrEmpty(imageUrl))
{
return StatusCode((int)HttpStatusCode.InternalServerError,
new { message = "Görsel yükleme hatası." });
}
//
Froala Editor'ün görseli
sayfada anında render edebilmesi için beklediği JSON
nesnesi: { "link": "url" }
return Ok(new
{ link = imageUrl });
}
}
KISIM 5: Uçtan Uca Arayüz ve Froala
JavaScript Entegrasyonu
Gelişmiş
blog ekleme arayüzünde hem Öne Çıkan Görsel (Featured
Image) yükleme butonunu hem de Content (İçerik) alanını Froala
WYSIWYG Editor ile donatıyoruz.
🛠️ Uygulama: Views/AdminBlogs/Add.cshtml Sayfasının Güncellenmiş Hali
Sayfamızın
en üstüne Froala Editor'ün
resmi CDN stil kütüphanelerini dahil ediyoruz:
<!-- Froala Editor CSS CDN Bağlantıları
-->
<link href="https://jsdelivr.net" rel="stylesheet" type="text/css" />
Form
yapısını hem bağımsız görsel seçimi hem de Froala
alanını kapsayacak şekilde güncelliyoruz. İleri seviye JavaScript kontrolü için
elementlere özel ID tanımlamaları yapıyoruz:
@model
WebApplication.Models.ViewModels.AddBlogPostRequest
<div class="container py-4">
<h2 class="mb-4">Gelişmiş Blog Editörü ve Görsel Entegrasyonu</h2>
<form method="post" class="row g-3 card
p-4 shadow-sm mx-auto">
<div
class="col-md-6">
<label class="form-label" asp-for="Heading">Başlık</label>
<input type="text" class="form-control" asp-for="Heading" />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="UrlHandle">URL Tanımlayıcı (Slug)</label>
<input type="text" class="form-control" asp-for="UrlHandle" />
</div>
<!--
ÖNE ÇIKAN GÖRSEL YÜKLEME ALANI (STANDALONE UPLOAD) -->
<div
class="col-md-12
bg-light p-3 rounded border">
<label class="form-label fw-bold">Öne Çıkan Görsel Yükle (Featured Image)</label>
<div
class="input-group">
<input type="file" id="featuredImageUpload" class="form-control" />
</div>
<!--
Yüklenen resmin URL'ini formda gizli tutup POST ile göndereceğiz -->
<input type="hidden" id="featuredImageUrl" asp-for="FeaturedImageUrl" />
<!--
Resim yüklenince anında ekranda görebilmek için önizleme
elementi -->
<div
class="mt-3
d-none" id="imagePreviewContainer">
<p
class="small text-muted mb-1">Görsel Önizlemesi:</p>
<img src="" id="featuredImageDisplay" class="img-thumbnail" style="max-height: 200px;" alt="Önizleme" />
</div>
</div>
<!--
FROALA WYSIWYG METİN EDİTÖRÜ ALANI -->
<div
class="col-12">
<label class="form-label fw-bold" asp-for="Content">Blog Detaylı İçeriği</label>
<textarea class="form-control" id="contentEditor" asp-for="Content" rows="10"></textarea>
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="Author">Yazar</label>
<input type="text" class="form-control" asp-for="Author" />
</div>
<div
class="col-md-6">
<label class="form-label" asp-for="SelectedTags">Etiketler</label>
<select class="form-select" asp-for="SelectedTags" asp-items="@Model.Tags" multiple="multiple" style="height: 100px;"></select>
</div>
<div
class="col-12
mt-4">
<button type="submit" class="btn btn-primary
px-5">Oluştur
ve Gönder</button>
</div>
</form>
</div>
🛠️ Gelişmiş
JavaScript Akışı: Froala ve Bağımsız Resim Yükleme
Yönetimi
Sayfanın
en altına yerleştireceğimiz bu JavaScript mimarisi iki büyük işi yönetir:
<!-- Froala Editor JavaScript CDN
Bağlantısı -->
<script type="text/javascript" src="https://jsdelivr.net"></script>
<script>
// 1. ÖNE ÇIKAN GÖRSEL İÇİN BAĞIMSIZ ASENKRON
API YÜKLEME KODU
const
featuredUploadElement = document.getElementById('featuredImageUpload');
const
featuredUrlInputElement = document.getElementById('featuredImageUrl');
const
previewContainer = document.getElementById('imagePreviewContainer');
const
imageDisplayElement = document.getElementById('featuredImageDisplay');
featuredUploadElement.addEventListener('change', async (e)
=> {
let
file = e.target.files[0];
if
(!file) return;
// Form verisi hazırlama
let
formData = new FormData();
formData.append('file',
file);
// API'ye
asenkron istek gönderme
try
{
let response = await fetch('/api/images', {
method: 'POST',
body: formData
});
if (response.ok) {
let result = await response.json();
// Gelen URL'i inputa aktarma ve önizleme
featuredUrlInputElement.value
= result.link;
imageDisplayElement.src
= result.link;
previewContainer.classList.remove('d-none');
} else {
alert('Görsel yüklenirken bir hata oluştu!');
}
} catch (error) {
console.error('Yükleme
hatası:',
error);
}
});
// 2. FROALA EDITOR WYSIWYG VE İÇERİKSEL
GÖRSEL API BAĞLANTISI
new
FroalaEditor('#contentEditor', {
// Editör içi görsel yükleme
parametreleri yapılandırılıyor
imageUploadURL:
'/api/images',
imageUploadMethod:
'POST',
imageUploadParam:
'file',
placeholderText:
'Yaratıcılığınızı konuşturun, blog içeriğini
buraya yazın...',
heightMin:
300,
// Hata yakalama mekanizması
events:
{
'image.error': function (error, response) {
console.error("Froala Görsel Yükleme Hatası:", error, response);
alert("Metin içerisine görsel yüklenirken hata oluştu.");
}
}
});
</script>
Müfredatımızın
bir sonraki aşaması olan "Displaying Blogs and Tags
To Users" (Blokların
ve Etiketlerin Kullanıcılara Gösterilmesi) bölümüne geçiyoruz.
Bu
kısımda; projenin ilk ayağa kalkışında veri tabanını otomatik besleyen kurumsal
Data Seeding (Veri Tohumlama) mimarisini,
vitrin arayüzü için Hero Section ve dinamik filtreleme butonlarını, son olarak
da zengin metin editöründen gelen HTML içerikleri XSS (Cross-Site Scripting)
güvenlik açıklarından koruyarak ekrana basan Tekil Blog Detay (Single Blog Post) sayfasını uçtan uca tasarlayacağız.
BÖLÜM 6: VİTRİN ARYÜZÜ VE DİNAMİK İÇERİK GÖSTERİMİ
KISIM 1: Kurumsal Data Seeding
(Veri Tohumlama) Altyapısı
Kurumsal
projelerde uygulamanın ilk kurulumunda veya test ortamlarında veri tabanına
varsayılan verilerin (örneğin varsayılan etiketler veya ilk blog yazısı)
otomatik enjekte edilmesi istenir. Bunun için DbContext'i kirletmek yerine, Data klasöründe bağımsız bir seeder sınıfı kurguluyoruz.
// Data/BlogDataSeeder.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Data;
using WebApplication.Models.Domain;
public static class BlogDataSeeder
{
public static async Task SeedAsync(BlogDbContext dbContext)
{
//
Eğer veri tabanı oluşturulmadıysa oluşturur, bekleyen migration'ları
basar
await dbContext.Database.MigrateAsync();
//
1. ETİKET (TAG) VERİSİ TOHUMLAMA
var
defaultTags = new
List<Tag>();
if (!await dbContext.Tags.AnyAsync())
{
defaultTags = new
List<Tag>
{
new Tag { Id = Guid.NewGuid(), Name = "dotnet8", DisplayName = ".NET 8" },
new Tag { Id = Guid.NewGuid(), Name = "csharp", DisplayName =
"C#" },
new Tag { Id = Guid.NewGuid(), Name = "architecture", DisplayName =
"Mimari Tasarım" }
};
await dbContext.Tags.AddRangeAsync(defaultTags);
await dbContext.SaveChangesAsync();
}
else
{
defaultTags = await dbContext.Tags.ToListAsync();
}
//
2. BLOG YAZISI (BLOGPOST) VERİSİ TOHUMLAMA
if (!await dbContext.BlogPosts.AnyAsync())
{
var
initialBlog = new
BlogPost
{
Id = Guid.NewGuid(),
Heading
= "ASP.NET Core
.NET 8 ile Kurumsal Mimariye Giriş",
PageTitle
= ".NET 8 Kurumsal Mimari ve Repository Pattern",
ShortDescription
= "Bu blog yazısında, asenkron programlama
ve repository pattern
mimarisinin detaylarını inceliyoruz.",
Content = "<h3>Kurumsal Mimari Neden Önemlidir?</h3><p>Geliştirdiğimiz
projelerin sürdürülebilir, test edilebilir ve esnek olması için katmanlı ve
soyutlanmış mimariler kurmalıyız. <strong>Froala editör</strong>
çıktısı olan bu metin, arayüzde ham HTML olarak render
edilecektir.</p>",
FeaturedImageUrl
= "https://unsplash.com",
UrlHandle
= "dotnet-8-kurumsal-mimari-giris",
PublishedDate
= DateTime.UtcNow,
Author = "Sistem Yöneticisi",
Visible
= true,
Tags
= defaultTags.Take(2).ToList() //
Tohumlanan ilk iki etiketi bu blog yazısına bağlıyoruz
};
await dbContext.BlogPosts.AddAsync(initialBlog);
await dbContext.SaveChangesAsync();
}
}
}
Bu
tohumlama mekanizmasını projenin başlangıç noktasına (Program.cs) entegre ediyoruz. Uygulama her ayağa kalktığında arka
planda veri tabanını kontrol edecek, eksik verileri tamamlayacaktır.
// Program.cs
içerisine, builder.Build()
satırından SONRA eklenecek blok:
using (var scope = app.Services.CreateScope())
{
var
services = scope.ServiceProvider;
try
{
var
context = services.GetRequiredService<BlogDbContext>();
//
Asenkron tetikleme
await BlogDataSeeder.SeedAsync(context);
}
catch (Exception ex)
{
var
logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Veri tabanı
tohumlanırken veya migration basılırken bir hata
oluştu.");
}
}
KISIM 2: Kullanıcı (Vitrin) Kontrolörü Tasarımı (HomeController)
Kullanıcıların
siteye girdiğinde göreceği ana sayfayı ve etiket filtrelemelerini yönetecek
olan HomeController yapısını kuruyoruz. Bu kontrolör, veri tabanındaki aktif (Visible == true) verileri süzerek getirmekle
yükümlüdür.
// Controllers/HomeController.cs
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models.ViewModels;
public class HomeController : Controller
{
private readonly IBlogPostRepository _blogPostRepository;
private readonly ITagRepository _tagRepository;
public HomeController(IBlogPostRepository blogPostRepository,
ITagRepository tagRepository)
{
_blogPostRepository
= blogPostRepository;
_tagRepository
= tagRepository;
}
[HttpGet]
public async Task<IActionResult>
Index(string? tagName)
{
//
Vitrin için kombine edilmiş bir ViewModel
oluşturacağız
var
allBlogs = await _blogPostRepository.GetAllAsync();
var
allTags = await _tagRepository.GetAllAsync();
//
Sadece sitede görünür olan blogları filtrele
var
visibleBlogs = allBlogs.Where(x
=> x.Visible);
//
Eğer kullanıcı üst menüden veya yan menüden bir etikete tıkladıysa filtre
uygula
if (!string.IsNullOrEmpty(tagName))
{
visibleBlogs = visibleBlogs.Where(x
=> x.Tags.Any(t => t.Name.Equals(tagName, StringComparison.OrdinalIgnoreCase)));
ViewData["CurrentFilter"] = tagName;
}
var
model = new HomeViewModel
{
BlogPosts
= visibleBlogs,
Tags = allTags
};
return View(model);
}
}
Ana
sayfada hem blog listesini hem etiket listesini aynı anda taşıyabilmek için
tasarlanan kombine model:
// Models/ViewModels/HomeViewModel.cs
using WebApplication.Models.Domain;
public class HomeViewModel
{
public IEnumerable<BlogPost> BlogPosts { get; set; } = new
List<BlogPost>();
public IEnumerable<Tag> Tags { get; set; } = new
List<Tag>();
}
KISIM 3: Ana Sayfa Arayüzü, Hero
Alanı ve Tag Bulutu
1. Ana Sayfa Görünümü (Views/Home/Index.cshtml)
Bu
sayfada kurumsal sitelerin vazgeçilmezi olan büyük bir karşılama alanı (Hero Section), yan
tarafta dinamik bir Tag Cloud (Etiket
Bulutu) ve sol tarafta ise blog kartları listelenir.
@model
WebApplication.Models.ViewModels.HomeViewModel
<!-- HERO SECTION: KULLANICI KARŞILAMA ALANI -->
<div class="p-5 mb-4 bg-dark text-white rounded-3 shadow" style="background: linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.8)),
url('https://unsplash.com') no-repeat center center; background-size: cover;">
<div
class="container-fluid py-5 text-center">
<h1 class="display-4 fw-bold">Teknoloji ve Yazılım Blogu</h1>
<p
class="col-md-8
mx-auto fs-5 text-light
opacity-75">
.NET 8,
C#, Kurumsal Mimari tasarımları ve modern yazılım pratiklerine dair aradığınız
her şey burada.
</p>
@if (ViewData["CurrentFilter"] != null)
{
<div
class="mt-3">
<span class="badge bg-warning
text-dark p-2 fs-6">Filtrelenen Etiket: #@ViewData["CurrentFilter"]</span>
<a
asp-action="Index" class="btn btn-sm
btn-outline-light ms-2">Filtreyi Temizle</a>
</div>
}
</div>
</div>
<div class="container">
<div
class="row">
<!--
SOL SÜTUN: BLOG KARTLARI LİSTESİ -->
<div
class="col-lg-8">
<h3 class="mb-4 pb-2 border-bottom">Son Yayınlanan Yazılar</h3>
@if (Model.BlogPosts != null && Model.BlogPosts.Any())
{
foreach (var blog in Model.BlogPosts)
{
<div class="card mb-4 shadow-sm
border-0 rounded-3 overflow-hidden">
@if (!string.IsNullOrEmpty(blog.FeaturedImageUrl))
{
<img src="@blog.FeaturedImageUrl" class="card-img-top" style="max-height: 350px; object-fit: cover;" alt="@blog.Heading">
}
<div class="card-body p-4">
<div class="d-flex gap-2 mb-2">
@foreach (var tag in blog.Tags)
{
<a asp-action="Index" asp-route-tagName="@tag.Name" class="badge bg-secondary
text-decoration-none">@tag.DisplayName</a>
}
</div>
<h2 class="card-title h3 fw-bold">@blog.Heading</h2>
<p class="card-text text-muted">@blog.ShortDescription</p>
<div class="d-flex justify-content-between
align-items-center mt-4 pt-3 border-top">
<div class="small text-muted">
<i class="bi bi-person"></i>
@blog.Author | <i class="bi bi-calendar"></i>
@blog.PublishedDate.ToString("dd
MMMM yyyy")
</div>
<a asp-controller="Blogs" asp-action="Detail" asp-route-urlHandle="@blog.UrlHandle" class="btn btn-primary
px-4 rounded-pill">Devamını Oku</a>
</div>
</div>
</div>
}
}
else
{
<div
class="alert alert-info text-center">Bu kriterlere uygun herhangi bir blog yazısı
bulunamadı.</div>
}
</div>
<!--
SAĞ SÜTUN: DİNAMİK ETİKET BULUTU (TAG CLOUD) -->
<div
class="col-lg-4">
<div
class="position-sticky" style="top: 2rem;">
<div
class="p-4
mb-3 bg-light rounded shadow-sm border">
<h4 class="fw-bold mb-3">Popüler Etiketler</h4>
<div class="d-flex flex-wrap
gap-2">
@if (Model.Tags !=
null && Model.Tags.Any())
{
foreach (var tag in Model.Tags)
{
<a asp-action="Index" asp-route-tagName="@tag.Name" class="btn btn-sm
btn-outline-dark rounded-pill
px-3">
#@tag.DisplayName
</a>
}
}
</div>
</div>
</div>
</div>
</div>
</div>
KISIM 4: Tekil Blog Detay Sayfası ve XSS Güvenlik Önlemi
Kullanıcı
"Devamını Oku" butonuna bastığında, veri tabanındaki sayısal Id
değeri yerine SEO uyumlu UrlHandle (Slug)
bilgisi üzerinden yönlendirilir.
1. Detay Kontrolörü (BlogsController)
Yeni
bir kontrolör oluşturarak slug eşlemesini asenkron
yürütüyoruz:
// Controllers/BlogsController.cs
using Microsoft.AspNetCore.Mvc;
public class BlogsController : Controller
{
private readonly IBlogPostRepository _blogPostRepository;
public BlogsController(IBlogPostRepository blogPostRepository)
{
_blogPostRepository
= blogPostRepository;
}
[HttpGet]
public async Task<IActionResult>
Detail(string urlHandle)
{
//
Tüm blogları çekip eşleşen slug'ı buluyoruz
var
allBlogs = await _blogPostRepository.GetAllAsync();
var
blogPost = allBlogs.FirstOrDefault(x
=> x.UrlHandle == urlHandle
&& x.Visible);
if (blogPost == null)
{
return NotFound();
}
return View(blogPost);
}
}
2. Güvenli HTML Çıktısı Alan Detay Görünümü (Views/Blogs/Detail.cshtml)
Froala editörden veritabanına HTML tagleri (<h3>, <strong>, <img>) ile kaydedilen içerikleri
standart @Model.Content ifadesiyle yazdırırsak, Razor
mimarisi güvenlik gereği bunu string olarak ekrana
basar (HTML Encode eder). Kodların tarayıcı
tarafından yorumlanıp şık bir blog sayfasına dönüşmesi için @Html.Raw() fonksiyonunu kullanmak
zorundayız.
⚠️ Kurumsal Güvenlik Notu:
Html.Raw() doğrudan XSS (Cross-Site Scripting) açıklarına davetiye
çıkarabilir. Froala editörün sadece Admin panelinden
yetkili kişilerce beslendiğinden emin olduğumuz için bu projede ham çıktıya
güveniyoruz.
@model
WebApplication.Models.Domain.BlogPost
@{
ViewData["Title"] = Model.PageTitle;
}
<div class="container py-5">
<div
class="row justify-content-center">
<div
class="col-lg-10
col-xl-8">
<!--
ÜST BAŞLIK VE KÜNYE ALANI -->
<header class="mb-4">
<h1 class="fw-bolder mb-1 display-5">@Model.Heading</h1>
<div
class="text-muted fst-italic mb-3
fs-6">
@Model.PublishedDate.ToString("dd MMMM yyyy") tarihinde <strong>@Model.Author</strong>
tarafından yayınlandı.
</div>
<!--
Bağlı Etiketler -->
<div
class="d-flex flex-wrap gap-2 mb-4">
@foreach (var tag in Model.Tags)
{
<span class="badge bg-dark
rounded-pill px-3 py-2">#@tag.DisplayName</span>
}
</div>
</header>
<!--
ÖNE ÇIKAN BÜYÜK GÖRSEL -->
@if (!string.IsNullOrEmpty(Model.FeaturedImageUrl))
{
<figure class="mb-5 shadow rounded
overflow-hidden">
<img class="img-fluid w-100" src="@Model.FeaturedImageUrl" style="max-height: 500px; object-fit: cover;" alt="@Model.Heading" />
</figure>
}
<!--
BLOG DETAYLI İÇERİK ALANI (HAM HTML ÇIKTISI) -->
<section class="mb-5 lh-lg fs-5 blog-content-area">
@Html.Raw(Model.Content)
</section>
<div
class="mt-5
pt-4 border-top">
<a
asp-controller="Home" asp-action="Index" class="btn btn-outline-dark
rounded-pill"><i class="bi bi-arrow-left"></i>
Ana Sayfaya Dön</a>
</div>
</div>
</div>
</div>
<!-- İçerik içi görsellerin taşmaması ve Bootstrap
uyumlu olması için CSS eklemesi -->
<style>
.blog-content-area img
{
max-width:
100%;
height:
auto;
border-radius:
8px;
margin:
1.5rem 0;
box-shadow:
0 4px 6px rgba(0,0,0,0.05);
}
.blog-content-area h3
{
font-weight: 700;
margin-top:
2rem;
margin-bottom:
1rem;
color:
#212529;
}
</style>
Müfredatımızın
en kritik ve hassas bölümlerinden biri olan "Authentication
and Authorization (ASP.NET Core Identity)" aşamasına geçiyoruz.
Kurumsal
projelerin standardına sadık kalarak, uygulamanın ana veritabanı
şeması ile üyelik sistemini tamamen birbirinden izole eden bağımsız bir AuthDbContext mimarisi kuracağız. Bu sayede güvenlik katmanını bağımsız
yönetebilecek, Identity'nin katı şifre politikalarını
kurumsal seviyede özelleştirecek ve Admin / SuperAdmin
/ User şeklinde gelişmiş, rol bazlı bir yetkilendirme (Role-Based Authorization) altyapısı
inşa edeceğiz.
BÖLÜM 7: KİMLİK DOĞRULAMA VE ROL BAZLI YETKİLENDİRME
(ASP.NET CORE IDENTITY)
KISIM 1: Identity Paketleri ve Bağımsız AuthDbContext
Altyapısı
Üyelik,
şifre kriptolama, token yönetimi ve rol sistemini
yönetebilmek için projemize NuGet üzerinden şu
kurumsal paketleri dahil ediyoruz:
1. AuthDbContext Sınıfının
Oluşturulması
Bu
sınıf, sistemin standart DbContext yapısından farklı olarak IdentityDbContext sınıfından türetilir. Bu sayede Microsoft'un üyelik için
geliştirdiği tüm hazır tablolar (Kullanıcılar, Roller, Rol Talepleri vb.)
otomatik olarak bu bağlama işlenir.
// Data/AuthDbContext.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class AuthDbContext : IdentityDbContext
{
public AuthDbContext(DbContextOptions<AuthDbContext>
options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
//
KURUMSAL DATA SEEDING: Sistem ilk ayağa kalktığında rolleri otomatik
tohumluyoruz
var
superAdminRoleId = "3235b2a0-4ff6-42f0-9f5b-1175e5dc8141";
var
adminRoleId = "a716ca1f-ef0e-4364-bbcf-05047b19cf1e";
var
userRoleId = "c2b0c1ee-c31a-4bc4-b778-d459fe1e5e6e";
var
roles = new
List
{
new IdentityRole {
Id = superAdminRoleId, Name
= "SuperAdmin", NormalizedName
= "SUPERADMIN" },
new IdentityRole {
Id = adminRoleId, Name = "Admin", NormalizedName = "ADMIN" },
new IdentityRole {
Id = userRoleId, Name = "User", NormalizedName = "USER" }
};
builder.Entity().HasData(roles);
//
KURUMSAL DATA SEEDING: Varsayılan bir SuperAdmin
kullanıcısı tohumlama
var
superAdminId = "c89b7eb9-73e4-44df-9118-202d0cf3f412";
var
superAdminUser = new
IdentityUser
{
Id = superAdminId,
UserName
= "superadmin@blogapp.com",
Email = "superadmin@blogapp.com",
NormalizedUserName
= "SUPERADMIN@BLOGAPP.COM",
NormalizedEmail
= "SUPERADMIN@BLOGAPP.COM"
};
//
Identity şifreleme algoritması ile varsayılan parolayı kriptoluyoruz
var
passwordHasher = new
PasswordHasher();
superAdminUser.PasswordHash
= passwordHasher.HashPassword(superAdminUser,
"SuperAdmin.2026!");
builder.Entity().HasData(superAdminUser);
//
Kullanıcı ile Rolü birleştirme (SuperAdmin
kullanıcısına SuperAdmin, Admin ve User rollerini
tanımlıyoruz)
var
userRoles = new
List
{
new IdentityUserRole<string> { RoleId =
superAdminRoleId, UserId = superAdminId },
new IdentityUserRole<string> { RoleId =
adminRoleId, UserId = superAdminId },
new IdentityUserRole<string> { RoleId =
userRoleId, UserId = superAdminId }
};
builder.Entity().HasData(userRoles);
}
}
2. appsettings.json
Tanımlaması ve Migration Basımı
Kimlik
doğrulama veritabanı adresini tanımlıyoruz (Aynı SQL
sunucusunda farklı bir DB şeması olarak ayrıştırıyoruz).
// appsettings.json içerisine ekleme
"ConnectionStrings": {
"BlogDbConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=BlogDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"BlogAuthConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=BlogAuthDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Package Manager Console üzerinden bu bağımsız Context
için migration üretip veri tabanını oluşturuyoruz:
Add-Migration "Identity
Initial Migration" -Context AuthDbContext
Update-Database
-Context AuthDbContext
KISIM 2: Program.cs Üzerinde
Kurumsal Güvenlik Konfigürasyonları
ASP.NET
Core Identity servislerini uygulamaya enjekte ederken
şifre uzunluğu, benzersiz karakter gereksinimleri ve Cookie
(Oturum Çerezi) yönetimini uçtan uca konfigüre ediyoruz.
// Program.cs
içerisine eklenecek Identity ve Cookie Güvenlik
Konfigürasyonları
// 1. AuthDbContext Enjeksiyonu
builder.Services.AddDbContext<AuthDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("BlogAuthConnectionString")));
// 2. Identity
Servislerinin ve Şifre Politikalarının Özelleştirilmesi
builder.Services.AddIdentity(options =>
{
//
Parola Kuralları
options.Password.RequireDigit
= true; //
Rakam zorunlu
options.Password.RequireLowercase
= true; //
Küçük harf zorunlu
options.Password.RequireNonAlphanumeric
= true; //
Özel karakter/sembol zorunlu (!, @, * vb.)
options.Password.RequireUppercase
= true; //
Büyük harf zorunlu
options.Password.RequiredLength
= 8; // En az
8 karakter
options.Password.RequiredUniqueChars
= 1; // En az
1 benzersiz karakter
//
Kilitleme Kuralları (Brute Force Koruması)
options.Lockout.DefaultLockoutTimeSpan
= TimeSpan.FromMinutes(15);
// 15 dakika kilitle
options.Lockout.MaxFailedAccessAttempts
= 5; // 5
hatalı denemede hesabı askıya al
//
Kullanıcı Kuralları
options.User.RequireUniqueEmail
= true; //
E-posta adresi sistemde benzersiz olmalı
})
.AddEntityFrameworkStores<AuthDbContext>();
// 3. Cookie (Oturum) Güvenliği Ayarları
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath
= "/Account/Login"; //
Yetkisiz bir sayfaya basıldığında yönlendirilecek giriş sayfası
options.AccessDeniedPath
= "/Account/AccessDenied"; // Rolü
yetmeyen bir yere girildiğinde yönlendirilecek sayfa
options.ExpireTimeSpan
= TimeSpan.FromDays(7);
// Oturum 7 gün boyunca açık kalabilir
options.SlidingExpiration
= true; //
Kullanıcı siteyi kullandıkça 7 günlük süre sıfırlanır, uzar
options.Cookie.HttpOnly
= true; //
XSS saldırılarıyla tarayıcı JavaScript'lerinin cookie'yi
çalmasını engeller
options.Cookie.SecurePolicy
= CookieSecurePolicy.Always; // Sadece HTTPS üzerinden cookie
iletimi sağlar
});
Program.cs'in
alt kısmındaki HTTP pipeline (Ara yazılımlar
- Middleware) alanına kimlik tanıma ve yetki
sorgulama mekanizmalarını doğru sıralama ile ekliyoruz:
// Program.cs - app.Build() sonrasındaki Middleware
sıralaması (Kritik Önem Taşır)
app.UseRouting();
app.UseAuthentication(); // Kimsin? (Giriş yapıldı mı kontrolü)
app.UseAuthorization();
// Yetkin Ne? (Girmeye
çalıştığın yere rolün yetiyor mu kontrolü)
app.MapControllerRoute(
name: "default",
pattern:
"{controller=Home}/{action=Index}/{id?}");
KISIM 3: Üyelik İşlemleri Kontrolör Katmanı (AccountController)
Giriş,
Kayıt ve Çıkış lojiklerini Microsoft'un UserManager ve SignInManager sınıflarını asenkron kullanarak yönetiyoruz.
1. Üyelik Görünüm Modelleri (ViewModels)
// Models/ViewModels/RegisterViewModel.cs
public class RegisterViewModel
{
public string Username { get;
set; }
public string Email { get;
set; }
public string Password { get;
set; }
}
// Models/ViewModels/LoginViewModel.cs
public class LoginViewModel
{
public string Email { get;
set; }
public string Password { get;
set; }
public string? ReturnUrl { get; set;
} // Kullanıcı engellendiği sayfaya giriş
sonrası otomatik dönebilsin diye
}
2. Tam Kapsamlı AccountController
Yapısı
// Controllers/AccountController.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models.ViewModels;
public class AccountController : Controller
{
private readonly UserManager _userManager;
private readonly SignInManager _signInManager;
public AccountController(UserManager userManager, SignInManager signInManager)
{
_userManager
= userManager;
_signInManager
= signInManager;
}
[HttpGet]
public IActionResult Register() => View();
[HttpPost]
public async Task<IActionResult>
Register(RegisterViewModel registerViewModel)
{
var
user = new
IdentityUser
{
UserName
= registerViewModel.Username,
Email = registerViewModel.Email
};
//
Kullanıcıyı veritabanına şifreleyerek ekliyoruz
var
identityResult = await _userManager.CreateAsync(user, registerViewModel.Password);
if (identityResult.Succeeded)
{
// Yeni kayıt olan herkese varsayılan olarak "User"
rolünü atıyoruz
var
roleResult = await _userManager.AddToRoleAsync(user, "User");
if (roleResult.Succeeded)
{
// Kayıt başarılıysa direkt giriş yaptır ve ana sayfaya
yönlendir
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
}
//
Hata varsa arayüze postala
foreach (var error in
identityResult.Errors)
{
ModelState.AddModelError("", error.Description);
}
return View(registerViewModel);
}
[HttpGet]
public IActionResult
Login(string? returnUrl)
{
var
model = new LoginViewModel
{ ReturnUrl = returnUrl };
return View(model);
}
[HttpPost]
public async Task<IActionResult>
Login(LoginViewModel loginViewModel)
{
//
E-posta adresi üzerinden kullanıcıyı sistemde sorguluyoruz
var
user = await _userManager.FindByEmailAsync(loginViewModel.Email);
if (user != null)
{
// Şifre eşleşmesini asenkron test ediyoruz
var
signInResult = await _signInManager.PasswordSignInAsync(user, loginViewModel.Password, isPersistent: false, lockoutOnFailure: true);
if (signInResult.Succeeded)
{
// Eğer kullanıcının gitmek isterken takıldığı bir gizli URL (ReturnUrl) varsa oraya yönlendir
if (!string.IsNullOrEmpty(loginViewModel.ReturnUrl)
&& Url.IsLocalUrl(loginViewModel.ReturnUrl))
{
return Redirect(loginViewModel.ReturnUrl);
}
return RedirectToAction("Index", "Home");
}
if (signInResult.IsLockedOut)
{
ModelState.AddModelError("", "Çok fazla hatalı
deneme! Hesabınız geçici olarak kilitlendi.");
return View(loginViewModel);
}
}
ModelState.AddModelError("", "E-posta veya
şifre hatalı.");
return View(loginViewModel);
}
[HttpGet]
public async Task<IActionResult>
Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}
[HttpGet]
public IActionResult AccessDenied() => View();
}
KISIM 4: Rol Bazlı Sayfa Güvenliği Kontrolü
Rol
mekanizması kurulduktan sonra tek yapmamız gereken, korumak istediğimiz
kontrolörlerin veya metotların tepesine [Authorize] özniteliğini eklemektir.
// Giriş yapmış
herhangi bir üye erişebilir
[Authorize]
public class UserDashboardController : Controller {
... }
// Sadece Admin veya SuperAdmin rolüne sahip yetkililer girebilir
[Authorize(Roles = "Admin,SuperAdmin")]
public class AdminBlogsController : Controller
{
//
Buradaki tüm CRUD metotları artık en üst düzey şifreleme oturumuyla
korunmaktadır.
}
KISIM 5: Navbar (Üst Menü)
Üzerinde Giriş/Çıkış Durum Kontrolü
Ziyaretçi
giriş yaptığında üst menüde adının yazması, "Giriş Yap/Kayıt Ol"
butonlarının gizlenip yerine "Çıkış Yap" ve yetkisi varsa "Admin
Paneli" butonunun gelmesi için _Layout.cshtml şablonuna şu kurumsal
mantığı entegre ediyoruz.
🛠️ Uygulama:
Dinamik Üst Menü Yapısı (Views/Shared/_Layout.cshtml içi)
@using
Microsoft.AspNetCore.Identity
@inject
SignInManager SignInManager
<nav class="navbar navbar-expand-sm
navbar-toggleable-sm navbar-dark
bg-dark border-bottom box-shadow mb-3">
<div
class="container">
<a
class="navbar-brand fw-bold" asp-controller="Home" asp-action="Index">EnterpriseBlog</a>
<div
class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-light" asp-controller="Home" asp-action="Index">Ana Sayfa</a>
</li>
<!--
KULLANICI ADMİN İSE ADMİN PANELİ LİNKLERİNİ GÖSTER -->
@if (SignInManager.IsSignedIn(User)
&& (User.IsInRole("Admin") || User.IsInRole("SuperAdmin")))
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle
text-warning" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown">
Admin Paneli
</a>
<ul class="dropdown-menu">
<li><a
class="dropdown-menu-item p-2 d-block text-dark text-decoration-none" asp-controller="AdminBlogs" asp-action="List">Blogları Yönet</a></li>
<li><a
class="dropdown-menu-item p-2 d-block text-dark text-decoration-none" asp-controller="AdminTags" asp-action="List">Etiketleri Yönet</a></li>
</ul>
</li>
}
</ul>
<!--
KULLANICI OTURUM DURUMUNA GÖRE SAĞ MENÜ HESAP ALANI -->
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item me-3">
<span class="nav-link text-info
fw-semibold">Selam, @User.Identity?.Name</span>
</li>
<li class="nav-item">
<a class="btn btn-outline-danger
btn-sm mt-1" asp-controller="Account" asp-action="Logout">Çıkış Yap</a>
</li>
}
else
{
<li class="nav-item me-2">
<a class="btn btn-outline-light
btn-sm" asp-controller="Account" asp-action="Login">Giriş Yap</a>
</li>
<li class="nav-item">
<a class="btn btn-primary
btn-sm" asp-controller="Account" asp-action="Register">Kayıt Ol</a>
</li>
}
</ul>
</div>
</div>
</nav>
Müfredatımızın
bir sonraki aşaması olan "Adding Like and Comment
Functionality In Our Blog" (Blogumuza Beğeni ve Yorum Fonksiyonlarının
Eklenmesi) bölümüyle en derin seviyeden mimariyi örmeye devam ediyoruz.
Bu
katmanda, web projemizin etkileşim kalitesini kurumsal seviyeye çıkaracağız.
Sayfa yenilenmeden arka planda beğeni skorunu güncelleyen asenkron
JavaScript Fetch API ve Web API entegrasyonunu,
ardından her yorumu Identity kullanıcı tablosuyla ilişkisel (Foreign Key) olarak eşleyen Yorum (Comment) mimarisini kuracağız.
BÖLÜM 8: ASENKRON BEĞENİ VE İLİŞKİSEL YORUM SİSTEMİ MİMARİSİ
KISIM 1: Etkileşim Domain Modelleri ve Çoka-Çok
Veri İlişkileri
Bir
blog yazısına kimlerin beğeni attığını tutmak (her kullanıcı bir yazıyı
yalnızca bir kez beğenebilmelidir) ve yazılan yorumların hangi kullanıcıya ait
olduğunu bilmek için iki yeni veritabanı modeli
tasarlıyoruz.
1. Beğeni (BlogPostLike) Modeli
// Models/Domain/BlogPostLike.cs
public class BlogPostLike
{
public Guid Id { get; set; }
public Guid BlogPostId { get;
set; }
public string UserId { get;
set; } //
IdentityUser nesnesinin Id
değeri (string)
//
Navigasyon Özelliği
public BlogPost BlogPost { get;
set; }
}
2. Yorum (BlogPostComment) Modeli
// Models/Domain/BlogPostComment.cs
public class BlogPostComment
{
public Guid Id { get; set; }
public string Description { get;
set; }
public Guid BlogPostId { get;
set; }
public string UserId { get;
set; } //
Yorumu yapan kullanıcının Identity Id'si
public DateTime DateAdded { get;
set; } = DateTime.UtcNow;
//
Navigasyon Özellikleri
public BlogPost BlogPost { get;
set; }
}
3. Ana Blog Veritabanı Modeline (BlogPost) İlişkilerin Eklenmesi
BlogPost sınıfımızın içine bu etkileşimlerin listelerini ekleyerek
veri tabanının ilişkisel bütünlüğünü (Include lojiklerini) hazır hale
getiriyoruz.
// Models/Domain/BlogPost.cs içerisine eklenecek koleksiyonlar:
public ICollection<BlogPostLike> Likes { get; set; } = new
List<BlogPostLike>();
public ICollection<BlogPostComment> Comments { get; set; } = new
List<BlogPostComment>();
Modelleri
BlogDbContext içerisine kaydedip Package
Manager Console'dan migration
basıyoruz:
// Data/BlogDbContext.cs içine eklenecek satırlar
public DbSet<BlogPostLike> BlogPostLike { get; set; }
public DbSet<BlogPostComment> BlogPostComment
{ get; set; }
Add-Migration "Like And Comment
Domain Added" -Context BlogDbContext
Update-Database
-Context BlogDbContext
KISIM 2: Etkileşim Repository
Katmanları (Arayüz ve Somut Sınıflar)
Veri
tabanı katmanına güvenli erişim için gerekli soyutlamaları asenkron kalıplarla
oluşturuyoruz.
1. Beğeni Katmanı
// Repositories/IBlogPostLikeRepository.cs
using WebApplication.Models.Domain;
public interface IBlogPostLikeRepository
{
Task<int> GetTotalLikesAsync(Guid blogPostId);
Task<IEnumerable<BlogPostLike>>
GetLikesForBlogAsync(Guid blogPostId);
Task<BlogPostLike> AddLikeForBlogAsync(BlogPostLike blogPostLike);
}
// Repositories/SqlBlogPostLikeRepository.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Data;
using WebApplication.Models.Domain;
public class SqlBlogPostLikeRepository : IBlogPostLikeRepository
{
private readonly BlogDbContext _dbContext;
public SqlBlogPostLikeRepository(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<BlogPostLike>
AddLikeForBlogAsync(BlogPostLike
blogPostLike)
{
await _dbContext.BlogPostLike.AddAsync(blogPostLike);
await _dbContext.SaveChangesAsync();
return blogPostLike;
}
public async Task<IEnumerable<BlogPostLike>> GetLikesForBlogAsync(Guid blogPostId)
{
return await _dbContext.BlogPostLike.Where(x => x.BlogPostId == blogPostId).ToListAsync();
}
public async Task<int>
GetTotalLikesAsync(Guid blogPostId)
{
return await _dbContext.BlogPostLike.CountAsync(x
=> x.BlogPostId == blogPostId);
}
}
2. Yorum Katmanı
// Repositories/IBlogPostCommentRepository.cs
using WebApplication.Models.Domain;
public interface IBlogPostCommentRepository
{
Task<BlogPostComment> AddAsync(BlogPostComment blogPostComment);
Task<IEnumerable<BlogPostComment>>
GetCommentsByBlogIdAsync(Guid blogPostId);
}
// Repositories/SqlBlogPostCommentRepository.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Data;
using WebApplication.Models.Domain;
public class SqlBlogPostCommentRepository : IBlogPostCommentRepository
{
private readonly BlogDbContext _dbContext;
public SqlBlogPostCommentRepository(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<BlogPostComment>
AddAsync(BlogPostComment blogPostComment)
{
await _dbContext.BlogPostComment.AddAsync(blogPostComment);
await _dbContext.SaveChangesAsync();
return blogPostComment;
}
public async Task<IEnumerable<BlogPostComment>> GetCommentsByBlogIdAsync(Guid blogPostId)
{
return await _dbContext.BlogPostComment
.Where(x => x.BlogPostId
== blogPostId)
.OrderByDescending(x => x.DateAdded)
.ToListAsync();
}
}
Yeni
servisleri Program.cs içerisine Scoped olarak dahil ediyoruz:
builder.Services.AddScoped<IBlogPostLikeRepository,
SqlBlogPostLikeRepository>();
builder.Services.AddScoped<IBlogPostCommentRepository,
SqlBlogPostCommentRepository>();
KISIM 3: Sayfa Yenilemeyen Beğeni API Kontrolörü (Web API)
Kullanıcı
beğeni butonuna tıkladığında sayfanın yukarı kaymasını veya yeniden
yüklenmesini önlemek için arka planda asenkron JSON dönen bir Web API
kuruyoruz.
1. DTO Modelleri
// Models/DTO/AddLikeRequest.cs
public class AddLikeRequest
{
public Guid BlogPostId { get;
set; }
public string UserId { get;
set; }
}
2. BlogPostLikeController Web API
// Controllers/API/BlogPostLikeController.cs
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models.Domain;
using WebApplication.Models.DTO;
[Route("api/[controller]")]
[ApiController]
public class BlogPostLikeController : ControllerBase
{
private readonly IBlogPostLikeRepository _blogPostLikeRepository;
public BlogPostLikeController(IBlogPostLikeRepository blogPostLikeRepository)
{
_blogPostLikeRepository
= blogPostLikeRepository;
}
[HttpPost]
[Route("Add")]
public async Task<IActionResult>
AddLike([FromBody] AddLikeRequest addLikeRequest)
{
//
Kullanıcının bu yazıyı daha önce beğenip beğenmediğini kontrol ediyoruz
var
allLikes = await
_blogPostLikeRepository.GetLikesForBlogAsync(addLikeRequest.BlogPostId);
var
hasLikedBefore = allLikes.Any(x
=> x.UserId == addLikeRequest.UserId);
if (hasLikedBefore)
{
return BadRequest(new { message = "Bu yazıyı zaten beğendiniz." });
}
var
model = new BlogPostLike
{
BlogPostId
= addLikeRequest.BlogPostId,
UserId = addLikeRequest.UserId
};
await _blogPostLikeRepository.AddLikeForBlogAsync(model);
return Ok();
}
}
KISIM 4: Blog Detay Sayfası Etkileşim ViewModel
Yapısı
Yorumları
ve beğenileri kullanıcıya sunabilmek ve form postalandığında verileri Identity
mimarisinden çekebilmek için detay görünüm modelini (ViewModel)
genişletiyoruz.
// Models/ViewModels/BlogDetailsViewModel.cs
using WebApplication.Models.Domain;
public class BlogDetailsViewModel
{
public Guid Id { get; set; }
public string Heading { get;
set; }
public string PageTitle { get;
set; }
public string Content { get; set; }
public string ShortDescription { get; set; }
public string FeaturedImageUrl { get; set; }
public string UrlHandle { get;
set; }
public DateTime PublishedDate { get;
set; }
public string Author { get; set; }
public bool Visible { get;
set; }
public ICollection<Tag> Tags { get; set; }
//
Etkileşim Göstergeleri
public int
TotalLikes { get;
set; }
public bool Liked { get;
set; } //
Mevcut giriş yapmış kullanıcı beğenmiş mi?
public string CommentDescription { get; set; } // Formdan gelecek yeni yorum alanı
public IEnumerable<BlogCommentWithUserViewModel> Comments
{ get; set; } // Listelenecek yorumlar
}
// Kurumsal DTO:
Yorumu yapan kullanıcının sadece e-posta/kullanıcı adını taşımak için
public class BlogCommentWithUserViewModel
{
public string Description { get;
set; }
public DateTime DateAdded { get;
set; }
public string Username { get;
set; }
}
KISIM 5: Vitrin BlogsController
Güncellemesi ve Yorum Ekleme POST Metodu
Mevcut
BlogsController sınıfımızı, detay sayfasına tıklandığında hem beğeni
skorunu hesaplayacak, hem yorum yazan kullanıcıların
Identity veritabanındaki isimlerini UserManager üzerinden dinamik eşleyecek şekilde güncelliyoruz.
// Controllers/BlogsController.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models.Domain;
using WebApplication.Models.ViewModels;
public class BlogsController : Controller
{
private readonly IBlogPostRepository _blogPostRepository;
private readonly IBlogPostLikeRepository _blogPostLikeRepository;
private readonly IBlogPostCommentRepository _blogPostCommentRepository;
private readonly UserManager _userManager;
private readonly SignInManager _signInManager;
public BlogsController(IBlogPostRepository blogPostRepository,
IBlogPostLikeRepository
blogPostLikeRepository,
IBlogPostCommentRepository
blogPostCommentRepository,
UserManager
userManager,
SignInManager
signInManager)
{
_blogPostRepository
= blogPostRepository;
_blogPostLikeRepository
= blogPostLikeRepository;
_blogPostCommentRepository
= blogPostCommentRepository;
_userManager
= userManager;
_signInManager
= signInManager;
}
[HttpGet]
public async Task<IActionResult>
Detail(string urlHandle)
{
var
allBlogs = await _blogPostRepository.GetAllAsync();
var
blogPost = allBlogs.FirstOrDefault(x
=> x.UrlHandle == urlHandle
&& x.Visible);
if (blogPost == null) return NotFound();
//
Beğeni Hesaplamaları
var
totalLikes = await _blogPostLikeRepository.GetTotalLikesAsync(blogPost.Id);
var
liked = false;
if (_signInManager.IsSignedIn(User))
{
var
userId = _userManager.GetUserId(User);
var
likes = await _blogPostLikeRepository.GetLikesForBlogAsync(blogPost.Id);
liked = likes.Any(x => x.UserId == userId);
}
//
Yorumları Çekme ve Kullanıcı Adlarıyla Eşleme (Kurumsal İzolasyon Örneği)
var
commentsDomain = await _blogPostCommentRepository.GetCommentsByBlogIdAsync(blogPost.Id);
var
commentsViewModel = new
List<BlogCommentWithUserViewModel>();
foreach (var comment in
commentsDomain)
{
var
user = await _userManager.FindByIdAsync(comment.UserId);
commentsViewModel.Add(new BlogCommentWithUserViewModel
{
Description
= comment.Description,
DateAdded
= comment.DateAdded,
Username
= user !=
null ? user.UserName
: "Bilinmeyen Kullanıcı"
});
}
var
model = new BlogDetailsViewModel
{
Id = blogPost.Id,
Heading =
blogPost.Heading,
PageTitle
= blogPost.PageTitle,
Content = blogPost.Content,
ShortDescription
= blogPost.ShortDescription,
FeaturedImageUrl
= blogPost.FeaturedImageUrl,
UrlHandle
= blogPost.UrlHandle,
PublishedDate
= blogPost.PublishedDate,
Author = blogPost.Author,
Tags = blogPost.Tags,
TotalLikes
= totalLikes,
Liked = liked,
Comments
= commentsViewModel
};
return View(model);
}
[HttpPost]
public async Task<IActionResult>
Detail(BlogDetailsViewModel
model)
{
if (!_signInManager.IsSignedIn(User))
{
return RedirectToAction("Login", "Account");
}
if (!string.IsNullOrEmpty(model.CommentDescription))
{
var
commentDomain = new
BlogPostComment
{
Id = Guid.NewGuid(),
Description
= model.CommentDescription,
BlogPostId
= model.Id,
UserId
= _userManager.GetUserId(User),
DateAdded
= DateTime.UtcNow
};
await _blogPostCommentRepository.AddAsync(commentDomain);
return RedirectToAction("Detail", new
{ urlHandle = model.UrlHandle
});
}
return View(model);
}
}
KISIM 6: Arayüz Entegrasyonu ve Asenkron JavaScript Fetch Kodu
1. Views/Blogs/Detail.cshtml İçerisine Beğeni ve Yorum Panelinin
Eklenmesi
Mevcut
detay sayfamızın altına asenkron beğeni butonunu ve yorum formunu
yerleştiriyoruz.
@model
WebApplication.Models.ViewModels.BlogDetailsViewModel
@using
Microsoft.AspNetCore.Identity
@inject
UserManager UserManager
<!-- Mevcut blog içeriğinin bittiği yere eklenecek etkileşim paneli
-->
<div class="card bg-light
my-5 p-4 border-0 shadow-sm rounded-3">
<div
class="d-flex align-items-center justify-content-between mb-4">
<!--
ASENKRON BEĞENİ BUTONU -->
<div>
@if (User.Identity.IsAuthenticated)
{
if (Model.Liked)
{
<button class="btn btn-success
rounded-pill px-4" disabled>
<i class="bi bi-hand-thumbs-up-fill"></i>
Beğendiniz
</button>
}
else
{
<button id="btnLike" class="btn btn-outline-primary
rounded-pill px-4">
<i class="bi bi-hand-thumbs-up"></i>
Beğen
</button>
}
}
else
{
<a
asp-controller="Account" asp-action="Login" asp-route-returnUrl="@Context.Request.Path" class="btn btn-outline-secondary
rounded-pill px-4">
<i class="bi bi-hand-thumbs-up"></i>
Beğenmek için giriş yapın
</a>
}
<span class="ms-3 fw-bold text-muted"><span
id="totalLikes">@Model.TotalLikes</span>
Beğeni</span>
</div>
</div>
<!--
YORUM YAPMA ALANI -->
<div
class="card mb-4 border-0 shadow-sm">
<div
class="card-body p-4">
<h5 class="fw-bold mb-3">Bir Yorum Bırakın</h5>
@if (User.Identity.IsAuthenticated)
{
<form method="post">
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="UrlHandle" />
<div class="mb-3">
<textarea class="form-control" rows="3" asp-for="CommentDescription" placeholder="Düşüncelerinizi paylaşın..." required></textarea>
</div>
<button type="submit" class="btn btn-dark
rounded-pill px-4">Yorumu Yayınla</button>
</form>
}
else
{
<div
class="alert alert-warning mb-0">Yorum yazabilmek için lütfen önce
<a asp-controller="Account" asp-action="Login">giriş yapın</a>.</div>
}
</div>
</div>
<!--
YORUMLARI LİSTELEME ALANI -->
<div
class="comments-list">
<h5 class="fw-bold mb-3">Yorumlar
(@Model.Comments.Count())</h5>
@if (Model.Comments != null && Model.Comments.Any())
{
foreach (var comment in Model.Comments)
{
<div
class="d-flex p-3 bg-white border rounded mb-3 shadow-xs">
<div class="flex-shrink-0">
<div class="bg-primary text-white
rounded-circle d-flex align-items-center justify-content-center
fw-bold" style="width: 40px; height:
40px;">
@comment.Username.Substring(0,
1).ToUpper()
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="d-flex justify-content-between
align-items-center mb-1">
<span class="fw-bold text-dark">@comment.Username</span>
<small class="text-muted">@comment.DateAdded.ToString("dd.MM.yyyy HH:mm")</small>
</div>
<p class="text-muted mb-0 small">@comment.Description</p>
</div>
</div>
}
}
else
{
<p
class="text-muted fst-italic ps-2">Henüz yorum yapılmamış. İlk yorumu
siz yapın!</p>
}
</div>
</div>
2. Fetch API ile Sayfa Yenilemeyen
Beğeni Tetikleyicisi (JavaScript)
Sayfanın
en altına yerleştireceğimiz bu asenkron kod bloğu, kullanıcı butona bastığı an
arka plandaki Web API'ye POST isteği fırlatır ve sayfa
yenilenmeden beğeni sayacını yukarı yuvarlar.
<script>
const
btnLikeElement = document.getElementById('btnLike');
const
totalLikesElement = document.getElementById('totalLikes');
if
(btnLikeElement) {
btnLikeElement.addEventListener('click', async ()
=> {
// API'ye
gönderilecek veri paketi hazırlanıyor
const payload = {
blogPostId: '@Model.Id',
userId: '@UserManager.GetUserId(User)'
};
try {
let response = await fetch('/api/blogpostlike/add',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body:
JSON.stringify(payload)
});
if (response.ok) {
// Sayfa yenilenmeden DOM
manipülasyonu ile beğeni sayısını 1 artırıyoruz
let currentLikes = parseInt(totalLikesElement.innerText);
totalLikesElement.innerText
= currentLikes + 1;
// Butonu başarıyla yeşile
çevirip kilitliyoruz
btnLikeElement.className
= "btn btn-success rounded-pill
px-4";
btnLikeElement.innerHTML
= "<i class='bi bi-hand-thumbs-up-fill'></i>
Beğendiniz";
btnLikeElement.disabled
= true;
} else {
let errData = await response.json();
alert(errData.message
|| "Beğeni kaydedilemedi.");
}
} catch (error) {
console.error("Beğeni
API hatası:",
error);
}
});
}
</script>
Müfredatımızın
bir sonraki aşaması olan "Manage Users - Create Read Delete"
(Kullanıcı Yönetimi - Oluşturma, Okuma, Silme) bölümü ile mimariyi
derinleştirmeye devam ediyoruz.
Bu
katmanda, sistem yöneticilerinin (Admin/SuperAdmin)
uygulamadaki tüm üyeleri tek bir panelden kontrol edebileceği Kurumsal
Kullanıcı Yönetim Paneli'ni
inşa edeceğiz. İstediğiniz doğrultusunda, kullanıcıların rollerini (Admin, User
vb.) dinamik olarak listelerken, bir kullanıcı silindiğinde onunla ilişkili tüm
etkileşimlerin (Yorumlar ve Beğeniler) veritabanında
tutarsızlık yaratmasını önleyen Kademeli Silme (Cascade
Delete / Güvenli Temizlik) mantığını hem veri
erişim hem de kontrolör katmanında uçtan uca kurgulayacağız.
BÖLÜM 9: ADMİN KULLANICI YÖNETİM PANELİ VE GÜVENLİ SİLME
ALTYAPISI
Üye
yönetimi doğrudan Identity altyapısını (AuthDbContext) ve etkileşimleri temizlemek
adına ana veritabanını (BlogDbContext) senkronize bir şekilde
yönetmeyi gerektirir.
KISIM 1: Güvenli Temizlik İçin Kademeli Silme (Cascade Delete) Lojiği
Bir
kullanıcı hesabı silindiğinde, o kullanıcının Id bilgisine bağlı olan tüm
yorum ve beğeniler veritabanında sahipsiz kalır ve
ilişkisel bütünlük hatası (Foreign Key
Conflict) fırlatır. Bunu önlemek için bağımsız veritabanı
temizleme fonksiyonunu tasarlıyoruz.
1. Etkileşim Temizleme Arayüzü ve Somut Sınıf Güncellemesi
Mevcut
etkileşim sınıflarımıza (veya yeni bir temizlik servisine) kullanıcının tüm
izlerini silecek asenkron metotları entegre ediyoruz.
// Repositories/IUserInteractionRepository.cs
public interface IUserInteractionRepository
{
Task ClearUserInteractionsAsync(string userId);
}
// Repositories/SqlUserInteractionRepository.cs
using Microsoft.EntityFrameworkCore;
using WebApplication.Data;
public class SqlUserInteractionRepository : IUserInteractionRepository
{
private readonly BlogDbContext _dbContext;
public SqlUserInteractionRepository(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task ClearUserInteractionsAsync(string userId)
{
//
1. Kullanıcının yaptığı tüm beğenileri bul ve veritabanından
temizle
var
userLikes = await _dbContext.BlogPostLike.Where(x => x.UserId == userId).ToListAsync();
if (userLikes.Any())
{
_dbContext.BlogPostLike.RemoveRange(userLikes);
}
//
2. Kullanıcının yaptığı tüm yorumları bul ve veritabanından
temizle
var
userComments = await _dbContext.BlogPostComment.Where(x =>
x.UserId == userId).ToListAsync();
if (userComments.Any())
{
_dbContext.BlogPostComment.RemoveRange(userComments);
}
//
Değişiklikleri ana veritabanına asenkron yansıt
await _dbContext.SaveChangesAsync();
}
}
Sisteme
bu yeni temizlik servisini Program.cs içinden dahil ediyoruz:
builder.Services.AddScoped<IUserInteractionRepository,
SqlUserInteractionRepository>();
KISIM 2: Kullanıcı Yönetimi Görünüm Modelleri (ViewModels)
Admin
panelindeki listeleme ve yeni kullanıcı oluşturma işlemlerinde veri transfer
güvenliğini korumak amacıyla özelleştirilmiş ViewModelleri
hazırlıyoruz.
1. Kullanıcı Listeleme ve Rol Gösterim Modeli
// Models/ViewModels/UserViewModel.cs
public class UserViewModel
{
public IEnumerable<UserDetailViewModel> Users { get; set; } = new
List<UserDetailViewModel>();
}
public class UserDetailViewModel
{
public string Id { get;
set; }
public string Username { get;
set; }
public string Email { get;
set; }
public IEnumerable<string> Roles { get; set; } = new
List<string>(); // Dinamik Rol Sütunu
İçin
}
2. Panelden Kullanıcı Oluşturma Modeli
// Models/ViewModels/CreateUserRequest.cs
using System.ComponentModel.DataAnnotations;
public class CreateUserRequest
{
[Required(ErrorMessage = "Kullanıcı adı zorunludur.")]
public string Username { get;
set; }
[Required(ErrorMessage = "E-posta adresi zorunludur.")]
[EmailAddress(ErrorMessage = "Geçerli bir e-posta adresi giriniz.")]
public string Email { get;
set; }
[Required(ErrorMessage = "Şifre zorunludur.")]
[MinLength(8, ErrorMessage
= "Şifre en az 8 karakter olmalıdır.")]
public string Password { get;
set; }
//
Admin mi yoksa standart kullanıcı mı olacağını belirlemek için checkbox/switch lojiği
public bool IsAdmin { get;
set; }
}
KISIM 3: Yetkili Yönetim Kontrolörü (AdminUsersController)
Bu
kontrolör sadece SuperAdmin ve Admin rollerine açık olup, UserManager, RoleManager ve yazdığımız IUserInteractionRepository bağımlılıklarını asenkron
olarak harmanlar.
// Controllers/AdminUsersController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApplication.Models.ViewModels;
[Authorize(Roles = "Admin,SuperAdmin")]
public class AdminUsersController : Controller
{
private readonly UserManager _userManager;
private readonly RoleManager _roleManager;
private readonly IUserInteractionRepository _userInteractionRepository;
public AdminUsersController(UserManager userManager,
RoleManager roleManager,
IUserInteractionRepository userInteractionRepository)
{
_userManager
= userManager;
_roleManager
= roleManager;
_userInteractionRepository
= userInteractionRepository;
}
[HttpGet]
public async Task<IActionResult>
List()
{
//
Tüm Identity kullanıcılarını çekiyoruz
var
users = await _userManager.Users.ToListAsync();
var
userDetailsList = new
List<UserDetailViewModel>();
foreach (var user in
users)
{
// Her kullanıcının rollerini dinamik olarak sorgulayıp
eşliyoruz
var
roles = await _userManager.GetRolesAsync(user);
userDetailsList.Add(new UserDetailViewModel
{
Id = user.Id,
Username
= user.UserName,
Email
= user.Email,
Roles
= roles
});
}
var
model = new UserViewModel
{
Users = userDetailsList
};
return View(model);
}
[HttpPost]
public async Task<IActionResult>
Create(CreateUserRequest createUserRequest)
{
if (!ModelState.IsValid)
{
return RedirectToAction("List");
}
var
identityUser = new
IdentityUser
{
UserName
= createUserRequest.Username,
Email = createUserRequest.Email
};
//
Kullanıcıyı kriptolu şifresiyle sisteme ekliyoruz
var
result = await _userManager.CreateAsync(identityUser, createUserRequest.Password);
if (result.Succeeded)
{
// Varsayılan olarak her yaratılana User rolü atanır
var
roles = new
List<string> { "User" };
// Eğer Admin switch'i işaretlendiyse
Admin rolünü de listeye dahil et
if (createUserRequest.IsAdmin)
{
roles.Add("Admin");
}
// Seçilen tüm rolleri kullanıcıya tanımla
await _userManager.AddToRolesAsync(identityUser, roles);
return RedirectToAction("List");
}
return RedirectToAction("List");
}
[HttpPost]
public async Task<IActionResult>
Delete(string id)
{
var
user = await _userManager.FindByIdAsync(id);
if (user != null)
{
// Kurumsal Güvenlik Kuralı: SuperAdmin
kendisini veya başka bir SuperAdmin'i yanlışlıkla
silemesin
var
userRoles = await _userManager.GetRolesAsync(user);
if (userRoles.Contains("SuperAdmin"))
{
TempData["ErrorMessage"] = "Sistem
koruması: SuperAdmin hesabı silinemez!";
return RedirectToAction("List");
}
// KADEMELİ SİLME (CASCADE DELETE) DEVREYE GİRİYOR:
// Önce kullanıcının ana DB'deki tüm yorum ve beğenilerini
kazıyoruz
await _userInteractionRepository.ClearUserInteractionsAsync(id);
// Son olarak kullanıcıyı Identity üyelik sisteminden kalıcı
olarak siliyoruz
var
result = await _userManager.DeleteAsync(user);
if (result.Succeeded)
{
return RedirectToAction("List");
}
}
return RedirectToAction("List");
}
}
KISIM 4: Kullanıcı Yönetim Paneli Arayüz Entegrasyonu
Aynı
arayüzde hem mevcut kullanıcıları ve rollerini listeleyecek, hem de Bootstrap Modal (Açılır Pencere) mimarisini
kullanarak sayfa değiştirmeden yeni kullanıcı ekleme formunu tetikleyeceğiz.
🛠️ Uygulama:
Kullanıcı Kontrol Paneli (Views/AdminUsers/List.cshtml)
@model
WebApplication.Models.ViewModels.UserViewModel
<div class="container py-4">
<div
class="d-flex justify-content-between align-items-center mb-4">
<h2>Sistem Üyeleri ve Yetki Yönetimi</h2>
<!--
MODAL TETİKLEYİCİ BUTON -->
<button type="button" class="btn btn-primary
rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i
class="bi bi-person-plus"></i>
Yeni Kullanıcı Yarat
</button>
</div>
@if (TempData["ErrorMessage"] != null)
{
<div
class="alert alert-danger shadow-sm mb-4">@TempData["ErrorMessage"]</div>
}
<div
class="card shadow-sm border-0
rounded-3">
<div
class="table-responsive">
<table class="table table-hover
align-middle mb-0">
<thead class="table-dark">
<tr>
<th>Kullanıcı Adı</th>
<th>E-posta</th>
<th>Atanmış Roller (Yetkiler)</th>
<th class="text-center">İşlem</th>
</tr>
</thead>
<tbody>
@if (Model.Users != null && Model.Users.Any())
{
foreach (var user in Model.Users)
{
<tr>
<td class="fw-bold">@user.Username</td>
<td>@user.Email</td>
<td>
@foreach
(var role in user.Roles)
{
var badgeColor = role == "SuperAdmin" ? "bg-danger" : (role == "Admin" ? "bg-warning text-dark" : "bg-info text-dark");
<span class="badge @badgeColor me-1 shadow-xs">@role</span>
}
</td>
<td class="text-center">
@if (!user.Roles.Contains("SuperAdmin"))
{
<form method="post" asp-action="Delete" asp-route-id="@user.Id" class="d-inline">
<button type="submit" class="btn btn-sm
btn-outline-danger px-3 rounded-pill"
onclick="return confirm('@user.Username adlı kullanıcıyı ve ona ait TÜM
yorum/beğenileri kalıcı olarak silmek istediğinize emin misiniz?');">
<i class="bi bi-trash"></i>
Hesabı Sil
</button>
</form>
}
else
{
<span class="text-muted small
fst-italic">Korumalı Sistem Hesabı</span>
}
</td>
</tr>
}
}
else
{
<tr>
<td colspan="4" class="text-center text-muted
py-4">Sistemde
kayıtlı üye bulunamadı.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- BOOTSTRAP MODAL: YENİ KULLANICI OLUŞTURMA FORMU -->
<div class="modal fade" id="createUserModal" tabindex="-1" aria-labelledby="createUserModalLabel" aria-hidden="true">
<div
class="modal-dialog modal-dialog-centered">
<div
class="modal-content border-0 shadow">
<div
class="modal-header bg-dark text-white">
<h5 class="modal-title" id="createUserModalLabel">Yeni Kullanıcı Ekle</h5>
<button type="button" class="btn-close btn-close-white" data-bs-shadow="none" data-bs-dismiss="modal" aria-label="Kapat"></button>
</div>
<form method="post" asp-action="Create">
<div
class="modal-body
p-4">
<div class="mb-3">
<label class="form-label fw-semibold">Kullanıcı Adı</label>
<input type="text" name="Username" class="form-control" placeholder="Örn: ahmet_dev" required />
</div>
<div class="mb-3">
<label class="form-label fw-semibold">E-posta Adresi</label>
<input type="email" name="Email" class="form-control" placeholder="Örn: ahmet@mail.com" required />
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Giriş Parolası</label>
<input type="password" name="Password" class="form-control" placeholder="En az 8 karakter, büyük-küçük harf ve sembol" required />
</div>
<div class="mb-3 pt-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="isAdminSwitch" name="IsAdmin" value="true">
<label class="form-check-label
fw-semibold text-warning" for="isAdminSwitch">Bu Kullanıcıyı 'Admin'
Yap</label>
</div>
</div>
</div>
<div
class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
<button type="submit" class="btn btn-success">Kullanıcıyı Kaydet</button>
</div>
</form>
</div>
</div>
</div>
Müfredatımızın
bir sonraki aşaması olan "Validations - Server Side and Client Side Validations" (Doğrulamalar - Sunucu ve İstemci Tarafı
Doğrulamaları) bölümüyle kurumsal mimarimizi örmeye devam ediyoruz.
Bir
web uygulamasında kullanıcıdan gelen verilerin güvenliği ve tutarlılığı iki
aşamalı doğrulanmalıdır:
Bu
katmanda; Data Annotations kullanarak gelişmiş
sunucu kontrollerini kurgulayacak, .NET'in kurumsal
yeteneklerinden biri olan Custom Validation Attribute (Özel
Doğrulama Özniteliği) mimarisini inşa edecek ve sayfa yenilenmeden anlık
çalışan jQuery Unobtrusive
İstemci Doğrulama scriptlerini uçtan uca entegre
edeceğiz.
BÖLÜM 10: GELİŞMİŞ SUNUCU VE İSTEMCİ TARAFI DOĞRULAMA
(VALIDATIONS) MİMARİSİ
KISIM 1: Custom Validation Attribute (Özel
Doğrulama) Yazımı
Müfredatın
"Custom Validations"
başlığı doğrultusunda, hazır kütüphanelerde bulunmayan kurumsal bir iş kuralını
uygulayacak özel bir öznitelik yazıyoruz.
// Validations/NotContainUsernameAttribute.cs
using System.ComponentModel.DataAnnotations;
public class NotContainUsernameAttribute : ValidationAttribute
{
private readonly string _usernamePropertyName;
public NotContainUsernameAttribute(string usernamePropertyName)
{
_usernamePropertyName
= usernamePropertyName;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
//
Formdan gelen şifre değeri
var
password = value as string;
//
Formdaki Username property'sine
yansıma (Reflection) ile erişiyoruz
var
usernameProperty = validationContext.ObjectType.GetProperty(_usernamePropertyName);
if (usernameProperty
!= null)
{
var
usernameValue = usernameProperty.GetValue(validationContext.ObjectInstance, null) as string;
if (!string.IsNullOrEmpty(password)
&& !string.IsNullOrEmpty(usernameValue))
{
// Şifre kullanıcı adını içeriyorsa hata fırlat
if (password.Contains(usernameValue, StringComparison.OrdinalIgnoreCase))
{
return new
ValidationResult(ErrorMessage
?? "Şifre, kullanıcı adınızı içeremez!");
}
}
}
return ValidationResult.Success;
}
}
KISIM 2: Görünüm Modellerinin Data Annotations
ile Donatılması
Mevcut
üyelik modellerimizi (Kayıt ve Giriş) hem hazır .NET doğrulama kurallarıyla hem
de yazdığımız özel öznitelikle (custom attribute) en üst düzeyde yapılandırıyoruz.
1. Güncellenmiş RegisterViewModel
// Models/ViewModels/RegisterViewModel.cs
using System.ComponentModel.DataAnnotations;
public class RegisterViewModel
{
[Required(ErrorMessage = "Kullanıcı adı alanı boş bırakılamaz.")]
[StringLength(20, MinimumLength
= 4, ErrorMessage = "Kullanıcı adı en az 4, en fazla 20 karakter
olmalıdır.")]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage
= "Kullanıcı adı sadece harf, rakam ve alt
çizgi (_) içerebilir.")]
public string Username { get;
set; }
[Required(ErrorMessage = "E-posta adresi alanı zorunludur.")]
[EmailAddress(ErrorMessage = "Geçerli bir e-posta adresi formatı giriniz.")]
public string Email { get;
set; }
[Required(ErrorMessage = "Şifre alanı boş bırakılamaz.")]
[DataType(DataType.Password)]
[StringLength(100, MinimumLength
= 8, ErrorMessage = "Şifreniz güvenlik protokolleri gereği en az 8 karakter
olmalıdır.")]
[NotContainUsername("Username", ErrorMessage = "Güvenlik uyarısı: Şifreniz kullanıcı adınızı
içeremez!")]
// Custom Attribute
public string Password { get;
set; }
[Required(ErrorMessage = "Şifre tekrar alanı zorunludur.")]
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "Girdiğiniz şifreler birbiriyle eşleşmiyor.")]
public string ConfirmPassword { get; set; }
}
2. Güncellenmiş LoginViewModel
// Models/ViewModels/LoginViewModel.cs
using System.ComponentModel.DataAnnotations;
public class LoginViewModel
{
[Required(ErrorMessage = "E-posta adresi girilmesi zorunludur.")]
[EmailAddress(ErrorMessage = "Geçerli bir e-posta formatı giriniz.")]
public string Email { get;
set; }
[Required(ErrorMessage = "Şifre alanı boş bırakılamaz.")]
[DataType(DataType.Password)]
public string Password { get;
set; }
public string? ReturnUrl { get; set;
}
}
KISIM 3: Sunucu Tarafı Kontrolleri (AccountController
Güncellemesi)
Kullanıcı
formu gönderdiğinde, Controller katmanında ModelState.IsValid komutuyla kuralların
çiğnenip çiğnenmediği asenkron kontrol edilir. Eğer bir ihlal varsa sunucu
sayfayı işlemden geçirmez, hataları arayüze geri postalar.
// Controllers/AccountController.cs içerisindeki Register
ve Login POST metotlarının güncel hali
[HttpPost]
public async Task<IActionResult>
Register(RegisterViewModel registerViewModel)
{
//
SUNUCU TARAFIDAKİ DOĞRULAMA KONTROLÜ (SERVER-SIDE VALIDATION)
if (!ModelState.IsValid)
{
//
Model kurallara uymuyorsa, doldurulan veriler ve hata mesajlarıyla birlikte
sayfayı aynen geri dön
return View(registerViewModel);
}
var
user = new
IdentityUser
{
UserName = registerViewModel.Username,
Email = registerViewModel.Email
};
var
identityResult = await _userManager.CreateAsync(user, registerViewModel.Password);
if (identityResult.Succeeded)
{
var
roleResult = await _userManager.AddToRoleAsync(user, "User");
if (roleResult.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
}
foreach (var error in
identityResult.Errors)
{
ModelState.AddModelError("", error.Description);
}
return View(registerViewModel);
}
[HttpPost]
public async Task<IActionResult>
Login(LoginViewModel loginViewModel)
{
//
SUNUCU TARAFIDAKİ DOĞRULAMA KONTROLÜ (SERVER-SIDE VALIDATION)
if (!ModelState.IsValid)
{
return View(loginViewModel);
}
var
user = await _userManager.FindByEmailAsync(loginViewModel.Email);
if (user != null)
{
var
signInResult = await _signInManager.PasswordSignInAsync(user, loginViewModel.Password, isPersistent: false, lockoutOnFailure: true);
if (signInResult.Succeeded)
{
if (!string.IsNullOrEmpty(loginViewModel.ReturnUrl)
&& Url.IsLocalUrl(loginViewModel.ReturnUrl))
{
return Redirect(loginViewModel.ReturnUrl);
}
return RedirectToAction("Index", "Home");
}
if (signInResult.IsLockedOut)
{
ModelState.AddModelError("", "Çok fazla hatalı
deneme yapıldı! Hesabınız geçici olarak kilitlenmiştir.");
return View(loginViewModel);
}
}
ModelState.AddModelError("", "E-posta veya
şifre hatalı.");
return View(loginViewModel);
}
KISIM 4: İstemci Tarafı Doğrulaması ve jQuery
Entegrasyonu
İstemci
tarafında sayfa yenilenmeden anlık uyarıları tetiklemek için Microsoft'un Unobtrusive Validation
mimarisini kullanıyoruz. Bu kütüphane, C# tarafında yazdığımız Data Annotations kurallarını otomatik olarak HTML5 data-val-* özniteliklerine dönüştürür
ve tarayıcıda çalıştırır.
1. Doğrulama Scriptleri Kısmi
Görünümü (Partial View)
Projedeki
Views/Shared/_ValidationScriptsPartial.cshtml dosyasının içeriğinin şu jQuery CDN kütüphanelerini barındırdığından emin oluyoruz:
<!-- Views/Shared/_ValidationScriptsPartial.cshtml -->
<script src="https://cloudflare.com"></script>
<script src="https://cloudflare.com"></script>
2. Gelişmiş Kayıt Sayfası Arayüzü (Views/Account/Register.cshtml)
Form
elemanlarının altına yerleştirdiğimiz <span asp-validation-for="..."> etiketleri, jQuery tetiklendiğinde hata mesajlarının basılacağı
alanlardır. Sayfanın en altındaki Section Scripts bloğu ise kütüphaneleri
sayfaya asenkron dahil eder.
@model
WebApplication.Models.ViewModels.RegisterViewModel
<div class="container py-5">
<div
class="row justify-content-center">
<div
class="col-md-6
col-lg-5">
<div
class="card shadow border-0 rounded-3
p-4">
<h3 class="text-center fw-bold
mb-4">Sisteme
Kayıt Ol</h3>
<div
asp-validation-summary="ModelOnly" class="alert alert-danger
small shadow-sm d-none" id="summaryAlert"></div>
<form method="post">
<div class="mb-3">
<label class="form-label fw-semibold" asp-for="Username">Kullanıcı Adı</label>
<input type="text" class="form-control shadow-xs" asp-for="Username" placeholder="Kullanıcı adınızı seçin" />
<!-- Anlık hata uyarı alanı -->
<span asp-validation-for="Username" class="text-danger small
mt-1 d-block fw-semibold"></span>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" asp-for="Email">E-posta Adresi</label>
<input type="email" class="form-control shadow-xs" asp-for="Email" placeholder="ornek@domain.com" />
<span asp-validation-for="Email" class="text-danger small
mt-1 d-block fw-semibold"></span>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" asp-for="Password">Şifre</label>
<input type="password" class="form-control shadow-xs" asp-for="Password" placeholder="Güçlü bir şifre girin" />
<span asp-validation-for="Password" class="text-danger small
mt-1 d-block fw-semibold"></span>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" asp-for="ConfirmPassword">Şifre Tekrar</