https://www.buraksenyurt.com/Burak Selim Şenyurt - .Net Core2023-04-13T08:52:32+00:00Matematik Mühendisi Bir Bilgisayar Programcısının NotlarıBurak Selim SenyurtBlogEngine.Net Syndication Generatorhttps://www.buraksenyurt.com/opml.axdBurak Selim SenyurtMatematik Mühendisi Bir Bilgisayar Programcısının Notlarıtr-TRBurak Selim Şenyurt0.0000000.000000https://www.buraksenyurt.com/post/asp-net-core-a-nasil-merhaba-derizAsp.Net Core'a Nasıl Merhaba Deriz?2021-04-25T09:00:00+00:00bsenyurt<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_cover.png" alt="" align="right" />Yazılım geliştirme işine ciddi anlamda başladığım yeni milenyumun başlarında .Net Framework sahanın yükselen yıldızıydı. Delphi’den kopup gelen Anders’in yarattığı C# programlama dilinin gücü ve .Net Framework çatısının vadettikleri düşünülünce bu son derece doğaldı. Aradan geçen neredeyse 20 yıllık süre zarfında .Net Framework’te evrimleşti ve sürekli güncellendi. Versiyon 2.0 ile gelen generic tipler, 3.0'la birlikte SQL yazar gibi sorgulanabilir nesneler<em class="jf">(LINQ-Language INtegrated Query)</em>, sonrasında karşımıza çıkan WCF<em class="jf">(Windows Communication Foundation)</em>, WF<em class="jf">(Workflow Foundation)</em>, Entity Framework vs derken Microsoft’un açık kaynak dünyasına girişi, benimsediği platform bağımsız stratejiler<em class="jf">(Miguel De Icaza’nın Mono’suna da saygı duyalım)</em>, Linux, MacOS gibi bir zamanların ciddi rakipleri ile el sıkışarak hamle yapması sonrasında da son birkaç yıllık zaman diliminde karşımıza çıkan .Net Core. Yeni gelişmeler Microsoft’un sıklıkla yaptığı üzere bazı kavram karmaşalarını da beraberinde getirdi elbette. En nihayetinde tek ve birleşik bir .Net 5 ortamından bahsedilmeye başlandı. <em>(Photo by <a href="https://unsplash.com/@element5digital?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Element5 Digital</a> on <a href="https://unsplash.com/s/photos/education?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>)</em></p>
<p>Gelişmeleri zaten sizler de benim gibi takip ediyorsunuzdur. Bu durum benim de kişisel olarak kendimi yenilemem gereken bir dönemi tetikledi. Bir süredir özellikle Amazon’dan getirttiğim kitaplardan .Net 5 dünyasını tanımaya, .Net 4.7.2 gibi versiyonlarda yazılmış uygulamarı yeni sürüme göç ettirmenin<em class="jf">(migration)</em> yollarını öğrenmeye çalışıyorum. Bu kişisel çabayı da çalışmakta olduğum şirketin iç eğitim programından gelen talepleri karşılamak için kullanıyorum.</p>
<p>Sabahsız gecelerimin birisinde zen merkezim olan çalışma odamdaki kanepeya uzanmış boş boş tavana bakıyordum. Hoş aklımda cevap arayan güzel bir soru da vardı. <a title="Doğuş Teknoloji Geleceğe Giriş" href="https://www.gelecegegiris.com" target="_blank">Geleceğe Giriş</a> programı kapsamındaki bir Asp.Net Core eğitimine nasıl başlamalıydım? Nasıl bir Hello World olmalıydı? Doğrudan üretilecek uygulamanın kendisini en başından gösterip; “İşte bu uygulamayı nasıl yazacağımızı adım adım öğreneceğiz” şeklinde mi yol almalıydım. Yoksa Hello World deme şekli .Net Core sonrası daha mı farklıydı?</p>
<p>Bu işe başladığım yıllarda beni eğitenler veya okuduklarım Nesne Yönelimli Dil<em>(Object Oriented Programming)</em> konusunun ne denli önemli olduğundan bahseder, kalıtım<em>(Inheritance)</em>, çok biçimlilik<em>(Polymorphism)</em> ve soyutlama<em>(Abstraction)</em> gibi kavramların önemine vurgu yapardı. İş, düşük maliyeti nedeniyle çok sık tercih edilen monolitik mimarinin en yaygın kullanılan örneklerinden olan çok katmanlı<em>(n-tier)</em> çözümlere geldiğinde ise mahşerin beş atlısı SOLID ilkeleri, sayısız yazılım prensibi ve tasarım kalıbı ile karşılaşırdık. Gerçekten yazılım mühendisliğinden bahsettiğimiz noktaya gelindiğinde ise Autofac, Ninject, Unity, Castle Windsor gibi bileşenler arası bağımlılıkları yöneten çatıları kullanmaya başlardık. O günleri düşünürken aklıma .Net Core'u<em>(esasında .Net 5'i)</em> bu bağlamda ele almak geldi. Çok üst düzey yetenekleri olmasa da zaten dahili bir DI<em>(Dependency Injection)</em> mekanizmasına sahipti.</p>
<p>Belki sadece DI deyip geçtiğimiz ve bazen şuursuzca IServiceCollection üzerinden bağımlıkları kayıt etmemize olanak sağlayan bu kavram esas itibariyle Single Responsibility, Dependency Inversion Principle ve Inversion of Control esasları üzerine oturuyor. Bu sebepten basit bir Asp.Net Core eğitimine başlarken bile sadece Model nesnesi oluşturup bir liste döndüren Controller ile View kullanmak kafi olmayabilir. Öncesinde ve mutlak suretle eğitimdeki değerli zihinlere .Net Core'un DI mekanizmasının nasıl çalıştığını, neden önemli olduğunu göstermek gerekir...</p>
<p>Diye notlar alarak geçmişim bu yazının başına. Amacım, eğitim için basit ve hızlı okunabilir bir ön doküman hazırlamaktı. Bu dokümanı eğitim katılımcılarına gönderip, "şuna bir göz atın, anlamaya çalışın, sondaki sorulara cevaplar bulun ve derse öyle gelin" demekti belki de. Sonunda aşağıdaki içeriğe sahip basit bir rehber ortaya çıktı<em>(Level 101 diyebiliriz)</em></p>
<p>Hello World'ler artık bildiğim Hello World'ler gibi değiller.</p>
<h2>Sıfır Noktası</h2>
<p>Şu bir gerçek ki, Asp.Net Core tarafında kullanılan MVC, Razor, Blazor, Web API vb uygulama tipleri ile bunların sıklıkla kullandığı Hosting, Routing, Logging, Configuration, ApplicationLifetime gibi servisler doğrudan Microsoft.Extensions.DependencyInjection yapısı üzerine oturuyorlar<em>(Bu arada Microsoft.Extensions.DependencyInjection kütüphanesinin harici olarak da kullanılabilen bir NuGet paketi olduğunu ve bu sepele bir Console uygulamasında dahi DI mekanizmasını kullanabilmemize olanak sağladığını da hatırlatalım)</em> Onlar için ekstra bir çaba sarf etmeden daha çalışma zamanı ayağa kalkarken sisteme dahil ediliyorlar. Aslında yine kavramlar arasında kayboluyor gibiyiz. Belki de DI kullanmadığımız bir örnekteki basit kusuru görmeye çalışırsak daha iyi olur. DI demişken bu kısaltmanın adını duymuş olmalısın; Dependency Injection! Bu terime alışsan iyi olur, nitekim şirketin temel ilkelerinden birisi de onunla iyi geçinmek. Ancak öncesinde sana problemi göstermem lazım. Yazılımcıların pek de sevmediği bir durum. Tightly-Coupled<em>(birbirine sıkı sıkıya bağlı)</em> olma hali. Haydi gel, bir örnekle durumu açıklayalım.</p>
<p>Sisteminde .Net 5 yüklüğü olduğunu varsayıyorum. Hangi platformda olduğunun çok da önemi yok. Bir Terminal penceresi aç ve aşağıdaki komutu işleterek basit bir MVC projesi oluştur.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet new mvc -o FunnyHello</pre>
<h2>Masum Kodlar Basamağı</h2>
<p>Sonrasında Visual Studio Code, Visual Studio 2019 Community Edition veya muadili bir IDE ile projeni aç. Model klasöründe aşağıdaki içeriğe sahip Game isimli bir sınıf oluştur ve ilk kodlarını yazmış ol. Sen ve arkadaşlarının sevdiği oyunların isimlerini ve liste fiyatlarını tutacağımız basit bir nesne bu aslında.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">namespace FunnyHello.Models
{
public class Game
{
public int Id { get; set; }
public string Title { get; set; }
public decimal ListPrice { get; set; }
}
}</pre>
<p>Başka bir zaman diliminde onu Entity Framework Core üstünden SQL ile ya da Azure Cosmos Db ile veya istediğin başka bir Repository ile ilişkilendirebilirsin. Şimdilik Web uygulaması alanında dolaşımda olacak ve kullanıcının göreceği sayfayı kurgulayan View nesnesi için anlam ifade eden bir model şablonu olduğunu söylesek yeterli. Sıradaki adımımız Data isimli bir klasör oluşturmak ve içerisine aşağıdaki içeriğe sahip GameRepository sınıfını yerleştirmek. </p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FunnyHello.Models;
using System.Collections.Generic;
namespace FunnyHello.Data
{
public class GameRepository
{
public List<Game> GetAllGames()
{
return new List<Game>
{
new Game{ Id=1, Title="Commandos II",ListPrice=10.5M },
new Game{ Id=2, Title="Prince of Persia",ListPrice=9.45M },
new Game{ Id=3, Title="Prince of Persia",ListPrice=9.45M }
};
}
}
}</pre>
<p>Bizi sonuca götürecek, görsel ortamda birkaç veriyi kullanmamızı sağlayan aptalca bir sınıftan başka bir şey değil ama senaryo için yeterli. Şu ana kadar seni zorlayan pek bir şey olmadığı düşüncesindeyim. Haydi o zaman devam edelim. Madem Model View Controller türevli bir Web uygulaması geliştiriyoruz, View ile Model arasındaki iletişim Controller sınıfının görevi olmalı. Öyleyse hali hazırda var olan Controllers klasörüne GameController isimli yeni bir sınıf ekle ve kodlamasını aşağıdaki gibi yaparak devam et. MVC ve detayları içinse üzülme. Eğitim sırasından ondan da bolca bahsedeceğiz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FunnyHello.Data;
using Microsoft.AspNetCore.Mvc;
namespace FunnyHello.Controllers
{
public class GameController : Controller
{
public IActionResult Index()
{
GameRepository gameRepository = new GameRepository();
var games = gameRepository.GetAllGames();
return View(games);
}
}
}</pre>
<p>Gayet prüzsüz bir sınıf. Controller türevli olması bir yana dursun Index isimli fonksiyon<em>(ki uygulama için çağırılabilir bir Action anlamına geliyor)</em> GameRepository sınıfını kullanarak oyun listesini alıp kendisi ile ilişkili olan View'a gönderiyor. Hangi View'a gideceğini nereden mi biliyor? Hımmm...Bunu bir düşünelim. GameController'ın Controller kelimesini çıkarırsak geriye Game kalıyor. View tarafında da Game isimli bir klasör olur ve içinde Index isimli bir sayfa olursa sanırım otomatik bir yönlendirme düzeneği tesis edilmiş olur. Aynı varsayılan şablonla gelen HomeController ve View/Home alıntdaki Index.cshtml düzeneğinde olduğu gibi. O halde sıradaki görevin belli. View klasörüne geçip Game isimli yeni bir klasör oluştur ve altına aşağıdaki içeriğe sahip Index.cshtml dosyasını ekle.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@model IEnumerable<Game>
<div>
<h1>Tüm Oyunlar</h1>
<hr />
<table>
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>List Price</th>
</tr>
</thead>
@foreach (var g in Model)
{
<tr>
<td>@g.Id</td>
<td>@g.Title</td>
<td>@g.ListPrice</td>
</tr>
}
</table>
</div></pre>
<p>Belki kafana takılan bazı sorular olabilir. Neden başlangıça @model diye bir direktif var? Bu sayfa ile arka plandaki nesneler arasında gerekli olan bağlantı nasıl gerçekleşiyor? for döngüsünü biliyorum lakin buradaki kullanım tüm oyun listesini dolaşmak için mi acaba? ve benzerleri. Lütfen sabırlı ol. Amacımız şimdilik bu detaylarla ilgili değil. Minik bir parça daha ekleyelim. Shared klasöründeki _Layout.cshtml sayfasını bul ve içerisine aşağıdaki kod parçasını ekle.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false"><li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Game" asp-action="Index">Games</a>
</li></pre>
<p>Nereye koyman gerektiğini söylemiyorum ancak basitçe bulacağından eminim ;) Tahmin edeceğin üzere yeni bir menü öğesi yerleştirdik ve ona basılınca hangi Controller nesnesinin hangi Action üyesinin tetiklenmesi gerektiği ifade ettik. Eğer hazırsan terminalden <em><strong>dotnet run</strong></em> komutunu vererek ya da Visual Studio ortamındaysan F5 tuşuna basarak örneği çalıştırabilirsin. Aşağıdaki ekran görüntüsündekine benzer bir sonuç elde etmeni bekliyorum.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_1.png" alt="" /></p>
<p>Nasıl? Hiç yoktan iyidir değil mi? Mesela oyun bilgilerinin veritabanından geldiğini düşün. Hatta yeni oyun ekleme, fiyat değiştirme, oyunlara kapak fotoğrafları ekleme, yorum alma ve puan verme gibi kullanıcı etkileşimi yüksek fonksiyonellikler dahil ettiğini düşün. Hatta önyüz tarafında hazır Bootstrap çatısını kullanarak makyaj yaptığını ve albenisi yüksek, moda tabirle UX<em>(User Experience)</em> açısından zengin bir uygulama inşa ettiğini. Etkileyici bir Web uygulaması ortaya koymamız işten bile değil :) Ama ortada bir sorun var gibi.</p>
<h2>Problem Ne?</h2>
<p>Şu anda bir MVC uygulamasına Hello World demiş olduğumuzu sanabilirsin. Biraz üstünde durup düşününce, Controller sınıfının ne yaptığını, View bileşeninin bir Action ile nasıl ilişkilendiğini ve kendisi ile alakalı model nesnelerini nasıl kullandığını anlamış olabilirsin. Ne var ki uygulama şirketimizde çalışan yazılımcıların rahatsız olacağı bir kod parçası içeriyor. Biraz düşünüp neresi olduğunu bulmak ister misin? Arzu edersen bunu bir kahve molası eşliğinde daha da derinlemesine düşünebilirsin.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/matt-hoffman-ZUUsGnG5zwc-unsplash.jpg" alt="" /></p>
<p>Photo by <a href="https://unsplash.com/@__matthoffman__?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Matt Hoffman</a> on <a href="https://unsplash.com/s/photos/coffee-break?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></p>
<p>Tekrar hoşgeldin ;) GameController içerisindeki aşağıdaki kullanıma odaklanmalısın. Bu kullanım yazılımcıların hoşuna gitmez. Gelecek ile ilgili endişeler duymalarına sebebiyet verir.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">GameRepository gameRepository = new GameRepository();</pre>
<p>Bunda ne sorun olabilir ki dediğini duyar gibiyim. Aynen bana da öğretildiği üzere oyun nesnesi üstünde temel CRUD<em>(Create Read Update Delete)</em> operasyonlarını üstelenen ve dolayısıyla sadece bu sorumluluğu üstüne alan bir sınıfın nesne örneğini alıp güzelce kullandın. Bir şekillde ifade etmek istersek kabaca aşağıdaki gibi bir durumun söz konusu olduğunu ifade edebilirim <em>(Ve lütfen çizimimin kötü olmasına aldırma)</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_2.png" alt="" /></p>
<p>İşte o terim; Tightly-Coupled! Yine karşımıza çıktı :D Sorun, GameController nesnesinin GameRepository sınıfını doğrudan kullanması. Bu sıkı bir arkadaşlığın göstergesi gibi. Ancak uygulama kodları arttıkça ve proje ister istemez büyüdükçe GameRepository nesnesinin farklı yerlerde kullanımı da söz konusu olacak. Ya Low-level bileşen olarak ifade edilen GameRepository'nin<em>(yapması gereken işle ilgili kaynaklara doğrudan erişip karmaşık bir şeyler yapan nesne)</em> işleyişi farklılaşır veya adı değişirse? Ya onu kullanan bir test metodunda gerçekten veritabanına gitmeden sırf test senaryosunun kalanını işletmek için hayali bir Game listesi döndürmesi istenen bir fonksiyon gerekirse? Mesela GameRepository, GameController'a küser ve fonksiyonunu kaldırırsa :P İşin şakası bir yana GameController'ın çalışması ve oyuncu listesini View'a vermesi, GameRepository'nin ellerindedir. Bu sıkı bağımlı bileşkeler GameRepository'yi başka bir şeyle değiştirmeyi zorlaştırır.</p>
<h2>Nasıl Çözeriz?</h2>
<p>Sanırım sorun kısmen de olsa anlaşıldı. Bu ikili arasındaki sıkı dostluğa lafımız yok ama ilişkilerine bir mesafe koymalarında yarar var. Peki ya bunu nasıl sağlarız? Aşağıdaki şekle bakmadan biraz düşün derdim ama şu anda onu gördüğünü biliyorum :)</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_3.png" alt="" /></p>
<p>Yapılması gereken GameController'ı GameRepository sınıfından koparmak ve aradaki ipleri gevşetmek<em>(Loosely-Coupled ilkesini sağlanması)</em> Bir başka deyişle, High-Level Component olan GameController ile asıl işi yapan Low-Level Component GameRepository arasına soyut bir katman<em>(abstraction layer)</em> koymak. Asıl işi yapan sınıfın detaylarını umursamayan ve asıl işi yapan sınıfın yaptığı işe ihtiyaç duyan GameController sınıfının isteklerine elçi olan. Ayrıca oyunun kurallarını bir sözleşme ile belirleme ve gerçekten de Controller'ın ihtiyacı olan fonksiyonları verecek asıl nesneyi kullandırma imkanına sahip olacağız. Nesne yönelimli diller açısından baktığımızda bunun en pratik yolu Interface tipini kullanmak. Şimdi üstünlüğü ele geçirelim. Yine Data klasörü altına geç ve IGameRepository isimli aşağıdaki arayüzü ekleyerek çalışmana devam et.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FunnyHello.Models;
using System.Collections.Generic;
namespace FunnyHello.Data
{
public interface IGameRepository
{
IEnumerable<Game> GetAllGames();
}
}</pre>
<p>GameRepository sınıfını bu arayüzden türet<em>(Belki bir dönüş tipi düzeltmesi de yapman gerekebilir)</em> Aynen aşağıdaki kod parçasında olduğu gibi.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FunnyHello.Models;
using System.Collections.Generic;
namespace FunnyHello.Data
{
public class GameRepository
:IGameRepository
{
public IEnumerable<Game> GetAllGames()
{
return new List<Game>
{
new Game{ Id=1, Title="Commandos II",ListPrice=10.5M },
new Game{ Id=2, Title="Prince of Persia",ListPrice=9.45M },
new Game{ Id=3, Title="Prince of Persia",ListPrice=9.45M }
};
}
}
}</pre>
<p>Güzellll! Gayet iyi gidiyorsun. Artık GameController sınıfına geçebilir ve GameRepository yerine eklediğimiz soyutlamayı kullanmasını sağlayabilirsin. Bunun için GamesController sınıfının ilgili interface tipi ile çalışmasını sağlaman lazım. Bildiğin gibi bir interface aslında soyutlama için kullanılan bir sözleşmedir<em>(Contract)</em> ve sadece çağırılacak asıl nesnenin içindeki fonksiyonların neler olduğunu GamesController'a söylemekle yükümlüdür. Şunu da biliyorsun ki Interface gibi arabulucu sözleşmeler sınıflar gibi örneklenip kullanılamazlar<em>(new operatörü ile onları örnekleyemezsin)</em> ama nesne referansı taşıyabilirler ;) Belki de onu Controller sınıfına Constructor metot üstünden alıp kullanabiliriz ;)</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FunnyHello.Data;
using Microsoft.AspNetCore.Mvc;
namespace FunnyHello.Controllers
{
public class GameController : Controller
{
private readonly IGameRepository _gameRepository;
public GameController(IGameRepository gameRepository)
{
_gameRepository = gameRepository;
}
public IActionResult Index()
{
//GameRepository gameRepository = new GameRepository();
var games = _gameRepository.GetAllGames();
return View(games);
}
}
}</pre>
<p>Harika! Sonuca çok yaklaştın. Haydi uygulamayı tekrar çalıştırda, her şey yolunda mı görelim ;)</p>
<h2>Aaa...Houston. We have a problem!</h2>
<p>Galiba sende benim gibi hiç beklenmedik bir hata ile karşılaştın.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_4.png" alt="" /></p>
<p>Bu çalışma zamanı hatası da nereden çıktı şimdi!? Doğruyu söylemek gerekirse pek de sevimli bir ekran görüntüsü değil. Oysaki uygulama derlenebiliyor. Senden ricam StackTrace içeriği ile birlikte hata mesajını dikkatlice okuman.</p>
<p>Sorunu görebildin mi?</p>
<p>GameController sınıfına tekrar dön. Yapıcı metot parametre olarak IGameRepository şeklinde bir interface referansı alıyor. Bir başka deyişle, IGameRepository arayüzünü uygulayan herhangi bir sınıf bu yapıcı metoda referans olarak taşınıyor. Lakin .Net çalışma zamanı bunu henüz bilmiyor. Bir yerlerde bir şekilde IGameRepository görüldüğü anda "Acaba bana ihtiyacım olan bir GameRepository nesnesi verebilir misin?" diyebilmeliyiz. İşte Dependency Inversion Principle'ın süreç yöneticisi Inversion of Control'un elçisi Dependency Injection Container'ların dile geldiği yerdeyiz.</p>
<h2>Ne Gerektiğini Söylemek</h2>
<p>.Net Core içerisindeki built-in DI mekanizması çalışma zamanında yukarıdaki senaryoda görülen bağımlıkların kolayca tanımlanmasına izin verir. Asp.Net tarafı söz konusuysa burası Startup sınıfı içerisindeki IServiceCollection arayüzünün kullanıldığı ConfigureServices metodudur. Oraya aşağıdaki kod parçasını eklemeni rica ediyorum <em>(AddTransient metoduna şimdilik takılma. Raf ömrüne göre farklı kullanım senaryolarımız da var)</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(); //Burası zaten var
services.AddTransient<IGameRepository, GameRepository>();
}</pre>
<p>Artık çalışma zamanında GameController nesnesi IGameRepository üstünden bir fonksiyon işletmek istediğinde gerçekten o işi yapacak asıl nesne<em>(ki senaryomuza göre GameController)</em> elinde hazır olacak. IGameRepository'nin belirlediği sözleşme kurallarının dışına çıkmadığın sürece GameController, GameRepository'deki değişimlerden zerre kadar etkilenmeyecek ;)</p>
<p>Tebrik ediyorum. Eğitimden önce yapman gereken hazırlığı bitirdin ve gerçek anlamda Asp.Net Core için Hello World dedin. Üstelik bunu Constructor Injection tekniği ile yaptın ki bunun dışında metot ve özellik(property) seviyesinde bile Injection tekniklerini kullanacaksın. Lakin her şey daha yeni başlıyor. Neredeyse tüm .Net 5 projelerinde bu DI mekanizmasını kullanacağız. Hatta yarın katmanlar artacak, servisler çoğalacak, repository'ler yerlerini belki de CQRS<em>(Command and Query Responsibility Segregation)</em> desenine bırakacak, nesne arası bağımlılıklar uzak servislere de sıçrayacak vs. Tüm bu serüven sırasında DI Container'lar hep seninle olacak. </p>
<p>Senden istediğim birkaç şey daha var. Bu bir sonraki adımın için iyi bir hazırlık olabilir. Şu senaryoyu düşün;</p>
<p>Sisteme yeni oyun ekleme özelliği sunan bir fonksiyonun olsun. Bir oyun eklendiğinde, üyelere mail ile bildirim yapacak bir sistem de kurgulamak istiyorsun. SQL'deki trigger veya Button'a basılınca çalışan Click olayı gibi. Şu an bunu GameRepository sınıfına ekleyeceğin bir Add metodu içinden yaparsın diye tahmin ediyorum. Gönderim işini ise MailSender isimli bir sınıfla gerçekleştirmeyi düşünebilirsin. Ancak GameRepository ile MailSender birbirlerine sıkı sıkıya bağlı <span style="text-decoration: underline;">olmamalılar</span>. Bu bağımlılığı çöz ;)</p>
<p>Tamam tamam. Seni rahat bırakacağım artık. Lütfen son olarak aşağıdaki maddelere de bir göz at ve cevaplarını dokümante etmeye çalış.</p>
<ul>
<li>Single Responsibility, Dependency Inversion prensiplerini, onları bilmeyen birisine nasıl anlatırsın?</li>
<li>Inversion of Control, Dependency Inversion Principle ile aynı şey midir? Farklarını nasıl tanımlarsın?</li>
<li>High-Level Component ve Low-Level Component ne demektir? Araştırıp birer cümle ile tarifler misin?</li>
<li>Projedeki Data içeriğini harici bir kütüphaneye alıp kullanabilir misin?</li>
<li>Sence DI Container kullanımının artıları nelerdir?</li>
<li>Constructor dışında bir nesne bağımlılığını bildirmenin farklı yolları olabilir mi? Varsa bunları araştırıp örnekler misin?</li>
<li>Örnekte kullanıdığımız Transient fonksiyonu tam olarak ne anlama geliyor? Onun yerini alacak farklı versiyonlar varsa bir bakar mısın?</li>
<li>Örnekte Built-In mekanizma yerine örneğin Unity veya Ninject'i kullanmayı dener misin?</li>
</ul>
<h2>Son Dakika Gelişmesi</h2>
<p>Eğer Constructor Injection dışındaki method, property ve view(Asp.Net MVC 6 sonrası geldi) türevli tekniklerin basit uygulamasına bakmak istersen <a href="https://github.com/buraksenyurt/hands-on-aspnetcore-di" target="_blank">github'a eklediğim hands-on-aspnetcore-di reposu</a>na uğramanı tavsiye edebilirim. Bu repoda varsayılan main haricinde initial, constructor-injection, method-injection, property-injection ve view-injection isimli ayrı branch'ler var. İşe yarayan bir örnek değil ama temiz bir biçimde bu farklı teknikleri nasıl uygulayabileceğini gösteriyor ;)</p>
<p>Eğitimde görüşmek üzere ;) Sağlıklı günler.</p>2021-04-25T09:00:00+00:00.net 5.net coreasp.net mvcdependency injectionsolidinversion of controldependency inversion principlemvcmodel view controllervisual studiobsenyurtYazılım geliştirme işine ciddi anlamda başladığım yeni milenyumun başlarında .Net Framework sahanın yükselen yıldızıydı. C# programlama dilinin gücü ve .Net Framework çatısının vadettikleri düşünülünce bu son derece doğaldı. Aradan geçen neredeyse 20 yıllık süre zarfında .Net Framework'te evrimleşti. Microsoft'un açık kaynak dünyasına girişi, cross-platform stratejileri, Linux gibi bir zamanların ciddi rakipleri ile el sıkışarak hamle yapması sonrasında da son beş yıllık zaman diliminde .Net Core hayatımıza girdi. Bu beraberinde Microsoft'un sıklıkla yaptığı üzere bazı kavram karmaşalarını da beraberinde getirdi. En nihayetinde tek ve birleşik bir .Net 5 ortamından bahsedilmeye başlandı...https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=fe4c3058-5349-418e-a7cd-ac48d69e77eb5https://www.buraksenyurt.com/trackback.axd?id=fe4c3058-5349-418e-a7cd-ac48d69e77ebhttps://www.buraksenyurt.com/post/asp-net-core-a-nasil-merhaba-deriz#commenthttps://www.buraksenyurt.com/syndication.axd?post=fe4c3058-5349-418e-a7cd-ac48d69e77ebhttps://www.buraksenyurt.com/post/tie-fighter-degil-project-tyeTie Fighter Değil, Project Tye!2021-03-30T15:00:00+00:00bsenyurt<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/tie-fighter.png" alt="" align="right" />Star Wars'ın figür kabul edilen gemilerinden birisi imparatorluk güçlerinin Tie Fighter'ıdır. Lord Vader ile özdeşlemiş olan bu figürün kulak tırmalayan ama rahatsız etmeyen sesinin Almanların İkinci Dünya savaşındaki hafif bombardıman uçaklarından birisi olan Junkers Ju-87 Stuka'dan (<em>Sturzkampfflugzeug)</em> geldiği bile söylenir.</p>
<p>Aslında ses tasarımcısı Ben Burtt bu efekti oluşturmak için bir filin başka bir file seslenirken çıkardığı bağrış ile ıslak kaldırımda giden araba seslerini birleştirmiştir. Lakin Tie kelimesi okunurken genellikle Tay veya Taiy diye okunur. Belki de okunmaz :P Benzer sesdeşlik Tie ile Tye arasında da vardır. Ancak Tye esasında Microsoft'un deneysel bir çalışmasıdır.</p>
<p>Github'un <a href="https://github.com/dotnet/tye" target="_blank">şuradaki</a> reposunda açık kaynak olarak yayınlanan Project Tye, Microsoft'un deneysel projelerinden birisi. En azından konuya çalıştığım tarih itibariyle böyleydi. Projenin iki temel amacı var; .Net tabanlı mikroservis çözümlerinin daha kolay geliştirilmesini sağlamak ve söz konusu çözümleri az zahmetle Kubernetes ortamına almak<em>(Deployment)</em> Buna göre birden fazla servisi tek komutla ayağa kaldırmak, Redis, RabbitMQ, Zipkin, Elastic Stack, Ingress vb normalde Sidecar container olabilecek bağımlılıkları kolayca yönetmek, kullanılacak servislerin ortam bağımsız rahatça keşfedilmesini sağlamak<em>(Service Discovery)</em>, uygulamaların container olarak evrilmesi için gerekli hazırlıkları otomatikleştirmek, olabildiğince basit ve tekil bir Kubernetes konfigurasyon dosyası desteği vermek, projenin genel amaçları olarak düşünülebilir.</p>
<p>Elbette bu komut satırı aracının faydalarını görebilmek için sahada denemek gerekir. Bu anlamda yararlandığım başlıca iki önemli kaynak var. Amazon'dan kısa süre önce aldığım <a href="https://www.amazon.com/Adopting-NET-Understand-architectures-migration/dp/1800560567" target="_blank">Adopting .NET 5: Understand modern architectures, migration best practices, and the new features in .NET 5</a> isimli kitap ve Microsoft Program Yöneticisi rolünde çalışan Amiee Lo'nun <a href="https://devblogs.microsoft.com/aspnet/introducing-project-tye/" target="_blank">şu adresteki</a> giriş makalesi. Her iki kaynaktaki örnekleri de kopyalama yapmadan bizzat yazarak çalıştım ve sonuçta github reposundan bazı notlar birikti. Şu anda bu notları bir araya topladığım yazıyı okumaktasınız.</p>
<p>Örneklere geçmeden önce uygulamaları geliştirdiğim sistemden bahsetmem gerekiyor. Windows 10 üzerinde, Visual Studio 2019 Community Edition kullanıyorum. Ortamda .Net 5 yüklü durumda. Kubernetes özelliği aktif olan bir Docker Desktop var. Dolayısıyla sonradan ihtiyacımız olacak kubectl komut satırı aracı kullanılabilir halde. Ayrıca Windows Subsystems on Linux<em>(WSL)</em>, 2.0 sürümüne güncellenmiş durumda. Geliştireceğimiz her iki örnekte Service Discovery için yerel bir adres kullanacak ancak gerçek hayat senaryolarında bunun yerini DockerHub veya Azure Container Registry gibi bir hizmet alması muhtemeldir. Tabii tüm bunların yanında bize tye komut satırı aracının kendisi de lazım :D İşte başlangıç adımları için gerekli terminal komutlarımız.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># Sisteme tye yüklemek için aşağıdaki terminal komutu kullanılabilir(Son sürüme bakmak lazım. Sonuçta bu şimdilik deneysel bir proje)
dotnet tool install -g Microsoft.Tye --version "0.5.0-alpha.20555.1"
# Kubernetes deployment öncesi Service Discovery için kullanacağımız local registry
docker run -d -p 5000:5000 --restart=always --name registry registry:2
# Docker Desktop tarafında Enable Kubernetes seçeneğinin de işaretli olması lazım
# Kubernetes'in etkin olduğunu anlamak içinse aşağıdaki komut işletilebilir
kubectl config current-context
# Bize docker-desktop cevabını vermeli</pre>
<h1>Hello World Örneği: StarCups</h1>
<p>StarCups kod adlı ilk çalışmada bir frontend, bir backend<em>(servis tabanlı)</em> ve birde Redis mevzu bahis. Senaryoda StarCups isimli hayali bir kahve firması var. HeadOffice isimli web arayüzünden İstanbul'un çeşitli semtlerindeki kahve dükkanlarının malzeme taleplerini anlık olarak görebiliyoruz. Malzeme bilgileri StockCollector isimli REST tabanlı çalışan bir Web API servisi üstünden geliyor. Redis ise StockCollector'un çektiği veriyi belli süre cache'lemek için kullanılıyor<em>(Aslında en genel uygulama geliştirme pratiği olarak düşünebiliriz. Önyüz tarafı iş fonksiyonellikleri için arka taraftaki bir servisle konuşur)</em> Bu Hello World kıvamındaki örnekte amaç, Tye aracı ile uygulamaların kolayca ayağa kaldırılması, denenmesi, zahmetsizce dockerize edilmesi, loglarına bakılması, çevre değişkenlerinin yaml bazlı yönetilmesi ve Kubernetes tarafına en basit şekliyle Deploy edilmesi şeklinde özetlenebilir. İlk çözümü oluşturmak için aşağıdaki terminal komutları ile hareket edebiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">mkdir Starcups
cd Starcups
# Bir tane frontend uygulaması. Razor tipinde.
dotnet new razor -n HeadOffice
# frontend'in konuşacağı bir WebAPI
dotnet new webapi -n StockCollector
dotnet new sln
dotnet sln add HeadOffice StockCollector
tye run</pre>
<p>Bu komut sonrası solution içerisindeki uygulamalar otomatik olarak kendileri için tahsis edilmiş process ve adreslerden ayağa kalkacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_1.png" alt="" /></p>
<p>Şu haldeyken tye ile çözümü çalıştırıp localhost:8000 adresine gidebiliriz. Her iki uygulama da Dashboard üstünde görünür ve ayrı ayrı incelenebilir ki inceleyin derim :) View kısmına bir bakın, Bindings kısmından sayfalara gitmeye çalışın. Tabii Api servis için bir rest çağrısı şeklinde gitmeniz gerekir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_2.png" alt="" /></p>
<p>Şık ve uygulamaların kolayca erişilip, loglarına bakıldığı arayüz dışında ortada henüz bir numara yok. Örneğin frontend ile backend şu anda birbirlerinden bihaberler. Frontend'in backend ile konuşuyor olması da lazımdı. Şimdi WebAPI tarafına OrderData sınıfını ekleyip WeatherForecastController tipini de OrderController olarak değiştirip kodlayarak ilerleyelim.</p>
<p>OrderData sınıfımız;</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
namespace StockCollector
{
public class OrderData
{
public string ShopName { get; set; }
public string ItemName { get; set; }
public double Quantity { get; set; }
public DateTime Time { get; set; }
}
}</pre>
<p>OrderController sınıfımız;</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace StockCollector.Controllers
{
[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
private static readonly string[] ShopNames = new[]
{
"Capitol", "Balat", "Taksim Meydan", "Pendik Marina", "Bebek", "Koşuyolu", "Bakırköy", "Moda", "Beşiktaş Arena", "Maslak 1881"
};
private static readonly string[] Items = new[]
{
"Peçete (100 * Adet)", "Karıştırma Kaşığı (100 * Adet)", "Şeker (Kilo)","Short Bardak (100 * Adet)"
};
private readonly ILogger<OrderController> _logger;
public OrderController(ILogger<OrderController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<OrderData> Get()
{
var rng = new Random();
return Enumerable.Range(1, 10).Select(index => new OrderData
{
ItemName = Items[rng.Next(Items.Length)],
Quantity = rng.Next(1, 10),
ShopName = ShopNames[rng.Next(ShopNames.Length)],
Time = DateTime.Now
}).ToArray();
}
}
}
</pre>
<p>Kod rastgele OrderData nesneler listesi üretip geri döndüren basit bir operasyona sahip. Frontend tarafının bu servise gelmesini istiyoruz. Normal şartlarda localhost üstündeki ilgili backend adresini alıp kullanan bir HttpClient nesnesi pekala işimizi görebilir. Lakin bu örneği yarın öbür gün Kubernetes'e alacağız. Dockerize edilerek çalışacak Container için adres bilgileri çevre değişkenlerden gelebilir, hatta uzak bir konfigurasyon yöneticisinden bile desteklenebilir. Yani frontend'in hangi servisteki backend uygulaması ile konuşacağını kolayca keşfedebilmesi önemlidir. Bu işi tye üstünden yapmak istediğimiz için frontend tarafında küçük bir hazırlık yapmalıyız. İlk olarak Microsoft.Tye.Extensions.Configuration nuget paketini HeadOffice uygulamasına ekleyelim.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">cd HeadOffice
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..</pre>
<p>Sonrasında HeadOffice isimli frontEnd uygulamasından REST çağrısı yaparken kullanacağımız OrderClient ve gelen veriyi nesne olarak ele alacağımız OrderData<em>(Backend taraftaki ile aynı yapıdadır)</em> sınıflarını geliştirelim.</p>
<p>OrderClient sınıfı REST çağrısı yapmamızı kolaylaştıran bir tip.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace HeadOffice
{
public class OrderClient
{
private readonly JsonSerializerOptions options = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
private readonly HttpClient client;
public OrderClient(HttpClient client)
{
this.client = client;
}
public async Task<OrderData[]> GetOrdersAsync()
{
var responseMessage = await this.client.GetAsync("/order");
var stream = await responseMessage.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<OrderData[]>(stream, options);
}
}
}</pre>
<p>Derken HeadOffice'deki Index.cshtml<em>(cs ile birlikte) </em>sayfasını da aşağıdaki gibi düzenleyelim.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Melaba!!! Kahvenin hası burada.</h1>
<p>Star Cups Mağzaları...</a>.</p>
</div>
Son Siparişler
<table class="table">
<thead>
<tr>
<th>Tarih</th>
<th>Dükkan</th>
<th>İstenen</th>
<th>Miktar</th>
</tr>
</thead>
<tbody>
@foreach (var ord in @Model.Orders)
{
<tr>
<td>@ord.Time.Ticks</td>
<td>@ord.ShopName</td>
<td>@ord.ItemName</td>
<td>@ord.Quantity</td>
</tr>
}
</tbody>
</table>
</pre>
<p>Index.cshtml.cs sınıfı</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
namespace HeadOffice.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public OrderData[] Orders { get; set; }
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public async Task OnGet([FromServices] OrderClient client)
{
Orders = await client.GetOrdersAsync();
}
}
}</pre>
<p>Tekrar tye tarafına dönelim. Çözüm içerisindeki servislerle ilgili çevre konfigurasyon ayarlamaları için bir yaml dosyasına ihtiyacımız olacak. Bu dosyayı solution klasöründe aşağıdaki terminal komutu ile kolayca oluşturabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye init</pre>
<p>Tye.yaml içeriği aşağıdaki gibi oluşur. Buna göre iki servis söz konusudur. Tye, .net odaklı bir enstrüman olduğundan solution içindeki proje dosyalarını otomatik olarak algılayıp gerekli servis bildirimlerini yapar.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">name: starcups
services:
- name: headoffice
project: HeadOffice/HeadOffice.csproj
- name: stockcollector
project: StockCollector/StockCollector.csproj</pre>
<p>Bu aşamada çözüm çalıştırılır ve tarayıcı ile HeadOffice uygulamasına gidilirse ekran görüntüsünde olduğu gibi servis tarafıyla konuşulabildiği görülür. Şu noktada HeadOffice tarafında, backend için bir adres bildirimi yapmadığımız dikkatinizden kaçmamalıdır. Tye çalışmaya başladığında backend'i hangi adresten ayağa kaldırdıysa, frontend tarafında da o adres kullanılır.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye run</pre>
<h2><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_3.png" alt="" /><br />StarCups için Redis Desteğinin Eklenmesi</h2>
<p>Dağıtık mimariler söz konusu olduğunda Redis, RabbitMQ gibi hizmetler eğer single node üstünde çalışılıyorsa genellikle Sidecar Container olarak ele alınabilirler. Tye bu konuda bize bazı kolaylıklar sağlar. Ne demek istediğimi anlatamabilmek için backend servisine Redis desteğini ekleyerek devam edelim. Redis desteği'ni de yaml dosyaları ile yöneteceğiz. Öncelikle backend uygulamasında Redis kullanabilmek için gerekli Nuget paketini ilave ediyoruz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">cd StockController
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
cd ..</pre>
<p>Sonrasında OrderController sınıfındaki Get metodunu Redis'i kullanacak hale getiriyoruz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">[HttpGet]
public async Task<string> Get([FromServices] IDistributedCache cache)
{
var keyOrder = await cache.GetStringAsync("keyOrder");
if (keyOrder == null)
{
_logger.LogInformation("Redis Key boştu");
var rng = new Random();
var orders = Enumerable.Range(1, 10).Select(index => new OrderData
{
ItemName = Items[rng.Next(Items.Length)],
Quantity = rng.Next(1, 10),
ShopName = ShopNames[rng.Next(ShopNames.Length)],
Time = DateTime.Now
}).ToArray();
keyOrder = JsonSerializer.Serialize(orders);
_logger.LogInformation($"Veri serileştirildi {keyOrder}");
await cache.SetStringAsync("keyOrder", keyOrder, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10)
});
}
return keyOrder;
}</pre>
<p>ve Redis için Startup.cs içerisindeki ConfigureService metodunda gerekli düzenlemeyi yapıyoruz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Redis için aşağıdaki satır eklendi
// Bağlantı bilgisi yaml üstünden gelecek
services.AddStackExchangeRedisCache(o =>
{
o.Configuration = Configuration.GetConnectionString("redis");
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "StockCollector", Version = "v1" });
});
}</pre>
<p>Burada altını çizmemiz gereken bir nokta var ki o da GetConnectionString'e gelen redis ifadesi. Normalde projemizin appSettings.json dosyasında redis için bir bölüm bulunmuyor. Tahmin edeceğiniz üzere buradaki redis adres tanımı tye.yaml üstünden okunuyor. Bu nedenle tye.yaml içeriğini aşağıdaki şekilde güncellemeliyiz.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">name: starcups
services:
- name: headoffice
project: HeadOffice/HeadOffice.csproj
- name: stockcollector
project: StockCollector/StockCollector.csproj
- name: redis
image: redis
bindings:
- port: 6379
connectionString: "${host}:${port}"
- name: redis-cli
image: redis
args: "redis-cli -h redis MONITOR"</pre>
<p>Güncel yaml içeriğinde redis ve redis-cli isimli iki yeni bildirim görüyorsunuz. Standart olarak 6379 portundan hizmet veren redis sunucusu ve kolay bir şekilde onu monitor etmemizi sağlayan redis-cli hizmeti. </p>
<p>Artık backend uygulaması Redis ile çalışır hale geldi. Bu aşamada yine tye run ile örneği çalıştırıp, redis servislerinin ayağa kalkıp kalkmadığına bakmak ve 10 saniyede bir cache'in düşüp yeni bilgilerin getirildiğini görmek iyi olacaktır. tye run ile sistem ayağa kaldırıldığında aşağıdaki ekran görüntüsünden de görüldüğü gibi redis hizmeti de çalışmaya başlar. Bu arada redis için docker imajı kullanıldığını fark etmiş olmalısınız. Yani redis hizmeti bir Container olarak ayağa kalkar. Aynı işleyip redis-cli hizmeti için de söz konusudur <em>(Buradan terminal komutu loglarını okumanın faydalarını da görebilirsiniz)</em></p>
<p> <img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_4.png" alt="" /></p>
<p>Tye dashboard üstünde de benzer şekilde redis ve redis-cli hizmetlerinin çalışıyor olduğunu görmemiz lazım.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_5.png" alt="" /></p>
<p>Hatta redis-cli loglarına gidersek cache'e atılan JSON içeriklerini de takip edebiliriz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_6.png" alt="" /></p>
<h2>StartCups'ın Kubernetes Ortamına Alınması</h2>
<p>Gelelim diğer bir hedefimize. Buraya kadar yapılan işlemler sayesinde solution içindeki uygulamaları bağımlı servisleri ile birlikte basitçe çalıştırıp, monitör edebildik. Ancak bunları Kubernetes gibi bir ortama nasıl alırız? Bu aşamada Sidecar gibi görünen redis için ayrı bir yaml dosyasına ihtiyacımız olacak. Bunu redis servisini Kubernetes ortamına ayrıca almak için kullanacağız. Söz konusu dosyayı aşağıdaki gibi oluşturabiliriz. </p>
<p>redis.yaml</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app.kubernetes.io/name: redis
app.kubernetes.io/part-of: starcups
spec:
selector:
matchLabels:
app.kubernetes.io/name: redis
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: redis
app.kubernetes.io/part-of: starcups
spec:
containers:
- name: redis
image: redis
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app.kubernetes.io/name: redis
app.kubernetes.io/part-of: starcups
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app.kubernetes.io/name: redis</pre>
<p>Redis için kubernetesçe bir içerik söz konusu. Kubernetes konusuna çok hakim olmadığım için anladığım kadarıyla ifade etmeye çalışayım. Kubernetes'e redis için kullanacağı docker imajını, replika adedini, port bilgisini, cpu ve memory gibi ayrılması istenen sistem kaynaklarını, kısaca dağıtım ve servis manifestosunu bildiriyoruz. Bu manifestoyu Kubernetes tarafının işletmesi içinse aşağıdaki terminal komutunu kullanmamız gerekiyor<em>(Yazının başlarında kubectl'ye ihtiyacımız olacağını söylemiştim)</em></p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">kubectl apply -f redis.yaml</pre>
<p>Redis'in Kubernetes tarafında ayağa kaldırılması tek başına yeterli değil. Buraya yapılan dağıtım sonrası servislerin keşfi için de bir registry kullanılması gerekiyor. Bunu tye.yaml dosyasında aşağıdaki gibi bildirebiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">name: starcups
registry: localhost:5000
services:
- name: headoffice
# Diğer kısımlar</pre>
<p>Tabii bunu söylemek de yeterli değil. localhost:5000 adresinde gerçekten bir Registry servisinin olması lazım. Bunun içinse aşağıdaki terminal komutuna ihtiyacımız var. registry imajını kullanan ve açıkça kapatılana kadar sürekli çalışacak bir container.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># container registry için aşağıdaki komut kullanılabilir.
docker run -d -p 5000:5000 --restart=always --name registry registry:2</pre>
<p>Kubernetes deployment işlemi için deploy komutunu aşağıdaki gibi kullanmamız gerekiyor. Harici bir servis olarak Redis kullandığımızdan, ona hangi adresle erişeceğimiz de sorulur. Bu soruyu <em>redis:6379</em> şeklinde cevaplayarak ilerleyebiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye deploy --interactive
# Aşağıdaki komutlar ile kubernetes deployment ve pod durumları kontrol edilir.
kubectl get deployment
kubectl get svc
kubectl get secrets
kubectl get pods</pre>
<p>İşlemler sırasında terminal hareketlilikleri takip edilirse, tye.yaml üstünde belirtilen projeler için Dockerize işlemlerinin otomatik olarak yapıldığı da görülebilir. Dikkat ederseniz herhangibir Dockerfile oluşturmadık. Deployment işlemi başarılı ise get pods ile aktif olarak çalışan pod'ları görebilmemiz gerekir. Aynen aşağıdaki ekran görüntüsüne olduğu gibi.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_7.png" alt="" /></p>
<p>Frontend uygulamasının web arayüzüne erişmek için port-forward işlemi uygulamamız gerekebilir<em>(Cluster dışından erişmek istediğimiz için)</em> Bunun için aşağıdaki terminal komutunu çalıştırmak yeterli olacaktır.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">kubectl port-forward svc/headoffice 80:80</pre>
<p>Sonrasında localhost:80 adresine gidilirse web uygulamasına ulaşıldığı ve anlık olarak kahve dükkanlarımızın beklediği malzemeler görülebilir. Aynen aşağıdaki ekran görüntüsünde olduğu gibi.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mart/screenshot_8.png" alt="" /></p>
<p>Çok doğal olarak şu noktada Kubernetes ortamına yapılan dağıtımı geri almak isteyebilirsiniz. Tye bu işlemi basitleştirir.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye undeploy</pre>
<p>Peki şimdi ne oldu? Normal bir .net çözüm ailesini kullanışlı bir dashboard üstünden izlemeyi, kolayca çalıştırmay<em>ı(Solution Run'dan farklı olarak)</em>, redis'i hem development hem kubernetes için nerededir diye düşünmeden bağlamayı ama daha da önemlisi bu çözümü kubernetes'e taşımak istersek o ortamda da çalışabileceğini görmüş olduk. Sizde bu örneği güzel bir şekilde tamamladıysanız ikincisine geçebiliriz. Bu kez senaryo <a href="https://www.amazon.com/Adopting-NET-Understand-architectures-migration/dp/1800560567">okuduğum kitaptan</a> geliyor.</p>
<h1>Bonus: SchoolOfMath Senaryosu</h1>
<p>Yeni pratiğimizde aşağıdaki şekilde görülen senaryo söz konusu olacak.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/Project_Tye_Senaryo.png" alt="" /></p>
<p>Çok daha keyifli bir senaryo olduğunu söyleyebilirim. Benim için yeni deneyimler içeriyordu. Kısaca çözümdeki aktörlerin ne işe yaradığını anlatarak devam edelim.</p>
<ul>
<li>Einstein, gRPC tabanlı bir servis sağlayıcı. İçinde Palindrom sayıları hesap eden<em>(Kitapta asal sayı buluyordu :P)</em> bir fonksiyon desteği sunuyor. Servis cache stratejisi için Redis'i kullanacak. Cache'te ne mi tutacağız? Daha önceden Palindrome olarak işaretlenmiş bir sayı varsa onu kendi adıyla Cache'e alacağız ve bir saat boyunca saklamasını isteyeceğiz. Aynı sayı tekrar istenirse hesaplanmadan doğrudan cache'den gelecek. Sırf Redis hizmeti bu senaryoda olsun diye. Ayrıca bir mesaj kuyruğu sistemi de var ki bu noktada RabbitMQ'dan yararlanacağız.</li>
<li>Evelyne, Bruce ve Madeleine aktörleri Worker tipinden istemci servisler<em>(Onları, ayağa kalktıktan sonra sürekli olarak talep gönderen servisler olarak düşünebiliriz)</em> Belli bir sayıdan başlayarak Eintesein'a talep gönderiyorlar ve gönderikleri sayının Palindrom olup olmadığını öğreniyorlar.</li>
<li>Robert ise RabbitMQ kuyruğunu dinleyen diğer bir Worker servis.</li>
</ul>
<p>Amacımız bir önceki örnekte olduğu gibi bu çözümü Tye destekli olarak inşa edip az zahmetle Kubernetes'e alabilmek.</p>
<h2>Proje İskeletinin Oluşturulması</h2>
<p>Bunun için aşağıdaki adımları icra edelim. Öncelike Palindrom sayı hesaplayan Einstein gRPC servisini geliştirelim.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">mkdir SchoolOfMath
cd SchoolOfMath
dotnet new sln
dotnet new grpc -n Einstein
dotnet sln add Einstein</pre>
<p>Protos klasöründeki greet.proto ile servis tarafını değiştirmemiz gerekiyor.</p>
<p>palindrome.proto içeriği şöyle oluşturulabilir. long tipinden değer alıp bool olarak cevap veren iki mesaj söz konusu. Fonksiyonumuz ise IsItPalindrome. gRPC için gerekli şemayı bu şekilde tanımlamış olduk.</p>
<pre class="brush:cpp;auto-links:false;toolbar:false" contenteditable="false">syntax = "proto3";
option csharp_namespace = "SchoolOfRock";
package palindrome;
service PalindromeFinder {
rpc IsItPalindrome (PalindromeRequest) returns (PalindromeReply);
}
message PalindromeRequest {
int64 number= 1;
}
message PalindromeReply {
bool isPalindrome= 1;
}</pre>
<p>PalindromeFinderServis sınıfı;</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Grpc.Core;
using Microsoft.Extensions.Logging;
using SchoolOfRock;
using System.Threading.Tasks;
namespace Einstein
{
public class PalindromeFinderService
: PalindromeFinder.PalindromeFinderBase
{
private readonly ILogger<PalindromeFinderService> _logger;
public PalindromeFinderService(ILogger<PalindromeFinderService> logger)
{
_logger = logger;
}
public override async Task<PalindromeReply> IsItPalindrome(PalindromeRequest request, ServerCallContext context)
{
long r, sum = 0, t;
var num = request.Number;
for (t = num; num != 0; num /= 10)
{
r = num % 10;
sum = sum * 10 + r;
}
if (t == sum)
return new PalindromeReply { IsPalindrome = true };
else
return new PalindromeReply { IsPalindrome = false };
}
}
}</pre>
<p>Servis tarafını şimdilik bırakalım ve ilk istemci uygulama kodlarını yazarak devam edelim.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet new worker -n Evelyne
dotnet sln add Evelyne
# Evelyne'nin gRPC servisini kullanabilmesi için gerekli Nuget paketleri eklenmelidir.
cd Evelyne
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Net.ClientFactory
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
# Ayrıca Tye konfigurasyonu için gerekli extension paketi de yüklenir
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..</pre>
<p>Visual Studio 2019 kullanıyorsak Add new gRPC Service Reference<em>(Connected Services kısmından)</em> ile Einstein'daki proto dosyasının fiziki adresini göstererek gerekli proxy tipinin üretilmesini kolayca sağlayabiliriz. İşte bu noktalarda Visual Studio ile çalışmanın avantajları ortaya çıkıyor. Uygulamanın program.cs ve worker.cs içeriklerini de düzenlememiz lazım.</p>
<p>Program sınıfı;</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SchoolOfRock;
using System;
namespace Evelyne
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
// gRPC istemcisini çalışma zamanına ekliyoruz
services.AddGrpcClient<PalindromeFinder.PalindromeFinderClient>(options =>
{
// servis adresini Tye extension fonksiyonu üstünden çekiyoruz
// Eğer debug modda çalışıyorsak (tye.yaml olmadan tye run ile mesela) einstein'ın 7001 nolu adresine yönlendiriyoruz.
options.Address = hostContext.Configuration.GetServiceUri("einstein") ?? new Uri("https://localhost:7001");
});
services.AddHostedService<Worker>();
});
}
}</pre>
<p>Program sınıfında gRPC servis adresinin nasıl alındığına dikkat edelim.</p>
<p>Worker sınıfı 100 milisaniyede bir Einstein servisine talep gönderecek şekilde kodlanmış durumda.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SchoolOfRock;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Evelyne
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly PalindromeFinder.PalindromeFinderClient _client;
// gRPC servisini constructor üzerinden içeriye enjekte ediyoruz
public Worker(ILogger<Worker> logger,PalindromeFinder.PalindromeFinderClient client)
{
_logger = logger;
_client = client;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Servisin ayağa kalkması için bir süre bekletiyoruz. Makine soğuk.
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
_logger.LogInformation("### Servis başlatılıyor ###");
long number = 1; // Evelyne, 1den itibaren sayıları hesap etmeye başlayacak
while (!stoppingToken.IsCancellationRequested)
{
try
{
var response = await _client.IsItPalindromeAsync(new PalindromeRequest { Number = number });
_logger.LogInformation($"{number}, palindrom bir sayıdır önermesinin cevabı = {response.IsPalindrome}\r");
}
catch (Exception ex)
{
// Bir exception oluşması halinde Worker'ın işleyişini durduracağız
if (stoppingToken.IsCancellationRequested)
return;
_logger.LogError(-1, ex, "Bir hata oluştu. Worker çalışması sonlanıyor.");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
number++;
if (stoppingToken.IsCancellationRequested)
break;
await Task.Delay(TimeSpan.FromMilliseconds(100), stoppingToken); // İstemci 100 milisaniyede bir ateş edecek :P
}
}
}
}</pre>
<p>İlk Worker servise benzer şekilde Bruce ve Madeleine isimli Worker servisleri de ekleyerek devam edebiliriz. Buradaki kodlar benzer olduğu için eklemedim ancak github üstünden alabilir ya da aşağıdaki notlarda olduğu gibi Palindrome başlangıç değerleriyle oynayarak yukarıdaki kodu kullanabilirsiniz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># Bruce için tek fark Palindrome sayı taleplerine 1den değil de 10000den başlamasıdır
dotnet new worker -n Bruce
dotnet sln add Bruce
cd Bruce
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Net.ClientFactory
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..
# Madeleine de benzer şekilde eklenir
dotnet new worker -n Madeleine
dotnet sln add Madeleine
cd Madeleine
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Net.ClientFactory
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration
cd ..</pre>
<p>Yukradaki işlemler tamamlandıktan sonra en azından aşağıdaki terminal komutu ile servislerin ayağa kalkıp kalkmadığına bakmakta yarar var. Bu arada uygulamalarımız için herhangi bir Dockerize işleminin olmadığı dikkatinizden kaçmamıştır diye düşünüyorum. Nitekim henüz Kubernetes hazırlıklarına başlamadık. Bu nedenle tye söz konusu uygulamaları localhost:random_port_number formasyonunda birer process olarak ayağa kaldırmıştır.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye run</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_2.png" alt="" /></p>
<h2><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_1.png" alt="" /><br />Redis ve RabbitMQ Desteğinin Eklenmesi</h2>
<p>İlk Hello World örneğinde Redis desteğini eklemiştik. Aynı adımları burada da uygulayacağız. Ayrıca rabbitmq hizmetini de dahil edeceğiz. Özellikle dağıtık mimarinin event-based modelinde uygulamalar arası haberleşmede mesaj bazlı kuyruk sistemleri sıklıkla karşımıza çıkıyor. Kafka ve RabbitMQ sanıyorum ki en çok başvurduklarımız. Dolayısıyla RabbitMQ için aranan Sidecar container'lardan birisi olduğunu ifade etsek yeridir. Şimdi gelin bu iki aktörü sisteme dahil ederek Kubernetes hazırlıklarına geçelim. </p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># İşe tye.yaml dosyasının oluşturulmasıyla başlıyoruz.
tye init
# tye.yaml dosyasına redis için gerekli ekleri yaptıktan sonra
# einstein (gRPC API servisimiz) cache desteği için gerekli nuget paketlerini ekleyip devam ediyoruz
cd einstein
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
#Sonrasında rabbitmq paketini ekliyoruz.
dotnet add package RabbitMQ.Client
cd ..</pre>
<p>Palindrome sayılar buldukça bunları RabbitMQ'ya mesaj olarak yollayacak bir düzenek ekleyeceğimizi de söylemiştik. RabbitMQ'da, Redis gibi çalışma zamanında ayakta olması beklenen bir servis. Bu nedenle tye.yaml dosyasında RabbitMQ için gerekli eklemeler aşağıdaki gibi yapılmalı.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">name: schoolofmath
registry: localhost:5000 # container registry adresi
services:
- name: einstein
tags:
- backend
project: Einstein/Einstein.csproj
replicas: 1
env: #rabbitmq için kullanıcı adı, şifre ve varsayılan kuyruk adı bildirimi
- RABBIT_USER=guest
- RABBIT_PSWD=guest
- RABBIT_QUEUE=palindromes
- name: evelyne
tags:
- client
project: Evelyne/Evelyne.csproj
- name: bruce
tags:
- client
project: Bruce/Bruce.csproj
- name: madeleine
tags:
- client
project: Madeleine/Madeleine.csproj
- name: robert
tags:
- middleware
project: Robert/Robert.csproj
- name: redis
tags:
- backend
image: redis
bindings:
- port: 6379
connectionString: "${host}:${port}"
- name: redis-cli #redis cache tarafında ne olduğunu izlemek için ekledik. Ancak mecburi değil. Opsiyonel.
tags:
- backend
image: redis
args: "redis-cli -h redis MONITOR"
- name: rabbitmq # RabbitMQ servisini MUI arabirimi ile birlikte ekliyoruz.
# Mui arabirimine aşağıdaki kriterlere göre localhost:15672'den quest/quest log in bilgisi ile erişebiliriz
tags:
- middleware
image: rabbitmq:3-management
bindings:
- name: mq-binding # mq_binding veya mui_binding şeklinde kullanınca K8s deploy işleminde kullanılan secret değerlerinde hata alındı. - veya . olarak yazılmalı.
port: 5672
protocol: rabbitmq
- name: mui-binding
port: 15672</pre>
<p>Elbette PalindromeFinderService sınıfı ve Startup.cs'in de Redis ve RabbitMQ için yeniden revize edilmeleri gerekiyor.</p>
<p>PalindromeFinderService sınıfı</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Einstein.Rabbit;
using Grpc.Core;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using SchoolOfRock;
using System;
using System.Threading.Tasks;
namespace Einstein
{
public class PalindromeFinderService
: PalindromeFinder.PalindromeFinderBase
{
private readonly ILogger<PalindromeFinderService> _logger;
private readonly IDistributedCache _cache;
private readonly PalindromeReply True = new() { IsPalindrome = true };
private readonly PalindromeReply False = new() { IsPalindrome = false };
private readonly IMessageQueueSender _mqSender;
private readonly string _queueName;
public PalindromeFinderService(ILogger<PalindromeFinderService> logger, IDistributedCache cache, IMessageQueueSender mqSender)
{
_logger = logger;
_cache = cache; //Dağıtık cache servisi olarak Redis konumlanacak. Startup'ta onu ekledik çünkü.
_mqSender = mqSender; // MQ nesnesini alıyoruz
_queueName = Constants.GetRabbitMQQueueName(); //MQ adını alıyoruz.
}
public override async Task<PalindromeReply> IsItPalindrome(PalindromeRequest request, ServerCallContext context)
{
long r, sum = 0, t;
var number = request.Number;
var inCache = await _cache.GetStringAsync(request.Number.ToString()); // bu sayı Redis Cache'te var mı?
if (inCache == "YES")
{
_logger.LogInformation($"{request.Number} palindrom bir sayıdır ve şu an Redis'ten getiriyorum. Hesap etmeye gerek yok");
return True;
}
for (t = number; number != 0; number /= 10)
{
r = number % 10;
sum = sum * 10 + r;
}
if (t == sum)
{
_logger.LogInformation($"{request.Number} palindrom bir sayı ama Redis cache'e atılmamış. Şimdi ekleyeceğim.");
// Sayı adını Key olarak kullanıp Cache'e atıyoruz ve ona value olarak YES değerini atıyoruz.
await _cache.SetStringAsync(request.Number.ToString(), "YES", new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60)
});
// Palindrome sayı ise onu Redis Cache'e atıyoruz.
// Ayrıca RabbitMQ kuyruğuna da sayıyı atıyoruz.
_mqSender.Send(_queueName, request.Number.ToString());
return True;
}
else
return False;
}
}
}</pre>
<p>Einstein, Startup.cs'in son hali;</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">using Einstein.Rabbit;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Einstein
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc();
// RabbitMQ Desteği eklendi
services.AddRabbitMQ();
// Redis bildirimini yaptık. PalindromeFinderService, consturctor'dan alacak.
services.AddStackExchangeRedisCache(o =>
{
o.Configuration = Configuration.GetConnectionString("redis") ?? "localhost:6379";
});
}
// Diğer kısımlar</pre>
<p>Kod tarafında RabbitMQ kullanımı için gerekli tipler, GoldenHammer isimli sınıfta yer alıyor. Bunu baştan yazmak biraz zahmetli ama yine de üşenmeyin yazın derim. Yazarken düşünecek ve neden böyle kullanılmış ki diyeceksiniz. Kitabın yönlendirmesi ile ben bu <a href="https://github.com/PacktPublishing/Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features/tree/master/Chapter04/microservicesapp" target="_blank">adrese</a> gittim ama kendimde teknik borç riskini göze alarak bir <a href="https://github.com/buraksenyurt/tye_sample_v2/tree/main/SchoolOfMath" target="_blank">GodObject oluşturdum.</a> Eğer sayfadan ayrılmadan kodu kullanmak isterseniz notların sonundaki <em>Yardımcı Kodlar</em> kısmından yararlanabilirsiniz. Bu noktada yine tye run ile ilerlemek önemli. Redis'in çalıştığından ve http://localhost:15672 adresine gittiğimizde RabbitMQ tarafının işler olduğundan emin olmakta fayda var.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_4.png" alt="" /></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_5.png" alt="" /></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_6.png" alt="" /></p>
<h2>Robert: AMQP İstemcisinin Eklenmesi</h2>
<p>Robert isimli Worker tipinden olan son istemci uygulama, RabbitMQ'ya atılan palindrome sayıları içeren mesajları okumakla görevli. Basit bir RabbitMQ Consumer olduğunu söyleyebiliriz. Einstein isimli servis Palindrome sayı buldukça RabbitMQ'ya bunu mesaj olarak yollayacak şekilde ayarlanmıştı. Consumer üstünden bunları yakalamayı bekliyoruz. Aşağıdaki terminal komutları ile Worker servisini oluşturalım.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet new worker -n Robert
dotnet sln add Robert
cd Robert
# RabbitMQ istemcisi olacağı için eklenecek paket
dotnet add package RabbitMQ.Client
# ve pek tabii Tye özelliklerini kullanabilmesi için de gerekli konfigurasyon paketi
dotnet add package --prerelease Microsoft.Tye.Extensions.Configuration</pre>
<p>Bu Worker'ın kodlarını da aşağıdaki gibi geliştirebiliriz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Robert
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
// Servis çalışmaya başladığı zaman devreye giren metodu ezip kendi istediklerimizi yaptırıyoruz.
public override async Task StartAsync(CancellationToken cancellationToken)
{
try
{
// RabbitMQ tarafı henüz ayağa kalkmamış olabilir diye burayı 1 dakika kadar duraksatalım
await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
// Rabbit ile konuşmak için kullanılacak kanal nesnesi alınıyor
var queue = CreateRabbitModel(cancellationToken);
// queue tanımlanır
queue.QueueDeclare(
queue: "palindromes",
durable: false,
exclusive: false,
autoDelete: false,
arguments: null
);
// Tanımlanan kuyruğu dinleyecek nesne örneklenir
var consumer = new EventingBasicConsumer(queue);
// dinlenen kuyruğa mesaj geldikçe tetiklenen olay metodu
consumer.Received += (model, arg) =>
{
var number = Encoding.UTF8.GetString(arg.Body.Span); // mesaj yakalanır
_logger.LogInformation($"Yeni bir palindrom sayısı bulunmuş: {number}");
};
queue.BasicConsume(
queue: "palindromes",
autoAck: true,
consumer: consumer);
}
catch (Exception exc)
{
_logger.LogError($"Bir hata oluştu {exc.Message}");
throw;
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(10000, stoppingToken);
}
}
private IModel CreateRabbitModel(CancellationToken cancellationToken)
{
try
{
// Önce bağlantı oluşturmak için factory nesnesi örneklenir
var factory = new ConnectionFactory()
{
HostName = Rabbit.Constants.GetRabbitMQHostName(), // Rabbit Host adresi alınır (Environment'ten gelir)
Port = Convert.ToInt32(Rabbit.Constants.GetRabbitMQPort()), // Port bilgisi
UserName=Rabbit.Constants.GetRabbitMQUser(), // Kullanıcı adı
Password=Rabbit.Constants.GetRabbitMQPassword() // ve Şifre
};
var connection = factory.CreateConnection(); // Bağlantı nesnesi oluşturulur. Exception yoksa bağlanmış demektir.
_logger.LogInformation("RabbitMQ ile bağlantı sağlandı");
return connection.CreateModel(); //Queue işlemleri için kullanılacak model nesnesi döndürülür
}
catch (Exception exc)
{
_logger.LogError($"Rabbit tarafına bağlanmaya çalışırken bir hata oluştu. {exc.Message}");
throw;
}
}
}
}</pre>
<p>Robert'ın kodları tamamlandıktan sonra <em>tye run</em> ile sistemi çalıştırıp dashboard üzerinden ulaşabileceğimiz logları kontrol etmekte yarar var. Bakalım Robert'ın loglarında RabbitMQ daki <em>palindromes</em> isimli kuyruğa düşen mesajlar var mı?</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_7.png" alt="" /></p>
<h2>Sadece Belli Uygulamaları Çalıştırmak</h2>
<p>İlerlemeden önce tye ile sadece belli uygulamaları nasıl çalıştıracağımıza da bir bakalım isterim. tye.yaml dosyasında tag bildirimlerini kullanarak <em>tye run</em> sonrası sadece belli servislerin ayağa kaldırılması sağlanabilir. Bu yaklaşım, Debug işlemleri için idealdir. N tane servisin olduğu bir senaryoda her şeyi ayağa kaldırmak yerine sadece istenenleri kurcalama noktasında çok faydalıdır. Söz gelimi yaml dosyamızda sadece middleware tag'ine sahip servisleri çalıştırmak istediğimizi düşünelim. run komutunu aşağıdaki gibi kullanabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye run --tags middleware #sadece middleware tag'ine sahip servisleri çalıştırır.</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_8.png" alt="" /></p>
<p>Birden fazla namespace'te bir arada ayağa kaldırılabilir. Mesela aşağıdaki kullanım ile backend ve middleware tag'ine sahip servisler ayağa kaldırılacaktır. Şimdi yaml içerisindeki tag elementlerinin ne işe yaradığınız daha iyi anlamış olmalısınız.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye run --tags backend middleware</pre>
<h3>Debug Etmek ve Breakpoint Noktalarına Geçmek</h3>
<p>Kod debug etmek adettendir :D Lakin tye ile çalışırken ayağa kaldırılan aktörleri debug etmek için biraz meşakkatli bir yol izlemek gerekiyor. İlk olarak gerekli yerlere breakpoint konulur. Örneğin;</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_9.png" alt="" /></p>
<p>Sonrasında aşağıdaki komut ile çözüm çalıştırlır.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye run --debug</pre>
<p>Debug edilmek istenen uygulamanının terminal loglarına düşen process id değeri bulunur.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_10.png" alt="" /></p>
<p>Visual Studio -> Debug -> Attach to Process adımları kullanılarak ilgili process çalışma zamanına alınır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_11.png" alt="" /></p>
<p>Çayımızdan/kahvemizden bir yudum alınır ve Breakpoint noktasına gelinmesi beklenir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_12.png" alt="" /></p>
<p>Hepsi bu kadar ;) Ya da doğru düzgün tasarladığımız hata yönetim mekanizmasının ürettiği sistem loglarına gidilir ve sorunun ne olduğu anlaşılmaya çalışılır.</p>
<h2>Kubernetes Deploy İşlemleri</h2>
<p>Artık notlarımızın sonuna doğru geliyoruz. Bal yapmayan arı olmamak için bu örneği de Kubernetes tarafına almamız lazım. Windows 10 üstündeki Docker Desktop'ın K8s Enabled özelliğinin açık olduğundan emin olalım. Buna göre sistemde tye.yaml tarafındaki servislerin alınabileceği bir Kubernetes Cluster mevcut kabul edilir. İkinci olarak bir container registry'ye ihtiyaç vardır ki ilk Hello World örneğimizde bunu localhost:5000 adresinde konuşlandırmıştık. Güncel örnek iki harici servis kullanmakta; Redis ve RabbitMQ. Bunları şu an için Kubernetes ortamına el yordamıyla kendi manifesto dosyaları üzerinden deploy etmemiz gerekiyor ama bu durum tye'ın ilerleyen sürümlerinde daha da kolaylaşabilir. Hello World örneğinde kullandığımız redis.yaml'ı burada da kullanabiliriz. RabbitMQ tarafı içinse aşağıdaki manifesto içeriği işimizi görecektir.</p>
<p>RabbitMQ.yaml</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">apiVersion: apps/v1
kind: Deployment
metadata:
name: rabbitmq
labels:
app.kubernetes.io/name: rabbitmq
app.kubernetes.io/part-of: schoolofmath
spec:
selector:
matchLabels:
app.kubernetes.io/name: rabbitmq
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/name: rabbitmq
app.kubernetes.io/part-of: schoolofmath
spec:
containers:
- name: rabbitmq
image: rabbitmq:3-management
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 5672
- containerPort: 15672
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
labels:
app.kubernetes.io/name: rabbitmq
app.kubernetes.io/part-of: schoolofmath
spec:
ports:
- port: 5672
protocol: TCP
targetPort: 5672
selector:
app.kubernetes.io/name: rabbitmq
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq-mui
labels:
app.kubernetes.io/name: rabbitmq
app.kubernetes.io/part-of: schoolofmath
spec:
type: NodePort
ports:
- port: 15672
protocol: TCP
targetPort: 15672
nodePort: 30072
selector:
app.kubernetes.io/name: rabbitmq</pre>
<p>Hem RabbitMQ hem de onu daha kolay okumamızı sağlayacak görsel MUI arabirimi için iki ayrı deployment tanımı söz konusudur. Bu dosyalardan yararlanarak ilgili servisleri Kubernetes ortamına aşağıdaki terminal komutları ile alabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">kubectl apply -f .\rabbitmq.yaml
kubectl apply -f .\redis.yaml</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_13.png" alt="" /></p>
<p>Kubernetes deployment adımını da aşağıdaki komutla başlatabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">tye deploy --interactive</pre>
<p>Büyük ihtimalle redis ve rabbitmq için adres sorulacaktır. Redis için redis:6379, rabbitmq içinse rabbitmq:5672<em>(Mui sebebiyle iki kez sorulabilir ki bana öyle oldu)</em> adresleri kullanılabilir. Sonuç olarak Docker Desktop'a baktığımızda dağıtımların yapıldığını görmeliyiz. </p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_14.png" alt="" /></p>
<p>Yukarıdaki ekran görüntüsünde dikkat edileceği üzere servislerimiz localhost:5000 ön adresi üzerine konumlanmış duruyorlar. Bunun sebebi container registry olarak bu adresi bildirmiş olmamız<em>(yaml dosyasındaki ilgili kısmı hatırlayın) </em></p>
<p>Tekrar belirtmekte fayda var ki kendi uygulamalarımız dağıtım işlemi sırasında yine otomatik olarak dockerize edilmişlerdir. Robert isimli Worker servise ait tye çalışma zamanının yaptıklarını aşağıdaki ekran görüntüsünde görebilirsiniz<em>(Normalde bunlar için bir Dockerfile hazırlamamız gerekirdi diye düşünüyorum)</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_16.png" alt="" /></p>
<p>Oluşan diğer imajları Docker Desktop üzerinde görebiliriz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_17.png" alt="" /></p>
<p>Şu anda RabbitMQ tarafı da aktif haldedir ve eğer localhost:30072 adresine gidersek o ana kadar ki mesaj trafiğini izleyebiliriz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_15.png" alt="" /></p>
<p>Yapılan Deployment işlemini geri almak ve Kubernetes dağıtımlarını kaldırmak içinse tye undeploy terminal komutu kullanılır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/screenshot_18.png" alt="" /></p>
<p>Bu çalışma deneysel bir projeyi hem basılı hem de çevrimiçi bir kaynaktan yazarak anlamam noktasında bana önemli değerler katmış durumda. Ancak işi burada bırakmamak lazım. Tye projesinin bir geleceği olacaksa diğer örnek kullanımları incelemekte de yarar var. Söz gelimi bir loglama senaryosunu işin içerisine katmak, performans izleme aktörünü dahil etmek gibi konular üstünde de denemeler yapmak yararlı olabilir. Dahası açık kaynak kod reposuna gidip tye run dediğimizde arka planda neler nasıl çalışıyoru anlamaya çalışmak çok daha yararlı olabilir. Bir teknoloji tüketicisi olarak en azından nasıl kullanılır ve ne işe yararı bir nebze olsun anladığımı ve siz değerli okurlarıma aktarabildiğimi düşünüyorum. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>
<h1>Kaynaklar</h1>
<p><a href="https://www.packtpub.com/product/adopting-net-5/9781800560567" target="_blank">Adopting .NET 5.</a> By Hammad Arif , Habib Qureshi</p>
<p><a href="https://devblogs.microsoft.com/aspnet/introducing-project-tye/" target="_blank">Introducing Project Tye</a> Amiee Lo, Program Manager, Microsoft ASP.NET</p>
<p><a href="https://github.com/dotnet/tye" target="_blank">Project Tye Github</a></p>
<p><a href="https://www.codemag.com/Article/2010052/Project-Tye-Creating-Microservices-in-a-.NET-Way" target="_blank">Project Tye: Creating Microservices in a .NET Way</a> Shayne Boyer, CODE Focus Magazine: 2020 - Vol. 17 - Issue 1 - .Net 5.0</p>
<p><a href="https://youtu.be/prbYvVVAcRs" target="_blank">Project Tye: Building Developer Focused Tooling for Kubernetes and .NET</a> - David Fowler</p>
<h1>Yardımcı Kodlar</h1>
<p>Notların dışına çıkmadan GoldenHammer ve Constant sınıflarını almak isterseniz aşağıdaki kod parçalarından yararlanabilirsiniz.</p>
<p>Robert projesindeki Constants.cs sınıfı</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
namespace Robert.Rabbit
{
/// Kaynak: https://github.com/PacktPublishing/Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features/tree/master/Chapter04/microservicesapp
public static class Constants
{
public const string RABBIT_HOST = "SERVICE__RABBITMQ__MQ_BINDING__HOST";
public const string RABBIT_PORT = "SERVICE__RABBITMQ__MQ_BINDING__PORT";
public const string RABBIT_ALT_HOST = "SERVICE__RABBITMQ__HOST";
public const string RABBIT_ALT_PORT = "SERVICE__RABBITMQ__PORT";
public const string RABBIT_ALT2_PORT = "RABBITMQ_SERVICE_PORT";
public const string RABBIT_USER = "RABBIT_USER";
public const string RABBIT_PSWD = "RABBIT_PSWD";
public const string RABBIT_QUEUE = "RABBIT_QUEUE";
public static string GetRabbitMQHostName()
{
var v = Environment.GetEnvironmentVariable(RABBIT_HOST);
if (string.IsNullOrWhiteSpace(v))
{
v = Environment.GetEnvironmentVariable(RABBIT_ALT_HOST);
if (string.IsNullOrWhiteSpace(v))
return "rabbitmq";
else return v;
}
else return v;
}
public static string GetRabbitMQPort()
{
var v = Environment.GetEnvironmentVariable(RABBIT_PORT);
if (string.IsNullOrWhiteSpace(v))
{
v = Environment.GetEnvironmentVariable(RABBIT_ALT_PORT);
if (string.IsNullOrWhiteSpace(v) || v == "-1")
return Environment.GetEnvironmentVariable(RABBIT_ALT2_PORT);
else return v;
}
else return v;
}
public static string GetRabbitMQUser()
{
var v = Environment.GetEnvironmentVariable(RABBIT_USER);
if (string.IsNullOrWhiteSpace(v))
return "guest";
else return v;
}
public static string GetRabbitMQPassword()
{
var v = Environment.GetEnvironmentVariable(RABBIT_PSWD);
if (string.IsNullOrWhiteSpace(v))
return "guest";
else return v;
}
public static string GetRabbitMQQueueName()
{
var v = Environment.GetEnvironmentVariable(RABBIT_QUEUE);
if (string.IsNullOrWhiteSpace(v))
return "primes";
else return v;
}
}
}</pre>
<p>Einstein tarafındaki GoldenHammer sınıfı</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System;
using System.Text;
namespace Einstein.Rabbit
{
// Kaynak: https://github.com/PacktPublishing/Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features/tree/master/Chapter04/microservicesapp
public interface IMQClient
{
IModel CreateChannel();
}
public interface IMessageQueueSender
{
public void Send(string queueName, string message);
}
public static class Constants
{
public const string RABBIT_HOST = "SERVICE__RABBITMQ__MQ_BINDING__HOST";
public const string RABBIT_PORT = "SERVICE__RABBITMQ__MQ_BINDING__PORT";
public const string RABBIT_ALT_HOST = "SERVICE__RABBITMQ__HOST";
public const string RABBIT_ALT_PORT = "SERVICE__RABBITMQ__PORT";
public const string RABBIT_ALT2_PORT = "RABBITMQ_SERVICE_PORT";
public const string RABBIT_USER = "RABBIT_USER";
public const string RABBIT_PSWD = "RABBIT_PSWD";
public const string RABBIT_QUEUE = "RABBIT_QUEUE";
public static string GetRabbitMQHostName()
{
var v = Environment.GetEnvironmentVariable(RABBIT_HOST);
if (string.IsNullOrWhiteSpace(v))
{
v = Environment.GetEnvironmentVariable(RABBIT_ALT_HOST);
if (string.IsNullOrWhiteSpace(v))
return "rabbitmq";
else return v;
}
else return v;
}
public static string GetRabbitMQPort()
{
var v = Environment.GetEnvironmentVariable(RABBIT_PORT);
if (string.IsNullOrWhiteSpace(v))
{
v = Environment.GetEnvironmentVariable(RABBIT_ALT_PORT);
if (string.IsNullOrWhiteSpace(v) || v == "-1")
return Environment.GetEnvironmentVariable(RABBIT_ALT2_PORT);
else return v;
}
else return v;
}
public static string GetRabbitMQUser()
{
var v = Environment.GetEnvironmentVariable(RABBIT_USER);
if (string.IsNullOrWhiteSpace(v))
return "guest";
else return v;
}
public static string GetRabbitMQPassword()
{
var v = Environment.GetEnvironmentVariable(RABBIT_PSWD);
if (string.IsNullOrWhiteSpace(v))
return "guest";
else return v;
}
public static string GetRabbitMQQueueName()
{
var v = Environment.GetEnvironmentVariable(RABBIT_QUEUE);
if (string.IsNullOrWhiteSpace(v))
return "palindromes"; // Consumer'ın dinyeceği varsayılan kuyruk adı. Normalde RABBIT_QUEUE ile çevre değişken üzerinden gelmezse bu kullanılır.
else return v;
}
}
public class RabbitMQClient : IMQClient
{
public string hostname { get; }
public string port { get; }
public string userid { get; }
public string password { get; }
private readonly ILogger _logger;
private readonly IConnection _connection;
private IModel _channel;
public RabbitMQClient(ILogger<RabbitMQClient> logger, IConfiguration configuration)
{
_logger = logger;
hostname = Constants.GetRabbitMQHostName();
port = Constants.GetRabbitMQPort();
userid = Constants.GetRabbitMQUser();
password = Constants.GetRabbitMQPassword();
try
{
logger.LogInformation($"RabbitMQ Bağlantısı oluşturuluyor. @ {hostname}:{port}:{userid}:{password}");
var factory = new ConnectionFactory()
{
HostName = hostname,
Port = int.Parse(port),
UserName = userid,
Password = password,
};
_connection = factory.CreateConnection();
}
catch (Exception ex)
{
logger.LogError(-1, ex, "RabbitMQ Bağlantısı oluşturulması sırasında hata oluştu.");
throw;
}
}
public IModel CreateChannel()
{
if (_connection == null)
{
_logger.LogError("RabbiMQ Kanal bağlantısı oluşturulması sırasında hata oluştu.");
throw new Exception("RabbitMQClient bağlantı hatası.");
}
_channel = _connection.CreateModel();
return _channel;
}
}
public class RabbitMQueueSender : IMessageQueueSender
{
private readonly ILogger<RabbitMQueueSender> _logger;
private readonly IMQClient _mqClient;
private IModel _mqChannel;
private string _queueName;
private IModel MQChannel
{
get
{
if (_mqChannel == null || _mqChannel.IsClosed)
_mqChannel = _mqClient.CreateChannel();
return _mqChannel;
}
}
public RabbitMQueueSender(ILogger<RabbitMQueueSender> logger, IMQClient mqClient)
{
_logger = logger;
_mqClient = mqClient;
}
public void Send(string queueName, string message)
{
if (string.IsNullOrWhiteSpace(queueName)) return;
if (string.IsNullOrWhiteSpace(_queueName))
{
_logger.LogInformation($"{queueName} isimli kuyruk ilk kez oluşturuluyor.");
MQChannel.QueueDeclare(queue: queueName,
durable: false,
exclusive: false,
autoDelete: false,
arguments: null);
_queueName = queueName;
}
_logger.LogInformation($"Mesaj kuyruğunu gönderiliyor. Queue Name:{queueName}");
var body = Encoding.UTF8.GetBytes(message);
try
{
MQChannel.BasicPublish(exchange: "",
routingKey: queueName,
basicProperties: null,
body: body);
}
catch (System.Exception ex)
{
ex.ToString();
}
_logger.LogInformation("Mesaj başarılı bir şekilde kuyruğa aktarıldı.");
}
}
public static class RabbitMQServiceCollectionExtensions
{
// Startup.cs'de RabbitMQ'yu servis listesine eklememizi sağlayan genişletme fonksiyonu
public static IServiceCollection AddRabbitMQ(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Add(ServiceDescriptor.Singleton<IMQClient, RabbitMQClient>());
services.Add(ServiceDescriptor.Singleton<IMessageQueueSender, RabbitMQueueSender>());
return services;
}
}
}</pre>2021-03-30T15:00:00+00:00.net core.net 5kubernetesdockerredisrabbitmqc#podscontaineryamlsidecar patterndağıtık mimarityekubectlwslredis-cliworker serviceservice discoverydocker hubbsenyurtAçık kaynak olarak yayınlanan Project Tye, Microsoft'un deneysel projelerinden birisi. En azından konuya çalıştığım tarih itibariyle böyleydi. Projenin iki temel amacı var; .Net tabanlı mikroservis çözümlerinin daha kolay geliştirilmesini sağlamak ve söz konusu çözümleri az zahmetle Kubernetes ortamına almak(Deployment) Buna göre birden fazla servisi tek komutla ayağa kaldırmak, Redis, RabbitMQ vb normalde Sidecar container olan bağımlılıkları kolayca yönetmek, kullanılacak servislerin kolayca keşfedilmesini sağlamak(Service Discovery), uygulamaların container olarak evrilmesi için gerekli hazırlıkları otomatikleştirmek, olabildiğince basit ve tekil bir Kubernetes konfigurasyon dosyası desteği vermek, projenin genel amaçları olarak düşünülebilir.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=6de98beb-5cea-42a6-8256-ae70d375e7fe2https://www.buraksenyurt.com/trackback.axd?id=6de98beb-5cea-42a6-8256-ae70d375e7fehttps://www.buraksenyurt.com/post/tie-fighter-degil-project-tye#commenthttps://www.buraksenyurt.com/syndication.axd?post=6de98beb-5cea-42a6-8256-ae70d375e7fehttps://www.buraksenyurt.com/post/ocelot-net-core-tarafinda-bir-api-gateway-denemesiOcelot - .Net Core Tarafında Bir API Gateway Denemesi2020-12-16T11:51:00+00:00bsenyurt<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/ocelot.png" alt="" align="right" />Uzun süre önce bankada çalışırken nereye baksam servis görüyordum. Bir süre sonra ana bankacılık uygulaması dahil pek çok ürünün kullandığı bu sayısız servisler ağının yönetimi zorlaşmaya başladı. Bir takım ortak işlerin daha kolay ve etkili yönetilmesi gerekiyordu. Müşterek bir kullanıcı doğrulama ve yetkilendirme kontrolü<em>(authentication & authorization),</em> yük dengesi dağıtımı (load balancing), birkaç servis talebinin birleştirilmesi ve hatta birkaç servis verisinin birleştirilerek döndürülmesi<em>(aggregation)</em>, servis verisinin örneğin XML'den JSON gibi farklı formata evrilmesi, servis geliş gidişlerinin loglanması, yönlendirmeler yapılması<em>(routing)</em>, performans için önbellek kullanılması<em>(caching)</em>, servis hareketliliklerini izlenmesi<em>(tracing)</em>, servislerin kolayca keşfedilmesi<em>(discovery)</em>, çağrı sayılarına sınırlandırma getirilmesi, bir takım güvenlik politikalarının entegre edilmesi, özelleştirilmiş delegeler yazılması<em>(custom handler/middleware)</em>, tüm uygulamalar için ortak bir servis geçiş kanalının konuşlandırılması ve benzerleri. Yazarken yoruldum, daha ne olsun :D Sonunda Java tabanlı WSO2 isimli bir API Gateway kullanılmasına karar verildi.</p>
<p>Geçtiğimiz günlerde de yine konuşma sırasında Ocelot isimli C# ile yazılmış açık kaynak bir ürünün adı geçti ve tabii ki bende bir merak uyandı. Kanımca hafif sıklet mikroservis ortamlarında veya servis odaklı mimari çözümlerinde düşünülebilir. Ama önce denemek ve nasıl işlediğini görmek gerekiyor, öyle değil mi? ;) Bu arada Ocelot'un oldukça doyurucu bir <a href="https://ocelot.readthedocs.io/en/latest/index.html" target="_blank">dokümantasyonu</a> olduğunu da belirteyim. Haydi gelin SkyNet derlememize başlayalım.</p>
<h2>Senaryo</h2>
<p>Örnekte şöyle bir senaryoyu icra etmeye çalışacağız; Oyuncu detaylarını getiren, ona öneri oyunları ürün olarak sunan, kazandığı bir promosyonu sisteme kaydetmesini sağlayan üç kobay servis tasarlayacağız. İstemci uygulama<em>(Postman bile yeterli olur)</em> bu birkaç servis çağrısı için API Gateway'e gelecek. Yani istemciler bu servisler için aslında tek bir noktaya gelip API Gateway üzerinden konuşacaklar. İlk etapta ocelot paketini kullanan gateway uygulaması basit bir router olacak. Hatta iki servis çıktısını birleştirip döndüren bir aggregation fonksiyonelliği de katacağız. Sonrasında Load Balancing işlevselliğini entegre edeceğiz.</p>
<h2>Hazırlıklar ve Kodlama</h2>
<p>Çok doğal olarak birkaç kobay servise ihtiyacımız var. Tamamını .net core web api olarak tasarlamak doğrusu işime geldi :) Ancak gerçek hayat senaryolarında farklı programlama dilleri ve çatıları ile geliştirilmiş servisler kullanmak daha mantıklı olacaktır.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">mkdir services
cd services
# İlk olarak kobay servislerimizi ekleyelim
# Fonksiyon başına bir servis gibi oldu ama
# amacımız bilindiği üzere Ocelot'un kurgusunu anlamak
# Oyuncu bilgilerini getireceğimiz bir servis
dotnet new webapi -o GamerService
# Oyuncuya önerilecek promosyonların çekileceği bir servis
dotnet new webapi -o PromotionService
# Oyuncunun daha önce satın almış olduğu ürünleri getirecek bir servis
dotnet new webapi -o ProductService
# ve Ocelot Servis Uygulamasının oluşturulup gerekli Nuget paketinin eklenmesi
cd ..
dotnet new web -o Bosphorus
dotnet add package ocelot
# Bu uygulamada kritik olan nokta ocelot konfigurasyonunun durduğu json dosya içerikleri
cd Bosphorus
touch ocelot.json</pre>
<p>Servislerimiz kobay niteliği taşıdıklarından birşeyler döndürseler yeterli. Yine de sayfanın dışına çıkmadan devam edebilmeniz için aşağıya gerekli kod parçalarını bırakıyorum<em>(<a href="https://github.com/buraksenyurt/skynet/tree/master/No%2037%20-%20Ocelot" target="_blank">İsteyenler SkyNet github reposuna uğrayıp indirebilirler de</a>)</em></p>
<p>GamerService içindeki PlayerController.cs</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace GamerService.Controllers
{
[ApiController]
[Route("[controller]")]
public class PlayerController : ControllerBase
{
private readonly ILogger<PlayerController> _logger;
public PlayerController(ILogger<PlayerController> logger)
{
_logger = logger;
}
/*
HTTP Get taleplerine karşılık verecek metodumuzdan geriye sembolik olarak bir Player nesnesi döndürüyoruz.
Player/19 gibi gelen taleplere cevap verecek
*/
[HttpGet("{id}")]
public Player Get(string id)
{
return new Player
{
Id = id,
Fullname = "Megen Enever",
Level = 58,
Location = "Dublin"
};
}
}
public class Player
{
public string Id { get; set; }
public string Fullname { get; set; }
public int Level { get; set; }
public string Location { get; set; }
}
}</pre>
<p>ve aynı servisi farklı port ile ayağa kaldıracağımızdan Program sınıfındaki UseUrls kullanımı.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// Farklı bir porttan yayın yapsın
webBuilder.UseStartup<Startup>().UseUrls("http://localhost:6501");
});</pre>
<p>ProductService içindeki ProductController sınıfı<em>(7501 Numaralı porttan kaldıracak şekilde Program sınıfını değiştirmeyi unutmayın)</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace ProductService.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly ILogger<ProductController> _logger;
public ProductController(ILogger<ProductController> logger)
{
_logger = logger;
}
/*
Oyuncu için önerilecek oyunları döndüren bir operasyonmuş gibi hayal edelim.
api/product/suggestions/1234 gibi HTTP Get taleplerine cevap verecek.
*/
[HttpGet("suggestions/{id}")]
public IEnumerable<Product> Get(string id)
{
var products = new List<Product>{
new Product{Id=1,Title="Commandos III",Price=34.50},
new Product{Id=2,Title="Table Child",Price=23.67},
new Product{Id=3,Title="League of Heros 2022",Price=145.99},
};
return products;
}
}
public class Product
{
public int Id { get; set; }
public string Title { get; set; }
public double Price { get; set; }
}
}</pre>
<p>PromotionService içerisindeki ApplierController sınıfı<em>(Bunu da 8501 nolu porttan ayağa kaldırmayı ihmal etmeyin)</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace PromotionService.Controllers
{
[ApiController]
[Route("[controller]")]
public class ApplierController : ControllerBase
{
private readonly ILogger<ApplierController> _logger;
public ApplierController(ILogger<ApplierController> logger)
{
_logger = logger;
}
/*
Birde HTTP Post deneyelim bari.
Sembolik olarak promosyon uygulayan bir metot olduğunu varsayalım.
*/
[HttpPost]
public IActionResult SetPromotoion(Code promoCode)
{
return Ok($"{promoCode.No} için {promoCode.Duration} gün süreli promosyon kullanıcı hesabına tanımlanmıştır");
}
}
public class Code
{
public string No { get; set; }
public int Duration { get; set; }
public int PlayerId { get; set; }
public int GameId { get; set; }
}
}</pre>
<p>İlk kobay servislerimiz hazır. Şimdi yapmamız gereken Ocelot paketini kullanan uygulamamızı geliştirmek. Basit bir Console olarak geliştirebiliriz. Program sınıfının kodunu aşağıdaki gibi yazarak devam edelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
// Ocelot için gerekli bildirimler
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using System.Net.Http;
using System.Threading;
namespace Bosphorus
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureServices(services =>
{
services
.AddOcelot() // Ocelot'u bildirdik
.AddDelegatingHandler<RequestInspector>(); // HttpClient isteklerinde araya girecek delegeyi bildirdik
}).ConfigureAppConfiguration((host, config) =>
{
config.AddJsonFile("ocelot.json"); // Ocelot ayarlarının alınacağı konfigurasyon dosyasını belirttik
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>().Configure(async app => await app.UseOcelot());
});
}
/*
Aşağıdaki sınıfın yardımıyla Ocelot'taki belirttiğimiz bir Route'a gelen HTTP istekleri işlenmeden önce araya girebiliriz.
Request içeriğine bakıp akışı değiştirebiliriz.
Bu temsilci sınıfını kullanacağımızı yukarıdaki AddDelegatingHandler metodunda belirttik.
Ayrıca ocelot.json içerisinde, örnek olması açısından /eagames/player/{id} adresine gelen taleplerde araya gireceğimizi belirttik.
*/
public class RequestInspector : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"\nDevam etmeden önce şu gelen Request içeriğini bir inceyelim\n{request.ToString()}\n");
return await base.SendAsync(request, cancellationToken);
}
}
}</pre>
<h2>İlk Deneme<em>(Aggregation ve Standart Routing)</em></h2>
<p>Öncelikle kobay servislerin ayağa kaldırılması lazım. GamerService, ProductService ve PromotionService isimli servisleri kendi klasörlerinde dotnet run ile çalıştırabiliriz. Kobay servisler aşağıdaki adreslerden devreye girecektir.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">GamerService -> http://localhost:6501
ProductService -> http://localhost:7501
PromotoionService -> http://localhost:8501</pre>
<p>Ocelot için çalışma zamanı ayarları bildiğiniz üzere json türünden konfigurasyon dosyasında tutulmaktadır. İlk versiyonunu aşağıdaki gibi yazıp ilerleyelim.</p>
<pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false">{
"Routes": [
{
"UpstreamPathTemplate": "/eagames/player/{id}",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamPathTemplate": "/player/{id}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 6501
}
],
"Key": "Player"
},
{
"UpstreamPathTemplate": "/eagames/product/{id}",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamPathTemplate": "/api/product/suggestions/{id}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7501
}
],
"Key": "Product"
},
{
"UpstreamPathTemplate": "/eagames/applypromo",
"UpstreamHttpMethod": [
"Post"
],
"DownstreamPathTemplate": "/applier",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 8501
}
]
}
],
"Aggregates": [
{
"RouteKeys": [
"Player",
"Product"
],
"UpstreamPathTemplate": "/{id}"
}
]
}</pre>
<p>Artık Bosphorus uygulamasını çalıştırıp localhost:5000/19 şeklinde bir talep gönderebiliriz. İlk örnek Aggregation durumunu taklit etmekte ve promosyon ekleme için yönlendirme yapmaktadır. Ayrıca GamerService ve ProductService'e ortak çağrı yapıp arka planda çağırılan servis çıktılarını tek bir JSON paketinde birleştirip geriye döndürür ;)</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_02.png" alt="" /></p>
<p>İlk örnekteki UpstreamPathTemplate tanımlarına göre http://localhost:5000/eagames/player/23 adresine yapılan çağrı esasında http://localhost:6501/player/23 adresine yönlendirilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_01.png" alt="" /></p>
<p>Benzer şekilde http://localhost:5000/eagames/product/23 şeklinde yapılacak çağrıda http://localhost:7501/api/product/suggestions/23 adresine yönlendirilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_03.png" alt="" /></p>
<p>PromotionService içerisinde bir de POST metodumuz var. Ocelot.JSON için yaptığımız tanıma göre http://localhost:5000/eagames/applypromo adresine gelen talebi, http://localhost:8501/applier adresine yönlendiriyor olmalı. İşte örnek POST içeriği ve sonuç...</p>
<pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false">{
"No":"PROMO-12345",
"Duration":30,
"GameId":102935,
"PlayerId":1
}</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_04.png" alt="" /></p>
<h2>İkinci Deneme<em>(Load Balancer)</em></h2>
<p>Bu kez Dockerize edilmiş bir Web API hizmetinden üç tanesini farklı portlarda ayağa kaldırıp Ocelot'un gelen talepleri bu adreslere dağıtmasını sağlamayı deneyelim. Temel amacımız ocelot konfigurasyonunda bunun nasıl ele alınacağını öğrenmek.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># Yine Services klasöründe RewardService isimli bir .Net Core Web API var
dotnet new webapi -o RewardService
cd RewardSercice
# Dockerize edeceğimiz
touch Dockerfile
# bin ve obj klasörlerini dışarıda bırakmak için
touch .dockerignore
# Dockerize için
docker build -t rewards .
# Dockerize ettiğimiz servisi çalıştırırken de aşağıdaki komutu kullanabiliriz
# Aynı servisin 3 farklı porttan çalışacak birer örneğini ayağa kaldırıyoruz
docker run -d -p 5555:80 -p 5556:80 -p 5557:80 rewards</pre>
<p>Servisin RewardController sınıfını ve Dockerfile içeriklerini aşağıdaki gibi yazabiliriz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace RewardService.Controllers
{
[ApiController]
[Route("[controller]")]
public class CalculatorController : ControllerBase
{
private static readonly string[] topics = new[]
{
"1000 Free Spell"
, "10 Free Coin"
, "30 Days Free Trail"
, "Gold Ticket"
,"Legendary Tournemant Pass"
,"1000 Free Retro Game"
,"One Day All Games Free"
};
private readonly ILogger<CalculatorController> _logger;
public CalculatorController(ILogger<CalculatorController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<Reward> Get()
{
var rng = new Random();
return Enumerable.Range(1, 3).Select(index => new Reward
{
Duration = rng.Next(7, 60),
Description = topics[rng.Next(topics.Length)]
})
.ToArray();
}
}
public class Reward{
public int Duration { get; set; }
public string Description { get; set; }
}
}</pre>
<p>ve Dockerfile;</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "RewardService.dll"]</pre>
<p>Bu sefer http://localhost:5555/Calculator , http://localhost:5556/Calculator ve http://localhost:5557/Calculator adreslerinden talep alan bir Web API servisimiz var. Load Balancer ayarlarını ocelot.json'a aşağıdaki gibi ekleyelim ve denemelerimize geçelim.</p>
<pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false">{
"DownstreamPathTemplate": "/calculator",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5555
},
{
"Host": "localhost",
"Port": 5556
},
{
"Host": "localhost",
"Port": 5557
}
],
"UpstreamPathTemplate": "/eagames/rewards",
"LoadBalancerOptions": {
"Type": "LeastConnection"
},
"UpstreamHttpMethod": [
"Get"
]
}</pre>
<p>Artık http://localhost:5000/eagames/rewards adresine geldiğimizde</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_05.png" alt="" /></p>
<p>Talepler LeastConnection seçimi nedeniyle her seferinde bir sonraki backend servisine yönlendirilecektir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_06.png" alt="" /></p>
<p>Diğer yandan hatırlayacağınız gibi gelen talepler sırasında araya girebileceğimizden bahsetmiştik. Bu sayede Ocelot'a gelen bir Http isteğine cevap dönmeden önce bir takım iş kurallarını işletmek mümkün olabilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/37/Screenshot_07.png" alt="" /></p>
<p>Gelelim bu SkyNet derlemesinin bomba sorularına :)</p>
<ul>
<li>Gateway arkasında XML içerik döndüren bir servis metodu olduğunu düşünelim. Gateway'e bu servis için gelen çağrı karşılığında XML yerine JSON döndürmemiz mümkün olur mu? Bunu Ocelot üzerinde nasıl tanımlarız?</li>
<li>Dockerize ettiğimiz servisi üç farklı porttan ayağa kaldırdığımız bir container başlattık. Ocelot'un Load Balancer ayarları gereği eagames/rewards'a gelen talepler arkadaki portlara seçilen stratejiye göre dağıtılıyor. Üç portta esas itibariyle aynı container'a<em>(80 portuna)</em> iniyor. Sizce gerçek anlamda bir Load Balancing oldu mu? Arkadaşlarınızla tartışınız.</li>
<li>Load Balancer senaryolarında Sticky Session dikkat edilmesi gereken bir konudur. Ocelot'ta Sticky Session desteği var mıdır araştırınız?</li>
</ul>
<p>Soruları düşünerkene örneği geliştirmeye de devam edebilirsiniz. Mesela en az iki servisi daha farklı programlama dilleri ile senaryoya dahil edebilir<em>(NodeJs, Java, Rust, GO)</em> ya da RewardService'in geriye döndürdüğü bedava ödüller listesindeki tekrar eden bilgileri tekleştirmek için gerekli kod düzenlemesini yapabilirsiniz. Bunlara ek olarak ürünü şirketinizde bir POC<em>(Proof of Concept)</em> çalışması olarak değerlendirip yük testi altında nasıl davranış sergileyeceğini araştırabilirsiniz.</p>
<p>Böylece geldik bir <a href="https://github.com/buraksenyurt/skynet" target="_blank">SkyNet</a> çalışmamızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2020-12-16T11:51:00+00:00ocelot.net corec#.net core web apirest servicesdiscoveryroutingload balancingaggregationbsenyurtUzun süre önce bankada çalışırken nereye baksam servis görüyordum. Bir süre sonra ana bankacılık uygulaması dahil pek çok ürünün kullandığı bu sayısız servisler ağının yönetimi zorlaşmaya başladı. Bir takım ortak işlerin daha kolay ve etkili yönetilmesi gerekiyordu. Müşterek bir kullanıcı doğrulama ve yetkilendirme kontrolü(authentication & authorization), yük dengesi dağıtımı (load balancing), birkaç servis talebinin birleştirilmesi ve hatta birkaç servis verisinin birleştirilerek döndürülmesi(aggregation), servis verisinin örneğin XML'den JSON gibi farklı formata evrilmesi, servis geliş gidişlerinin loglanması, yönlendirmeler yapılması(routing), performans için önbellek kullanılması(caching), servis hareketliliklerini izlenmesi(tracing), servislerin kolayca keşfedilmesi(discovery), çağrı sayılarına sınırlandırma getirilmesi, bir takım güvenlik politikalarının entegre edilmesi, özelleştirilmiş delegeler yazılması(custom handler/middleware), tüm uygulamalar için ortak bir servis geçiş kanalının konuşlandırılması ve benzerleri. Yazarken yoruldum, daha ne olsun :D Sonunda Java tabanlı WSO2 isimli bir API Gateway kullanılmasına karar verildi...https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=91f4e4b0-1ea0-48a0-8bdb-7ce8f92721a615https://www.buraksenyurt.com/trackback.axd?id=91f4e4b0-1ea0-48a0-8bdb-7ce8f92721a6https://www.buraksenyurt.com/post/ocelot-net-core-tarafinda-bir-api-gateway-denemesi#commenthttps://www.buraksenyurt.com/syndication.axd?post=91f4e4b0-1ea0-48a0-8bdb-7ce8f92721a6https://www.buraksenyurt.com/post/net-core-tarafindan-rabbitmq-ya-mesaj-gondermek-ve-java-tarafindan-dinlemek.Net Core Tarafından RabbitMQ'ya Mesaj Göndermek ve Java Tarafından Dinlemek2020-11-23T21:00:00+00:00bsenyurt<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/38/queue.png" alt="" align="right" />Çok sık karşılaştığımız senaryolardan birisidir; Bir uygulama kendi bünyesinde gerçekleşen bir olay sonrası başka bir uygulamayı haberdar etmek ister ya da başka bir uygulamanın yaptıklarından haberdar olmak isteyen bir uygulama vardır :) Bunun bir çok sebebi olabilir. Örneğin uygulamalar farklı teknolojilerde yazılmıştır ancak ortak iş süreçleri üzerinde koşmaktadır. Gerçek bir senaryo üzerinden hareket edersek konu daha anlaşılır olabilir. </p>
<p>Kargo çıkışı gerçekleştiren yeni nesil bir uygulama bu çıkışlar için düzenlediği irsaliyelerin bir devlet kurumuna gönderilmesi sırasında yine aynı kurumun legacy diyebileceğimiz başka bir sisteminin süreçlerine dahil olmak durumunda olsun. Bu sürecin belli noktalarında uygulamaların birbirini haberdar etmesi gerektiğini de varsayalım. Tipik olarak gerçekleşen bir olay sonrası bu olaya ait diğer uygulamanın ihtiyacı olan bilgilerin gönderilmesi bekleniyor. Çok doğal olarak bu iletişimi senkronize etmek çok mantıklı olmaz. Nitekim anlık iş yükü çok yüksek sayılara çıkabilir ve bekleme süreleri diğer uygulamanın sürecini olumsuz etkileyebilir. Asenkron kuyruk sistemi bu dar boğazı aşmada önemli bir rol oynamaktadır.</p>
<p>Öyleyse çok basit bir kurgu ile bunu kendi sistemlerimizde uygulamaya çalışalım. Örnek çalışmadaki amacımız RabbitMQ'yu Heimdall üstünde olabilecek en basit haliyle çalıştırmak, bir .Net Core Console uygulamasından belli bir konu başlığı<em>(topic)</em> için bu kuyruğa mesaj bırakmak ve oldukça yabancısı olduğum Spring tarafındaki bir Java uygulamasından da gönderilen mesajları yakalamak. Öyleyse hiç vakit kaybetmeden işe başlayalım. Örneği deneyimlediğim Heimdall<em>(Ubuntu 20.04)</em> üzerinde koşan bir RabbitMQ hizmeti mevcut değil. Aslında kurmayı da istemiyorum. Docker veya Docker-Compose kullanmak çok daha mantıklı.</p>
<p>Aşağıdaki terminal komutları ile başlayalım.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">touch docker-compose.yml
# Çalıştırmak için
docker-compose up</pre>
<p>docker-compose.yml içeriğini aşağıdaki gibi oluşturabiliriz. Dikkat edileceği üzere RabbitMQ imajını kullanarak gerekli port ayarlamaları ile birlikte sistemi ayağa kaldırıyoruz.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">rabbitmq:
image: rabbitmq:management
ports:
- "5672:5672"
- "15672:15672"</pre>
<p>Mesaj gönderecek .Net Core uygulamasını oluşturmak içinse aşağıdaki adımları takip edebiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># SENDER
# RabbitMQ'ya mesaj gönderecek uygulamamız bir .Net Core uygulaması olacak
dotnet new console --name CargoBase
# Gerekli Nuget paketleri de aşağıdaki gibi ekleyebiliriz
# Birisi RabbitMQ ile konuşmak için
dotnet add package RabbitMQ.Client
# Diğer Console uygulamasında nesneyi JSON serileştirmekte yardımcı olsun diye
dotnet add package Newtonsoft.Json</pre>
<p>CargoBase isimli uygulamaya ait program kodunuysa aşağıdaki gibi yazabiliriz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using RabbitMQ.Client;
using System.Text;
using Newtonsoft.Json;
namespace CargoBase
{
class Program
{
static void Main(string[] args)
{
Random _random = new Random();
// Factory nesnesi üstünden RabbitMQ'ya bir bağlantı açacağız
var factory = new ConnectionFactory() { HostName = "localhost" };
using (var connection = factory.CreateConnection())
{
// sonrasında kanal tanımlama ve mesaj gönderme işi için gerekli nesneleri üreteceğiz
using (var channel = connection.CreateModel())
{
string queueName = "package-state-action";
// Bir kuyruk tanımladık. Kargonun durum değişikliği ile alakalı bir kuyruk gibi düşünelim
channel.QueueDeclare(queue: queueName,
durable: false,
exclusive: false,
autoDelete: false,
arguments: null);
// Kuyruğu JSON olarak serileştirilmiş bir nesne koyalım. Kobay nesnemiz Package türünden bir örnek.
var package = JsonConvert.SerializeObject(
new Package
{
SerialNo = _random.Next(1, 1000),
State = "Ready",
Weight = _random.NextDouble()*100,
Time = DateTime.Now.ToString()
});
// nesne içeriğini kanala yazmak için Byte[] dizisine çeviriyoruz
var body = Encoding.UTF8.GetBytes(package);
Console.WriteLine($"{package} içeriği gönderilecek");
// routingKey bilgisi ile de yukarıda tanımlanan kanala mesajımızı bırakalım
channel.BasicPublish(exchange: "",
routingKey: queueName,
basicProperties: null,
body: body);
}
Console.WriteLine("Kargo için durum bilgisi yayınlandı. Çıkmak için bir tuşa basınız");
Console.ReadLine();
}
}
}
public class Package
{
public int SerialNo { get; set; }
public string State { get; set; }
public double Weight { get; set; }
public string Time { get; set; }
}
}</pre>
<p>Konsolumuz bazı rastgele değerlerden oluşan bir paket bilgisini kuyruğa göndermekte. Alıcı uygulamayı Java tarafında geliştireceğiz ama işimizi kolaylaştırmak adına Spring Boot'tan yararlanacağız. Bu nedenle <a href="https://start.spring.io/" target="_blank">Spring Initializer</a> adresine gidip gerekli proje bilgilerini doldurup oluşan uygulamayı sisteme indirip kullanabiliriz. Ben ilgili bilgileri aşağıdaki ekran görüntüsünde olduğu gibi doldurdum. Tabii burada kritik nokta Spring for RabbitMQ kütüphanesinin de bağımlı bileşen olarak belirtilmesi.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/38/Screenshot_01.png" alt="" /></p>
<p>Java projesi oluştuktan sonra RabbitMQ tarafını dinleyecek ve gelen JSON mesajını nesne olarak karşılayacak sınıfları da yazmamız gerekiyor<em>(JSON serileştirme için com.fasterxml.jackson.core bağımlılığının da eklenmesi gerekir. Ben ilk etapta unutmuşum, siz ihmal etmeyin)</em></p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># RabbitMQ mesajlarını dinleyecek java servisini
# src/main/java/com/azon/cargotracer altında oluşturabiliriz
touch EventListener.java PackageInfo.java</pre>
<p>EventListener.java içeriğini aşağıdaki gibi yazabiliriz.</p>
<pre class="brush:java;auto-links:false;toolbar:false" contenteditable="false">package com.azon.cargotracer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Service;
/*
MessageListener'dan türeyen bu sınıfın ezdiğimiz(override)
onMessage metodu üzerinden, RabbitMQ tarafında ilgili kuyruğa atılmış mesajın gövdesini yakalayabiliriz.
*/
@Service
public class EventListener implements MessageListener {
public void onMessage(Message message) {
// Message tipinin getBody fonksiyonu ile kuyruk mesajının içeriğini aldık
String content = new String(message.getBody());
System.out.println("\n" + content + "\n");
try {
/*
İçeriği JSON formatında göndermiştik.
Jackson isimli paketten yararlanarak bu içeriği Java tarafındaki PackageInfo nesnemize dönüştürebiliriz.
Gelen JSON içeriğini Java tarafında nesne olarak ele alabilmek için...
*/
ObjectMapper objectMapper = new ObjectMapper();
PackageInfo packageInfo = objectMapper.readValue(content, PackageInfo.class);
System.out.println(
packageInfo.SerialNo + "," + packageInfo.State + "," + packageInfo.Weight + "," + packageInfo.Time);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}</pre>
<p>PackageInfo.java;</p>
<pre class="brush:java;auto-links:false;toolbar:false" contenteditable="false">package com.azon.cargotracer;
/*
.Net Core tarafında RabbitMQ kuyruğuna attığımız Package sınıfı JSON formatta serileşerek yollanıyor.
Onun Java tarafındaki izdüşümü kabul edeceğimiz sınıfı aşağıdaki gibi tanımlayabiliriz.
*/
public class PackageInfo {
public int SerialNo;
public String State;
public double Weight;
public String Time;
public int getSerialNo() {
return SerialNo;
}
public void setSerialNo(int value) {
SerialNo = value;
}
public String getState() {
return State;
}
public void setState(String value) {
State = value;
}
public double getWeight() {
return SerialNo;
}
public void setWeight(double value) {
Weight = value;
}
public String getTime() {
return Time;
}
public void setTime(String value) {
Time = value;
}
}</pre>
<h2>Çalışma Zamanı</h2>
<p>Kurguyu işletmek için üç uygulama çalıştırmamız gerekiyor. Öncelikle docker-compose ile RabbitMQ tarafını ayağa kaldırmalıyız. Ardından mesaj gönderecek olan dotnet core uygulamasını başlatabiliriz. Java uygulaması çalıştırıldıktan sonra ilgili RabbitMQ kuyruğunu dinleme konumunda kalacaktır. Onu dotnet core uygulamasından önce de çalıştırabiliriz. Sonuç itibariyle .Net uygulamasından mesaj basıldıkça Java arabirimine çıkması gerekmektedir.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># Rabbit tarafı için
docker-compose up
# Console uygulaması için (Mesaj gönderen taraf)
dotnet run
# Maven ile Java tarafını başlatmak için
./mvnw spring-boot:run</pre>
<p>İşte çalışma zamanından bir görüntü.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/38/Screenshot_03.png" alt="" /></p>
<p>Dikkat edileceği üzere Console uygulamasından gönderdiğimiz JSON mesaj içeriği, Java uygulamasına ait terminal ekranına da düşmüştür. Bu arada uygulama kodlarına <a href="https://github.com/buraksenyurt/skynet/tree/master/No%2038%20-%20Spring%20RabbitMQ%20and%20DotNetCore" target="_blank">skynet github reposu</a> üzerinden erişebilirsiniz. Kodlara eriştiğinizde şu soruya cevap aramanızı öneririm; Varsayılan halde Java uygulaması localhost sunucusuna ve standart RabbitMQ portuna gideceğini nereden biliyor? Bu sorulara ek olarak kurguyu biraz daha öteye taşıyabilirsiniz. Örneğin Java uygulamasını birden fazla kuyruğu dinleyecek şekilde organize etmeyi deneyebilir ve hatta kullandığınız kuyruğa başka platformda yazılmış programlardan mesaj gönderip kimden geldiğini anlamaya çalışabilirsiniz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2020-11-23T21:00:00+00:00.net corejavac#rabbitmqdockerspringspring bootdocker composejsonmavenbsenyurtÇok sık karşılaştığımız senaryolardan birisidir; Bir uygulama kendi bünyesinde gerçekleşen bir olay sonrası başka bir uygulamayı haberdar etmek ister ya da başka bir uygulamanın yaptıklarından haberdar olmak isteyen bir uygulama vardır :) Bunun bir çok sebebi olabilir. Örneğin uygulamalar farklı teknolojilerde yazılmıştır ancak ortak iş süreçleri üzerinde koşmaktadır. Gerçek bir senaryo üzerinden hareket edersek konu daha anlaşılır olabilir. Kargo çıkışı gerçekleştiren yeni nesil bir uygulama bu çıkışlar için düzenlediği irsaliyelerin bir devlet kurumuna gönderilmesi sırasında yine aynı kurumun legacy diyebileceğimiz başka bir sisteminin süreçlerine dahil olmak durumunda olsun. Bu sürecin belli noktalarında uygulamaların birbirini haberdar etmesi gerektiğini de varsayalım. Tipik olarak gerçekleşen bir olay sonrası bu olaya ait diğer uygulamanın ihtiyacı olan bilgilerin gönderilmesi bekleniyor. Çok doğal olarak bu iletişimi senkronize etmek çok mantıklı olmaz. Nitekim anlık iş yükü çok yüksek sayılara çıkabilir ve bekleme süreleri diğer uygulamanın sürecini olumsuz etkileyebilir. Asenkron kuyruk sistemi bu dar boğazı aşmada önemli bir rol oynamaktadır.https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=c7e23887-8ff5-4e3c-8ff6-305d5d2ea1370https://www.buraksenyurt.com/trackback.axd?id=c7e23887-8ff5-4e3c-8ff6-305d5d2ea137https://www.buraksenyurt.com/post/net-core-tarafindan-rabbitmq-ya-mesaj-gondermek-ve-java-tarafindan-dinlemek#commenthttps://www.buraksenyurt.com/syndication.axd?post=c7e23887-8ff5-4e3c-8ff6-305d5d2ea137https://www.buraksenyurt.com/post/net-core-web-api-tarafinda-sqlkata-ile-sevimli-sql-islemleri.Net Core Web Api Tarafında SqlKata ile Sevimli SQL İşlemleri2020-10-28T14:14:00+00:00bsenyurt<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/kata.png" alt="" align="right" />Veri odaklı uygulamalarda sorgu komutlarını çalıştırmak için kullandığımız birçok hazır altyapı var. Örneğin .Net dünyasına baktığımızda en temel seviyede Ado.Net ve Object Relational Mapping tarafında Entity Framework sıklıkla karşılaştıklarımız arasında. <a href="https://sqlkata.com/" target="_blank">SqlKata</a>'da bunlardan birisi olarak düşünülebilir. Bir süredir de sağda solda okuduğum makale ve github çalışmalarından dolayı merak edip kurcalamak istediğim bir kütüphane. Öncelikle ismi çok hoş<em>(Code Kata'yı çağrıştırıyor bana)</em></p>
<p>C# ile geliştirimiş paketin temel amacı SqlServer, PostgreSql, Firebird, MySql gibi veritabanları için kod tarafında ortak bir sorgu oluşturma/derleme arabirimi sunmak ama bunu LINQ sorgu metotları üzerinden SQL dilinin anlaşılır rahatlığında, injection yemeden<em>(Parameter Binding tekniğini kullandığı için) </em>ve ön bellekleme<em>(caching)</em> gibi performans artırıcıları kullanarak sağlamak. Tabii konuşmayı bir kenara bırakıp kod yazarak onu tanımaya çalışmak en doğrusu. Ben örneği Heimdall<em>(Ubuntu-20.04)</em> üzerinde ve Visual Studio Code arabirimini kullanarak geliştirmekteyim<em>(Uygulama kodlarının tamamına <a href="https://github.com/buraksenyurt/skynet/tree/master/No%2035%20-%20Hiyaaa%20This%20is%20SqlKata" target="_blank">SkyNet github reposu</a> üzerinden erişebilirsiniz)</em></p>
<p>Örneğimizde SqlKata paketini bir PostgreSql veritabanı üzerinden kullanacağız. Daha önceden de sıklıkla yaptığımız üzere Docker imajından yararlanabiliriz. Ancak elimde içeriği ile dolu dolu hazır bir veritabanı olsa güzel olabilir. Microsoft'un Adventure Works ve Northwind gibi, yazılım eğitimlerinde sıklıkla kullanılan efsane veritabanları olduğunu bilirsiniz. PostgreSql için hazırlanmış olan hatta Docker Container olarak çalıştırılabilecek bir versiyonunu da <a href="https://github.com/pthom/northwind_psql" target="_blank">şu github adresinden</a> tedarik edebiliyoruz. Üstelik güzel bir ilişkisel diagram da mevcut<em>(repodaki wind-of-change klasöründe kurulumu için gerekli SQL dosyasını bulabilirsiniz)</em> Docker için kullanacağımız kompozisyonun içeriği ise aşağıdaki gibidir.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">version: '3'
services:
db:
image: postgres:12
environment:
POSTGRES_DB: northwind
POSTGRES_USER: scoth
POSTGRES_PASSWORD: tiger
ports:
- "5433:5433"
volumes:
- ./dbdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
command: -p 5433</pre>
<p>İlgili terminal komutu ile docker container ayağa kaldırıldıktan sonra yüklenen Northwind veritabanı içerisine şöyle bir bakmakta da yarar var. En azından verilerin geldiğini görelim.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false"># container'ı ayağa kaldırmak için wind-of-change klasöründe aşağıdaki terminal komutunu kullanabiliriz
docker-compose up
# ikinci bir terminalden veya pgAdmin gibi bir araçla Northwind içeriğine bakabiliriz
# Ben scoth kullanıcı adını tanımlamıştım.
docker-compose exec db psql -U scoth -d northwind
# Yukarıdaki komut sonrası açılan psql cli'de birkaç SQL ifadesi deneyebiliriz
# Örneğin En pahalı 3 ürünü listeleylim
Select product_name,unit_price from products order by unit_price desc limit 3;
# veya kategorilerin adlarını
Select category_name from categories;</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_01.png" alt="" /></p>
<p>Pek tabii amacımız SqlKata ile bu PostgreSql veritabanına bağlanıp bir takım işlemler yapabilmek. Rapor çekmek, satır ekleyip silmek ve benzer işlemleri icra edebiliriz. Deneme kodları için bir .Net Core Web Api projesi pekala uygun bir çözüm. REST servis çağrıları ile uçtan uca çalışan bir örneği denemiş de oluruz. Öyleyse aşağıdaki terminal komutlarından yararlanarak projemizi oluşturalım ve gerekli Nuget paketlerini de ekleyerek yolumuza devam edelim.</p>
<p># Önce src klasöründe bir api projesi açayım<br />dotnet new webapi -o northwind-api</p>
<p># Gerekli paketleri yükleyelim<br /># Postgresql için npsql ve SqlKata için SqlKata :)<br />dotnet add package Npgsql<br />dotnet add package SqlKata<br />dotnet add package SqlKata.Execution</p>
<p>SQLKata ile ilgili örnek kullanımlar Controller sınıflarımızda yer alıyor. Basit olması açısından Category, Product ve Customer tabloları için birer Controller tipi mevcut. İlk olarak bu sınıfları yazarak işe başlayabiliriz. CustomerController içerisinde hangi şehirde kaç müşteri olduğu bilgisini raporlayan basit bir operasyon bulunuyor.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
/*
SqlKata kullanımı için eklenen namespace bildirimleri
*/
using SqlKata;
using SqlKata.Execution;
namespace NorthwindApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CustomerController : ControllerBase
{
private readonly ILogger<CustomerController> _logger;
private readonly QueryFactory _queryFactory;
public CustomerController(ILogger<CustomerController> logger, QueryFactory queryFactory)
{
_logger = logger;
_queryFactory = queryFactory;
}
/*
Hangi şehirde kaç müşterimiz olduğunu döndüren action.
customers tablosunda city bilgisine göre gruplama yapıp, count alıyoruz yani.
Örnek sorgu -> https://localhost:5001/api/customer/cityreport
*/
[HttpGet("cityreport")]
public IActionResult GetCustomerCountsByCity()
{
var report = _queryFactory
.Query("customers")
.Select("city")
.SelectRaw("count(customer_id) as count") // aggregation yaptığımız yer
.GroupBy("city") // city alanına göre grupluyoruz
.HavingRaw("count(customer_id)>1") // toplam müşteri sayısı 1in üstünde olanlar için (having e bir bakayım demiştim)
.Get();
return Ok(report);
}
}
}</pre>
<p>ProductController ise iki operasyon sunmakta. Birisinde satışta olmayan/üretilmeyen ürünlerin listesini döndürüyoruz<em>(Siz var olanları çekmeyi de deneyin)</em> Diğeri ise kategoriye göre ürün listesini sayfa bazlı döndürüyor. REST tipindeki servislerde liste bazlı operasyonların büyük boyutta veri döndürmesi çok tercih edilen bir durum değil. Bunun yerine veriyi sayfalama tekniği ile döndürmek hem performans hem de veri güvenliği açısından daha doğru. Sayfalamanın SQLKatacasını aşağıdaki kod parçasında görebilirsiniz :)</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
/*
SqlKata kullanımı için eklenen namespace bildirimleri
*/
using SqlKata;
using SqlKata.Execution;
namespace NorthwindApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly ILogger<ProductController> _logger;
/*
Startup tarafında bildirimini yaptığımız QueryFactory nesnesini
constructor ile buraya enjekte ettik. Böylece controller içindeki
tüm action metotlarında SqlKata yı kullanabileceğiz.
*/
private readonly QueryFactory _queryFactory;
public ProductController(ILogger<ProductController> logger, QueryFactory queryFactory)
{
_logger = logger;
_queryFactory = queryFactory;
}
/*
İlk SqlKata denemem.
products tablosunda discontinued olanların listesini çekmeye çalışıyoruz
Geriye JSON içerik dönecektir
*/
[HttpGet("Discontinued/")]
public IActionResult GetDiscontinuedProducts()
{
var products = _queryFactory
.Query("products") // products tablosu için sorgu hazırlanacak
.Select("product_id", "product_name", "unit_price") // sadece bu alanlar getirilecek
.Where("discontinued", 1) // discontinued değeri 1 olanlar çekilecek
.Get();
//_logger.LogInformation($"{DateTime.UtcNow.ToLongTimeString()} - ProductController - GET");
return Ok(products);
}
/*
Parametre olarak gelen kategori altındaki ürünleri sayfalayarak getiren action.
Sayfalama için Limit ve Offset fonksiyonlarını kullanıyoruz.
Route üstünden gelen page değerine göre bir konuma gidip o konumdan itibaren 5 kayıt gösteriyoruz.
Örnek sorgu -> https://localhost:5001/api/product/Beverages/3
*/
[HttpGet("{categoryName}/{page}")]
public IActionResult GetProductsByCategory(string categoryName, int page)
{
var products = _queryFactory
.Query("products as p")
.Join("categories as c", "p.category_id", "c.category_id")
.Select(
"c.category_name",
"p.{product_id,product_name,unit_price,units_in_stock}")
.Limit(5)
.Offset((page - 1) * 5)
.Get();
return Ok(products);
}
}
}</pre>
<p>CategoryController tarafında kullandığımız Category sınıfı;</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">namespace NorthwindApi.Model
{
public class Category{
public int CategoryId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public byte[] Picture { get; set; }
}
}</pre>
<p>CategoryController, yeni kategori eklenmesi, silinmesi ve kategori listesinin döndürülmesi ile ilgili metotlar barındırıyor.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NorthwindApi.Model;
/*
SqlKata kullanımı için eklenen namespace bildirimleri
*/
using SqlKata;
using SqlKata.Execution;
namespace NorthwindApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CategoryController : ControllerBase
{
private readonly ILogger<CategoryController> _logger;
private readonly QueryFactory _queryFactory;
public CategoryController(ILogger<CategoryController> logger, QueryFactory queryFactory)
{
_logger = logger;
_queryFactory = queryFactory;
}
/*
Yeni bir kategori eklemek için kullanacağımız post action.
Parametre olarak gelen JSON içeriğindeki alanları kullanıyor.
Insert işlemi sonucuna göre de Ok veya 500 dönüyoruz.
Adres : https://localhost:5001/api/category
Metot : HTTP Post
Body :
{
"CategoryId":10,
"Name": "Kitap",
"Description": "Kitap konulu ürünler"
}
*/
[HttpPost]
public IActionResult AddCategory(Category category)
{
try
{
var inserted_id = _queryFactory
.Query("categories")
.Insert(new
{
category_id = category.CategoryId,
category_name = category.Name,
description = category.Description
});
return Ok(category);
}
catch (Exception exp)
{
_logger.LogError(exp.Message);
return StatusCode(500, "Kategori ekleme işlemi başarısız!");
}
}
/*
Denemeler sırasında categories tablosunu kirletecek yeni satırlar ekledim tabii :)
Silme operasyonu da lazım.
Örnek sorgu https://localhost:5001/api/category/10
Metot HTTP Delete
*/
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
int deleted = _queryFactory.Query("categories").Where("category_id", id).Delete();
return Ok(deleted);
}
/*
Kategorileri listeleyen action
https://localhost:5001/api/category
*/
[HttpGet]
public IActionResult GetCategories()
{
var categories = _queryFactory
.Query("categories")
.OrderBy("category_name")
.Get();
return Ok(categories);
}
}
}</pre>
<p>ve tabii SQLKata için gerekli Middleware tanımlarını da içeren startup dosyamız.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
/*
Postgresql ve SqlKata için gerekli namespace bildirimleri
*/
using SqlKata;
using SqlKata.Compilers;
using SqlKata.Execution;
using Npgsql;
namespace NorthwindApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
/*
QueryFactory sınıfını burada kayıt edip controller'lara constructor üzerinde enjekte ederek kullandırtabiliriz.
Oluştururken Postgresql bağlantı bilgisini veriyoruz.
Ayrıca sorgular için gerekli derleyici nesnesi de üretiliyor
*/
services.AddScoped(factory =>
{
return new QueryFactory
{
Compiler = new PostgresCompiler(),
// Varsayılan olarak Postgresql 5432 portunu kullanıyor.
// Ben docker-compose'da dışarıya 5433 portundan açtığım için farklı.
Connection = new NpgsqlConnection("Server=127.0.0.1;Port=5433;Username=scoth;Password=tiger;Database=northwind")
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}</pre>
<p>Artık uygulamamız test sürüşüne hazır ;) Web API hizmeti çalışmaya başlamadan önce tahmin edeceğiniz gibi docker-compose ile PostgreSql Container örneğinin ayağa kalkmış olması gerekiyor. Sonrasında aşağıdaki terminal komutu ile ilerleyebiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet run watch</pre>
<p>İlk olarak artık satışta olmayan ürünleri çekmeye çalışalım. Bunun için servise</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">https://localhost:5001/api/product/discontinued</pre>
<p>şeklinde bir talep göndermemiz yeterli. Aşağıdakine benzer bir sonuç almamız gerekiyor.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_02.png" alt="" /></p>
<p>Bir kategoriye ait ürünlerin birkaç bilgisini getiren ama bunu yaparken sayfalama tekniğini kullanan operasyon içinse aşağıdaki gibi bir talep yollanabilir.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">https://localhost:5001/api/product/Beverages/3</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_04.png" alt="" /></p>
<p>Dikkat edileceği üzere 3ncü sayfadan itibaren ilk 5 kayıt getirilmiştir. En basit operasyonlardan birisi de tüm kategorilerin çekilmesidir :)</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">https://localhost:5001/api/category</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_05.png" alt="" /></p>
<p>Müşterilerin bulundukları şehre göre guruplandığı raporu ise aşağıdaki taleple çekebiliriz.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">https://localhost:5001/api/customer/cityreport</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_06.png" alt="" /></p>
<p>Hemen yeni bir kategori eklemeyi deneyelim. Ekleme işlemini Postman ile denemek için aşağıdaki örnek bilgileri kullanabilirsiniz.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">Adres: https://localhost:5001/api/category
Metod: HTTP Post
Body: json
Örnek İçerik:
{
"CategoryId":10,
"Name": "Kitap",
"Description": "Kitap konulu ürünler"
}</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_07.png" alt="" /></p>
<p>Tabii aynı Id ile bir kategori eklemek istersek exception yönetimimize göre aşağıdaki gibi bir çıktı almamız gerekir<em>(Bu hata mesajını biraz daha anlamlı hale getirmek yerinde olabilir. Nitekim işlem başarısız ama neden başarısız olduğu istemci tarafında tam olarak anlaşılmıyor)</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_08.png" alt="" /></p>
<p>Eklediğimiz kategoriyi silmek içinse bir HTTP Delete çağrısı yapmalıyız.</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">Adres: https://localhost:5001/api/category/10
Metot: HTTP Delete</pre>
<p>Bu arada örneği çalışırken karşılaştığım enteresan bir durum vardı. Bu durumu açıklamak için localhost:5001/api/product/Beverages talebini göz önüne alalım. Beverages kategorisindeki ürünlerin listesini almayı bekliyoruz. Ancak kod tarafında yapacağımız minik bir değişiklik yüzünden aşağıdaki ekran görüntüsünde olduğu gibi connection string bilgisini görme ihtimalimiz var. Öncelikle bu nasıl mümkün olabilir, ikinci olarak bunun önüne nasıl geçeriz? Lütfen yorumlarınızı esirgemeyin, tüm okurlarımız faydalansın.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2020/skynet/35/Screenshot_03.png" alt="" /></p>
<p>SQL sorgulama komutlarına hakim olanlar için LINQ<em>(Language INtegrated Query)</em> ile birleştirilen bu kullanım şekli oldukça pratik görünüyor. Üretim ortamında da değerlendirilebilir mi henüz emin değilim ancak pilot olarak denenebilir. Yüksek transaction içeren servis operasyonlarında nasıl bir tepki vereceğine bakacak şekilde yük testlerine tabi tutmak karar verme noktasında yardımcı olabilir.</p>
<p>SQLKata'yı tanımaya çalıştığımız bu örnek üzerinde yapabileceğiniz daha birçok şey var. Örneğin OData altyapısını kullanmadan servisi OData standartlarına uyumlu hale getirmeyi deneyebilirsiniz ya da PostgreSql yerine MySql gibi bir veritabanını kullanabilirsiniz. SQL sorgularını daha yakından tanımak için farklı operasyonları devreye alabilirsiniz. Son bir haftada<em>(ya da belirtilen aylarda)</em> verilen siparişlerin listesini çekmek, talepte bulunan kullanıcının yaşadığı bölgeye göre o yörede en çok satılan ürünleri raporlamak vb. Temel CRUD operasyonlarını saymıyorum bile ;) Böylece geldik bir <a href="https://github.com/buraksenyurt/skynet" target="_blank">SkyNet</a> derlememizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>2020-10-28T14:14:00+00:00.net coresqlkatapostgresqlc#sqllinq.netnorthwinddockerdocker composebsenyurtVeri odaklı uygulamalarda sorgu komutlarını çalıştırmak için kullandığımız birçok hazır altyapı var. Örneğin .Net dünyasına baktığımızda en temel seviyede Ado.Net ve Object Releational Mapping tarafında Entity Framework sıklıkla karşılaştıklarımız arasında. SqlKata'da bunlardan birisi olarak düşünülebilir. Bir süredir de sağda solda okuduğum makale ve github çalışmalarından dolayı merak edip kurcalamak istediğim bir kütüphaneydi. C# ile geliştirimiş paketin temel amacı SqlServer, PostgreSql, Firebird, MySql gibi veritabanları için kod tarafında ortak bir sorgu oluşturma/derleme arabirimi sunmak ama bunu LINQ sorgu metotları üzerinden SQL dilinin anlaşılır rahatlığında, injection yemeden(Parameter Binding tekniğini kullandığı için) ve cache gibi performans artırıcıları kullanarak sağlamak...https://www.buraksenyurt.com/pingback.axdhttps://www.buraksenyurt.com/post.aspx?id=8e0c79f5-5e9f-46c8-865e-75cd5865dd6f0https://www.buraksenyurt.com/trackback.axd?id=8e0c79f5-5e9f-46c8-865e-75cd5865dd6fhttps://www.buraksenyurt.com/post/net-core-web-api-tarafinda-sqlkata-ile-sevimli-sql-islemleri#commenthttps://www.buraksenyurt.com/syndication.axd?post=8e0c79f5-5e9f-46c8-865e-75cd5865dd6f