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:

  1. Microsoft.EntityFrameworkCore.SqlServer (SQL Server bağlantısı için)
  2. Microsoft.EntityFrameworkCore.Tools (Taşıma/Migration komutlarını çalıştırmak için)

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:

  1. Öne çıkan görsel alanına dosya seçildiğinde arka plandaki api/images API'mize asenkron FormData gönderir, dönen URL'i gizli input alanına yazar ve önizlemeyi açar.
  2. contentEditor alanını Froala Editor'e dönüştürür ve editör içi zengin görsel yükleme rotasını bizim API endpoint'imize bağlar.

<!-- 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. Microsoft.AspNetCore.Identity.EntityFrameworkCore
  2. Microsoft.EntityFrameworkCore.SqlServer

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:

  1. İstemci Tarafı (Client-Side): Kullanıcı henüz formu göndermeden, tarayıcı üzerinde JavaScript/jQuery yardımıyla yapılan anlık kontrollerdir. Sunucuya gereksiz istek gitmesini engeller ve kullanıcı deneyimini maksimize eder.
  2. Sunucu Tarafı (Server-Side): Kullanıcı istemci kontrollerini bypass etse bile (örneğin JavaScript'i kapatarak veya Postman ile doğrudan istek atarak) sunucuya ulaşan verilerin C# katmanında tekrar denetlenmesidir. Güvenliğin asıl kalbidir.

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</