.Net Core Mvc Haber Portalını SignalR ve Redis Ile Optimize Etme

Selamlar,

Bu makalede, bir önceki yazıda bahsedilen .Net Core Mvc’de Bir Haber Başlığını Url’e Koyma konusuna, kalındığı yerden devam edilecektir. Yüksek trafik alan portallarda, gösterilecek olan datanın dağıtık bir memoryden çekilmesi, bazen hayati bir önem taşımaktadır. İşte bu yazıda, Redis üzerinde Distributed Cache ile bu durumun nasıl yönetilebileceğini hep beraber inceleyeceğiz. Ayrıca girilen yeni bir datanın real time olarak, clientlara SignalR ile push edilmesi, bu makalenin diğer bir konusudur.

Redis:

.Net Core üzerinde Redis konusu, bu makaleden  daha detaylı bir şekilde incelenebilir. Bu projede macOS High Sierra işletim sistemi üzerinde, ilgili Redis kurulumları ve kod geliştirmeleri yapılacaktır. Makinanızda Redis’in olduğu varsayılarak, Redis sunucu ve client’ı Macbook üzerinde aşağıdaki komutlar ile ayağa kaldırılır.

Server: Zorunlu

Client: Zorunlu değil.

Net Core Mvc projesinde Redis için ServiceStac kütüphanesi, aşağıdaki komut ile projeye implemente edilir.

.Net Core Projesinde Redis Servisinin tanıtılması, aşağıda görüldüğü gibi Startup.cs altında yapılmaktadır.

NewsController: Aşağıda görüldüğü gibi NewsController Constructor’ında “IDistributedCache” nesnesi, dependency injection ile sisteme dahil edilmiştir. Ayrıca “_distributedCache” değişkenine, ilgili IDistributedCache redis nesnesi atanmıştır.

NewsController/AddRedisCache(): Clientlara gösterilecek haber datasının yani “List<News>”‘ın, yüksek trafik’de çekilecek requestlere karşı, sunucuların ayakta kalabilmesi adına, tüm işlemler database veya servise gidilmeden direk dağıtık yapıdaki bir sunucuya ait memoryden sağlanmaktadır.

  • Redis’de saklanacak datanın cache ismi, “cacheKey” parametresi ile atanmıştır.
  • var dataNews = await _distributedCache.GetAsync(cacheKey)” : Daha önceden ilgili key’e ait redisde bir kayıt var mı diye asenkron olarak bakılmıştır.
  • if (dataNews == null)” : Eğer herhangi bir kayıt yok ise koşulu tanımlanmıştır.
  • var data = JsonConvert.SerializeObject(allData);” : Bir önceki makalede tanımlı “static List<News>” tipindeki “model” değişkeni, “allData” adında bir parametre olarak atanmış ve “Newtonsoft” ile Serialize edilip string’e çevrilmiştir.
  • var option = new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(cacheTime))” : Burada SetSlidingExpiration() methodu ile, Redis’de ilgili key ile belli bir zaman mesela, bu örnekte “cacheTime” parametresi ile gönderilen 30sn içinde hiçbir işlem yapılmaz ise, cache’in düşürülmesi sağlanmıştır.
  • option.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cacheTime)” : Burada AbsoluteExpirationRelativeToNow() methodu ile, Redis’de ilgili keye ait cache ile işlem yapılıp yapılmadığına bakılmaksızın 30sn sonunda düşürüleceği tanımlanmıştır.
  • “await _distributedCache.SetAsync(cacheKey, dataByte, option) :” Redis’e ilgili “List<News>” datası, parametre olarak gönderilen “cacheKey” ismi altına 30sn timeout süreli olarak atanmıştır.
  • var newsString = await _distributedCache.GetStringAsync(cacheKey);” : Belirtilen “cacheKey” parametresine göre ilgili data string olarak çekilir.
  • return JsonConvert.DeserializeObject<List<News>>(newsString);” : Redisden çekilen string data, “Newtonsoft” ile “<List<News>”‘e  Deserialize edilip, geri dönülür.

