Yazar: gokhan

  • ElasticSearch’te Collapse ile Sonuçları Tekilleştirme (E-Ticaret Varyasyon Senaryosu)

    ElasticSearch’te Collapse ile Sonuçları Tekilleştirme (E-Ticaret Varyasyon Senaryosu)

    Elasticsearch ile bir e-ticaret sitesinin arama deneyimini iyileştirmeye çalışırken fark ettiğim bir sorunu sizlerle de paylaşmak istiyorum. Bu sayede “collapse” metodunu da birlikte incelemiş olacağız.

    Sorun:

    Varyasyonların bulunduğu bir e-ticaret sitesinde, özellikler varyasyonlar üzerinden ilişkilendirilir, bu nedenle aramalar da varyasyonlar üzerinden gerçekleştirilir. Ama arama sonuçları genelde ürünler üzerinden listelenir.

    Örnek:

    • Nike Air Max – Kırmızı – 42
    • Nike Air Max – Kırmızı – 43
    • Nike Air Max – Siyah – 42

    Arama sonuçlarımızda bu şekilde varyasyonlar oluşur.

    Ama biz şunu isteriz:

    • Tek ürün kartı
    • İçinde varyasyonlar

    Örnek varyasyon veri modeli:

    JSON
    {
      "id": "variant_1",
      "product_id": 1001,
      "product_name": "Nike Air Max",
      "color": "Kırmızı",
      "size": 42,
      "price": 3500,
      "stock": 5
    }
    JSON
    {
      "id": "variant_2",
      "product_id": 1001,
      "product_name": "Nike Air Max",
      "color": "Kırmızı",
      "size": 43,
      "price": 3500,
      "stock": 3
    }

    Bu şekilde 100’lerce ürün/varyasyon olduğunu varsayalım. Kullanıcı nın “nike ayakkabılar” aramasını gerçekleştirdiğini düşünelim.

    Bu durumda tüm ürünler arama sonuçlarında tekrar edecektir. Bunu önlemek için kullanacağımız özellik “collapse” özelliğidir.

    Örnek Düzenlenmiş Sorgu:

    JSON
    GET products/_search
    {
      "query": {
        "match": {
          "product_name": "nike"
        }
      },
      "collapse": {
        "field": "product_id"
      }
    }

    “collapse” özelliğini kullanarak arama sonuçlarında ne kadar tekrar ederse etsin aynı üründen 1 varyasyon göstermesini sağladık.

    • Ürünler product_id’ye göre gruplanmış oldu.
    • Her üründen bir sonuç göründü.

    Ek olarakk varyasyonları da ilişkili olarak getirmek istersek “inner_hits” kullanabiliriz.

    Böylelikle her ürün için bir ana ürün ve altında da sonuçlarla ilişkili varyasyonlar olacaktır.

    JSON
    GET products/_search
    {
      "query": {
        "match": {
          "product_name": "nike"
        }
      },
      "collapse": {
        "field": "product_id",
        "inner_hits": {
          "name": "variants",
          "size": 5,
          "sort": [
            { "price": "asc" }
          ]
        }
      }
    }

    Ama bu ekstra performans kaybına yol açacaktır. Dikkatli kullanmalıyız.

    Bunlara dikkat edelim:

    • “collapse” sadece keyword, numeric alanlarla çalışır.
    • “text” alanları ile çalışmaz
    • “doc_values” açık olmalıdır.

    Bu yazımızda bir elastic search sorgusunu nasıl gruplarız ve tekrar eden sonuçlardan kurtuluruz bundan bahsettim. Bir sonraki yazıda görüşmek üzere.

  • Son ürünü iki kişinin aynı anda satın alması nasıl engellenir?

    Son ürünü iki kişinin aynı anda satın alması nasıl engellenir?

    Bir e-ticaret sitesi geliştirdiğinizi düşünelim, hangi dilde olduğu fark etmez, güncel teknolojiler kullandığınızı varsayalım.

    Trafik artmaya başladı ve ürün satışları da doğal olarak artıyor, ve zaman zaman özellikle düşük stoklu ürünlerde olmayan stokların satışını fark ettiniz.

    Stok’da son ürün kaldığında birden fazla kişinin bu ürünü satın aldığını fark ettiniz.

    Bunun literatürdeki adı race condition ve overselling şeklinde.

    Bu yazıda bu tarz bir durumda neler yapılabilir bundan bahsedeceğim, özellikle veritabanı seviyesine odaklanacağız. Bu operasyonları güvenli şekilde gerçekleştirmemiz için aşağıdaki veritabanlarından birini kullandığınızı varsayıyorum.

    Önerilen veritabanları

    • MSSQL
    • MYSQL
    • POSTGRESQL

    Bu veritabanlarının hepsi bu yazıda işleyeceğimiz özellikleri destekleyen SQL veritabanı yazılımlarıdır.

    Tüm yöntemlerde transaction açmanız ve başarılı olursa commit etmeniz gerektiğini hatırlatmak isterim, her maddede bunu tekrar etmemek için baştan söylüyorum.

    Hemen yöntemlere geçelim.

    Database Transaction + Lock

    Bu yöntem, çözümlerimiz arasında en çok kullanılan ve klasik bir yöntem diyebiliriz.

    Bu yöntemin temel mantığı veritabanında işlem yapmadan önce bu işlemi yaptığımız satırlarda başkası işlem yapmasın demek oluyor.

    Örnek SQL

    SQL
    SELECT * FROM products WHERE id = 1 FOR UPDATE;

    İşlemin başında satır kilitlenir.

    Diğer kullanıcılar işlemin bitmesini beklerler.

    İlk işlem bitince sıradaki işlem ile devam edilir.

    Yoğun trafikte yavaşlatır.

    Deadlock riski yaratır.

    Kullanım örneği ( Temsili kod SQL)

    SQL
    BEGIN TRANSACTION
    
    SELECT stock FROM products WHERE id=1 FOR UPDATE
    
    IF stock > 0:
        UPDATE products SET stock = stock - 1
        COMMIT
    ELSE:
        ROLLBACK

    Optimistic Lock

    Bu yöntem benim de en sevdiğim ve bir çok noktada bolca kullandığım bir yöntem, Optimistic Lock yöntemi yani versiyon sütunu eklemek.

    Ben sadece stok değil şifre değiştirme gibi durumlarda da bu sistemi kullanıyorum, veri tutarlılığını özellikle JWT üzerinden kontrol etmek için en garanti yöntem ama tabi bu yazının konusu JWT değil.

    Burada kullanacağımız mantık çok basit, bir version ve updated_at sütunu ile verinin sorguladığımız andaki ve güncellediğimiz andaki tutarlılığını kontrol ediyoruz.

    SQL
    UPDATE products
    SET stock = stock - 1, version = version + 1
    WHERE id = 1 AND version = 5;

    Dediğim gibi bir çok noktada kullanılabilecek bir yöntem. Farklı senaryolarda da ilaç gibi geliyor.

    Tek sıkıntısı çakışma durumunda işlemi iptal etmekden başka çaremiz yok.

    Atomic Update

    SQL
    UPDATE products
    SET stock = stock - 1
    WHERE id = 1 AND stock > 0;

    Burada da aslında benzer bir mantık, stok sıfır olmadığı sürece bir azaltmasını rica ediyoruz veritabanından. Bu çok da basit bir yöntem ben çok tercih etmiyorum ama tabi siz bilirsiniz.

    Yazımızın genelinden de anlayabileceğiniz gibi amacımız veritabanımızın sağladığı özelliklere öncelik vermek.

    Şimdi sırada ise farklı bir yöntem var, ek bir yazılımdan destek alacağız.

    Queue / Message Queue (RabbitMQ vb.)

    Bankacılık uygulamalarına kadar yüksek trafikli tüm işlemlerde bu yöntem kullanılır, Siparişler sıraya alınır ve arkaplanda kullanıcıyı da bekletmeden işlenir. Sonrasında kullanıcıya gerekli bildirimler yapılır.

    • Sipariş bekleme listesine alınır
    • Kulalnıcıya siparişinin onay beklediği ekran gösterilir
    • Arkaplanda sipariş işlenir (QUEUE)
    • Kullanici tarayici veya mail yoluyla bilgilendirilir/yönlendirilir

    Rezervasyon Sistemi

    Bir başka yöntem de kullanıcılar için sepete eklendiğinde rezerve stoklar atamaktır, bunun örneği bu yazımız için çok uzun olacak o yüzden çok ayrıntıya girmiyorum.

    • Kullanıcı sepete ekler
    • Sepete ekleme ile birlikte bir stok tutucu oluşturulur ve ürün bu kullancıya rezerve edilir bir süreliğine.
    • kullanıcı satın alma sırasında yine stok kontrolü ile birlikte satın alır
    • kullanıcı belli bir süre satın almazsa stok serbest bırakılır.

    Sonuç

    Benim önerim kesinlikle QUEUE sistemleri de içinde barındıran hibrit bir sistem.

    Örneğin Rabbit MQ + Database Transaction + Lock

    • Önce sipariş queue ye atılır.
    • Kullanıcı bekleme ekranına gönderilir.
    • Optimistik Lock + Database Transaction Lock ile birlikte sipariş işlenir.
    • Kullanıcı bilgilendirir.

  • #2 Gelişme: JWT + SignalR ile güvenli sohbet odası ve direk mesajlaşma

    #2 Gelişme: JWT + SignalR ile güvenli sohbet odası ve direk mesajlaşma

    Önceki yazımda, bir .NET Web API projesine basit şekilde SignalR entegre etmiş ve temel bir sohbet odası oluşturmuştuk. Oradaki amacımız, temel WebSocket mantığını ve .NET Web API proje yapısını anlamaktı. Bu yazıda ise bir adım daha ileri gidip JWT ile kimlik doğrulama, oturum yönetimi ve yetki kontrolü ekleyeceğiz.

    Hadi sıfırdan, adım adım başlayalım!

    Paketler

    Öncelikle bir .net core web api projesi oluşturuyorum ve signalR paketini ve gerekli JWT paketlerini ekliyorum.

    Visual studio ve nuget paket yöneticisini kullanarak kolayca yapabilirsiniz, konsol örneklerini de buraya bırakıyorum.

    Bash
    #Proje oluşturuyoruz
    dotnet new webapi -n SignalRJwtChatApi
    # proje dizinine geçiyoruz
    cd SignalRJwtChatApi
    # Gerekli paketler 1 signalR
    dotnet add package Microsoft.AspNetCore.SignalR
    # Gerekli paketler 2 JWT
    dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

    Gerekli paketleri ister nuget paket yöneticisi isterseniz konsoldan kurduktan sonra test ediyoruz.

    Bash
    dotnet run

    Proje Yapısı

    Öncelikle proje/dosya yapımızı oluşturalım.

    • Controllers
    • Hubs
    • Entities
    • DTOs
    • Services

    Konumuz veritabanı veya katmanlı mimari olmadığı için daha fazla katman oluşturmayacağım, bu şekilde bir örnek JWT ve SignalR kullanımını anlamamız için yeterli.

    Burada en farklı yapı Hubs dizini olacaktır, SignalR ile ilgili konfigürasyonu / ayarları burada tutacağız, onun dışında veritabanına ihtiyacımız yok şimdilik.

    Şimdilik kullanıcı bilgisini Liste olarak DI içinde tutacağız. Programı durdurunca tüm bilgi silinecek fakat bu örnek için gayet yeterli.

    Şimdı sırayla dosyalarımızı oluşturalım.

    Modeller

    Entities/User.cs

    C#
    namespace SignalRJwtChatApi.Entities;
    
    public class User
    {
        public string Id { get; set; } = Guid.NewGuid().ToString();
        public string Username { get; set; } = string.Empty;
        public string Password { get; set; } = string.Empty;
        public string Role { get; set; } = "User";
    }

    Bu model:

    Sistemimideki kullanıcıyı temsil edecek, ileride DB tablosu olarak da kullanabiliriz fakat şimdilik sadece bir kontrat gibi düşünebilirsiniz.

    DTOs

    DTO’lar request’ler de taşıyacağımız veriyi temsil edecek bizim için.

    Öncelikle kullanıcıların sisteme kaydı ile başlıyoruz.

    DTOs/RegisterRequest.cs

    C#
    namespace SignalRJwtChatApi.DTOs;
    
    public class RegisterRequest
    {
        public string Username { get; set; } = string.Empty;
        public string Password { get; set; } = string.Empty;
    }

    DTOs/LoginRequest.cs

    C#
    namespace SignalRJwtChatApi.DTOs;
    
    public class LoginRequest
    {
        public string Username { get; set; } = string.Empty;
        public string Password { get; set; } = string.Empty;
    }

    DTOs/AuthResponse.cs

    C#
    namespace SignalRJwtChatApi.DTOs;
    
    public class AuthResponse
    {
        public string Token { get; set; } = string.Empty;
        public string Username { get; set; } = string.Empty;
    }

    Kullanıcı girişi için kullanacağımız response sınıfımız, giriş sonrası veya kayıt sonrası bu şekilde bir response döneceğiz

    Servisler

    Sırada servis sınıflarımız var, bu sınıflar uygulamamızda iş parçacıklarını temsil ediyor, controller’ın şişmemesi ve karmaşıklaşmaması için oluşturduğumuz ara sınıflar.

    Services/UserService.cs

    C#
    using SignalRJwtChatApi.Entities;
    
    namespace SignalRJwtChatApi.Services;
    
    public class UserService
    {
        private readonly List<User> _users = new();
    
        public User? GetByUsername(string username)
        {
            return _users.FirstOrDefault(x => 
                x.Username.Equals(username, StringComparison.OrdinalIgnoreCase));
        }
    
        public User? GetById(string id)
        {
            return _users.FirstOrDefault(x => x.Id == id);
        }
    
        public User Create(string username, string password, string role = "User")
        {
            var user = new User
            {
                Username = username,
                Password = password,
                Role = role
            };
    
            _users.Add(user);
            return user;
        }
    
        public bool ValidateCredentials(string username, string password)
        {
            return _users.Any(x =>
                x.Username.Equals(username, StringComparison.OrdinalIgnoreCase) &&
                x.Password == password);
        }
    }

    Kullanıcı Girişi

    Sırada kullanıcının işlemler için ulaşacağı endpointler var.

    Controllers/AuthController.cs

    C#
    using Microsoft.AspNetCore.Mvc;
    using SignalRJwtChatApi.DTOs;
    using SignalRJwtChatApi.Services;
    
    namespace SignalRJwtChatApi.Controllers;
    
    [ApiController]
    [Route("api/[controller]")]
    public class AuthController : ControllerBase
    {
        private readonly UserService _userService;
        private readonly JwtService _jwtService;
    
        public AuthController(UserService userService, JwtService jwtService)
        {
            _userService = userService;
            _jwtService = jwtService;
        }
    
        [HttpPost("register")]
        public IActionResult Register(RegisterRequest request)
        {
            var existingUser = _userService.GetByUsername(request.Username);
    
            if (existingUser is not null)
                return BadRequest("Bu kullanıcı adı zaten kullanılıyor.");
    
            var user = _userService.Create(request.Username, request.Password);
    
            return Ok(new
            {
                message = "Kullanıcı oluşturuldu",
                user.Id,
                user.Username,
                user.Role
            });
        }
    
        [HttpPost("login")]
        public IActionResult Login(LoginRequest request)
        {
            var isValid = _userService.ValidateCredentials(request.Username, request.Password);
    
            if (!isValid)
                return Unauthorized("Kullanıcı adı veya şifre hatalı");
    
            var user = _userService.GetByUsername(request.Username)!;
            var token = _jwtService.GenerateToken(user);
    
            return Ok(new AuthResponse
            {
                Token = token,
                Username = user.Username
            });
        }
    }

    Şimdi sırada Program.cs ve SignalR konfigürasyonu var.

    Program.cs

    Paketleri dahil ediyoruz.

    C#
    // öncelikle kullandığımız paketleri dahil ediyoruz. projemizin en üstüne
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.IdentityModel.Tokens;
    using System.Text;
    using SignalRJwtChatApi.Services;
    

    Servislerimizi kaydediyoruz

    C#
    builder.Services.AddSingleton<UserService>();
    builder.Services.AddScoped<JwtService>();

    JWT Authentication ayarlarını ekliyoruz.

    C#
    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!);
    
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
    
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(key)
        };
    
        // SignalR için kritik kısım
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
    
                var path = context.HttpContext.Request.Path;
    
                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/chatHub"))
                {
                    context.Token = accessToken;
                }
    
                return Task.CompletedTask;
            }
        };
    });

    Normalde JWT header’dan gelir ama biz SignalR kullandığımız için Query String den alıyoruz.
    context.Request.Query[“access_token”]

    bu kısım bunu temsil ediyor.

    Authorization Ekleyelim

    C#
    builder.Services.AddAuthorization();

    Sırada SignalR eklemek var

    C#
    builder.Services.AddSignalR();

    Middleware’leri aktif edelim

    C#
    app.UseAuthentication();
    app.UseAuthorization();

    Hub endpointlerini açalım

    C#
    app.MapHub<ChatHub>("/chatHub");

    Henüz hub oluşturmadık ama Program.cs de eklememiz gerekenleri şimdilik ekleyelim.

    Program.cs son hali:

    C#
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.IdentityModel.Tokens;
    using System.Text;
    using SignalRJwtChatApi.Services;
    using SignalRJwtChatApi.Hubs;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers();
    
    builder.Services.AddSingleton<UserService>();
    builder.Services.AddScoped<JwtService>();
    
    builder.Services.AddSignalR();
    
    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!);
    
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
    
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(key)
        };
    
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
    
                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/chatHub"))
                {
                    context.Token = accessToken;
                }
    
                return Task.CompletedTask;
            }
        };
    });
    
    builder.Services.AddAuthorization();
    
    var app = builder.Build();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapControllers();
    app.MapHub<ChatHub>("/chatHub");
    
    app.Run();

    SignalR & Hub Kısmı

    SignalR da kullanıcıların birer id ye sahip olması gerekli o nedenle bir ID provider oluşturmamız gerekitor.

    Services/CustomUserIdProvider.cs

    C#
    using System.Security.Claims;
    using Microsoft.AspNetCore.SignalR;
    
    namespace SignalRJwtChatApi.Services;
    
    public class CustomUserIdProvider : IUserIdProvider
    {
        public string? GetUserId(HubConnectionContext connection)
        {
            return connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        }
    }

    Program.cs de tanıtalım

    C#
    builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();

    SignalR Hub’u oluşturalım

    Hubs/ChatHub.cs

    C#
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.SignalR;
    using System.Security.Claims;
    
    namespace SignalRJwtChatApi.Hubs;
    
    [Authorize]
    public class ChatHub : Hub
    {
        // ROOM JOIN
        public async Task JoinRoom(string roomName)
        {
            var username = Context.User?.Identity?.Name;
    
            await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
    
            await Clients.Group(roomName)
                .SendAsync("UserJoined", $"{username} joined {roomName}");
        }
    
        // ROOM LEAVE
        public async Task LeaveRoom(string roomName)
        {
            var username = Context.User?.Identity?.Name;
    
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
    
            await Clients.Group(roomName)
                .SendAsync("UserLeft", $"{username} left {roomName}");
        }
    
        // ROOM MESSAGE
        public async Task SendMessageToRoom(string roomName, string message)
        {
            var username = Context.User?.Identity?.Name;
            var userId = Context.UserIdentifier;
    
            await Clients.Group(roomName)
                .SendAsync("ReceiveMessage", new
                {
                    userId,
                    username,
                    roomName,
                    message,
                    sentAt = DateTime.UtcNow
                });
        }
    
        // PRIVATE MESSAGE
        public async Task SendPrivateMessage(string targetUserId, string message)
        {
            var username = Context.User?.Identity?.Name;
            var senderId = Context.UserIdentifier;
    
            await Clients.User(targetUserId)
                .SendAsync("ReceivePrivateMessage", new
                {
                    fromUserId = senderId,
                    fromUsername = username,
                    message,
                    sentAt = DateTime.UtcNow
                });
        }
    }


    Test & FrontEnd

    Frontend kısmı (test için)

    C#
    <!DOCTYPE html>
    <html>
    <head>
        <title>SignalR JWT Chat</title>
    </head>
    <body>
    
    <h2>Login</h2>
    <input id="username" placeholder="username" />
    <input id="password" placeholder="password" />
    <button onclick="login()">Login</button>
    
    <hr/>
    
    <h2>Chat</h2>
    
    <input id="room" placeholder="room name" />
    <button onclick="joinRoom()">Join Room</button>
    
    <br/><br/>
    
    <input id="message" placeholder="message" />
    <button onclick="sendRoomMessage()">Send Room Message</button>
    
    <br/><br/>
    
    <input id="targetUserId" placeholder="target userId" />
    <input id="privateMessage" placeholder="private message" />
    <button onclick="sendPrivate()">Send Private Message</button>
    
    <hr/>
    
    <h3>Logs</h3>
    <ul id="logs"></ul>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.5/signalr.min.js"></script>
    
    <script>
        let token = "";
        let connection = null;
    
        function log(msg) {
            const li = document.createElement("li");
            li.innerText = msg;
            document.getElementById("logs").appendChild(li);
        }
    
        async function login() {
            const username = document.getElementById("username").value;
            const password = document.getElementById("password").value;
    
            const res = await fetch("https://localhost:5001/api/auth/login", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ username, password })
            });
    
            const data = await res.json();
            token = data.token;
    
            log("Login başarılı, token alındı");
    
            connectSignalR();
        }
    
        function connectSignalR() {
            connection = new signalR.HubConnectionBuilder()
                .withUrl("https://localhost:5001/chatHub", {
                    accessTokenFactory: () => token
                })
                .withAutomaticReconnect()
                .build();
    
            // ROOM mesajı
            connection.on("ReceiveMessage", (msg) => {
                log(`[ROOM] ${msg.username}: ${msg.message}`);
            });
    
            // PRIVATE mesaj
            connection.on("ReceivePrivateMessage", (msg) => {
                log(`[PRIVATE] ${msg.fromUsername}: ${msg.message}`);
            });
    
            connection.on("UserJoined", (msg) => log(msg));
            connection.on("UserLeft", (msg) => log(msg));
    
            connection.start()
                .then(() => log("SignalR bağlandı"))
                .catch(err => console.error(err));
        }
    
        function joinRoom() {
            const room = document.getElementById("room").value;
            connection.invoke("JoinRoom", room);
        }
    
        function sendRoomMessage() {
            const room = document.getElementById("room").value;
            const message = document.getElementById("message").value;
    
            connection.invoke("SendMessageToRoom", room, message);
        }
    
        function sendPrivate() {
            const targetUserId = document.getElementById("targetUserId").value;
            const message = document.getElementById("privateMessage").value;
    
            connection.invoke("SendPrivateMessage", targetUserId, message);
        }
    </script>
    
    </body>
    </html>
  • #1 Giriş : ASP.NET Core + SignalR Kullanımı, Sohbet Odası, Gerçek Zamanlı İşlemler ( Realtime Web Socket)

    #1 Giriş : ASP.NET Core + SignalR Kullanımı, Sohbet Odası, Gerçek Zamanlı İşlemler ( Realtime Web Socket)

    Merhaba bu yazı dizisinin konusu ASP.NET CORE ve SignalR kullanarak gerçek zamanlı işlemleri gerçekleştirmek. SignalR ASP.NET dünyasında çokça kullanılan basit ve kullanışlı bir websocket paketi. Bu yazı dizisinin amacı baştan sona bir mesajlaşma sistemi geliştirmek fakat tabiki ilk yazımızın konusu daha basit olacak.


    Websocket gerçek zamanlı işlemler için en iyi çözüm olan web protokollerinden biri ve websitelerde bildirim & mesajlaşma gibi bir çok olayda kullanılıyor.

    HTTP ve WebSocket arasındaki fark nedir?

    Çok önceden geliştirdiğim uygulamalarda gerçek zamanlı verileri almak için saniyede bir HTTP isteği atar ve güncel veriyi çekerdim 😊

    Tabiki bu yanlış bir yöntem ve gereksiz bir işlem. Sunucuya yük bindiriyor. Bunun yerine olay olunca haberdar edilen iki yönlü bir protokol olan WebSocket’i tercih etmek çok daha mantıklı.

    WebSocket iki yönlü bir iletişim protokolü, olay olduğunda bağlı olan client’lerin hepsini veya istediğimiz client’i bilgilendirebilmemizi sağlıyor.

    Öncelikle bu yazıda amacımız temel leri öğrenmek, yani signalr ile websocket bağlantısını kurmak ve chatroom mantığıyla yani gönderdiğimiz mesajın herkes’e ulaştığı bir senaryoyu gerçekleştirmek.

    Gereksinimler

    • İsteyen kullanıcı odaya girebilir, herhangi bir kimlik belirtmek zorunda değildir.
    • Giren kullanıcı mesaj gönderebilir ve bu mesaj herkese iletilir.

    İlk aşamada bu şekilde basit tutacağım fakat ilerleyen yazılarda tam işlevli ve güvenli bir mesajlaşma sistemine dönüştüreceğim.

    Back-End Kısmı

    Kullanılacak Paketler

    Öncelikle ASP.NET Core projemizde signalr paketimizi nuget ile projemize dahil ediyoruz. Ben consol komutunu palaşacağım ama siz paket yöneticisini de kullanabilirsiniz.

    Bash
    dotnet add package Microsoft.AspNetCore.SignalR

    Ben .NET 10 için hazırlıyorum bu yazıyı.

    Projede WEB.API namespace kullanacağım.

    Hub (Bağlantı Noktası) Oluşturma

    Paketimizi yüklediğimize göre öncelikle websocket uygulamamız için bir bağlantı noktası oluşturmalıyız. Bunun için SignalR da hazır gelen Hub sınıfını kullanacağım.Bu sınıfı kalıtım yaparak kendi Hub yani bağlantı noktamızı oluşturacağız ve içerisine metodlarımızı ekleyeceğiz.

    Hubs/MessageHub.cs Son hali

    C#
    using Microsoft.AspNetCore.SignalR;
    
    namespace WEB.API.Hubs
    {
        public class MessageHub : Hub
        {
        }
    }
    

    Burada basitçe Hub yani bağlantı noktası tanımladık, içi boş kalabilir.

    Kritik satır:

    Hub oluşturduğumuza göre sırada bu hub’u Program.cs dosyamız üzerinden DI’ yapısına kaydetmek var.

    Servis Kaydı ve Aktifleştirme

    Program.cs Son hali

    C#
    using WEB.API.Hubs;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
    builder.Services.AddOpenApi();
    builder.Services.AddSignalR();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.UseStaticFiles(); // ndex.html dosyamızın tanınması için statik dosya desteğini aktifleştirdik
    
    app.MapHub<MessageHub>("/hubs/message-room"); // bağlantı noktamızı aktifleştirdik.
    
    app.Run();
    

    Kritik Noktalar:

    Burada kritik satırlar öncelikle servis kaydı satırı. Bu satır ile SignalR servisini kaydettik.

    C#
    builder.Services.AddSignalR(); // SignalR servisini kaydettik.

    İkinci kritik satırlar ise statik dosya kullanımını ve hub bağlantı kontasını aktif ettiğimiz satırlar.

    C#
    app.UseStaticFiles();
    
    app.MapHub<MessageHub>("/hubs/message-room");

    Bu sayede tarayıcıda room.html dosyasını kullanabileceğiz ve /hubs/message-room bağlantı noktamız websocket bağlantısı için aktif olacak.

    Back – End Mesaj Gönderme Endpointi

    Mesaj gönderirken kullanacağımız DTO’muzu yanımlıyoruz.

    SendMessageDTO.cs

    C#
    namespace WEB.API.DTOs
    {
        public class SendMessageDTO
        {
            public string Message { get; set; }
        }
    }
    

    Controllers/MessageRoomController.cs Son hali

    C#
    using Microsoft.AspNetCore.SignalR;
    using Microsoft.AspNetCore.Mvc;
    using WEB.API.Hubs;
    using WEB.API.DTOs;
    
    namespace WEB.API.Controllers
    {
        [ApiController]
        [Route("api/message-room")]
        public class MessageRoomController : ControllerBase
        {
            private readonly IHubContext<MessageHub> _hubContext;
            public MessageRoomController(IHubContext<MessageHub> hubContext)
            {
                _hubContext = hubContext;
            }
            [HttpPost]
            [Route("create")]
            public async Task<IActionResult> CreateMessage( [FromBody] SendMessageDTO dto)
            {
                await _hubContext.Clients.All
                    .SendAsync("MessageSend",dto.Message);
    
                return Ok(new { Message = "Message Sent" });
            }
    
        }
    }
    

    Kritik noktalar

    DI aracılığıyla tanıttığımız hub’u kullanıyoruz.

    C#
     private readonly IHubContext<MessageHub> _hubContext;
            public MessageRoomController(IHubContext<MessageHub> hubContext)
            {
                _hubContext = hubContext;
            }

    Tüm kullanıcılara mesaj gönderiyoruz:

    C#
    await _hubContext.Clients.All
                    .SendAsync("MessageSend",message);

    Frontend Kismi

    Backend kısmında gerekli değişiklikleri yaptığımıza göre artık front-end kısmına geçebilriz. Kullanıcımız için basit bir mesaj listesi ve mesaj gönderme butonu yapıyoruz. Hemen ardından da official signalr kütüphanesi ile zaten adresini aktifleştirdiğimiz bağlantı noktasına bağlanıyoruz.

    wwwroot/room.html son hali

    HTML
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>Message Room</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/9.0.6/signalr.min.js"></script>
        <script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
    </head>
    <body>
    
    
        <div class="card">
            <h1>Messajlar</h1>
            <ul class="messages">
            </ul>
            <textarea id="messageForm"></textarea>
            <input type="submit" id="sendMessageButton" value="Send Message" />
        </div>
    
        <script>
            const connection = new signalR.HubConnectionBuilder()
                .withUrl("/hubs/message-room")
                .build();
    
            connection.on('MessageSend', function (message) {
                addMessageToList(message);
            });
    
            connection.start();
    
            function addMessageToList(message) {
                $('.messages').append("<li>" + message + "</li>");
    
            }
    
            $('#sendMessageButton').click(function () {
                $.ajax({
                    url: "/api/message-room/create",
                    method: "POST",
                    contentType: "application/json; charset=utf-8",
                    data: JSON.stringify({ message: $("#messageForm").val() }),
                    success: function () {
                        $("#messageForm").val(""); // temizle
                    },
                    error: function (xhr) {
                        console.log("Error:", xhr.status, xhr.responseText);
                    }
                });
            });
        </script>
        <style>
            .card {
                display: flex;
                flex-direction: column;
                gap: 10px;
                max-width: 500px;
                margin: 0 auto;
                background: #ddd;
                border: 1px solid #ccc;
                border-radius: 5px;
                padding: 10px;
                min-height: 100px;
            }
    
            .messages {
                background: #ccc;
                border-radius: 5px;
                padding: 0;
                display: flex;
                flex-direction: column;
                gap: 10px;
            }
    
            .messages li {
                list-style: none;
                padding: 5px;
            }
    
            #sendMessageButton {
                background: #0094ff;
                color: #fff;
                border: 0;
                padding: 10px;
            }
        </style>
    </body>
    </html>


    Kritik Noktalar:

    Bağlantı oluşturma kodu:

    JavaScript
     const connection = new signalR.HubConnectionBuilder()
         .withUrl("/hubs/message-room")
         .build();
    
     connection.on('MessageSend', function (message) {
         addMessageToList(message);
     });

    Bu kod aracılığıyla /hubs/message-room adresine yani odamıza bağlandık ve MessageSend olayını dinliyoruz mesaj geldiği zaman addMessageToList fonksiyonuna aktarıyoruz mesajımızı ve o da mesaj listemize ekliyor.

    Mesaj göndermek için ise:

    JavaScript
     $('#sendMessageButton').click(function () {
         $.post('api/message-room/create', { message: $('#messageForm').val() })
     });

    Butonumuz’a click eventi gönderildiğinde ilgili endpoint aracılığıyla mesajımızı backend’e gönderiyoruz o da tüm kullanıcılar ile paylaşıyor.

    Video Uygulama


    Özet

    Bu yazımızda bisit bir mesajlaşma odası uygulaması geliştirdik.ASP.NET Core Web API + SignalR kullandık. Şimdilik gönderenin bile belli olmadığı bir sistem geliştirdik. İlerleyen yazılarda adım adım bunu ilerleteceğim.

    Projenin son hali github da her bölüm için ayrı branch açacağım.

    Bu yazının branch’ı

    https://github.com/gokhancelebi/ChatRoomExampleProject/tree/bolum-1

  • RabbitMQ Nedir? Hangi Problemi Çözüyor?

    RabbitMQ Nedir? Hangi Problemi Çözüyor?

    RabbitMQ nedir sorusunu sormadan önce Event-driven architecture nedir sorusu ile yazıma başlamak istiyorum. RabbitMQ bu sistemde kullandığımız bir araçtır çünkü.

    Event Driven Architecture Nedir?

    EDA bir yazılım dizayn pattern’i dir ve yazılımımızın event yani olaylara vereceği reaction yani tepkileri vurgulayan ve bu şekilde ölçeklenebilir sistemler dizayn etmemizi sağlar.

    Bu yöntem çok popüler bir yöntemdir ve yazılımınızı alt bölümlere ayırmanızı birbirine bağlı olmadan bu bölümlerin çalışmasını sağlar.

    Projemiz büyüdüğünde bu sistemi geleneksel yazılım tasarımlarından daha iyi yönetilebilir projeler oluşturmamızı sağlar.

    Kısaca yazılımımızdaki olaylara verilecek tepkiler üzerinden sistemimizi tasarlamamıza verilen isimdir.

    Yazılımımızın başka başka parçaları farklı cihazlarda bile çalışabilir ve birbirlerinden haberdar olmalarına çok da gerek kalmaz, örneğin tek bilmeleri gereken yeni bir kullanıcının kaydoluğudur ve burumda gereken işlemi birbirlerinden haberleri olmadan gerçekleştirirler.

    Biz yazılımımızda üye kaydolma event’i gerçekleştiğinde trigger’lanacak sistemleri kaydederiz ve onlar da ayrı ayrı çalışır!

    Bu sistemlerden biri fail olursa diğeri zarar görmeden hayatına devam eder!

    Aynı kaynakları paylaşmadıkları için de birbirlerini bloklamazlar!

    Kullanıcı bir butona bastı ve bu butonun yapması gereken diyelim ki sipariş oluşturmak, fatura oluşturmak, stok düşmek ve aynı zamanda bunları gerekli yerlere mail göndermek diyelim. Bu durumda normal geleneksel uygulamalarda bunları sırayla yapar ve ziyaretçilerimizi bekletiriz. Ama event-driven architecture’da “order.created” event’i order bilgisi ile birlikte bu işlemleri yapan ayrı ayrı sistemlere mesaj gönderir, yeni sipariş oluştu her biriniz üzerinize düşenleri gerçekleştirin der ve kullanıcıya da siparişşin oluşturulduğu cevabını döner, kullanıcı bilgisayarını kapatır arkaplanda sistem faturayı, stok düşme işlemini, email ile bilgilendirme işlemlerini yapar ve ziyaretçinin bundan haberi bile olmaz!

    Evet bu sistemler birbiri ile nasıl haberleşecek sorusu aklına gelmiştir diye düşünüyorum, hemen burada çevreye neredeyse piyasa standartı olmuş bir sistem giriyor. RabbitMQ!

    RabbitMQ Nedir?

    RabbitMQ açık kaynak kodlu bir message broker yazılımıdır, bu event-driven architecture içerisinde sistemlerin birbiri ile iletişim kurmasını sağlar, bir event gerçekleştiğinde bu event’den haberdar olması gereken, o event’e abone olmuş consumer yani tüketicileri haberdar eder.

    RabbitMQ mesajları öncelikle “exchange” dediğimiz depolara gönderir ve ondan sonra da route key lerine göre abonelerine ulaştırır!

    RabbitMQ “First-in, First-out algoritmasına göre çalışır, ilk giren event ilk işlenir,

    RabbitMQ özellikle micro-servis mimarisinde oldukça popülerdir!

  • FluentValidation Global Validation Middleware ile Model Binding Hataları Yakalamak

    FluentValidation Global Validation Middleware ile Model Binding Hataları Yakalamak

    Bir önceki yazımda asp.net core projesine basitçe fluentvalidation ekledim ve request’leri valide ettim. Burada bir ufak sorun var oda model binding hataları, bu şekilde manuel validasyon yapmadan önce DTO’ request body den değerler alır ve nesne oluşturur.

    • Tip uyumsuzluğu
    • Eksik property
    • Farklı veri

    Sebepleri ile bu DTO nesnesi oluşamaz bile ve uygulamamız validasyondan önce crush olur.

    Önceki yazı

    Bunu çözmek için uygulamamızı önceden middleware ile valide etmemiz gerekir ki bunun için ek tek bir class yeterlidir.

    Bu yazıda ek bir middleware ekleyerek global bir validasyon sağlayacağım ve sorunumuz ortadan kalkmış olacak

    📄 Middlewares/ValidationMiddleware.cs

    C#
    using FluentValidation;
    using FluentValidation.Results;
    using System.Text.Json;
    
    namespace FluentValidationDemo.Middlewares
    {
        public class ValidationMiddleware
        {
            private readonly RequestDelegate _next;
    
            public ValidationMiddleware(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task InvokeAsync(HttpContext context, IServiceProvider serviceProvider)
            {
                var method = context.Request.Method;
    
                // Sadece POST ve PUT isteklerde çalışsın
                if (method != HttpMethods.Post && method != HttpMethods.Put)
                {
                    await _next(context);
                    return;
                }
    
                var endpoint = context.GetEndpoint();
                if (endpoint is null)
                {
                    await _next(context);
                    return;
                }
    
                context.Request.EnableBuffering();
                using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
                var body = await reader.ReadToEndAsync();
                context.Request.Body.Position = 0;
    
                if (string.IsNullOrWhiteSpace(body))
                {
                    await _next(context);
                    return;
                }
    
                try
                {
                    var controllerActionDescriptor = endpoint.Metadata
                        .OfType<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>()
                        .FirstOrDefault();
    
                    if (controllerActionDescriptor == null)
                    {
                        await _next(context);
                        return;
                    }
    
                    foreach (var parameter in controllerActionDescriptor.Parameters)
                    {
                        var validatorType = typeof(IValidator<>).MakeGenericType(parameter.ParameterType);
                        var validator = serviceProvider.GetService(validatorType) as IValidator;
                        if (validator == null) continue;
    
                        object? model = null;
                        try
                        {
                            var options = new JsonSerializerOptions
                            {
                                PropertyNameCaseInsensitive = true,
                                NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.Strict
                            };
    
                            model = JsonSerializer.Deserialize(body, parameter.ParameterType, options);
                        }
                        catch (JsonException)
                        {
                            await WriteErrorResponse(context, StatusCodes.Status400BadRequest, new[]
                            {
                                new { field = "body", message = "Invalid JSON format or wrong data types." }
                            });
                            return;
                        }
    
                        if (model == null)
                        {
                            await WriteErrorResponse(context, StatusCodes.Status400BadRequest, new[]
                            {
                                new { field = "body", message = "Request body could not be deserialized." }
                            });
                            return;
                        }
    
                        ValidationResult result = await validator.ValidateAsync(new ValidationContext<object>(model));
                        if (!result.IsValid)
                        {
                            await WriteErrorResponse(context, StatusCodes.Status400BadRequest,
                                result.Errors.Select(e => new { field = e.PropertyName, message = e.ErrorMessage }));
                            return;
                        }
                    }
                }
                catch (Exception ex)
                {
                    await WriteErrorResponse(context, StatusCodes.Status500InternalServerError, new[]
                    {
                        new { field = "internal", message = $"Validation middleware error: {ex.Message}" }
                    });
                    return;
                }
    
                context.Request.Body.Position = 0;
                await _next(context);
            }
    
            private static async Task WriteErrorResponse(HttpContext context, int statusCode, IEnumerable<object> errors)
            {
                context.Response.StatusCode = statusCode;
                context.Response.ContentType = "application/json";
                var response = new
                {
                    success = false,
                    errors
                };
                await context.Response.WriteAsync(JsonSerializer.Serialize(response));
            }
        }
    
        public static class ValidationMiddlewareExtensions
        {
            public static IApplicationBuilder UseValidationMiddleware(this IApplicationBuilder app)
            {
                return app.UseMiddleware<ValidationMiddleware>();
            }
        }
    }
    

    Bu middleware tanıladıktan sonra Program.cs dosyamıza da ekliyoruz.

    Program.cs’ middleware tanımlamak için tek satır kod eklememiz yetiyor bildiğiniz gibi.

    C#
    app.UseValidationMiddleware();

    Tam olarak bu kadar.

    Ve tabi program.cs son hali

    C#
    
    using FluentValidation;
    using FluentValidationDemo.Middlewares;
    using FluentValidationDemo.Validators;
    
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    
    builder.Services.AddOpenApi();
    // Swagger.NET 9’da manuel açılmalı:
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    // ✅ FluentValidation ayarları 
    builder.Services.AddValidatorsFromAssemblyContaining<RegisterValidator>();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API V1");
            options.RoutePrefix = string.Empty;
        });
    }
    
    app.UseHttpsRedirection();
    
    app.UseValidationMiddleware();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Artık controller’da tekrar valide etmene gerek yok, controller’daki validasyon kodlarını silebilirsin.

    Bu yazımızdan önce aşağıdaki yazıyı incelemeni tavsiye ederim.

  • ASP.NET CORE + FluentValidation ile Form Verisi Doğrulama

    ASP.NET CORE + FluentValidation ile Form Verisi Doğrulama

    Bu yazıda asp.net core web api projemizde fluentvalidation ile veri doğrulama işlemini gerçekleştireceğim, adım adım en basit haliyle anlatacağım ki siz de projenize entegre ederken sorun yaşamayın. Sadece validasyon kısmına odaklanacağım.

    Annotation tabanlı validasyona göre çok daha esnek olan FluentValidation paketi .net dünyasında en çok kullanılan validasyon yöntemi.

    En önemli faydalarından biri validasyon mantığını kurallar ile ayrı bir dosyada tanımlayabiliyorsunuz ve böylelikle kodunuz daha temiz ve yönetilebilir hale geliyor. İleride validasyon kuralı eklemek isterseniz ilgili dosyayı açarak ekleyebilirsiniz.

    Hali hazırda bir .net 9 asp.net web api projesi başlattığınızı varsayacağım ve hemen paketlerimi ekleyerek başlayacağım.

    FluentValidation paketini test etmek için ek olarak swagger’de kuracağım. Bildiğiniz gibi .net 9 ile birlikte default olarak entegre gelmiyor ve basit bir kaç adımla projemize dahil edebiliyoruz.

    Öncelikle paketlerimizi yükleyerek başlayalım.

    C#
    dotnet add package FluentValidation.DependencyInjectionExtensions
    dotnet add package Swashbuckle.AspNetCore

    Ben CLI kullanacağım fakat siz isterseniz nuget paket yöneticisi ile de dahil edebilirsiniz aynı kapıya çıkar.

    Paketlerimzi eklediğimize göre hemen DTO’ları tanımlayarak başlayalım.

    Projemizde tek endpoint olacak kullanıcı kayıt endpointi ve nested(iç içe) DTO objelerini valide etmemiz gerekecek, RegisterDTO içerisinde AddressInfoDTO olacak.

    📄 DTOs/RegisterDTO.cs

    C#
    namespace FluentValidationDemo.DTOs
    {
        public class RegisterDTO
        {
            public string? FirstName { get; set; } = string.Empty;
            public string? LastName { get; set; } = string.Empty;
            public string? Email { get; set; } = string.Empty;
            public AddressInfoDTO? AddressInfo { get; set; }
        }
    }

    📄 DTOs/AddressInfoDTO.cs

    C#
    namespace FluentValidationDemo.DTOs
    {
        public class AddressInfoDTO
        {
            public string? Phone { get; set; } = string.Empty;
            public string? Zip { get; set; } = string.Empty;
        }
    }

    Sırada validasyon sınıfımız var, projemiz içinde istediğmiiz yerde kullanabilmek için validasyonumuzu ayrı bir sınıf olarak kodluyoruz.

    Öncelikle alt nesne olan AddressInfoValidator sınıfını kodluyoruz.

    📄 Validators/AddressInfoValidator.cs

    C#
    using FluentValidation;
    using FluentValidationDemo.DTOs;
    
    namespace FluentValidationDemo.Validators
    {
        public class AddressInfoValidator : AbstractValidator<AddressInfoDTO>
        {
            public AddressInfoValidator()
            {
                RuleFor(x => x.Phone)
                    .NotEmpty().WithMessage("Telefon numarası zorunludur.")
                    .Matches(@"^\+?\d{10,15}$").WithMessage("Telefon numarası formatı geçersiz.");
    
                RuleFor(x => x.Zip)
                    .NotEmpty().WithMessage("Posta kodu zorunludur.")
                    .Length(4, 10).WithMessage("Posta kodu 4 ile 10 karakter arasında olmalıdır.");
            }
        }
    }
    

    Sınıfımız AbstractValidator sınıfından türetilmiş olacak ve <AddressInfoDTO> valide ettiğimiz sınıfı belirtmiş olacağız.

    Sırada ana validasyon sınıfımız var, aynı şekilde AbstractValidator<RegisterDTO> üzerinden türeterek kurallarımızı tanımlıyoruz.

    📄 Validators/RegisterValidator.cs

    C#
    using FluentValidation;
    using FluentValidationDemo.DTOs;
    
    namespace FluentValidationDemo.Validators
    {
        public class RegisterValidator : AbstractValidator<RegisterDTO>
        {
            public RegisterValidator()
            {
                RuleFor(x => x.FirstName)
                    .NotEmpty().WithMessage("Ad alanı boş olamaz.")
                    .MinimumLength(2).WithMessage("Ad en az 2 karakter olmalıdır.");
    
                RuleFor(x => x.LastName)
                    .NotEmpty().WithMessage("Soyad alanı boş olamaz.");
    
                RuleFor(x => x.Email)
                    .NotEmpty().WithMessage("Email zorunludur.")
                    .EmailAddress().WithMessage("Email formatı geçersiz.");
    
                // Nested validator kullanımı:
                RuleFor(x => x.AddressInfo)
                    .SetValidator(new AddressInfoValidator()!)
                    .When(x => x.AddressInfo != null);
            }
        }
    }

    Controller’da kullanımı

    controller’da kullanırken önce DI ile enjecte ettiğimiz sınıfı property olarak tanımlıyoruz.

    C#
      private readonly IValidator<RegisterDTO> _validator;
    
      public RegisterController(IValidator<RegisterDTO> validator)
      {
          _validator = validator;
      }

    Sonrasında bu sunufı controller içinde ilgili fonksiyonda manuel olarak kullanıyoruz.

    C#
    [HttpPost("signup")]
            public IActionResult Register([FromBody] RegisterDTO dto)
            {
                ValidationResult result = _validator.Validate(dto);
    
                if (!result.IsValid)
                {
                    var errors = result.Errors
                        .Select(e => new { Field = e.PropertyName, Message = e.ErrorMessage })
                        .ToList();
    
                    return BadRequest(new
                    {
                        Success = false,
                        Errors = errors
                    });
                }
    
                return Ok(new
                {
                    Success = true,
                    Message = "Kayıt başarılı.",
                    Data = dto
                });
            }
        }

    Controller’in son hali:

    📄 Controllers/RegisterController.cs

    C#
    using FluentValidation;
    using FluentValidation.Results;
    using FluentValidationDemo.DTOs;
    using Microsoft.AspNetCore.Mvc;
    
    namespace FluentValidationDemo.Controllers
    {
        [ApiController]
        [Route("api/[controller]")]
        public class RegisterController : ControllerBase
        {
            private readonly IValidator<RegisterDTO> _validator;
    
            public RegisterController(IValidator<RegisterDTO> validator)
            {
                _validator = validator;
            }
    
            [HttpPost("signup")]
            public IActionResult Register([FromBody] RegisterDTO dto)
            {
                ValidationResult result = _validator.Validate(dto);
    
                if (!result.IsValid)
                {
                    var errors = result.Errors
                        .Select(e => new { Field = e.PropertyName, Message = e.ErrorMessage })
                        .ToList();
    
                    return BadRequest(new
                    {
                        Success = false,
                        Errors = errors
                    });
                }
    
                return Ok(new
                {
                    Success = true,
                    Message = "Kayıt başarılı.",
                    Data = dto
                });
            }
        }
    }
    

    Program.cs dosyamızda aşağıdaki satır ile tanımladığımız validasyon sınıfını DI ine enjecte ediyoruz ve tabi opsiyonel swagger ayarları var ben test etmek için swagger’da kurdum siz kurmak zorunda değilsiniz.

    C#
    // ✅ FluentValidation ayarları 
    builder.Services.AddValidatorsFromAssemblyContaining<RegisterValidator>();
    

    Program.cs son hali bu şekilde:

    📄 Program.cs

    C#
    
    using FluentValidation;  
    using ValidationProjecr.Validators; 
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    
    builder.Services.AddOpenApi();
    // Swagger.NET 9’da manuel açılmalı:
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    // ✅ FluentValidation ayarları 
    builder.Services.AddValidatorsFromAssemblyContaining<RegisterValidator>();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API V1");
            options.RoutePrefix = string.Empty;
        });
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Bu videoda DI ile FluentValidation injecte ettik ve ilgili endpoint içeri,sinde çağıarark hataları kullanıcıya gösterdik.

    Bir sonraki yazıda görüşmek üzere.

    Örnek test sonucu ve api response

    Form verisini doğruladık herşey çok güzel ama bitmedi, dto oluşmadan önce tip uyumsuzlukları varsa eğer DTO hiç oluşmaz ve biz validasyonu hiç gerçekleştiremeyiz. Bu nedenle DTO oluşmadan önce bir global validasyon middleware’e ihtiyacımız var, bize ulaşmdan önce valide edecek nesnelerimizi ve biz her adımda tek tek uğraşmayacağız.

  • ASP.NET CORE SWAGGER AUTHORIZE BUTONU İLE JWT TEST ETMEK

    ASP.NET CORE SWAGGER AUTHORIZE BUTONU İLE JWT TEST ETMEK

    ASP.NET Core projelerinde swagger kurulumu’nu bir önceki yazıda anlatmıştım. Bu yazıda sıra geldi JWT (Json Web Token) ile istek göndermeye.

    Swagger’da yaptığımız varsayılan konfigürasyon ile sadece swagger dökümanları oluşuyor JWT desteği için küçük bir ayar gerekli.

    Öncelikle Program.cs dosyasının bir önceli versiyonunu baz alarak güncellenmiş halini paylaşıyorum.

    C#
    using Microsoft.OpenApi.Models;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
    builder.Services.AddOpenApi();
    
    
    // Swagger servisini ekle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen(options =>
    {
        // 🔒 Swagger'da Bearer token giriş alanı tanımla
        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Name = "Authorization",
            Type = SecuritySchemeType.Http,
            Scheme = "Bearer",
            BearerFormat = "JWT",
            In = ParameterLocation.Header,
            Description = "Lütfen 'Bearer {token}' formatında giriniz. (örnek: Bearer eyJhbGciOiJIUzI1...)"
        });
    
        // Her endpoint'te Bearer zorunlu olsun
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                Array.Empty<string>()
            }
        });
    });
    
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API V1");
            options.RoutePrefix = string.Empty;
        }); // set options to root directory of api
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Burada yaptığım tek değişiklik DI ile AddSwaggerGen metoduna bazı ayarlar eklemek oldu.

    Bu satırı:

    C#
    builder.Services.AddSwaggerGen();

    Bunun ile değiştirdim:

    C#
    
    builder.Services.AddSwaggerGen(options =>
    {
        // 🔒 Swagger'da Bearer token giriş alanı tanımla
        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Name = "Authorization",
            Type = SecuritySchemeType.Http,
            Scheme = "Bearer",
            BearerFormat = "JWT",
            In = ParameterLocation.Header,
            Description = "Lütfen 'Bearer {token}' formatında giriniz. (örnek: Bearer eyJhbGciOiJIUzI1...)"
        });
    
        // Her endpoint'te Bearer zorunlu olsun
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                Array.Empty<string>()
            }
        });
    });

    Ek olarak sayfanın başına :

    C#
    using Microsoft.OpenApi.Models;

    Paketimi dahil ettim ekledim (eğer yoksa)

    Sonuç:

    Peki biz burada ne yaptık.

    Kısaca AddSecurityRequirement ve AddSecurityDefinition kullanarak endpointlerimize header üzerinden JWT gönderimini mümkün kıldık.

    ASP.NET Core projenize JWT Entegrasyonu için bu yazıyı takip edebilirsiniz :

  • AutoMapper Nedir, Nasıl Kullanılır ASP.NET CORE + EF CORE Örnek Anlatım

    AutoMapper Nedir, Nasıl Kullanılır ASP.NET CORE + EF CORE Örnek Anlatım

    Automapper, adından da anlayabileceğiniz gibi bir objeyi başka bir objeye dönüştüren bir yapıdır. Hemen basit kullanım örneği ile sizlere bunu anlatalım.

    Katmanlı bir mimari oluştururken benzer verileri tutan bir çok nesne ile uğraşırız, request gelir UserRequestDTO ile request içeriğini alırız, sonrasında ise User Enttiy oluşturur bunu veritabanına eklemeye çalışırız.

    Eğer manuel bir eşleme yapacak olursak her property’i teker teker birbirine eşitleriz ve yeni nesnemizi oluştururuz.

    Automapper’den önce şu şekilde ilerliyordu:

    C#
    public class User
    {
        public int Id { get; set; }
        public string FullName { get; set; }
        public string Email { get; set; }
    }
    
    public class UserDto
    {
        public string FullName { get; set; }
        public string Email { get; set; }
    }

    User ve UserDto entity’leri arasında dönüşüm yapmak için şu şekilde manuel bir kodlama yapmamız gerekiyordu.

    C#
    var user = new User { Id = 1, FullName = "Gökhan Çelik", Email = "[email protected]" };
    
    var dto = new UserDto
    {
        FullName = user.FullName,
        Email = user.Email
    };

    Böylelikle User modelinden yeni bir UserDto oluşturmuış oluyorduk.

    AutoMapper ile birlikte her kullanımda bu şekilde manuel uğraşmak ve kod tekrarına sebep olmak yerine bir ara metod oluşturuyoruz ve bu metod bize dönüştürüp yeni nesneyi veriyor.

    Öncelikle automapper için paketimizin kurulumunu yapalım.

    C#
    dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

    Şimdi ise User ve UserDto sınıfları arası nesneleri otomatik çevirecek mapper sınıfımızı yazalım.

    📁Mappings/UserProfile.cs

    C#
    using AutoMapper;
    using BookStore.Api.Models; // örnek namespace
    using BookStore.Api.DTOs;
    
    namespace BookStore.Api.Mappings
    {
        public class UserProfile : Profile
        {
            public UserProfile()
            {
                CreateMap<User, UserDto>();
                CreateMap<UserDto, User>(); // iki yönlü istersen
            }
        }
    }

    Burada CreateMap ile hangi tür sınıfları birbirine dönüştürmek istediğimizi belirtiyoruz ve gerisi ile mapper ilgileniyor.

    Peki diyelim ki sınıflarımızın property isimleri birbiri ile uymuyor. Bu durumda ne yapacağız?

    Bu durumda da aşağıdaki gibi özelleştirmeler yapabiliriz.

    C#
    // Bu tamamen opsiyonal bir özelleştirme
    CreateMap<User, UserDto>()
        .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.FullName));
    

    Automapper sınıfımızı kullanmadan önce DI ile projemize tanıtmamız gerekiyor.

    📁Program.cs

    C#
    builder.Services.AddAutoMapper(typeof(Program));

    Peki şimdi sırada gerçek kullanım örneği var. Mapper sınıfımızı oluşturduk. Nesneleri birbirine nasıl dönüştüreceğimizi tanımladık, şimdi bunu contoller içinde kullanalım.

    C#
    using AutoMapper;
    using Microsoft.AspNetCore.Mvc;
    
    [ApiController]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        private readonly IMapper _mapper;
    
        public UsersController(IMapper mapper)
        {
            _mapper = mapper;
        }
    
        [HttpGet]
        public IActionResult GetUser()
        {
            var user = new User { Id = 1, FullName = "Gökhan Çelik", Email = "[email protected]" };
            var dto = _mapper.Map<UserDto>(user);
            return Ok(dto);
        }
    }
    

    Bu şekilde basitçe User sınıfımızın nesnesini USerDto olarak dönüştürmüş olduk…

    Buraya kadar yazımı okuduysanız düşüncelerinizi yorum olarak bırakmayı unutmayın.

    Özet

    Automapper basitçe sınıfların nesnelerini birbirine dönüştüren bir ara sınıf diyebiliriz.

  • ASP.NET CORE SWAGGER KURULUMU VE  NEDİR?

    ASP.NET CORE SWAGGER KURULUMU VE NEDİR?

    Swagger’ın değerini sadece kullananlar bilir, developer dostudur swagger, yarı yolda bırakmaz. Müşteriye & kullanıcıya ulaşmadan 1 tıkla test ettirir endpointleri, kabul edilen json yapısını hazır bekletir. Sen sadece TRY’a tıklarsın sonucu görürsün. Bazen crash olur hatanı görürsün bazen success olur mutlu olursun. Dürüst adamdır neyse sonuç onunla yüzleştirir seni…

    Bu yazımızın konusu ASP.NET CORE projesi için swagger entegrasyonu. Bildiğin gibi swagger artık direk kurulu gelmiyor ama hala iki tık uzağımızda.

    Nedir bu swagger?

    Swagger bir api dökümantasyon ve endpoint test aracıdır. Bir web api geliştirdiğinde onu otomatik olarak dökümante eder, ve sana tüm endpointlerin bir listesini çıkartır. Swagger bir OpenAPI Specification (OAS) standartıdır. Test arayüzü sunar, annotation ve metod isimlerinden ve parametlererinden otomatik girdileri field olarak karşına çıkartır.

    • Otomatik kodları tarar ve endpointleri oluşturur.
    • Test arayüzü sunar.
    • Authorize butonuna tıklayarak JWT girebilirsin ve bu sayede korumalı endpoinleri de test edebilirsin.

    ASP.NET Core ‘da Swagger kuralım (.net 9 ve üzeri)

    Öncelikle paketlerimizi ekleyelim projemize, arayüz kullananlar direk nuget üzerinden de yapabilir.

    PowerShell
    dotnet add package Swashbuckle.AspNetCore
    

    Bu kodu proje dizininde çalıştırırsan swagger projene konuk oyuncu olarak katılır. Öyle misafir gibi de davranmaz hemen birkaç komutla işe koyulur…

    Paketimizi kurduktan sonra tek bir dosya üzerinden development ortamımıza dahil edebiliriz swagger’ı adım adım anlatacağım ve en sonda Program.cs’ın son halini vereceğim.

    Ekleyeceğimiz kod satırları sırayla şu şekilde.

    Program.cs Son hali

    C#
     
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
    builder.Services.AddOpenApi();
    
    
    // Swagger servisini ekle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API V1");
            options.RoutePrefix = string.Empty;
        }); // set options to root directory of api
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Şimdi adım adım nasıl entegre ettiğimizi anlatalım, entegrasyonumuz iki adımdan oluşuyor öncelikle dependency injection kısmı sonrasında ise option ayarları.

    “builder.Services.AddOpenApi();” satırından sonra ve “var app = builder.Build();” satırından önce swagger servislerini eklemiş olduk.

    C#
    builder.Services.AddOpenApi(); // bu satırdan sonra
    
    // Swagger servisini ekle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build(); // bu satırdan önce

    sonrasında sadece development mode da çalışması için:

    C#
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
    }

    bu if blogu içeriğini şu şekilde düzenledik:

    C#
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi();
        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("/swagger/v1/swagger.json", "ECommerce API V1");
            options.RoutePrefix = string.Empty;
        }); // set options to root directory of api
    }

    Burada swagger’ın gerekli option’larını ve ana dizinde açılması için gerekli ayarlarını yapmış olduk.

    sonrasında projemizi çalıştırdığımızda ana dizine gittiğimizde direk swagger gökümantasyonu bizi karşılıyor:

    Bir sonraki yazımızda swagger ile JWT authorized api testi sizlerle olacak.