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.
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 =1FORUPDATE;
İş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
BEGINTRANSACTIONSELECT stock FROM products WHERE id=1FORUPDATEIF stock >0:UPDATE products SET stock = stock -1COMMITELSE: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 productsSET stock = stock -1, version=version+1WHERE id =1ANDversion=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 productsSET stock = stock -1WHERE id =1AND 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.
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.
Ö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şturuyoruzdotnetnewwebapi-nSignalRJwtChatApi# proje dizinine geçiyoruzcdSignalRJwtChatApi# Gerekli paketler 1 signalRdotnetaddpackageMicrosoft.AspNetCore.SignalR# Gerekli paketler 2 JWTdotnetaddpackageMicrosoft.AspNetCore.Authentication.JwtBearer
Gerekli paketleri ister nuget paket yöneticisi isterseniz konsoldan kurduktan sonra test ediyoruz.
Bash
dotnetrun
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;publicclassUser{publicstring Id {get;set;}=Guid.NewGuid().ToString();publicstring Username {get;set;}=string.Empty;publicstring Password {get;set;}=string.Empty;publicstring 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.
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;publicclassUserService{privatereadonly 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);returnuser;}publicboolValidateCredentials(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]")]publicclassAuthController: ControllerBase{privatereadonly UserService _userService;privatereadonly JwtService _jwtService;publicAuthController(UserService userService, JwtService jwtService){_userService=userService;_jwtService=jwtService;}[HttpPost("register")]public IActionResult Register(RegisterRequest request){var existingUser =_userService.GetByUsername(request.Username);if(existingUserisnotnull)returnBadRequest("Bu kullanıcı adı zaten kullanılıyor.");var user =_userService.Create(request.Username,request.Password);returnOk(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)returnUnauthorized("Kullanıcı adı veya şifre hatalı");var user =_userService.GetByUsername(request.Username)!;var token =_jwtService.GenerateToken(user);returnOk(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üneusing Microsoft.AspNetCore.Authentication.JwtBearer;using Microsoft.IdentityModel.Tokens;using System.Text;using SignalRJwtChatApi.Services;
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
dotnetaddpackageMicrosoft.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{publicclassMessageHub: 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/openapibuilder.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ştirdikapp.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.
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.
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.
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.
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!
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.
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{publicclassValidationMiddleware{privatereadonly RequestDelegate _next;publicValidationMiddleware(RequestDelegate next){_next=next;}publicasync Task InvokeAsync(HttpContext context, IServiceProvider serviceProvider){var method =context.Request.Method;// Sadece POST ve PUT isteklerde çalışsınif(method!=HttpMethods.Post&&method!=HttpMethods.Put){await_next(context);return;}var endpoint =context.GetEndpoint();if(endpointisnull){await_next(context);return;}context.Request.EnableBuffering();usingvar reader =new StreamReader(context.Request.Body, leaveOpen:true);var body =awaitreader.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 incontrollerActionDescriptor.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){awaitWriteErrorResponse(context,StatusCodes.Status400BadRequest,new[]{new{field="body",message="Invalid JSON format or wrong data types."}});return;}if(model==null){awaitWriteErrorResponse(context,StatusCodes.Status400BadRequest,new[]{new{field="body",message="Request body could not be deserialized."}});return;} ValidationResult result =awaitvalidator.ValidateAsync(new ValidationContext<object>(model));if(!result.IsValid){awaitWriteErrorResponse(context,StatusCodes.Status400BadRequest,result.Errors.Select(e =>new{field=e.PropertyName,message=e.ErrorMessage}));return;}}}catch(Exception ex){awaitWriteErrorResponse(context,StatusCodes.Status500InternalServerError,new[]{new{field="internal",message=$"Validation middleware error: {ex.Message}"}});return;}context.Request.Body.Position=0;await_next(context);}privatestaticasync Task WriteErrorResponse(HttpContext context,int statusCode, IEnumerable<object> errors){context.Response.StatusCode=statusCode;context.Response.ContentType="application/json";var response =new{success=false,errors};awaitcontext.Response.WriteAsync(JsonSerializer.Serialize(response));}}publicstaticclassValidationMiddlewareExtensions{publicstatic IApplicationBuilder UseValidationMiddleware(this IApplicationBuilder app){returnapp.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.
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.
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.
namespace FluentValidationDemo.DTOs{publicclassAddressInfoDTO{publicstring? Phone {get;set;}=string.Empty;publicstring? 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{publicclassAddressInfoValidator: AbstractValidator<AddressInfoDTO>{publicAddressInfoValidator(){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{publicclassRegisterValidator: AbstractValidator<RegisterDTO>{publicRegisterValidator(){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.
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.
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.
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/openapibuilder.Services.AddOpenApi();// Swagger servisini eklebuilder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen(options =>{// 🔒 Swagger'da Bearer token giriş alanı tanımlaoptions.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 olsunoptions.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ımlaoptions.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 olsunoptions.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.
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.
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.
Ş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 namespaceusing BookStore.Api.DTOs;namespace BookStore.Api.Mappings{publicclassUserProfile: Profile{publicUserProfile(){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ştirmeCreateMap<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]")]publicclassUsersController: ControllerBase{privatereadonly IMapper _mapper;publicUsersController(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);returnOk(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.
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/openapibuilder.Services.AddOpenApi();// Swagger servisini eklebuilder.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 eklebuilder.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.