NewsControler/Index.cshtml : Aşağıda görüldüğü gibi Ana sayfaya gönderilecek datalar, Redis Cache üzerinden alınmakta ve yüksek trafik durumunda büyük ölçüde performans kazanılmaktadır. Ayrıca bu örnekte belirlenen 30sn süre dolduğunda, Redis üzerinde ilgili key’e ait “List<News>” data’sı güncellenmektedir. Güncelleme durumunda, Redis ServiceStack kütüphanesi ile çalışma durumunda, harici bir LOCK mekanizmasının kullanılmasına gerek yoktur. Bu işi Redisin, kendisi halletmektedir.

Admin:

Şimdi sıra geldi yeni bir haber giriş ekranına:

Startup.cs/Configuration: Routing kısmına, aşağıdaki “admin” ve “adminsave” templateleri eklenir. “admin”: Yeni bir haber girişinin yapılacağı sayfadır. “adminsave” : Yeni bir haber girildiği zaman, form post durumunda gidilecek action’ın ta kendisidir.

Admin.cshtml: Aşağıdaki sayfada tüm input elementleri “<form>” html elementi içerisinde tanımlanmıştır. Form submit işleminde, “asp-controller” ile tanımlı “News” controller’ına ait,  “asp-action” ile tanımlı “SaveNews()” action’ına gidilir. News model’in propertyleri olan “Title” ve “Detail” alanlarına ait inputlar, tanımlandıktan sonra ilgili haber resmini upload etmek için NewsImage adında “<input type=”file”>” html elementi aşağıdaki gibi sayfa içerisinde tanımlanır.

Not: Unutulmaması gereken bir konu da Html element name‘lerinin model’deki adları ile aynı olması gerektiğidir. Gene “file” tipindeki “name=NewsImage” html elementi, SaveNews() methodunda beklenen 2. parametre ismi ile aynı olmak zorundadır.

Şimdi sıra geldi Admin formun post işleminden sonra, gidilen SaveNews() Methodunun kodlanmasına.

Örnek yeni bir haber girişi:

NewsController/SaveNews(): SaveNews() methodu parametre olarak “News” tipinde bir değişken ve “IFormFile”  tipinde 2. bir parametre beklemektedir. Bu işlem 4 adımdan oluşmaktadır.

  • 1. adım Upload Images:
    • if (NewsImage != null && NewsImage.Length > 0)” : Gönderilen Image null ise işlem yapılmaz.
    • var fileName = ContentDispositionHeaderValue.Parse(NewsImage.ContentDisposition).FileName.Trim(‘”‘)” : Uzantısı ile beraber Upload edilen resmin ismi alınır. Örnek “OPTIMIZATION.png”
    • “var FileExtension = Path.GetExtension(fileName)” : Yüklenen resmin uzantısı alınır. Örnek “.png”.
    • “var PureFileName = Path.GetFileNameWithoutExtension(fileName)” : Yüklenen resmin uzantısız adı alınır. Örnek “OPTIMIZATION”.
    • “var newFileName = PureFileName + ‘_’ + myUniqueFileName + FileExtension” : Yüklenen resme, benzersiz bir ad atanır. Örnek : “OPTIMIZATION_d898202a-07c4-4d1e-9fdc-caf88fc7f193.png”.
    • “news.Image = newFileName” : Model’deki string “Image” alanına karşılık gelen bu Unique dosya ismi atanır.
    • “using (var stream = new FileStream(fileName, FileMode.Create))” : Yeni bir FileStream, kaydedilecek resim dosyasının adına göre oluşturulur.
    • “await NewsImage.CopyToAsync(stream)” : Resim belirtilen klasöre asenkron olarak kaydedilir.
  • 2. adım Static Model’e Ekleme:
    • “model.Insert(0,news)” : En son eklenen haberin, en başta gelmesi için ilk sıradaki [0]’ıncı index’e iliştirilmiştir.
  • 3. adım Redis’e eklemek :
    • “var data = await AddRedisCache(model, 30, “NewsData”, true)” : Burada amaç, haber sayfasına yeni gelen clientların, eklenen en son haberi de görebilmesi için, Redis cache’in düşmesini beklemeden (bu örnekte 30sn), son güncel modelin eskisinin üzerine ezilmesidir.
  • 4. adım “return RedirectToAction(“Index”)” : Güncelleme yapan client’ın, Index sayfasına yönlenerek son eklenen haberi en tepede görmesi sağlanır. Not: Editör, eklediği haberin nasıl göründüğüne bakmak isteyebilir.

Not : Yüksek trafikli sitelerde, image dosyaları asla bu makalede olduğu gibi localde bulundurulmamalıdır. CDN dediğimiz firmalardan ya da cloude hizmetlerinden faydalanılarak, ilgili contentler remote bir sistemden yayımlanmalıdır.

SignalR:

Şimdi sıra geldi, yeni eklenen haber datasının, diğer clientlara SignalR ile push edilmesine:

.Net Core üzerinde SignalR hakkında daha detaylı bilgiye burdaki makaleden erişebilirsiniz.

Şimdi sıra geldi signlR için yapılması gerekenlere:

1-) “.csproj” dosyasına “<PackageReference Include=“Microsoft.AspNetCore.SignalR” Version=“1.0.0-alpha2-final” />” : SignalR kütüphanesi eklenir. SignalR Hub class’ının tanımlanması ve istenen Client’lara push işleminin yapılması için, eklenmesi gereken bir kütüphanedir.

2-) “.csproj” dosyasına “<PackageReference Include=”Microsoft.AspNetCore.SignalR.Client” Version=”1.0.0-alpha2-final” />” : SignalR kütüphanesi eklenir. Bu kütüphanenin eklenmesindeki amaç, SignalR Hub class’ına uzaktan erişmek ve ona ait methodların trigger edilmesini sağlamaktır. Bu 2 kütüphane eklendikten sonra “dotnet restore” komutunun çalıştırılması unutulmamalıdır..

codernews.csproj:

3-)Startup.cs’de ConfigureServices’e yandaki kod eklenir==> “services.AddSignalR()” Böylece proje ilk ayağa kalkarken .Net Core üzerinde SignalR servisleri de tanımlanmış olunur.

Startup.cs/ConfigureServices:

4-)  Aşağıdaki tanımlı Routing ile hem client tarafından SignalR sınıfına bağlanılabilmesi, hem de server side tarafından ilgili SignalR Hub sınıfına erişilebilmesi için “newspush” anahtar kelimesi tanımlanır.

Startup.cs/Configure:

5-) Son olarak client side tarafda kullanılan “<script src=”js/signalr-client-1.0.0-alpha2-final.min.js”></script>” İlgili SignalR script kütüphanesi “npm install @aspnet/signalr-client –save”  komutu ile indirilebilir. Ya da “npm install @aspnet/signalr-client” komutu ile ilgili kütüphane, golbal’e de indirilebilir. İlgili dosya, globalde yandaki klasörden erişilebilir==>”/usr/local/lib/node_modules/@aspnet/signalr-client/dist/browser/signalr-client-1.0.0-alpha2-final.min.js“. Bu script dosyasının projede “wwwroot” altında “js” klasörü altına kopyalanması gerekmektedir.

Şimdi gelin hep beraber “NewsPush” adında SignalR Hub sınıfımızı oluşturalım:

NewsPush.cs: Aşağıda görüldüğü gibi “PushNews()” methodu, SignalR Hub sınıfına bağlı olan tüm clientların, client side tarafdaki “GetNews()” methodunu, yeni girilen news model parametresi ile beraber tetiklemektedir.

News.cs: News modeline “UrlTitle” adından yeni bir property eklenmiştir. Bunun nedeni, client side taraftaki PushNews() function’ına gönderilen yeni girilen haber’e ait title’ın, url satırana uygun hale getirilip bu alana atanmasıdır. Eğer bu alan olmasa idi, title’dan url’e client side taraflı dönüşümünün yapılacağı bir javascript function’ının yazılması gerekmekteydi.

Models/UrlExtension: Yukarıda bahsedilen title’dan url’e yapılacak dönüşüm için, string tipinde extension aşağıdaki gibi yazılmıştır.

Şimdi sıra geldi yeni bir haber girişi olduğu zaman, ilgili SignalR Hub sınıfına bağlanılıp, PushNews() methodunu tetiklemeye.

News/SaveNews() (Güncellenen): Aşağıda sadece SignalR için yeni eklenen kısımlar anlatılmıştır. Kodun tamamını sayfanın sonundaki Github source kodundan alabilirsiniz.

  • “news.ID = MaxID + 1” : Yeni girilen News tipindeki habere, o ana kadar girilmiş en büyük ID’nin 1 fazlası atanır.
  • “news.UrlTitle = news.Title.FriendlyUrl()” : Haberin title’ı geçerli bir Url’e dönüştürülüp yeni eklenen “UrlTitle” propertysine atanır.
  • //Upload Images, // Add to Model işlemleri yapılır.
  • “Connect().Wait();” : SignalR NewPush Hub sınıfına connect olunur ve bağlantı işlemi gerçekleşene kadar beklenir. Bu methodun içeriği makalenin devamında anlatılacaktır.
  • “await connectionSignalR.InvokeAsync(“PushNews”, news);” NewPush SignalR hub sınıfına ait “PushNews()” methoduna, yeni girilen News parametre olarak gönderilerek tetiklenir.
  • //Add Redis Cache ve //Redirec To Index işlemleri yapılır.

News/Connect(): SignalR sınıfına local IP ve router’da tanımlı “newspush” keyword’ü ile erişilip, bağlanılır.

News/Index.cshtml : Gelin hep beraber SignalR ile Index sayfamıza gelen yeni kodları hep beraber inceleyelim.

  • “<script src=”js/signalr-client-1.0.0-alpha2-final.min.js”></script>” : SignalR Javascript kütüphanesi sayfaya eklenir. Dikkat edilirse, artık SignalR Jquery kütüphanesine ihtiyaç duymamaktadır.
  • “let connection = new signalR.HubConnection(‘/newspush’)” : “NewPush” signalR Hub sınıfına connect olunur.
  • “var modelData=@Html.Raw(Json.Serialize(Model))” : Burası önemli. Her yerde göremiyeceğiniz bir kod satırı ile karşı karşıyayız:) SignalR, en nihayetinde client tarafında javascript ile çalışan bir yapıdır. Bu örnekde DB’den çekilen datalar, Razor ile “@Model” altında client side tarafa gönderilmiştir. Kısaca server side tarafdan gelen bu List<News> view modelin javascript tarafında kullanılması için “modelData” değişkenine, “@Html.Raw()” ile string’e çevrilip atanması sağlanmıştır. Peki bu ne işimize yarıyacak ? Böylece SignalR ile server side taraftan sadece yeni girilen haber datası tüm  clientlara  push edilecektir, geri kalan tüm “List<News>” datası, “modelData” değişkeninden alınacaktır. Böylece büyük bir performans kazancı sağlanacaktır.
  • “connection.on(‘GetNews’, data => { “: SignalR ile client side tarafda tetiklenecek “GetNews()” functionı tanımlanmıştır.
  • “var div = document.getElementById(‘newsContent’)” : “GetNews()” methodu ile javascript tarafında oluşturulacak html data, “newsContent” divinin içine basılmıştır.
  • “var html='<table class=”table”><tr>'” : Div içine basılacak html manuel olarak oluşturulmaya başlanır.
  • “modelData.splice(0, 0, data)”: Yeni girilen News önceden çekilen List<News> datasının başına eklenir. Böylece son gelen haberin en başta gösterilmesi sağlanır.
  • “for(var i=0;i<modelData.length;i++)” : Tüm haber datası gezilerek  Mvc Razor ile ekrana  basılan haberler, burada yine aynı şekilde Javascript ile modelData gezilerek, dynamic Html oluşturulup ekrana basılmaktadır. Her satırda 2 haber basılma algoritması, yine burada tekrardan kurgulanmıştır.
  • “connection.start()” : Clinet, SignalR NewsPush Hub sınıfına connect olur.

Not: İşte tüm bunların başlangıç noktası, görsel tarafta haberlerin 2şerli olarak sıralanması için gerekli olan algoritmadır. Sayfa ilk yüklendiğinde Razor View Engine ile haberler listelenmektedir. Fakat SignalR client side tarafta Javascript ile çalışmakta, haberlerin 2 şerli dizilmesi için tanımlanan div içinde kalan tüm html’in dynamic olarak tekrardan oluşturulması gekemektedir. Aslında, buradaki esas sorun Razor View Engine’in backend, SignalR’ın client side tarafda javascript ile çalışmasıdır.

Peki çözüm nedir ? Çözüm SignalR kullanılan sayfalarda, aynı dili konuşmak adına, Razor View Engin yerine Modern Javascript Framework’lerinden birinin kullanılmasıdır. Angular, React, Vue Js gibi kütüphanelerden biri kullanılsa idi, yeni bir haber girişinde tekrardan Html oluşturulmasına gerek kalmazdı. Var olan model’e, yeni kaydedilen News datası kolaylıkla eklenebilirdi. Böylece son gelen haber, tüm clientlarda en üst sırada gözükürdü.

Bu makalede Mvc Razor View Engin ile çalışırken SignalR ile ne gibi sorunlar yaşayabileceğimizi gerçekçi bir senaryo üzerinde inceledik. Bu inceleme sırasında, çeşitli çözüm yollarını hep beraber tartışdık. Ayrıca yüksek trafik durumlarında, Redis Cache ile çalışarak nasıl performansı arttırabileceğimiz hep beraber gördük. SignalR’da performans için en çok dikkat edilmesi gereken durum, clientlara gönderilecek paketin mümkün olduğunca küçük olmasıdır. Bu örnekte de sadece yeni girilen data, tüm clientlara push edilmiş, geri kalan tüm data, önceden gönderilmiş olan “Mvc @Model’den” bir javascript değişkenine aktarılmıştır. Böylece büyük bir performans kazancına gidilmiştir.

Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source:

Source Code: https://github.com/borakasmer/Optimized-Create-Url-From-News-Title-Redis-SignalR

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

5 Cevaplar

  1. Samet dedi ki:

    Hocam çok iyi örnekler veriyorsunuz, çok teşekkürler.

  2. Hüseyin Burak dedi ki:

    MVC ile ilgili blog’unuzda bir hazine varmış yeni farkettim:) Hakkınızda yazınızı okuyunca daha öncede sitenize uğradığımı hatırladım ama mahrum kalmışım bilgilerinizden. Sitenizi şimdi sık kullanılanlarıma ekledim. Teşekkür ediyorum bu bilgiler için size.

    İyi çalışmalar.

    • borsoft dedi ki:

      Teşekkürler Hüseyin Bey.
      Zararın neresinden dönsek kardır:)

      İyi çalışmalar.

  3. Ali dedi ki:

    Hocam merhaba. RedisCache’e ekleme islemini SaveNews asamasinda yapsak durum nasil olurdu? Index’te AddRedisCache demek ile SaveNews icinde AddRedisCache demek arasinda bir fark var midir?

    Tesekkurler.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.