https://www.buraksenyurt.com/
Burak Selim Şenyurt - Asp.Net Core
2021-07-02T08:20:54+00:00
Matematik Mühendisi Bir Bilgisayar Programcısının Notları
Burak Selim Senyurt
BlogEngine.Net Syndication Generator
https://www.buraksenyurt.com/opml.axd
Burak Selim Senyurt
Matematik Mühendisi Bir Bilgisayar Programcısının Notları
tr-TR
Burak Selim Şenyurt
0.000000
0.000000
https://www.buraksenyurt.com/post/asp-net-core-once-razor-sonra-blazor
Asp.Net Core - Önce Razor Sonra Blazor
2021-05-02T21:30:00+00:00
bsenyurt
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mayis/jack-finnigan-00yDgACVeMA-unsplash.jpg" alt="" align="right" />Kendime geldiğimde hiçbir şey göremediğimi fark ettim. Üstüme çöken zifiri karanlığa rağmen halen daha hayatta olduğuma dair tek şey yağmur damlalarının birkaç metre üstümde olduğunu sandığım metal tavana vurarak çıkardıkları seslerdi. Ensemden neredeyse ayak parmaklarıma kadar yayılan ağrı hiçbir şeyi umursamaz bir tavırda yattığım yerden doğrulmamı güçleştiriyordu. Son hatırladığım CloudTown'dan birkaç sibernetik coder ile karşılaştığım belli belirsiz yansımalardan ibaretti. Kısa süre sonra yakınlarımda koşuşturan bazı ayak sesleri işittim. Yer yer duraksıyor yer yer su birikintilerine girip çıkıyorlardı. Fısıltılar daha duyulur sesler haline gelmeye başladı. Tavandaki kapağı açmak üzere içlerinden birinin elindeki anahtarları hazırladığını işittim. Sonrası gözlerim için çok korkunç bir deneyimdi. Bu zifiri karanlıkta ne kadar kaldığımı bilmiyorum ama gözlerim dışardan gelen o parlak ışığa karşı adeta haykırıyordu. Üstüme boca edilen bir kova soğuk suyun ardından gelen kaba ses ise çok tanıdıkdı. Ve şöyle seslendim; "Reyzor! Sen haaa" :P</p>
<p>Efendim bendeniz yine yazıya giriş yapacak güzel bir şeyler bulamayınca böyle garip bir hikayeyi ortaya atıverdim. Gel gelelim konumuz baş karakterimiz Reyzor ile de alakalı.(Photo by <a href="https://unsplash.com/@jackofallstreets?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Jack Finnigan</a> on <a href="https://unsplash.com/s/photos/rain?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>)</p>
<p>Asp.Net Core 5 cephesine baktığımızda üç temel uygulama modelini desteklediğini görürüz. Web uygulamaları, servisler ve gerçek zamanlı iletişim araçları. Gerçek zamanlı<em>(Real-Time)</em> iletişim tarafında SignalR karşımıza çıkar. Servisleri göz önüne aldığımızda ise Web API ve gRPC başrolde yer alır. Web tarafını düşündüğümüzde oldukça zengin bir ürün çeşitliliği söz konusudur. MVC<em>(Model View Controller)</em>, Razor Pages, Blazor, Angular ve React tabanlı SPA'lar<em>(Single Page Applications)</em>. Kuvvetle muhtemel Asp.Net Core 5 tarafına yeni başladıysanız ve haberleri yeterince takip ediyorsanız en çok ilgi çeken geliştirme çatısının Blazor olduğunda hem fikir sayılırız. Fakat Blazor' a doğrudan geçmeden önce bazı temellerin de öğrenilmesi gerekir.</p>
<p>Söz gelimi dahili Dependency Injection mekanizmasının nasıl çalıştığını anlamak bunlardan sadece birisidir. Bana göre bir diğer önemli konu da Razor View Engine ya da sık telafuz edilen adıyla Razor'dur. Nitekim MVC, Razor Pages ve Blazor geliştirme çatıları alt yapı olarak Razor View Engine üzerine oturmaktadır. Dolayısıyla aynı bileşen yazım şablonlarını farklı web geliştirme çatıları için kullanabiliriz. Bu yetenek çatılar arası geçiş yapmamızı da kolaylaştırır. Öğrenilmesi oldukça kolay olan Razor, sadece C# ve HTML bilgisi gerektirir. Son yıllarda desteklediği yardımcı takılar<em>(Tag helper)</em> sayesinde arayüz tasarımcıları için dostane bir sözdizimi<em>(syntax)</em> sunar. Ayrıca test edilebilirliği basitleştirir. Razor en temel tanımıyla dinamik içerikle HTML çıktısı üretmeyi amaçlayan C# temelli bir işaretleme şablonudur<em>(Markup Template)</em> İşte bu yazıdaki amacımız örnek kodlar yardımıyla ona merhaba demek.</p>
<p>Dilerseniz vakit kaybetmeden örnek kodlara geçelim. Microsoft .Net 5 yüklü herhangi bir platformda aşağıdaki terminal komutları ile işe başlayabiliriz. Sonrasında Visual Studio Code veya Visual Studio Communit Edition veya kendimizi rahat hissettiğimiz bir IDE ile ilerleyebiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet new sln -o HelloRazor
cd .\HelloRazor\
dotnet new mvc -o FirstContact
dotnet sln add .\FirstContact\</pre>
<p>HelloRazor isimli bir Solution var. Ona FirstContact isimli bir de MVC<em>(Model View Controller)</em> projesi ekledik. Bu, en hafif MVC şablonu olarak düşünülebilir. Varsayılan haliyle baktığımızda dahi Razor tabanlı sayfalar içerdiğini görebiliriz<em>(Index.cshtml, Privacy.cshmtl gibi)</em> Cshtml uzantılı bu dosyalar kendi içlerinde hem HTML hem de sunucu tarafında çalışan C# kodlarını barındırır. Bu noktadan sonra karşımıza sıklıkla @ sembolünün çıkacağını ifade edebilirim. @ Sembolü ile C# ifadelerini veya kod bloklarını HTML içerisinde kullanabiliriz. Hemen index.cshtml dosyasına geçelim ve içeriğini aşağıdaki gibi güncelleyelim.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@{
ViewData["Title"] = "Space Traveler's Base";
}
<div>
<h1 class="display-4">@ViewData["Title"]</h1>
<p>
Today is @DateTime.Now.DayOfWeek ! Well, where do you want to go?
</p>
<ul>
@{
var locations = new List<string> { "Sirius", "Altair", "Betelgeuse", "Algol", "Messier 31", "Eta Carinae" };
var count = locations.Count;
foreach (string location in locations)
{
<li>@location</li>
}
}
</ul>
</div></pre>
<p>Index.cshtml içerisinde hem HTML hem de C# kodları olduğunu görebiliriz. @{ } ifadeleri razor kod blokları olarak adlandırılır. İçlerinde özgürce C# kodlaması yapabiliriz. Kodun iki yerinde bu kullanım söz konusu. İkinci kullanım biraz daha ilgi çekici. string türden bir List nesnesini kullanarak tarayıcıya çeşitli yıldızların isimlerini basıyoruz. @location kullanımı mutlaka dikkatinizi çekmiştir. Zaten bir Razor kod bloğundaysak neden döngüdeki location değişkeni başında @ işareti var? Çünkü kod bloğu içerisinde bir HTML elementi kullandık. li elementi kullanılan kısımda bir anda Razor kod bloğu dışına çıkmış ve HTML dünyasına girmiş oluruz. Doğrudan HTML içerisinde C# ifadelerini çalıştırmamız gereken bu gibi hallerde, ifadenin başına @ işareti koyarak hareket edebiliriz<em>(Razor Implicit Expression olarak adlandırılan teknik).</em> location için geçerli olan bu kullanımın bir benzeri de hangi günde olduğumuzu yazdıran kısımda yer almaktadır. </p>
<p>Uygulamayı çalıştırdığımızda aşağıdakine benzer bir sonuç almamız gerekir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mayis/hellomvc_12.png" alt="" /></p>
<p>Pek tabii locations listesinin bir model nesnesi baz alınarak farklı bir bileşenden View tarafına çekilmesi asıl amaç olmalıdır. MVC ve Blazor uygulamalarında sıklıkla bavşuracağımız bir yoldur. Bu durumu daha iyi anlamak için Model klasörü altına StarModel isimli bir sınıf oluşturarak devam edelim. Bu literatürde ViewModel olarak adlandırılan tiptir.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System.ComponentModel.DataAnnotations;
namespace FirstContact.Models
{
public class StarModel
{
[Required]
[Display(Name="Star No")]
public int ID { get; set; }
[Required]
[MinLength(3,ErrorMessage = "The name of the star must be at least 3 characters.")]
public string Name { get; set; }
[Required]
[Range(1,750)]
[Display(Name="Distance from Earth(LY)")]
public double Distance { get; set; }
[Required]
public double SurfaceTemperature { get; set; }
}
}</pre>
<p>Özelliklerin üzerinde kullanılan nitelikler<em>(Attribute)</em> şu an için gerekli değil ama yazının ilerleyen kısmındaki Tag Helper kullanımında işimize yarayacaklar. Bunu bir kenara bırakırsak basit bir ViewModel tanımladığımızı söyleyebiliriz. Bu modele ait yükleme, silme, güncelleme gibi işlemleri ise farklı bir sınıfın sorumluluğuna vermemiz doğru olacaktır. Örneğin Data isimli yeni bir klasör altına koyacağımız Star isimli bir sınıf bu iş için ideal görünüyor <em>(Konumuzla doğrudan alakalı olmasa da alışkanlık edinmemiz için bir interface tipi ile birlikte bu bileşenleri tanımlamakta ve Dependency Injection ile birlikte kullanmakta yarar var)</em></p>
<p>IStar arayüzü;</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FirstContact.Models;
using System.Collections.Generic;
namespace FirstContact.Data
{
public interface IStar
{
List<StarModel> GetStars();
}
}</pre>
<p>ve Star sınıfı;</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">using FirstContact.Models;
using System.Collections.Generic;
namespace FirstContact.Data
{
public class Star
:IStar
{
public List<StarModel> GetStars()
{
return new List<StarModel>
{
new StarModel{ID=1,Name="Sirius",Distance=8.60,SurfaceTemperature=9940},
new StarModel{ID=2,Name="Altair",Distance=16.73,SurfaceTemperature=7700},
new StarModel{ID=3,Name="Betelgeuse",Distance=642.5,SurfaceTemperature=126000},
new StarModel{ID=4,Name="Algol",Distance=92.95,SurfaceTemperature=13000},
new StarModel{ID=5,Name="Eta Carinae",Distance=7.5,SurfaceTemperature=35200},
};
}
}
}</pre>
<p>Sadece Index sayfasında kullandığımız yıldız listesinin bir benzerini döndüren kobay bir sınıf. Peki Razor View Engine tarafı bu sınıfı nasıl kullanacak? Şu anki MVC senaryomuza göre HomeController tarafındaki hazır Action metodu bunun için uygun görünüyor.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using FirstContact.Data;
using Microsoft.AspNetCore.Mvc;
namespace FirstContact.Controllers
{
public class HomeController : Controller
{
private readonly IStar _star;
public HomeController(IStar star)
{
_star = star;
}
public IActionResult Index()
{
var starList = _star.GetStars();
return View(starList);
}
}
}</pre>
<p>Index metodunda bir Star nesne örneği kullanarak yıldız listesini almaktayız. Sonrasında bu listeyi View metoduna parametre olarak geçerek Home isimli View'a göndermekteyiz. Sınıfın yapıcı metodunda görüleceği üzere Star bileşenini Dependency Injection Container'dan istiyoruz. Dolayısıyla Star bileşeninin DI servislerine eklenmesi gerekiyor. Daha önceki bir yazımızda bu konuya değinmiştik ;) <a href="https://www.buraksenyurt.com/post/asp-net-core-a-nasil-merhaba-deriz" target="_blank">Nasıl yaparım diyorsanız buradaki yazıya bakmanızı önerebilirim</a> ama "işimi uzatma" derseniz de yapmanız gereken tek şey Startup sınıfındaki ConfigureServices metoduna aşağıdaki satırı eklemekten ibaret.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">services.AddTransient<IStar, Star>();</pre>
<p>Bu ara hazırlıklardan sonra yeniden Razor tarafına dönelim ve Index sayfasının içeriğini aşağıdaki gibi değiştirelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">@{
ViewData["Title"] = "Space Traveler's Base";
}
@model List<StarModel>
<div>
<h1 class="display-4">@ViewData["Title"]</h1>
<p>
Today is @DateTime.Now.DayOfWeek ! Well, where do you want to go?
</p>
<ul>
@{
foreach (var star in Model)
{
<li>@star.Name (@star.Distance) Light Year from Earth</li>
}
}
</ul>
</div></pre>
<p>Bir önceki Razor örneğinden farklı olarak burada dikkat etmemiz gereken en önemli nokta @model ve Model enstrümanları. @model direktifi ile sayfanın kullanacağı ViewModel nesnesini belirtiyoruz. Star sınıfındaki GetStars metodunun dönüş tipini düşününce bunun List<StarModel> olması son derece doğal. Başta yapılan bu işaretleme, sayfanın devamındaki Model değişkenlerinin List<StarModel> tipinden bir referans olacağını belirtmekte. Bu nedenle for döngüsü içerisinde @star ifadesinden sonra StarModel nesnesinin özelliklerine erişebiliyoruz. Model nesnesini kimin doldurduğu sorusunun cevabını ise HomeController sınıfının Index metodundaki View çağrısı vermekte.</p>
<blockquote>
<p>Yaptığımız örnek MVC tarafına daha uygun. @model direktifi bu örnekte bir veri nesnesini referans ediyorken Razor Pages yapısında code-behind dosyasında yer alan<em>(Index.cshtml.cs gibi)</em> bir sınıf işaret edilir. Bir başka deyişle @ ile kullanılan built-in direktiflerin seçilen web geliştirme çatısına göre farklılıklar göstermesi olasıdır. </p>
</blockquote>
<p>Yaptığımız bu son değişikliklere göre çalışma zamanı çıktısı aşağıdaki gibi olacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mayis/hellomvc_13.png" alt="" /></p>
<p>Fark ettiyseniz Razor söz dizimi oldukça kolay. Görselliği basit dokunuşlarla artırmak da mümkün. Yardımcı takılara<em>(Tag Helper)</em> değinmeden önce dilerseniz sayfamızı aşağıdaki şekilde yeniden düzenleyelim.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@{
ViewData["Title"] = "Space Traveler's Base";
}
@model List<StarModel>
<div>
<h1 class="display-4">@ViewData["Title"]</h1>
<p>
Today is @DateTime.Now.DayOfWeek ! Well, where do you want to go?
</p>
<table class="table table-dark">
<thead class="bg-primary">
<tr>
<th>
No
</th>
<th>
Name
</th>
<th>
Distance (LY)
</th>
<th>
Surface Temperature (Kelvin)
</th>
</tr>
</thead>
<tbody>
@foreach (var star in Model)
{
<tr>
<td>
<label>@star.ID</label>
</td>
<td>
<label class="font-weight-bold">@star.Name</label>
</td>
<td>
<label class="font-italic">@star.Distance</label>
</td>
<td>
<label>@star.SurfaceTemperature</label>
</td>
</tr>
}
</tbody>
</table>
</div></pre>
<p>Bu sefer HTML Table elementini işin içerisine kattık. Sonuç biraz daha umut verici. En azından benim için ;)</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mayis/hellomvc_14.png" alt="" /></p>
<p>Şu ana kadar yaptıklarımızla Razor söz dizimini en temel haliyle tanıdık ve bir Razor sayfasını çalışacağı örnek bir Model ile nasıl bağlayacağımızı öğrendik. Bilmemiz gereken giriş seviye konulardan bir diğeri de Tag Helper kullanımıdır. Tag Helper ifadeleri giriş kontrollerinde<em>(input)</em>, veri doğrulamasında<em>(validation)</em>, yönlendirmelerde<em>(routing) </em>ve form ektileşimlerinde<em>(actions)</em> kullanılabilen basitleştirici ifadeler olarak düşünülebilir.</p>
<blockquote>
<p>Normalde HTML Helper'lar söz konusudur ve Visual Studio editörünün Scaffolding mekanizması otomatik View üretimlerinde ağırlıklı olarak @Html.DisplayNameFor, @Html.DisplayFor ve @Html.ActionLink gibi fonksiyon çağrımlarını kullandırır. Ancak son yıllarda yardımcı takı ifadeleri hem okunabilir olmaları hem de bir HTML elementine doğrudan ataşlanabilmeleri sebebiyle öne çıkmaktadır. HTML Helper'lar bazen okunması zor fonksiyon ifadelerinden oluşur.</p>
</blockquote>
<p>Şimdi Home klasörü altına Add isimli bir View ekleyelim. Galaksi veritabanımıza yıldız eklemek için kullanılan basit bir form olduğunu düşünebiliriz. Bu form içerisinde de bazı yardımcı takılardan faydalanacağız.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@{
ViewData["Title"] = "Space Traveler's Base";
}
@section scripts{
<partial name="_ValidationScriptsPartial" />
}
@model StarModel
<div>
<form asp-controller="Home" asp-action="OnSave" method="post">
<div class="form-group">
<label asp-for="@Model.ID"></label><br />
<input asp-for="@Model.ID" /><br />
<span asp-validation-for="@Model.ID" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.Name"></label><br />
<input asp-for="@Model.Name" /><br />
<span asp-validation-for="@Model.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.Distance"></label><br />
<input asp-for="@Model.Distance" /><br />
<span asp-validation-for="@Model.Distance" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="@Model.SurfaceTemperature"></label><br />
<input asp-for="@Model.SurfaceTemperature" /><br />
<span asp-validation-for="@Model.SurfaceTemperature" class="text-danger"></span>
</div>
<div class="form-group">
<button class="btn-primary" type="submit">Save</button>
</div>
</form>
</div></pre>
<p>asp- şeklinde başlayan ifadeler tag helper bildirimleridir. Örneğin form elementinde asp-controller ve asp-action isimli iki yardımcı kullanılmıştır. Bildiğiniz üzere bir web formunu sunucu tarafına gönderirken form elementinden yararlanılır. Submit tipinden bir butona basıldığında hangi Controller'ın hangi Action fonksiyonunun devreye gireceğini bu yardımcı takılar ile belirleyebiliriz. Buna göre HomeController'ın OnSave isimli metodu çağıralacaktır.</p>
<p>Kullanılan bir diğer yardımcı takı ise asp-for'dur. Kullanıldığı HTML elemanına göre farklı davranışlar sergileyebilir. Bir label ile ilişkilendirildiğinde ViewModel nesnesinin varsa Display değerini, yoksa özellik adını kullanır. input elementi ile kullanıldığında ise ekrandan girilen içeriğin modelin hangi özelliğine bağlanacağını belirtir. Sayfada bazı doğrulama kontrolleri de söz konusudur. Girdi ihlallerine ait bilgiler span elementleri içerisinde yazılırken asp-validation-for isimli yardımcı takı kullanılmıştır. Buna göre takriben aşağıdaki ekran görüntüsündekine benzer bir sonuç elde ederiz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mayis/hellomvc_15.png" alt="" /></p>
<p>Konumuz giriş verilerinin kontrolü değil bu nedenle çok fazla detaya girmiyoruz. Lakin örnekte kullandığımız sayfanın HTML çıktısına bakarsak asp-validation-for bilgilerinin istemci bazlı doğrulama işlemlerinde kullanılan jQuery için data-val-* formatına evrildiğini de görebiliriz. Örneğin Name input kontrol için üç karakterden az olmaması gerektiğini ifade etmiştik ya da dünyaya olan uzaklığın sıfır ışık yılı olmayacağını. Aşağıdaki ekran görüntüsünden de fark edileceği üzere tek bir asp-for-validation bildirimi HTML çıktısında data-val, data-val-minlength, data-val-minlength-min, data-val-required şeklinde çözümlenmiştir. Bunların bir kısmının da ViewModel nesnesinde kullandığımız DataAnnotations niteliklerinden<em>(Attribute kullanımlarına bakın)</em> kaynaklandığını ifade edebiliriz. Dolayısıyla Razor tarafındaki bir tag helper'ın işi nasıl kolaylaştırdığını net bir şekilde görmüş oluyoruz<em> (Blazor tarafında doğrulama için asp-validaton-for yerine ValiadationMessage elementi kullanılmaktadır. Söz dizimi şuna benzer; <ValidationMessage For="@(()=>Model.Name)" /> )</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Mayis/hellomvc_16.png" alt="" /></p>
<p>Razor tarafında kullanılan pek çok yardımcı takı var. Bir tanesini daha örneğe eklemeye ne dersiniz? Söz gelimi yıldızların dahil olduğu birkaç galaksiyi bir Enum sabiti olarak tuttuğunuzu ve yeni yıldız eklerken de bunları bir select elementinde göstermek istediğinizi düşünün. Bunun için asp-items isimli yardımcı takıyı kullanabilirsiniz. Haydi bir deneyin ;) Buna ek olarak geliştirdiğimiz yıldız ekleme sayfasını HTML Helper fonksiyonları ile tekrardan yazmaya çalışın ve Tag Helper'lar ile aradaki farklılıkları kıyaslayın.</p>
<p>Başta da belirttiğimiz gibi önce Razor sonra Blazor. Hatta önce Razor sonra MVC, sonra Razor Pages ve daha sonra Blazor :) Nitekim bu çatılar kendileri için ortak olsa da Razor View Engine'i farklı şekilde kullanmaktalar. Örneğin MVC tarafında Razor sayfalarına yapılan yönlendirmeler Controller tarafından ele alınırken, Razor Pages yapısında pages klasörü altındaki sayfalara fiziki bir yönlendirme söz konusudur. Söz gelimi pages/space/star.cshtml şeklinde bir sayfamız varsa, bu sayfaya çalışma zamanında /space/start şeklinde ulaşabiliriz. Hatta Razor Pages tarafında sayfalar genellikle .cshtml ve .cshtml.cs şeklinde ikiye ayrılır ki bu kurguyu benim gibi Asp.Net Web Forms çağından gelenler iyi bilir. Ama en nihayetinde hepsinin altında yatan temel olgu Razor View Engine kullanımıdır. </p>
<p>Bu kısa çalışmada MVC, Blazor ve Razor Pages gibi çatıların temel yapıtaşı olan Razor View Engine tarafını basitçe anlamaya çalıştık. Artık bunun üstüne daha zengin Web uygulama örnekleri geliştirebileceğinizi düşünüyorum. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize sağlıklı günler dilerim.</p>
2021-05-02T21:30:00+00:00
asp.net 5
asp.net core
razor
c#
cshtml
tag helper
razor pages
bsenyurt
Asp.Net Core 5 cephesine baktığımızda üç temel uygulama modelini desteklediğini görürüz. Web uygulamaları, servisler ve gerçek zamanlı iletişim araçları. Gerçek zamanlı(Real-Time) iletişim tarafında SignalR karşımıza çıkar. Servisleri göz önüne aldığımızda ise Web API ve gRPC başrolde yer alır. Web tarafını düşündüğümüzde oldukça zengin bir ürün çeşitliliği söz konusudur. MVC(Model View Controller), Razor Pages, Blazor, Angular ve React tabanlı SPA'lar(Single Page Applications). Kuvvetle muhtemel Asp.Net Core 5 tarafına yeni başladıysanız ve haberleri yeterince takip ediyorsanız en çok ilgi çeken geliştirme çatısının Blazor olduğunda hem fikir sayılırız. Fakat Blazor' a doğrudan geçmeden önce bazı temellerin de öğrenilmesi gerekir.
https://www.buraksenyurt.com/pingback.axd
https://www.buraksenyurt.com/post.aspx?id=bf9215b0-8695-4dee-a0e8-d6d8b103baac
2
https://www.buraksenyurt.com/trackback.axd?id=bf9215b0-8695-4dee-a0e8-d6d8b103baac
https://www.buraksenyurt.com/post/asp-net-core-once-razor-sonra-blazor#comment
https://www.buraksenyurt.com/syndication.axd?post=bf9215b0-8695-4dee-a0e8-d6d8b103baac
https://www.buraksenyurt.com/post/asp-net-core-dependency-lifetimes
Asp.Net Core - Dependency Lifetimes
2021-04-30T23:54:00+00:00
bsenyurt
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_11.png" alt="" align="right" />Çalışmakta olduğum şirketin çok büyük bir ERP<em>(Enterprise Resource Planning)</em> uygulaması var. Microsoft .Net Framework 1.0 sürümünde düşünce olarak hayat geçirilip geliştirilmeye başlanmış. Milyonlarca satır koddan ve sayısız sınıftan oluşan, katmanlı monolitik mimari üstünde yürüyen, sahada on binden fazla personelin kullandığı çok etkili bir ürün. Geçtiğimiz yıl bu uygulamanın modernizasyonu kapsamında başlatılan IT4IT çalışmaları bünyesinde nesne bağımlılıklarının yönetimi için Dependency Injection mekanizmasının nimetlerinden de epeyce yararlanıldı. Doğruyu söylemek gerekirse koda yaptıkları dokunuşları hayranlıkla izledim.</p>
<p>Elbette başa dert olan ve sahada fark edilmesi güç bazı konular da gündeme gelmedi değil. Bunlarda birisi de bağımlı nesnelerin yaşam ömürleri ile alakalıydı. Gerçekten böylesine büyük bir sistemde AddTransient ile mi gitmeli yoksa AddScoped olarak mı bırakmalı gibi sorulara cevap vermek kolay değil. Öncelikle şu nesne yaşam ömrü meselesini anlamak gerekiyor. Bende hazır evden çıkmamız yasak kitaplarıma gömülmüşken bu meseleyi iyice bir öğreneyim istiyorum. Kapak fotoğrafı mı? Her zaman ki gibi konumuzla bir alakası yok. Sadece yazıyı yazarken dinlemekte olduğum Bon Jovi'nin 1984 çıkışlı stüdyo albümüne ait :D </p>
<p>Aslında Asp.Net 5 açısından bakıldığında da Dependency Injection ile ilişkili kafa karıştıran ve saha çözümlerinde dikkat gerektiren konulardan birisi servis yaşam süreleri<em>(Hoş, .Net Remoting ve WCF tarafındaki nesne yaşam döngülerini düşününce nispeten çok daha kolay bir konu) </em>Bu kısa yazıda söz konusu meseleyi öğrendiğim kadarıyla sizlere anlatmaya çalışacağım. Örneğimiz <a href="https://www.buraksenyurt.com/post/asp-net-core-dependency-injection-turleri" target="_blank">bir önceki yazıda</a> da değindiğimiz .Net çözümü<em>(hands-on-aspnetcore-di)</em> üzerinde koşuyor olacak. Ayrıca kodun detaylarına <a href="https://github.com/buraksenyurt/hands-on-aspnetcore-di/tree/lifetimes" target="_blank">github adresinden</a> bakabilir ve eksik kısımları tamamlayabilirsiniz. Ben odaklanmamız gereken yerleri ve sonuçları paylaşmaya çalışarak bakmamız gereken alanı daraltmak niyetindeyim. Her şeyden önce senaryomuza bir göz atalım<em>(Taslak çizimin kusurlarını lütfen mazur görün)</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_10.png" alt="" /></p>
<p>Anlamsız bir model ancak nesne yaşamlarını öğrenmek için hem kitaplarda hem de internet kaynaklarında kullanılan yaygın bir yöntemi değerlendireceğiz; Guid tipi yardımıyla hayattaki nesnelerin takibi. Senaryomuzda GameController tipinin bağımlı olduğu dört farklı bileşen var. Bu bağımlılıklar IGameRepository, IPartRepository, IShopRepository ve arayüzleri üstünden gelen sınıflar ile PerformanceCounter tipi. İşin ilginç yanı PerformanceCounter sınıfının da IGameRepository, IPartRepository ve IShopRepository referansları üzerinden gelen bileşenlere bağımlılığı var. Bu kurguda amaç, çalışma zamanında DI Container servislerine kayıt edilen IGameRepository, IPartRepository ve IShopRepository türevlerinin, PerformanceCounter içerisine alınırken farklı yaşam süresi seçimlerine göre nasıl tepki geliştirdiklerini öğrenmek.</p>
<p>Dependency Injection servis koleksiyonuna kayıt edilen bileşenler için normal şartlarda üç tip yaşam ömrü seçeneği bulunuyor. Transient, Scoped ve Singleton. Genellikle konuya yabancı olan ben gibiler kolaya kaçıp Transient seçeneğini tercih ediyor. Fakat duruma göre uygun olan modeli belirlemek lazım. Örneğin Entity Framework tarafına ait DbContext servisi kayıt edilirken neden Scoped olarak dahil ediliyor? Peki ya ILogger'ın varsayılan ömrü neden Singleton? Dolayısıyla aradaki farkları anlamamız önemli.</p>
<p>DI Container'a Scoped türünde kayıt edilen bir servis her web talebi için yeniden örnekleniyor. Singleton modelinde ise servis bir kere örnekleniyor ve uygulama<em>(Web App)</em> ayakta kaldığı sürece yaşamaya devam ediyor. Dolayısıyla onu çözümleyen<em>(Resolve)</em> bileşenler hep aynı nesne örneğini kullanıyorlar. Son olarak Transient seçeneğinde, bağımlı bileşen her nerede çözümlenirse çözümlensin hep yeni bir örneği oluşturularak kullanılıyor.</p>
<p>İyi güzel hoş ama bunu canlı bir örnekle nasıl analiz ederiz? Yukarıdaki şekle göre gerekli kodlarımızı yazmaya başlayım. IGameRepository, IShopRepository ve IPartRepository arayüzleri Guid tipinden birer özellik sunuyorlar. Bu Guid'leri onları uygulayan asıl bileşenlerin<em>(Concrete Instance)</em> çalışma zamanındaki takibini yapmak için kullanacağız. IShopRepository ve ShopRepository tiplerinin içeriğini aşağıda bulabilirsiniz. Diğerleri de benzer bir düzeneğe sahipler.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using C64Portal.Models;
using System;
namespace C64Portal.Data
{
public interface IShopRepository
{
public Guid InstanceID { get; set; }
void Sell(Game game,decimal offer);
}
}</pre>
<p>ve onu uygulayan asıl sınıf<em>(Concrete Class)</em>.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using C64Portal.Models;
using C64Portal.Queue;
using System;
using System.Collections.Generic;
namespace C64Portal.Data
{
public class ShopRepository
: IShopRepository
{
public Guid InstanceID { get; set; }
public ShopRepository() :this(Guid.NewGuid())
{
}
public ShopRepository(Guid instanceID)
{
InstanceID = instanceID;
}
public void Sell(Game game, decimal offer)
{
// Do Something
}
}
}</pre>
<p>İşe yarayan bir fonksiyon yok ancak yapıcı metodun<em>(constructor)</em> nasıl kullanıldığı bizim için önemli. ShopRepository sınıfına ait bir nesne örneklenirken yeni bir Guid oluşturuyoruz. Varsayılan yapıcı metot, DI kayıt işlemi<em>(Register)</em> sırasında gerekli olduğu için çağrıldığında parametre ile donatılan diğer yapıcı metodu tetikliyor. Doğal olarak seçilen lifetime kriterine göre takip edeceğimiz benzersiz bir değere sahip olmuş olacağız. Diğer arayüz ve uyarlamalarını yazdıktan sonra PerformanceCounter sınıfını da aşağıdaki gibi geliştirebiliriz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
namespace C64Portal.Data
{
public class PerformanceCounter
{
public Guid ShopRepositoryID { get; set; }
public Guid GameRepositoryID { get; set; }
public Guid PartRepositoryID { get; set; }
private readonly IGameRepository _gameRepository;
private readonly IShopRepository _shopRepository;
private readonly IPartRepository _partRepository;
public PerformanceCounter(IGameRepository gameRepository, IShopRepository shopRepository, IPartRepository partRepository)
{
_gameRepository = gameRepository;
_shopRepository = shopRepository;
_partRepository = partRepository;
GameRepositoryID = _gameRepository.InstanceID;
ShopRepositoryID = _shopRepository.InstanceID;
PartRepositoryID = _partRepository.InstanceID;
}
public void CalculateMemoryUsage()
{
//Do Something
}
}
}</pre>
<p>İğrenç bir sınıf değil mi? :D Ancak yapıcı metoda yine dikkat edelim. Sınıfın bağımlı olduğu bileşenler, tasarladığımız arayüzler üzerinden çözümlenerek içeri alınıyor ve gelen nesne örneklerinin Guid tipli özelliklerinin herbiri için ayrılmış alanlara atanıyorlar. Şimdi de GameController içeriğini aşağıdaki gibi değiştirelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using C64Portal.Data;
using C64Portal.Models;
using C64Portal.Queue;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace C64Portal.Controllers
{
public class GameController : Controller
{
private readonly IGameRepository _gameRepository;
private readonly IShopRepository _shopRepository;
private readonly IPartRepository _partRepository;
private readonly PerformanceCounter _performanceCounter;
private readonly ILogger<GameController> _logger;
public GameController(
IGameRepository gameRepository
,IShopRepository shopRepository
,IPartRepository partRepository
, PerformanceCounter performanceCounter
, ILogger<GameController> logger)
{
_logger = logger;
_performanceCounter = performanceCounter;
_gameRepository = gameRepository;
_shopRepository = shopRepository;
_partRepository = partRepository;
}
public IActionResult Index()
{
_logger.LogInformation($"\n[SINGLETON]\tShopRepo ID:{_shopRepository.InstanceID},In Perf Counter:{_performanceCounter.ShopRepositoryID}");
_logger.LogInformation($"\n[TRANSIENT]\tGameRepo ID:{_gameRepository.InstanceID},In Perf Counter:{_performanceCounter.GameRepositoryID}");
_logger.LogInformation($"\n[SCOPED ]\tPartRepo ID:{_partRepository.InstanceID},In Perf Counter:{_performanceCounter.PartRepositoryID}");
var games = _gameRepository.GetAllGames();
_performanceCounter.CalculateMemoryUsage();
return View(games);
}
public IActionResult Create()
{
return View();
}
[HttpPost]
public IActionResult Create(Game game)
{
_gameRepository.Publisher = new RabbitPublisher();
_gameRepository.Create(game);
return RedirectToAction("Index");
}
}
}</pre>
<p>GameRepository'dekine benzer bir durum burada da söz konusu. Sadece fazladan PerformanceCounter ve ILogger bağımlılıkları da var. Lakin fazladan dediğimiz PerformanceCounter kullanımı önemli. Web uygulaması çalıştığında GameController tipi her ne zaman çağırılırsa yapıcı metodu sebebiyle DI Container'dan IGameRepository, IShopRepository, IPartRepository ve PerformanceCounter referansları isteyecek. Bu da asıl sınıfların örneklendiği<em>(Constructor'ların tetiklenmesi)</em> ya da örneklenmeyip örneklenmiş olanların verildiği bir operasyon anlamına geliyor. Diğer yandan PerformanceCounter'ın çağırılması halinde onun da istediği IGameRepository, IPartRepository ve IShopRepository referansları var. PerformanceCounter sınıfı bunları da DI Container'dan isteyecek<em>(Hatta onu bilerek AddTransient olarak kayıt edeceğiz ki her örneklendiğinde DI'dan diğer arayüz referanslarını istesin)</em> İşte bu ikinci isteklerde söz konusu servislerin hangi yaşam döngüsü seçeneğine göre kaydedildiği önem kazanıyor. Diğer yandan ufak bir detay ama Index isimli Action içerisinde bir Log yayınladığımızı da fark etmiş olmalısınız. Loglamayı, Controller'a gelindiğinde ve Index fonksiyonu çağırıldığında oluşan bağımlı bileşenlerin güncel Guid değerlerini kaydetmek için kullanıyoruz. Bu arada tüm bileşenlerin Constructor Injection tekniği ile çözümlendiğine dikkat edin ve başka hangi tekniklerden bahsetmiştik hatırlayın. </p>
<blockquote>
<p>Bu arada loglamayı dilerseniz fiziki olarak bir Text dosyasına da yapabilirsiniz. Ben bunun için Serilog.Extensions.Logging.File isimli Nuget paketini projeye ekledim ve Startup sınıfındaki Configure metodunu da aşağıdaki gibi değiştirdim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILoggerFactory loggerFactory)
{
var path = Directory.GetCurrentDirectory();
loggerFactory.AddFile($"{path}\\Logs\\Log.txt");</pre>
</blockquote>
<p>Gelelim bileşenlerin DI Servis kataloğuna kayıt edilmesine ki burası yazımızın dönüm noktası. Bunun için Startup sınıfındaki ConfigureServices metodunu aşağıdaki gibi kullanabiliriz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddTransient<IGameRepository, GameRepository>();
services.AddScoped<IPartRepository, PartRepository>();
services.AddSingleton<IShopRepository, ShopRepository>();
services.AddTransient<PerformanceCounter>();
services.AddTransient<DataCollectorService>();
}</pre>
<p>IGameRepository üstünden bağlanan GameRepository, AddTransient fonksiyonu ile eklenmiş durumda. Buna göre kendisine her ihtiyaç duyulduğunda tekrardan örneklenecek. Yani onun adına hep yeni bir Guid değeri görmemiz gerekiyor. PartRepository sınıfı ise AddScoped metodu ile dahil edilmiş durumda. Buna göre aynı Scope içerisinde kalındığı sürece hem Controller hem de PerformanceCounter'da tekil bir PartRepository nesnesinin kullanılmasını bekliyoruz. Ta ki farklı bir scope'a geçip tekrar buraya dönene kadar<em>(Bunu diğer bir Controller'a geçip geri gelerek kontrol edebiliriz)</em> Son olarak sıra AddSingleton ile eklenen ShopRepository nesnesinde. Buna göre web uygulaması çalıştığı sürece, sayfa yenilense<em>(Örneğin F5 ile)</em> ya da farklı Controller ve Action metotları çalışsa bile, uygulama yeniden başlatılıncaya kadar tek bir ShopRepository örneğinin kullanılıyor olması lazım.</p>
<p>Bu aşamaya geldiyseniz uygulamayı çalıştırıp logları takip etmeniz yeterli. Ben örneğin çalışma zamanına ait iki ekran görüntüsü bırakmak istiyorum. İlki komut satırından yürütülen çalışma zamanına ait. Console penceresine düşen logları görebilirsiniz<em>(O değilde kopyala yapıştırın acı bir sonucu var burada. İki tane Prince of Persia eklenmiş yahu)</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_8.png" alt="" /></p>
<p>Paylaşmak istediğim diğer görüntü ise Guid bilgilerini topladığım Excel'e ait.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_9.png" alt="" /></p>
<p>Guid değerlerinin hangi durumda nasıl farklılaştığını görebiliyor musunuz? Bir nesnenin hangi aksiyonda nasıl davranış sergilediğini anlamak oldukça kolay. ShopRepository sisteme Singleton modelde alındığı için hangi aksiyon olursa olsun üretilen Guid hep aynı kalmakta. Sayfa yenilense de scope değişse de fark etmiyor. Yani GameController için de, onun içinden çağırılan PerformanceCounter için de aynı nesne kullanılıyor ve sayfa yenilense bile bu nesne yaşamaya devam ediyor. Lakin PartRepository nesnesine ait Guid bilgisi gerçekleşen aksiyon bazında değişmiş görünüyor. Fakat bir fark var. Aynı Scope'a dahil olan PerformanceCounter'da aynı PartRepository nesne örneğini kullanıyor. Bu nedenle Guid aksiyon bazında aynı kalmış halde. Bu noktada Scoped tekniğinin, Singleton ile sürekli olarak karıştırıldığını ifade edebilirim. Biri uygulama ayakta kaldığı müddetçe aynı kalırken diğeri sadece ortak Scope'a dahil olan farklı aksiyonlar boyunca aynı kalıyor. O nedenle yapılan her aksiyonda yeni bir PartRepository örnekleniyor ve hem GameController hem PerformanceCounter bu aynı nesneyi kullanıyor. GameRepository ise oldukça şımarık :) Aksiyon ne olursa olsun hep yeni bir Guid oluşmuş görünüyor; GameController tarafında da PerformanceCounter tarafında da.</p>
<p>İşte bu kadar :)</p>
<p>Bu örnekle bağımlı bileşenlerin nesne ömürlerinin nasıl şekillendiği kafamda biraz daha netleşmiş oldu. Elbette gerçek hayat senaryolarında bu seçimler oldukça kritik öneme sahip. Tüm uygulama yaşamı boyunca yaşayacak bir nesne örneği her ne kadar cazip görünse de bellek tüketiminin bir anda artmasına sebebiyet verebilir. Ya da web talebi için bir nesne örneklenmesi, ilk oluşturma maliyeti yüksek olan bileşenler düşünüldüğünde performans kaybına neden olabilir. Her nesne gerektiğinde yeni bir örnek oluşturulması basit bir seçim gibi dursa da network trafiğinin aşırı derecede artmasına sebebiyet verebilir. Kaynaklar en kötü karar bile kararsızlıktan iyidir felsefesini benimseyerek AddTransient olarak ilerleyin diyor. Bense vakaya göre seçim yapmamız gerekiğini düşünüyorum<em>(It depends hali)</em>. Notlarıma burada son vermeden önce araştırmanız için iki konu bırakıyorum.</p>
<ul>
<li>Sizce bir arayüz üstünden n sayıda bağımlı bileşeni DI Container servisine kayıt edebilir miyiz?</li>
<li>Çalışma zamanının herhangi bir noktasında DI Container'a kayıt edilmiş servisleri tek tek veya toplu olarak silebilir miyiz?</li>
</ul>
<p>Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize sağlıklı, huzur dolu günler dilerim.</p>
2021-04-30T23:54:00+00:00
dependency injection
di container
lifetimes
addsingleton
addscoped
addtransient
asp.net core
asp.net 5
mvc
controller
logging
bsenyurt
Asp.Net 5 tarafında Dependency Injection ile ilişkili kafa karıştıran ve saha çözümlerinde dikkat gerektiren konulardan birisi de servis yaşam süreleridir(lifetimes) Bu kısa yazıda söz konusu meseleyi basit bir şekilde anlamaya çalışacağız. Örneğimiz bir önceki yazımızda da kullandığımız .Net çözümü üzerinde geliştirilecek. Dolayısıyla kodun detaylarına github adresinden bakabilirsiniz. Ben odaklanmamız gereken kısımları ve sonuçları paylaşmaya çalışarak bakmamız gereken alanı daraltmaya çalışacağım. Dilerseniz neler yaptığımız bir bakalım.
https://www.buraksenyurt.com/pingback.axd
https://www.buraksenyurt.com/post.aspx?id=c4933ccd-f899-42b5-aa0e-b4612f30411a
0
https://www.buraksenyurt.com/trackback.axd?id=c4933ccd-f899-42b5-aa0e-b4612f30411a
https://www.buraksenyurt.com/post/asp-net-core-dependency-lifetimes#comment
https://www.buraksenyurt.com/syndication.axd?post=c4933ccd-f899-42b5-aa0e-b4612f30411a
https://www.buraksenyurt.com/post/asp-net-core-dependency-injection-turleri
Asp.Net Core - Dependency Injection Türleri
2021-04-29T11:03:00+00:00
bsenyurt
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_7.png" alt="" align="right" />Ayakta durmuş odanın camından dışarıyı izlerken yazıya nasıl bir giriş yapsam diye düşünüyordum. Baharın etkisi ile yapraklarını açmış meşenin yavaş yavaş gölgelediği caddeden İtalyan bayrağı kasklı bir motosikletli geçti aniden. Sadece birkaç metre gerisinden de onu neredeyse aynı süratle takip eden martıya binmiş bir genç. Kaldırımda bir elinde alışveriş poşeti ötekinde onu yola doğru çekiştiren haylazla birlikte yürümeye çalışan orta yaşlarında bir kadın. Hemen binanın önündeki basket sahasında da yaşları beş ile on beş arasında değişen on çocuk. Futbol oynuyorlar. Bağrışlar, çağrışlar. Çekişmeli de gidiyor ama herkesin yüzünde bir maske. Eve kapanmak zorunda kalmadan önce çocukların son bir bahar ziyafetini izliyorum diye iç geçiriyorum.</p>
<p>On yedi günlük evden çıkma yasaklarının bir gün öncesi çünkü bugün, 29 Nisan 2021 Perşembe. Pek tabii hayat evde de olsa devam ediyor. Bende bu dönemi iyi değerlendirmek adına yaz aylarında vereceğim şirket eğitimleri için Amazon'dan getirttiğim kitapları çalışmaya ağırlık vereyim diyorum. Malum .Net 5 güldür güldür geleli çok oldu ve orada öğrenmem gereken birçok konu birikti. En önemli şey ise öğrendiğim bir konuyu olabildiğince basit şekilde anlatabilmek. Bakalım bu yazıda bunu başarabilecek miyim?</p>
<p>Hatırlayacağınız üzere <a href="https://www.buraksenyurt.com/post/asp-net-core-a-nasil-merhaba-deriz" target="_blank">bir önceki yazımızda</a> Asp.Net 5 tarafında nasıl Hello World diyebileceğimizi incelemeye çalışmıştık<em>(Henüz okumadıysanız bir göz atmanızda yarar var)</em> O çalışmada ana odak noktamız dahili Dependency Injection mekanizmasının nasıl kullanıldığını görmekti. Kobay senaryomuzdaki en önemli noktalardan birisi de GameController sınıfı içerisinde IGameRepository yardımıyla low-level bir bileşenin kullanımıydı. Burada Constructor Injection tekniğinden yararlandığımızı ifade etmiştik. Bu teknik dışında kullanabileceğimiz versiyonlar da var. Bağımlı nesne çözümlemesini metot üzerinden, Property yardımıyla ve Asp.Net MVC 6 ile gelen @inject direktifi yoluyla da gerçekleştirebiliriz. İşte bu yazımızdaki amacımız aynı senaryoyu devam ettirerek söz konusu tekniklerin nasıl uygulanabileceğini öğrenmek. Yazıdaki kod parçaları <a href="https://github.com/buraksenyurt/hands-on-aspnetcore-di" target="_blank">şuradaki github hesabımda</a> yer alıyor. Initial, constructor-injection, method-injection, property-injection ve view-injection şeklinde farklı branch'ler içeriyor. Bu branch'lerde ilgili tekniklerin proje üstünden ayrı ayrı uygulanış şekillerini takip edebilirsiniz. Yazı boyunca ise odak noktamızı kaybetmemek adına sadece gerekli kod parçalarını kullanacağım. Paralel hareket etmeniz gerekebilir. Hazırsanız başlayalım;</p>
<h2>Method Injection</h2>
<p>Aslında yapıcı metot(Constructor) bir metottur. Dolayısıyla neden bu şekilde farklı bir uygulama tekniği olduğunu düşünebilirsiniz. Ne var ki, Constructor tekniğinde bağımlı nesnenin çözümlenmesi<em>(DI'ın Resolution aşaması) </em>ona ihtiyaç duyan nesne örneklenirken gerçekleşir. Bazı durumlarda sadece belli bir metot içerisinden kullanılan bağımlı nesneler de olabilir. Sanırım örnek bir senaryo üzerinden gidersek konu daha anlaşılır olacak.</p>
<p>Oyun portaline yenilerini eklemek için bir fonksiyon yazacağımızı düşünelim. Ayrıca her oyun eklendiğinde bir dış sisteme mesajla bildirim yapamak istiyoruz. RabbitMQ gibi bir kuyruk sistemi, veritabanı ya da doğrudan e-posta sunucusu bile olabilir. En nihayetinde GameRepository'nin Create metodu içerisinde bu gönderim işlemini yapmaya karar veriyoruz. Bununla birlikte mesaj yayınlama işini üstlenen asıl sınıfı kullanmak yerine, sadece gönder demenin daha doğru olacağını da biliyoruz. Çünkü bu sıkı bağlı<em>(tightly-coupled)</em> yapıda söz konusu olan xyz sistemi için gerekli gönderim adımlarını, bununla ilgisi olmayan GameRepository sınıfının anlamasına gerek yok. Hatta tightly-coupled durumlarda ısrarla kaçınmaya da çalışıyoruz. Haydi gelin kodlamaya başlayalım. İlk olarak IPublisher isimli bir arayüz geliştirelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public interface IPublisher
{
void Send(string message);
}</pre>
<p>IPublisher arayüzü senaryomuz için oldukça ilkel bir sözleşme sunuyor. Geriye değer döndürmeyen ve string tipte parametre alan Send isimli bir metot bildirimi taşıyor. Bu arayüzü kullanan örnek bir sınıfı da aşağıdaki gibi geliştirdiğimizi düşünelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public class RabbitPublisher
: IPublisher
{
public void Send(string message)
{
//todo something
}
}</pre>
<p>Biliyorum, fonksiyon içerisinde bir şey yapmıyoruz ama unutmayın; amacımız Method Injection'ı uygulamak. Bu durumda GameRepository sınıfı için düşündüğümüz Create metodunu nasıl yazarız, bir düşünün. İdeal olanı aşağıdaki kod parçasında olduğu gibidir.<em>(Bu arada IGameRepository arayüzünde de Create bildiriminin yapılması gerektiğini hatırlatayım. Nitekim IGameRepository üstünden kullanacağımız bir fonksiyon olmalı)</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public Game Create(Game game, IPublisher publisher)
{
publisher.Send("A new game has been added to inventory");
return game;
}</pre>
<p>Metodun belki de en önemli kısmı ikinci parametresidir ve IPublisher arayüz referansı kullanılmaktadır. Dolayısıyla onu uygulayan bir nesneyi metot içerisine alabilir ve Send fonksiyonunu çağırabiliriz. Bir başka deyişle Create metodunun ihtiyacı olan asıl nesne arayüz üzerinden kullanılır. Bu, Method Injection tekniği ile bağımlılığın çözümlenmesidir. Ancak ortada halen daha bir soru var. Create metodunun hangi IPublisher türevi ile çalışacağını nerede söyleyeceğiz? Yani bağımlı nesne için gerekli kayıt işlemini<em>(Dependency Injection Service Registration)</em> nerede yapacağız? Tahmin edileceği üzere bu sorunun cevabı Create metodunun çağırıldığı yerdir ve GameController sınıfındaki Create metodu bunun için uygundur. </p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">[HttpPost]
public IActionResult Create(Game game)
{
_gameRepository.Create(game, new RabbitPublisher());
return RedirectToAction("Index");
}</pre>
<p>Dikkat edileceği üzere ikinci parametrede bir RabbitPublisher nesne örneği kullanılıyor. Yani Create metodunun ihtiyaç duyduğu asıl nesneyi metot üzerinden göndermiş oluyoruz. </p>
<h2>Property Injection</h2>
<p>Yukarıdaki senaryoyu düşündüğümüzde aklımıza şöyle bir soru da gelebilir; ortada bir IPublisher referansı yoksa Send metodunun <span style="text-decoration: underline;">çalışmamasını</span> nasıl sağlarız? Yani Create metodunun IPublisher ile çalışmasını opsiyonel olarak sunmak istersek nasıl bir yol izleriz? Bu senaryo için IGameRepository arayüzünü aşağıdaki gibi değiştirelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public interface IGameRepository
{
IEnumerable<Game> GetAllGames();
IPublisher Publisher { get; set; }
Game Create(Game game);
}</pre>
<p>Dikkat edileceği üzere Create metodunun parametresi olarak kullandığımız IPublisher arayüzünü, özellik olarak tip seviyesine aldık. Çok doğal olarak GameRepository sınıfının içeriği de buna uygun şekilde değiştirilmeli.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">public class GameRepository
: IGameRepository
{
public IPublisher Publisher { get; set; }
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 }
};
}
public Game Create(Game game)
{
if (Publisher != default)
Publisher.Send("A new game has been added to inventory");
return game;
}
}</pre>
<p>Lütfen Create metodunun içerisindeki if kullanımına dikkat edelim. Eğer IPublisher türünden olan Publisher isimli özellik<em>(property)</em> gerçekten bir nesne referansı taşıyorsa Send metodu çağırılacaktır. Böylece Create metodunun bağımlılığını property seviyesine çekerek tercihe bırakmış olduk. Tabii bağımlı nesnenin kayıt işlemini de yapacağımız bir yer olmalı öyle değil mi? Yine GameController sınıfındaki Create metodunda bunu yapabiliriz.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">[HttpPost]
public IActionResult Create(Game game)
{
_gameRepository.Publisher = new RabbitPublisher();
_gameRepository.Create(game);
return RedirectToAction("Index");
}</pre>
<p>Görüldüğü üzere _gameRepository nesnesinin<em>(ki o da GameController sınıfına Constructor üzerinden enjekte edilmektedir)</em> Publisher özelliğine yeni bir RabbitPublisher referansı atadık. Dolayısıyla Create metodu çağrıldığında RabbitMQ'ya mesaj gönderen asıl fonksiyon işleyecektir. Lakin Publisher özelliğine bir atama yapılmazsa herhangi bir gönderim işlemi de olmayacaktır. Seçime bağlı bu nesne çözümlemesi için Property Injection tekniğini nasıl kullanacağımızı da görmüş olduk.</p>
<h2>View Injection</h2>
<p>Aslında Asp.Net tarafına MVC 6 ile birlikte gelen ve genel Dependency Injection teknikleri içerisinde olmadığını düşündüğüm bir yöntem daha var. @inject direktifinin kullanımı. MVC/MVVM desenlerinde bir View'u, Controller'dan veya View-Model'den ayrıştırmak istediğimiz durumlarda kullanabileceğimiz bir yöntem olarak karşımıza çıkıyor. Yöntem sayesinde DI Container üstünden kayıt edilen bir nesne veya servis metodunun View üstünden doğrudan çağrılması sağlanabiliyor. Senaryomuzda portaldeki hareketliliklerle ilgili veri toplayan ve örneğin aktif kullanıcıların sayısını veren aşağıdaki gibi bir sınıf olduğunu düşünelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public class DataCollectorService
{
public async Task<int> GetActiveUserCount()
{
return await Task.FromResult(new Random().Next(10,50));
}
}</pre>
<p>DataCollectorService içerisindeki GetActiveUserCount metodunun ne iş yaptığının çok önemi yok ama bu metodu bir View bileşeninde aşağıdaki gibi doğrudan kullanmamız mümkün.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@inject C64Portal.Agent.DataCollectorService dataCollectorService
<div>
<h2>Envanterdeki C64 Oyunları</h2>
<hr />
<h3>Current active user count is @await dataCollectorService.GetActiveUserCount() </h3>
</div></pre>
<p>Tabii örneği bu haliyle çalıştırıp Inventory sayfasına gitmek istersek kaçınılmaz olarak aşağıdaki hata mesajı ile karşılaşırız.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_5.png" alt="" /></p>
<p>View nesnesi bir nesne çözümlemek istemektedir ancak bu nesne dahili DI Container'ın servis koleksiyonunda yer almamaktadır. Dolayısıyla Startup sınıfındaki ConfigureServices metodunda DataCollectorService isimli servis için bir kayıt işlemi yapılmalıdır.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddTransient<IGameRepository, GameRepository>();
services.AddTransient<DataCollectorService>();
}</pre>
<p>Sonrasında uygulamanın sorunsuz çalıştığı gözlemlenebilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2021/Nisan/hellomvc_6.png" alt="" /></p>
<p>Bu ve önceki yazıyla birlikte Asp.Net 5'in temel Dependency Injection uygulama tekniklerini görmüş olduk. Tabii Dependency Injection konusu bunlarla bitmiyor. ConfigureServices metodunda servisleri kayıt altına alırken hep AddTransient metodunu kullandığımızı fark etmiş olmalısınız. Oysa ki AddScope ve AddSingleton metotları da var. Yani kayıt altına alınan bir DI servisinin hangi anda örnekleneceğini ve yaşam ömrünün ne olacağını da belirleyebiliyoruz. Bu konu ile ilgili fırsatım olursa bir şeyler karalamaya çalışacağım. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize sağlıklı günler dilerim.</p>
2021-04-29T11:03:00+00:00
dependency injection
asp.net core
asp.net core mvc
.net 5
method injection
property injection
view injection
di container
bsenyurt
Hatırlayacağınız üzere bir önceki yazımızda Asp.Net 5 tarafında nasıl Hello World diyebileceğimizi incelemeye çalışmıştık(Henüz okumadıysanız bir göz atmanızda yarar var) Bu deneyimde ana odak noktamız dahili Dependency Injection mekanizmasının nasıl kullanıldığını görmekti. Kobay senaryomuzdaki en önemli noktalardan birisi de GameController sınıfı içerisinde IGameRepository yardımıyla low-level bir bileşenin kullanılmasıydı. Burada Constructor Injection tekniğinden yararlandığımızı ifade etmiştik. Bu teknik dışında kullanabileceğimiz versiyonlar da var. Metot üzerinden, bir Property yardımıyla ve Asp.Net MVC 6 ile gelen @inject direktifi yoluyla, nesne bağımlılıklarını çözümleyebiliriz. İşte bu yazımızdaki amacımız aynı senaryoyu devam ettirerek söz konusu tekniklerin nasıl uygulanabileceğini öğrenmektir. Hazırsanız başlayalım.
https://www.buraksenyurt.com/pingback.axd
https://www.buraksenyurt.com/post.aspx?id=44c8c57b-338c-4fe0-bf0f-5916517bd022
1
https://www.buraksenyurt.com/trackback.axd?id=44c8c57b-338c-4fe0-bf0f-5916517bd022
https://www.buraksenyurt.com/post/asp-net-core-dependency-injection-turleri#comment
https://www.buraksenyurt.com/syndication.axd?post=44c8c57b-338c-4fe0-bf0f-5916517bd022
https://www.buraksenyurt.com/post/bir-web-uygulamasinda-gantt-chart-kullanimi
Bir Web Uygulamasında Gantt Chart Kullanımı
2019-07-26T13:00:00+00:00
bsenyurt
<p><img style="float: right;" src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/windofchange2.png" alt="" />Beğenerek dinlediğim Scorpions grubunun en güzel şarkılarından birisidir Wind of Change. Değişim rüzgarları uzun zamandır hayatımın bir parçası aslında. Sanıyorum ilk olarak 2012 yılında o zamanlar çalışmakta olduğum turuncu bankada başlamıştı esintiler. Çevik dönüşüm süreci kapsamında uzun zamandır var olan şelale modelinin ağır ve hantal işleyişi yerine daha hızlı reaksiyon verme kabiliyeti kazanmak içindi her şey. Benzer bir dönüşüm süreci geçtiğimiz sene içerisinde şu an çalışmakta olduğum mavi renkli teknoloji şirketinde de başlatıldı.</p>
<p>Her iki şirketin bu dönüşüm sürecindeki en büyük problemi ise oturmuş kültürel işleyiş yapısının değişime karşı geliyor olmasıydı. Hal böyle olunca her iki firmada dışarıdan yetkin danışmanlıklar alarak dönüşümü daha az sancılı geçirmeye çalıştı. Genelde ölçek olarak büyük bir firmaysanız bu tip dijital dönüşümler hem uzun hem de sancılı olabiliyor. </p>
<p>Lakin bu acıyı azaltmak için yapılanlar bazen bana çok garip gelir. Servisten iner girişe doğru ilerlersiniz. Girdiğiniz andan itibaren masanıza varıncaya dek o dijital dönüşümün bilinçaltınıza yollanan mesajlarını görürsünüz. Koridorun duvarında, bindiğiniz asansörün aynasında, tuvaletin kapısında, tavandan sarkan kartonlarda, takım panolarında, bir önceki gün dağıtılan mouse pad'lerin üzerinde, bardağınızda, bilgisayarınızın duvar kağıdında...Tüm eşyalar çoktan dijitalleşmiş ve çevikleşmiştir esasında ama önemli olan bireyin değişimidir. Kurumsal kimliğin en temel yapı taşı olan çalışanların her birinin dönüşüme ayak uydurması gerekir. Farkındalığı olan takımların bu tip dönüşümleri daha çabuk kabullendiği ve kolayca adapte olduğu gözden kaçırılmamalıdır. Olay renkli temalarla binaları giydirmekten, ilkeleri oyunlaştırarak anlatmaktan çok daha ötedir. Bu esas itibariyle bir felsefe kabülü, ciddi bir dönüşümdür. </p>
<p>Diğer yandan dijital dönüşüm başlar başlamaz bunu sorgulamadan kabul etmek de çok doğru değildir. Değişime direnç göstermek değil ama neden öyle olması gerektiğini sorgulamaktan bahsediyorum. Sorgusuz sualsiz kabullerin sonucu çoğunlukla çevik süreçlerin mükemmel olduğu görüşü ifade edilir ve fakat pekala buna ihtiyaç duyuluncaya kadar şelale modeli ile de başarılar elde edilmiştir. Değişen dünya artık o model tarafından yönetilememekte ve müşteri ihtiyaçları atik bir şekilde giderilememektedir. En basit terk ediş sebebi belki de bu şekilde özetlenebilir.</p>
<p>Pekala ben neden bu kadar felsefik konuşmaya çalışıyorum? Çeviklikten yanayım ama şelale modeli ile de yıllarca çalışmış birisiyim ve o <a href="https://github.com/buraksenyurt/saturday-night-works" target="_blank">saturday-night-works seansı</a>nda daha çok şelale modelinde karşımıza çıkan bir tablo ile haşır neşirdim. Konu Gantt şemasıydı. Başlayalım mı?</p>
<p>Henry Gantt tarafından icat edilen <a href="https://www.gantt.com/" target="_blank">Gantt tabloları</a> proje takvimlerinin şekilsel gösteriminde kullanılmaktadır. Temel olarak yatay çubuklardan oluşan bu tablolarda proje planlarını, atanmış görevleri, tahmini sürelerini ve genel olarak gidişatı görmek mümkündür. Excel üzerinde bile kullanılabilen Gantt Chart'lar sanıyorum proje yöneticilerinin de vazgeçilmez araçlarındandır. Benim <a href="https://github.com/buraksenyurt/saturday-night-works/tree/master/No%2023%20-%20Gantt%20Chart%20on%20AspNet%20Core" target="_blank">23 numaralı saturday-night-works çalışması</a>ndaki amacım ise dhtmlxGantt isimli Javascript kütüphanesinden yararlanarak bir Asp.Net Core projesinde Gantt Chart kullanabilmekti. </p>
<p>Kısaca kurgudan da bahsedeyim. Görevlere ait bilgiler SQLite veri tabanıyla beslenecek. Önyüz bu veriyi kullanırken REST tipinden servis çağrıları gerçekleştirilecek. Malum veri sunucu tarafında, Gant Chart ise kullanıcı etkileşimiyle birlikte HTML sayfasında. Yani dhtmlxGantt kütüphanesi listeleme, ekleme, silme ve güncelleme gibi operasyonlar için Web API tarafına Post, Put, Delete ve Update çağrıları gönderecek. Sunucu tarafında daha çok servis odaklı bir uygulama olacağını ifade edebiliriz. Kütüphanenin kullandığı veri modelini C# tarafında konumlandırabilmek için DTO<em>(Data Transform Object)</em> nesnelerinden yararlanırken, sunucu tarafı operasyonlarında Model ve Controller katmanlarına başvuracağız. Heyecanlandınız, motive oldunuz, hazırsınız değil mi? :) Öyleyse notların derlenmesine başlayalım.</p>
<p>Bu arada örneği her zaman olduğu gibi WestWorld <em>(Ubuntu 18.04 64bit)</em> üzerinde geliştirmişim. İlk olarak boş bir web uygulaması oluşturalım. Ardından wwwroot klasörü ve içerisine index.html dosyasını ekleyerek devam edelim.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet new web -o ProjectManagerOZ</pre>
<blockquote>
<p>Örnekteki gantt chart çizimi için kullanılan <a href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css" target="_blank">CSS dosyasına şu adresten</a>, <a href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js" target="_blank">Javascript dosyasına da bu adresten</a> ulaşabilirsiniz. Bu kaynakları offline çalışmak isterseniz bilgisayara indirdikten sonra wwwroot altındaki alt klasörlerde<em>(css, js gibi)</em> konuşlandırabilirsiniz.</p>
</blockquote>
<p><strong>Index.html</strong></p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false"><!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Project - 19</title>
<link href="css/dhtmlxgantt.css"
rel="stylesheet" type="text/css" />
<script src="js/dhtmlxgantt.js"></script>
<script>
// index.html dokükamanı yüklendiğinde ilgili fonksiyon devreye girerek
// proje veri içeriğini ekrana basacak
document.addEventListener("DOMContentLoaded", function(event) {
// standart zaman formatını belirtiyoruz
gantt.config.xml_date = "%Y-%m-%d %H:%i";
gantt.init("project_map");
// veri yükleme işinin üstlenildiği kısım
// tahmin edileceği üzere /api/backlog şeklinde bir REST API çağrısı olacak
// bu kod tarafındaki Controller ile karşılanacak
gantt.load("/api/backlog");
// veri işleyicisi (bir web api servis adresi gibi düşünülebilir)
var dp = new gantt.dataProcessor("/api/");
dp.init(gantt);
// REST tipinden iletişim sağlanacak
dp.setTransactionMode("REST");
});
</script>
</head>
<body>
<h2>Apollo 19 Project Plan</h2>
<div id="project_map" style="width: 100%; height: 100vh;"></div>
</body>
</html></pre>
<p>Uygulamamız grafik verilerini göstermek için SQLite veri tabanını kullanıyor. Bu enstrümanı Entity Framework kapsamında ele alabilmek için projeye Microsoft.EntityFrameworkCore.SQLite paketini eklemeliyiz. Bunun için terminalden aşağıdaki komutu çalıştırabiliriz. </p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet add package Microsoft.EntityFrameworkCore.SQLite</pre>
<p>Sonrasında appsettings.json içeriğine bir bağlantı cümlesi ilave edebiliriz. Bunu ilerleyen kısımlarda startup dosyasında kullanacağız. Apollo.db fiziki veri tabanı dosyamızın adı ve root altındaki db klasörü içerisinde yer alacak.</p>
<pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false">{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"ApolloDataContext": "Data Source=db/Apollo.db"
},
"AllowedHosts": "*"
}</pre>
<p>Pek tabii modellerimizi, API servis tarafı haberleşmesi için controller tiplerimizi ve hatta gantt chart kütüphanesindeki tiplerle entity modelleri arasındaki dönüşümleri kolaylaştıracak DTO nesnelerimizi geliştirmemiz gerekiyor. Uzun bir maraton olabilir. İlk olarak model sınıflarını yazarak başlayalım. Bir Models klasörü oluşturup altına Context ile model sınıflarımızı<em>(ki gantt chart kütüphanesine göre Link ve Task isimli sınıflarımız olmalı)</em> ekleyerek çalışmamıza devam edebiliriz.</p>
<p><strong>Link sınıfı</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
/*
Task'lar arasındaki ilişkinin tutulduğu Entity sınıfımız
Eğer iki Task birbiri ile bağlıysa bu sınıfa ait nesne örnekleri üzerinden ilişkilendirebiliriz.
*/
namespace ProjectManagerOZ.Models
{
public class Link
{
public int Id { get; set; }
public string Type { get; set; }
public int SourceTaskId { get; set; }
public int TargetTaskId { get; set; }
}
}</pre>
<p><strong>Task sınıfı</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
/*
proje görevlerinin verisinin tutulduğu Entity sınıfımız
Tipik olarak görevle ilgili bilgiler yer alır.
Açıklaması, süresi, hangi durumda olduğu, bağlı olduğu başka bir task varsa O, başlangıç tarihi, tipi vs
*/
namespace ProjectManagerOZ.Models
{
public class Task
{
public int Id { get; set; }
public string Text { get; set; }
public DateTime StartDate { get; set; }
public int Duration { get; set; }
public decimal Progress { get; set; }
public int? ParentId { get; set; }
public string Type { get; set; }
}
}</pre>
<p>ve <strong>ApolloDataContext</strong> sınıfı ki bu alışkın olduğumuz tipik DataContext tipimiz. Görüldüğü üzere içerisinde görevleri ve aralarındaki ilişkileri temsil eden veri setleri sunmakta.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.EntityFrameworkCore;
namespace ProjectManagerOZ.Models
{
// Entity Framework DB Context sınıfımız
public class ApolloDataContext
: DbContext
{
public ApolloDataContext(DbContextOptions<ApolloDataContext> options)
: base(options)
{
}
// Proje görevleri ile bunlar arasındaki olası ilişkileri temsil eden özelliklere sahip
public DbSet<Task> Tasks { get; set; }
public DbSet<Link> Links { get; set; }
}
}</pre>
<h2>Küçük Bir Middleware Ayarı</h2>
<p>Uygulamamız ayağa kalktığında veri tabanının boş olma ihtimaline karşın onu doldurmak isteyebiliriz. Bunun için DataFiller isimli sınıfımız ve içerisinde Prepare isimli static bir metoduz var. Ancak söz konusu metodu host çalışma zamanında ayağa kalkarken çağırmak istiyoruz. Bunun için Program sınıfında kullanılan IWebHostBuilder üzerinden işletilebilecek bir operasyon tesis etmek lazım. Bunun için IWebHost türevlerine uyarlanabilecek bir genişletme metodu<em>(extension method)</em> işimizi görecektir. Bu metod çalışma zamanında entity servisinin yakalanması ve Prepare operasyonun enjekte edilmesi açısından dikkate değerdir.</p>
<p><strong>DataFiller sınıfı</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.Linq;
using ProjectManagerOZ.Models;
namespace ProjectManagerOZ.Initializers
{
/*
Bu sınıfın amacı başlangıçta boş olan veritabanı tablolarına
örnekte kullanabilmemiz için ilk verileri eklemek.
Bu amaçla örnek task ve link'ler oluşturuluyor.
Mesela Epic bir Work Item ve ona bağlı User Story'ler gibi
*/
public static class DataFiller
{
public static void Prepare(ApolloDataContext context)
{
if (context.Tasks.Any()) //Eğer veritabanında en az bir Task varsa zaten veri içeriyor demektir. Bu durumda initalize işlemine gerek yok.
return;
// Parent task'ı oluşturuyoruz (ParentId=null)
var epic = new Task
{
Text = "JWT Implementation for Category WebAPI",
StartDate = DateTime.Today.AddDays(1),
Duration = 5,
Progress = 0.4m,
ParentId = null,
Type = "Epic"
};
context.Tasks.Add(epic); //Task örneğini context'e ekleyip
context.SaveChanges(); //tabloyada yazıyoruz
var story1 = new Task
{
Text = "I want to develop tokenizer service",
StartDate = DateTime.Today.AddDays(1),
Duration = 4,
Progress = 0.5m,
ParentId = epic.Id, //story'yi epic senaryoya ParentId üzerinden bağlıyoruz. Aynı bağlantı Story2 içinde gerçekleştiriliyor
Type = "User Story"
};
context.Tasks.Add(story1);
context.SaveChanges();
var story2 = new Task
{
Text = "I have to implement tokinizer service",
StartDate = DateTime.Today.AddDays(3),
Duration = 5,
Progress = 0.8m,
ParentId = epic.Id,
Type = "User Story"
};
context.Tasks.Add(story2);
context.SaveChanges();
var epic2 = new Task
{
Text = "Create ELK stack",
StartDate = DateTime.Today.AddDays(3),
Duration = 3,
Progress = 0.2m,
ParentId = null,
Type = "Epic"
};
context.Tasks.Add(epic2);
context.SaveChanges();
var story3 = new Task
{
Text = "We have to setup Elasticsearch",
StartDate = DateTime.Today.AddDays(6),
Duration = 6,
Progress = 0.0m,
ParentId = epic2.Id,
Type = "User Story"
};
context.Tasks.Add(story3);
context.SaveChanges();
var story4 = new Task
{
Text = "We have to implement Logstash to Microservices",
StartDate = DateTime.Today.AddDays(6),
Duration = 2,
Progress = 0.3m,
ParentId = epic2.Id,
Type = "User Story"
};
context.Tasks.Add(story4);
context.SaveChanges();
var story5 = new Task
{
Text = "We have to setup Kibana for Elasticsearch",
StartDate = DateTime.Today.AddDays(6),
Duration = 2,
Progress = 0.0m,
ParentId = epic2.Id,
Type = "User Story"
};
context.Tasks.Add(story5);
context.SaveChanges();
// Oluşturduğumuz proje görevleri arasındaki ilişkileri oluşturuyoruz
List<Link> taskLinks = new List<Link>{
new Link{SourceTaskId=epic.Id,TargetTaskId=story1.Id,Type="1"},
new Link{SourceTaskId=epic.Id,TargetTaskId=story2.Id,Type="1"},
new Link{SourceTaskId=epic2.Id,TargetTaskId=story3.Id,Type="1"},
new Link{SourceTaskId=story3.Id,TargetTaskId=story4.Id,Type="1"},
new Link{SourceTaskId=story4.Id,TargetTaskId=story5.Id,Type="1"},
new Link{SourceTaskId=epic.Id,TargetTaskId=epic2.Id,Type="2"}
};
taskLinks.ForEach(l => context.Links.Add(l));
context.SaveChanges();
}
}
}</pre>
<p><strong>DataFillerExtension</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ProjectManagerOZ.Models;
/*
DataFillerExtension sınıfı InitializeDb isimli bir extension method içeriyor.
Bu metodu IWebHost türevli nesne örneklerine uygulayabiliyoruz.
Amaç çalışma zamanında host ortamı inşa edilirken Middleware katmanında araya girip
veritabanı üzerinde Prepare operasyonunu icra ettirmek.
Bu genişletme fonksiyonunu Program.cs içerisinde kullanmaktayız.
*/
namespace ProjectManagerOZ.Initializers
{
public static class DataFillerExtensions
{
public static IWebHost InitializeDb(this IWebHost webHost)
{
// çalışma zamanını servislerinin üreticisini örnekle
var serviceFactory = (IServiceScopeFactory)webHost.Services.GetService(typeof(IServiceScopeFactory));
// Bir Scope üret
using (var currentScope = serviceFactory.CreateScope())
{
// Güncel ortamdan servis sağlayıcısını çek
var serviceProvider = currentScope.ServiceProvider;
// Servis sağlaycısından sisteme enjekte edilmiş entity context'ini iste
var dbContext = serviceProvider.GetRequiredService<ApolloDataContext>();
// context'i kullanarak veritabanını dolduran fonksiyonu çağır
DataFiller.Prepare(dbContext);
}
// IWebHost örneğini yeni bezenmiş haliyle geri döndür
return webHost;
}
}
}</pre>
<p>Bu yapılanmayı kullanbilmek için program sınıfını şöyle değiştirelim.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ProjectManagerOZ.Initializers;
namespace ProjectManagerOZ
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args)
.Build()
.InitializeDb() // IWebHost için yazdığımız genişletme metodu.
.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseUrls("http://localhost:5402");
}
}</pre>
<p>Dikkat edileceği üzere InitiateDb isimli metod CreateWebHosBuilder dönüşünden kullanılabiliyor.</p>
<p>Çalışmamıza startup sınıfına geçerek devam edelim. Burada Entity Framework servisinin çalışma zamanına enjekte edilmesi, statik web sayfası hizmetinin açılması, Web API tarafı için MVC özelliğinin etkinleştirilmesi, SQLite veri tabanı için gerekli bağlantı bilgisinin konfigurasyon dosyasından alınması gibi işlemlere yer veriyoruz.</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.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore; //EF Core kullanacağımız için eklendi
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ProjectManagerOZ.Models;
namespace ProjectManagerOZ
{
public class Startup
{
/*
Configuration özelliği ve Startup'ın overload edilmiş Constructor metodu varsayılan olarak gelmiyor.
ApolloDbContext için gerekli connection string bilgisine ulaşacağımız Configuration nesnesine
erişebilmek amacıyla eklendiler
*/
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// appsettings'den SQLite için gerekli connection string bilgisini aldık
var conStr = Configuration.GetConnectionString("ApolloDataContext");
// ardından SQLite için gerekli DB Context'i servislere ekledik
// Artık modellerimiz SQLite veritabanı ile çalışacak
// Bu işlemler runtime'de gerçekleşecek
services.AddDbContext<ApolloDataContext>(options => options.UseSqlite(conStr));
services.AddMvc(); // Web API Controller'ının çalışabilmesi için ekledik
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// wwwroot altındaki index.html benzeri sayfaları kullanabileceğimizi belirttik
app.UseDefaultFiles();
// ayrıca wwwroot altındaki css, image gibi asset'lerinde kullanılacağı ifade edildi
app.UseStaticFiles();
app.UseMvc(); // Web API Controller'ının çalışabilmesi için ekledik
}
}
}</pre>
<p>Veri modeli, verinin başlangıçta oluşturulması için gerekli adımlar ile çalışma zamanına ait bir takım kodlamaları halletmiş durumdayız. Veri tabanı tarafı ile konuşurken işimizi kolaylaştıracak DTO<em>(Data Transform Object)</em> nesneleri ile bu işin kontrolcülerini kodlayarak ilerleyelim. dto isimli bir klasör oluşturup içerisine aşağıdaki kod parçalarına sahip TaskDTO ve LinkDTO sınıflarını ekleyelim.</p>
<p><strong>TaskDTO</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Text.Encodings.Web;
using ProjectManagerOZ.Models;
/*
DTO sınıfımız WebAPI tarafında, Gantt kütüphanesi ile olan haberleşmedeki mesajlaşmalarda kullanılan modeli tanımlıyor.
Arka plandaki Task nesnemizden ziyade Gantt kütüphanesinin istediği alan adlarına sahip. Sözgelimi Task tipinde StartDate varken
burada start_date kullanılmakta.
Peki tabii API Controller metodlarındaki Task ve TaskDTO arasındaki dönüşümleri kolayaştırmak adına bilinçli olarak operatörlerin
aşırı yüklendiğini görüyoruz.
*/
namespace ProjectManagerOZ.DTO
{
public class TaskDTO
{
public int id { get; set; }
public string text { get; set; }
public string start_date { get; set; }
public int duration { get; set; }
public decimal progress { get; set; }
public int? parent { get; set; }
public string type { get; set; }
public string target { get; set; }
public bool open
{
get { return true; }
set { }
}
public static explicit operator TaskDTO(Task task)
{
return new TaskDTO
{
id = task.Id,
text = HtmlEncoder.Default.Encode(task.Text),
start_date = task.StartDate.ToString("yyyy-MM-dd HH:mm"),
duration = task.Duration,
parent = task.ParentId,
type = task.Type,
progress = task.Progress
};
}
public static explicit operator Task(TaskDTO task)
{
return new Task
{
Id = task.id,
Text = task.text,
StartDate = DateTime.Parse(task.start_date, System.Globalization.CultureInfo.InvariantCulture),
Duration = task.duration,
ParentId = task.parent,
Type = task.type,
Progress = task.progress
};
}
}
}</pre>
<p><strong>LinkDTO</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Text.Encodings.Web;
using ProjectManagerOZ.Models;
/*
DTO sınıfımız WebAPI tarafında, Gantt kütüphanesi ile olan haberleşmedeki mesajlaşmalarda kullanılan modeli tanımlıyor.
Arka plandaki Link nesnemizden ziyade Gantt kütüphanesinin istediği alan adlarına sahip. Peki tabii API Controller metodlarındaki
Link ve LinkDTO arasındaki dönüşümleri kolayaştırmak adına bilinçli olarak operatörlerin aşırı yüklendiğini görüyoruz.
*/
namespace ProjectManagerOZ.DTO
{
public class LinkDTO
{
public int id { get; set; }
public string type { get; set; }
public int source { get; set; }
public int target { get; set; }
public static explicit operator LinkDTO(Link link)
{
return new LinkDTO
{
id = link.Id,
type = link.Type,
source = link.SourceTaskId,
target = link.TargetTaskId
};
}
public static explicit operator Link(LinkDTO link)
{
return new Link
{
Id = link.id,
Type = link.type,
SourceTaskId = link.source,
TargetTaskId = link.target
};
}
}
}</pre>
<p>WebAPI tarafının web sayfası üzerinden gelecek HTTP çağrılarına cevap vereceği yerler Controller sınıfları. Kullanılan gantt chart kütüphanesinin işleyiş şekli gereği Link ve Task tipleri için ayrı ayrı controller sınıflarının yazılması gerekiyor.</p>
<p><strong>LinkController</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ProjectManagerOZ.Models;
using ProjectManagerOZ.DTO;
using Microsoft.EntityFrameworkCore;
/*
Link nesneleri ile ilgili CRUD operasyonlarını üstlenen Web API Controller sınıfımız
*/
namespace ProjectManagerOZ.Controllers
{
[Produces("application/json")] // JSON formatında çıktı üreteceğimizi belirtiyoruz
[Route("api/link")] // Gantt Chart kütüphanesinin beklediği Link API adresi
public class LinkController
: Controller
{
// Controller içerisine pek tabii ApolloDataContext'imizi geçiyoruz.
private readonly ApolloDataContext _context;
public LinkController(ApolloDataContext context)
{
_context = context;
}
// Yeni bir Link eklerken devreye giren HTTP Post metodumuz
[HttpPost]
public IActionResult Create(LinkDTO payload)
{
var l = (Link)payload;
_context.Links.Add(l);
_context.SaveChanges();
/*
Task örneğinde olduğu gibi istemci tarafına oluşturulan Link
örneğine ait Id değerini göndermemiz lazım ki, takip eden Link bağlama,
güncelleme veya silme gibi işlemler çalışabilsin.
tid, istemci tarafının beklediği değişken adıdır.
*/
return Ok(new
{
tid = l.Id,
action = "inserted"
});
}
/*
Bir Link'i güncellemek istediğimizde devreye giren metodumuz
*/
[HttpPut("{id}")]
public IActionResult Update(int id, LinkDTO payload)
{
// Gelen payload içeriğini backend tarafındaki model sınıfına dönüştür
var l = (Link)payload;
// id eşlemesi yap
l.Id = id;
// durumu güncellendiye çek
_context.Entry(l).State = EntityState.Modified;
// ve değişiklikleri kaydedip
_context.SaveChanges();
// HTTP 200 döndür
return Ok();
}
/*
HTTP Delete operasyonuna karşılık gelen ve
parametre olarak gelen id değerine göre silme işlemini icra eden metodumuz
*/
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
// Link örneğini bul
var l = _context.Links.Find(id);
if (l != null)
{
// Entity Context'inden ve
_context.Links.Remove(l);
// Kalıcı olarak veritabanından sil
_context.SaveChanges();
}
return Ok();
}
// Tüm Link örneklerini döndüren HTTP Get metodumuz
[HttpGet]
public IEnumerable<LinkDTO> Get()
{
return _context.Links
.ToList()
.Select(t => (LinkDTO)t);
}
// Belli bir Id değerine göre ilgili Link nesnesinin DTO karşılığını döndüren HTTP Get metodumuz
[HttpGet("{id}")]
public LinkDTO GetById(int id)
{
return (LinkDTO)_context
.Links
.Find(id);
}
}
}</pre>
<p><strong>TaskController</strong></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ProjectManagerOZ.Models;
using ProjectManagerOZ.DTO;
namespace ProjectManagerOZ.Controllers
{
[Produces("application/json")]
[Route("api/task")] // Gantt Chart kütüphanesinin beklediği Task API adresi
public class TaskController
: Controller
{
// Controller içerisine pek tabii ApolloDataContext'imizi geçiyoruz.
private readonly ApolloDataContext _context;
public TaskController(ApolloDataContext context)
{
_context = context;
}
// HTTP Post metodumuz
// Yeni bir Task eklemek için kullanılıyor
[HttpPost]
public IActionResult Create(TaskDTO task)
{
// Mesaj parametresi olarak gelen TaskDTO içeriğini Task tipine dönüştürdük
var payload = (Task)task;
// Task'ı Context'e ekle
_context.Tasks.Add(payload);
// Kalıcı olarak kaydet
_context.SaveChanges();
/*HTTP 200 Ok dönüyoruz
Dönerken de oluşan Task Id değerini de yolluyoruz
Bu Child task'ları bağlarken veya bir Task'ı silerken
gerekli olan bir bilgi nitekim. Aksi halde istemci
tarafındaki Gantt kütüphanesi kiminle işlem yapması gerektiğini bilemiyor.
İnanmıyorsanız sadece HTTP 200 döndürüp durumu inceleyin :)
*/
return Ok(new
{
tid = payload.Id,
action = "inserted"
});
}
// HTTP Put ile çalıştırılan güncelleme metodumuz
// Parametrede Task'ın id bilgisi gelecektir
[HttpPut("{id}")]
public IActionResult Update(int id, TaskDTO task)
{
// Mesaj ile gelen TaskDTO örneğini dönüştürüp id değerini verdik
var payload = (Task)task;
payload.Id = id;
// id'den ilgili Task örneğini bulduk
var t = _context.Tasks.Find(id);
// alan güncellemelerini yaptık
t.Text = payload.Text;
t.StartDate = payload.StartDate;
t.Duration = payload.Duration;
t.ParentId = payload.ParentId;
t.Progress = payload.Progress;
t.Type = payload.Type;
// değişiklikleri veritabanına kaydettik
_context.SaveChanges();
// HTTP 200 Ok dönüyoruz
return Ok();
}
// HTTP Delete yani silme işlemi için çalışacak metodumuz
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
// Task'ı bulalım ve eğer varsa
var task = _context.Tasks.Find(id);
if (task != null)
{
// önce Context'ten
_context.Tasks.Remove(task);
// sonra veritabanından silelim
_context.SaveChanges();
}
// HTTP 200 Ok dönüyoruz
return Ok();
}
// HTTP Get karşılığı çalışan metodumuz
// Tüm Task'ları geri döndürür
[HttpGet]
public IEnumerable<TaskDTO> Get()
{
return _context.Tasks
.ToList()
.Select(t => (TaskDTO)t);
}
// HTTP Get ile ID bazlı çalışan metodumuz
// Belli bir ID'ye ait Task bilgisini verir
[HttpGet("{id}")]
public TaskDTO GetById(int id)
{
return (TaskDTO)_context
.Tasks
.Find(id);
}
}
}</pre>
<p>Web sayfasına HTTP Get ile çekilen görev listesi ve ilişkilerin gant chart'ın istediği tiplere dönüştürülmesi gerekiyor. İşte DTO dönüşümlerinin devreye girdiği yer. Bunun için<strong> MainController </strong>isimli tipi kullanmaktayız.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ProjectManagerOZ.Models;
using ProjectManagerOZ.DTO;
namespace ProjectManagerOZ.Controllers
{
[Produces("application/json")]
[Route("api/backlog")] // Bu adres bilgisi index.html içerisinde de geçiyor. Bulun ;)
public class MainController : Controller
{
// Controller içerisine pek tabii ApolloDataContext'imizi geçiyoruz.
private readonly ApolloDataContext _context;
public MainController(ApolloDataContext context)
{
_context = context;
}
// HTTP Get ile verinin çekildiği metodumuz. Talebi index.html sayfasından yapıyoruz
[HttpGet]
public object Get()
{
// Task ve Link veri setlerini TaskDTO ve LinkDTO tipinden nesnelere dönüştürdüğümüz dikkatinizden kaçmamıştır.
// Bunun sebebi Gantt'ın beklediği veri tipini sunan DTO sınıfı ile backend tarafında kullandığımız sınıfların farklı olmasıdır.
// Dönüş olarak kullandığımız nesne data ve links isimli iki özellik tutuyor.
// data özelliğinde Task bilgilerini
// links özelliğinde de tasklar arasındaki bağlantı bilgilerini dönüyoruz
// bu format özelleştirilmediği sürece Gantt Chart'ın beklediği tiptedir
return new
{
data = _context.Tasks
.OrderBy(t => t.Id)
.ToList()
.Select(t => (TaskDTO)t),
links = _context.Links
.ToList()
.Select(l => (LinkDTO)l)
};
}
}
}</pre>
<h2>SQLite Ayarlamaları</h2>
<p>Kodlarımızı tamamladık lakin testlere başlamadan önce SQLite veri tabanının oluşturulması gerekiyor. Tipik bir migration süreci çalıştıracağız. Bunun için terminalden aşağıdaki komutları kullanabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet ef migrations add InitialCreate
dotnet ef database update</pre>
<p>İlk satır işletildiğinde DataContext türevli sınıf baz alınarak migration planları çıkartılır. Planlar hazırlandıktan sonra ikinci komut ile update işlemi yürütülür ve ilgili tablolar SQLite veri tabanı içerisine ilave edilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/Cover_1.png" alt="" /></p>
<h2>Çalışma Zamanı</h2>
<p>Kod ilk çalıştırıldığında eğer Tasks tablosunda herhangibir kayıt yoksa aşağıdaki gibi bir kaç verinin eklendiği görülecektir. </p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_2.png" alt="" /></p>
<p>Benzer şekilde Links tablosuna gidilirse görevler arası ilişkilerin eklendiği de görülecektir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_3.png" alt="" /></p>
<blockquote>
<p>Visual Studio Code tarafında SQLite veri tabanı ile ilgili işleri görsel olarak yapabilmek için <a href="https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite" target="_blank">şu eklentiyi</a> kullanabilirsiniz.</p>
</blockquote>
<p>Uygulamamızı terminalden</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet run</pre>
<p>komutu ile çalıştırdıktan sonra Index sayfasını talep edersek bizi bir proje yönetim ekranının karşıladığını görebiliriz ;) Bu sayfanın verisi tahmin edeceğiniz üzere MainController tipine gelen HTTP Get çağrısı ile sağlanmaktadır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_4.png" alt="" /></p>
<p>Burada dikkat edilmesi gereken bir nokta var. Gantt Chart için yazılmış olan kütüphane standart olarak Task ve Link tipleri ile çalışırken REST API çağrılarını kullanmaktadır. Yeni bir öğe eklerken POST, bir öğeyi güncellerken PUT ve son olarak silme işlemlerinde DELETE operasyonlarına başvurulur. Eğer örnek senaryomuzda TaskController ve LinkController tiplerinin POST, PUT, DELETE ve GET karşılıklarını yazmassak arabirimdeki değişiklikler sunucu tarafına aktarılamayacak ve aşağıdaki ekran görüntüsündekine benzer hatalar alınacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_5.png" alt="" /></p>
<p>HTTP çağrıları LinkController ve TaskController sınıflarınca ele alındıktan sonra ise grafik üzerindeki CRUD<em>(CreateReadUpdateDelete)</em> operasyonlarının SQLite tarafına da başarılı bir şekilde aktarıldığı görülebilir. Örnekte üçüncü bir ana görev ile alt işi girilmiş, bir takım görevler üzerinde güncellemeler yapılmış ve görevler arası bağlantılar kurgulanmıştır. WestWorld çalışma zamanına yansıyan örnek ekran görüntüsü aşağıdaki gibidir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_6.png" alt="" /></p>
<p>Bu oluşumun sonuçları SQLite veritabanına da yansır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_7.png" alt="" /></p>
<p>Tüm CRUD operasyonları aşağıdaki ekran görüntüsüne benzer olacak şekilde HTTP çağrıları üzerinden gerçeklenir. Bunu F12 ile geçeceğiniz bölümdeki Network kısmından izleyebilirsiniz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/07/23/credit_8.png" alt="" /></p>
<p>Çalışma zamanı testlerini de tamamladığımıza göre yavaş yavaş derlememizi noktalayabiliriz.</p>
<h2>Ben Neler Öğrendim?</h2>
<p>Kopyala yapıştır yasağım nedeniyle yazılması uzun süren bir örnekti ama öğrenmek için tatbik etmek en güzel yöntemdir. Üstelik bu şekilde hatalar yaptırıp neyin ne için kullanıldığını ve nasıl olması gerektiğini de anlamış oluruz. Söz gelimi POST metodlarından üretilen task veya link id değerlerini döndürmezseniz bazı şeylerin ters gittiğini görebilirsiniz. Gelelim neler öğrendiğime...</p>
<ul>
<li>Gantt Chart'ları xdhtmlGantt asset'leri ile nasıl kolayca kullanabileceğimi</li>
<li>IWebHost türevli bir tipe extension method yardımıyla yeni bir işlevselliği nasıl kazandırabileceğimi</li>
<li>Bu işlevsellik içerisinde servis sağlayıcısı üzerinde Entity Context'ini nasıl yakalayabileceğimi</li>
<li>Gantt Chart'ın ön yüzde kullandığı task ve link tipleri ile Model sınıfları arasındaki dönüşümlerde DTO tiplerinden yararlanmam gerektiğini</li>
<li>DTO'lar içerisinde dönüştürme<em>(cast)</em> operatörlerinin nasıl aşırı yüklenebileceğini<em>(operator overloading)</em></li>
<li>Gantt Chart kütüphanesinin backend tarafı ile REST tipinden Web API çağırıları yaparak konuştuğunu</li>
<li>Gantt Chart için kullanılan API Controller'larda HTTP Post için tid'nin önemini</li>
</ul>
<p>Bu uzun ve komplike örnekte ele almaya çalıştığımız Gantt Chart kütüphanesini eminim ki kullanmayacaksınız. Malum bir çoğumuz artık VSTS gibi ortamların bize sunduğu Scrum panolarında işlerimizi yürütüyor ve iterasyon bazlı planlamalar yaptığımızdan bu tip Waterfall'a dönük tabloları çok fazla ele almıyoruz. Yine de örneğe uçtan uca yazılan bir uygulama gözüyle bakmanızı tavsiye edebilirim. Böylece geldik bir maceramızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.</p>
2019-07-26T13:00:00+00:00
.net core
asp.net core
gantt chart
chart
javascript
gantt
css
sqlite
entity framework
entity framework core
html
mvc
datacontext
extension methods
dependency injection
dto
data transfer object
rest
project management
waterfall
bsenyurt
Henry Gantt tarafından icat edilen Gantt tabloları, proje takvimlerinin şekilsel gösteriminde kullanılmaktadır. Temel olarak yatay çubuklardan oluşan bu tablolarda proje planlarını, task'ları, süreleri ve ilerleyişi görmek mümkündür. Excel üzerinde bile kullanılabilen Gantt Chart'lar sanıyorum proje yöneticilerinin de vazgeçilmez araçlarındandır. Benim amacım ise dhtmlxGantt isimli Javascript kütüphanesinden yararlanarak bir Asp.Net Core projesinde Gantt Chart kullanmak.
https://www.buraksenyurt.com/pingback.axd
https://www.buraksenyurt.com/post.aspx?id=b968c0ce-215b-470e-a2ee-d2041612084d
1
https://www.buraksenyurt.com/trackback.axd?id=b968c0ce-215b-470e-a2ee-d2041612084d
https://www.buraksenyurt.com/post/bir-web-uygulamasinda-gantt-chart-kullanimi#comment
https://www.buraksenyurt.com/syndication.axd?post=b968c0ce-215b-470e-a2ee-d2041612084d
https://www.buraksenyurt.com/post/razor-dunyasindaki-ilk-adimlarim
Razor Dünyasındaki İlk Adımlarım
2019-05-17T12:53:00+00:00
bsenyurt
<p><img style="float: right;" src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/zekimuren.png" alt="" />Bizim servisin dönüş yolculuğu bir başkadır. Her gün yaklaşık git gel neredeyse seksen kilometrelik yol teperiz<em>(Daha ne kadar teperim bilemiyorum tabii)</em> Dönüş yolculuğumuz trafiğin durumuna göre bazen çok uzun sürer. İşte böyle akşamların çok özel bir anı vardır.</p>
<p>Şekerpınardan yola çıkan yüzler Ümraniye sapağına girmek üzere otobandan ayrıldığımızda gülümser. Sadece evlerimize yaklaştığımız ve günün yorgunluğunu atmak üzere ayakkabılarımızı fırlatacağımız için değil, sevgili İhsan Bey radyosunu açıp Zeki Müren'den Müzeyyen Senar'dan Safiye Ayla'dan Muazzez Ersoy'dan ve daha nice değerli sanatçımızdan oluşan koleksiyonunu dinletmeye başladığı için de tebessüm ederiz.</p>
<p>Şirkete ilk başladığım günlerde servisteki pek çok kişi bana bakıp rapçi olduğumu düşünmüş ve İhsan Bey'in çaldığı şarkıları pek sevemeyeceğime kanaat getirmişti. Aslında lise yıllarında sıkı bir Heavy Metal'ci olan ben büyüdükçe farklı tınıları, farklı kültürlerin tonlamalarını da dinler olmuştum. Müziğin dili, dini, ırkı olmaz diyenlerdenim. Zaman geçtikçe ve özellikle plak merakım da başlayınca Aşık Veysel'den Joe Satriani'ye, Coşkun Sabah'tan Pink Floyd'a, Barış Manço'dan Metallica'ya, Sezen Aksu'dan Mozart'a kadar çok geniş bir müzik keyfine ulaştığımı fark ettim. Bu konuya nereden mi geldik? Microsoft'un Razor'unu kurcalarken kaleme aldığım derlemeye nasıl bir giriş yaparım diye düşünürken aklıma gelen ACDC'nin The Razors Edge albümünden. Haydi başlayalım ;)</p>
<p><a href="https://github.com/buraksenyurt/saturday-night-works/tree/master/No%2021%20-%20Introducing%20Razor" target="_blank">Saturday-Night-Works çalışmalarımdaki 21 numaralı örnek</a>teki amacım, Microsoft'un Asp.Net Core MVC tarafında özellikle sayfa odaklı senaryolar için geliştirdiği Razor çatısını tanımaktı. Bu çatıda sayfalar doğrudan istemci taleplerini karşılayıp arada bir Controller'a uğramadan sayfa modeli<em>(PageModel)</em> ile konuşabilmekte. Razor sayfaları SayfaAdı.cshtml benzeri olup kullandıkları sayfa modelleri SayfaAdi.cshtml.cs şeklinde oluşturuluyor. Genel hatları ile URL yönlendirmeleri aşağıdaki tablodakine benzer şekilde olmakta. Örneğin /Book adresine göre pages klasöründeki Book.cshtml isimli sayfa talep edilmiş oluyor. Sayfanın arka plan kodları da aynı klasördeki cs dosyasında yer alıyor. Web standartları gereği /Index ve / talepleri aynı route adres olarak değerlendiriliyor. Tabii adreslere farklı şekillerde adresleme yapmakta mümkün. Tablodaki /Category önekli adres yönlendirmeleri bu anlamda düşünülebilir. Elbette konuyu anlamanın en iyi yolu bir örneği çalışmaktan geçiyor.</p>
<table border="1">
<tbody>
<tr>
<td><strong> Örnek URL Adresi </strong></td>
<td><strong> Karşılayan Razor Sayfası</strong></td>
<td><strong> Model Nesnesi</strong></td>
</tr>
<tr>
<td> /Book</td>
<td> pages/Book.cshtml</td>
<td> pages/book.cshtml.cs</td>
</tr>
<tr>
<td> /Category/Product</td>
<td> pages/Category/Product.cshtml </td>
<td> pages/Category/Product.cshtml.cs </td>
</tr>
<tr>
<td> /Category</td>
<td> pages/Category/Index.cshtml</td>
<td> pages/Category/Index.cshtml.cs</td>
</tr>
<tr>
<td> /Category/Index</td>
<td> pages/Category/Index.cshtml</td>
<td> pages/Category/Index.cshtml.cs</td>
</tr>
<tr>
<td> /Index</td>
<td> pages/Index.cshtml</td>
<td> pages/Index.cshtml.cs</td>
</tr>
<tr>
<td> /</td>
<td> pages/Index.cshtml</td>
<td> pages/Index.cshtml.cs</td>
</tr>
</tbody>
</table>
<blockquote>
<p>Çalışmada veri girişi yapılabilen basit bir form tasarlayıp, Razor'un kod dinamiklerini anlamak istedim. İlk aşamada bilgileri InMemory veri tabanında tutmayı planladım. Son aşamada ise SQLite veri tabanını devreye aldım.</p>
</blockquote>
<h2>Başlangıç</h2>
<p>Hazırsanız ilk adımlarımızla işe başlayalım. Ben diğer pek çok örnekte olduğu gibi kodlamayı WestWorld<em>(Ubuntu 18.04, 64bit)</em> üzerinde Visual Studio Code aracıyla gerçekleştirmekteyim. Linux tarafında Razor uygulamalarını oluşturmak için en azından .Net Core 2.2'ye ihtiyacımız var. Projeyi aşağıdaki terminal komutunu kullanarak oluşturabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet new webapp -o MyBookStore</pre>
<p>Açılan uygulama iskeletini biraz inceleyecek olursak Razor sayfaları ve ilişkili model sınıflarının Pages klasöründe konuşlandırıldığını görebiliriz. Static HTML dosyaları, Javacript kütüphaneleri ve CSS içerikleri de wwwroot altında bulunmaktadır. Resim, video vb varlıkları da bu klasör altında toplayabiliriz. Şu haliyle bile uygulamayı ayağa kaldırıp varsayılan olarak gelen içerikle çalışmamız mümkün. Ancak bizim amacımız okuduğumuz kitapları yöneteceğimiz basit bir Web arayüzü geliştirmek.</p>
<h2>Geliştirme Safhası</h2>
<p>Gelelim kod tarafına. Burada kitap ekleme, listeleme ve düzenleme işlemleri için bir takım sayfalarımız mevcut. Ancak öncelikle Data isimli bir klasör oluşturup StoreDataContext.cs ve Book.cs isimli Entity sınıflarını ekleyerek işe başlayalım. Tahmin edeceğiniz üzere Entity Framework Core ile entegre ettiğimiz bir ürünümüz var.</p>
<p><em>StoreDataContext.cs</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using Microsoft.EntityFrameworkCore;
namespace MyBookStore.Data
{
public class StoreDataContext
: DbContext
{
public StoreDataContext(DbContextOptions<StoreDataContext> options)
: base(options)
{
// InMemory db kullanacağımız bilgisi startup'cs deki
// Constructor metoddan alınıp base ile DbContext sınıfına gönderilir
}
public DbSet<MyBookStore.Data.Book> Books { get; set; } // Kitapları tutacağımız DbSet
}
}</pre>
<p><em>Book.cs</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.ComponentModel.DataAnnotations;
namespace MyBookStore.Data
{
/*
Book entity sınıfının özelliklerini DataAnnotations'dan gelen çeşitli
attribute'lar ile kontrol altına alıyoruz.
Zorunlu alan olma hali, sayısallar ve string'ler için aralık kontrolü yapmaktayız.
Buradaki ErrorMessage değerleri, Razor Page tarafında Validation işlemi sırasında
değer kazanır ve gerektiğinde uyarı olarak sayfada gösterilirler.
*/
public class Book
{
public int Id { get; set; }
[Required(ErrorMessage = "Kitabın adını yazar mısın lütfen")]
[StringLength(60, MinimumLength = 2, ErrorMessage = "En az 2 en fazla 60 karakter")]
public string Title { get; set; }
[Required(ErrorMessage = "Kaç sayfalık bir kitap bu")]
[Range(100, 1500, ErrorMessage = "En az 100 en çok 1500 sayfalık bir kitap olmalı")]
public int PageCount { get; set; }
[Required(ErrorMessage = "Liste fiyatı girilmeli")]
[Range(1, 100, ErrorMessage = "En az 1 en çok 100 liralık kitap olmalı")]
public double ListPrice { get; set; }
[Required(ErrorMessage = "Kısa da olsa özet gerekli")]
[StringLength(250, MinimumLength = 50, ErrorMessage = "Özet en az 50 en fazla 250 karakter olmalı")]
public string Summary { get; set; }
[Required(ErrorMessage = "Yazar veya yazarlar olmalı")]
[StringLength(60, MinimumLength = 3, ErrorMessage = "Yazarlar için en az 3 en fazla 60 karakter")]
public string Authors { get; set; } //TODO Author isimli bir Entity modeli kullanalım
}
}</pre>
<blockquote>
<p>Örnek ilk başta InMemory veri tabanını kullanacak şekilde tasarlanmıştır. Bu nedenle Startup.cs dosyasındaki ConfigureServices metodunda aşağıdaki gibi bir enjekte söz konusudur.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">// InMemory veritabanı kullanacağımız DbContext'imizi DI ile ekledik
services.AddDbContext<StoreDataContext>(options=>options.UseInMemoryDatabase("StoreLook"));</pre>
<p>SQLite kullanımına geçildiğindeyse buradaki servis entegrasyonu şöyle olmalıdır.</p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">// appsettings'den SQLite için gerekli connection string bilgisini aldık
var conStr=Configuration.GetConnectionString("StoreDataContext");
// ardından SQLite için gerekli DB Context'i servislere ekledik
// Artık modellerimiz SQLite veritabanı ile çalışacak
services.AddDbContext<StoreDataContext>(options=>options.UseSqlite(conStr));</pre>
</blockquote>
<p>Kitap ekleme fonksiyonelliği için Pages klasörüne ekleyeceğimiz AddBook.cshtml ve AddBook.cshtml.cs tipleri kullanılmaktadır. Bunlar Razor Page ve Model nesnelerimiz. </p>
<p><em>AddBook.cshtml</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyBookStore.Data;
namespace MyBookStore.Pages
{
// İsimlendirme standardı gereği Razor sayfa modelleri 'Model' kelimesi ile biter
public class AddBookModel : PageModel // PageModel türetmesi ile bir model olduğunu belirttik
{
private readonly StoreDataContext _context;
//BindProperty özelliği ile Book tipinden olan BookData özelliğini Razor sayfasına bağlamış olduk.
[BindProperty]
public Book BookData { get; set; }
public AddBookModel(StoreDataContext context)
{
_context = context; // Db Context'i kullanabilmek için içeriye aldık
}
// Asenkron olarak çalışabilen ve sayfadaki Submit işlemi sonrası tetiklenen Post metodumuz
// Tipik olarak Razor sayfasındaki model verisini alıp DbSet'e ekliyor ve kayıt ediyoruz.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var addedBook=_context.Books.Add(BookData).Entity;
Console.WriteLine($"{addedBook.Title} eklendi");
await _context.SaveChangesAsync();
return RedirectToPage("/Index"); // Kitap eklendikten sonra ana sayfaya yönlendirme yapıyoruz
}
}
}</pre>
<p><em>AddBook.cshtml.cs</em></p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@page // sayfanın bir razor page olduğunu belirttik
@model MyBookStore.Pages.AddBookModel // sayfanın konuşacağı model sınıfını işaret ettik.
<html>
<body>
<h2>Yeni bir kitap eklemek ister misin?</h2>
<form method="POST">
<!--BookData sayfaya bağladığımız entity tipinden nesne örneği.
Bunu bağlamak için AddBookModel sınıfında BindProperty niteliği ile işaretlenmiş
bir özellik tanımladık. Her input kontrolünde dikkat edileceği üzere asp-for
niteliği ile bir özelliğe bağlantı yapılmakta -->
<div class="input-group mb-3">
<input type="text" asp-for="BookData.Title" placeholder="Başlığı" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<input type="text" asp-for="BookData.Authors" placeholder="Yazarları" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<input type="text" asp-for="BookData.PageCount" placeholder="Sayfa sayısı" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<input type="text" asp-for="BookData.ListPrice" placeholder="Liste fiyatı" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<textarea asp-for="BookData.Summary" placeholder="Kısa bir özeti" class="form-control" aria-label="Özet"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">Kaydet</button>
</form>
<!--asp-for kullanılan tüm elementler için çalışacak olan
validation işleminin sonuçları buraya yansıtılıyor-->
<div asp-validation-summary="All"></div>
</body>
</html></pre>
<p>Kitap bilgilerini düzenlemek içinse EditBook.cshtml ve EditBook.cshtml.cs isimli tipleri kullanmaktayız.</p>
<p><em>EditBook.cshtml.cs</em></p>
<pre class="brush:csharp;auto-links:false;toolbar:false" contenteditable="false">using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyBookStore.Data;
namespace MyBookStore.Pages
{
public class EditBookModel
: PageModel
{
// EditBook.cshtml sayfasına BookData özelliğini bağlamak için bu nitelik ile işaretledik
[BindProperty]
public Book BookData { get; set; }
private StoreDataContext _context;
public EditBookModel(StoreDataContext context)
{
_context = context;
}
// Güncelleme sayfasına id bilgisi parametre olarak gelecektir
// Bunu kullanarak ilgili kitabı bulmaya ve bulursak BindProperty özelliği taşıyan
// BookData isimli özelliğe bağlıyoruz.
public async Task<IActionResult> OnGetAsync(int id)
{
BookData = await _context.Books.FindAsync(id);
if (BookData == null) // Eğer bulunamassa ana sayfaya geri dön
{
return RedirectToPage("/index");
}
return Page(); //Bulunduysa sayfada kal
}
public async Task<IActionResult> OnPostAsync()
{
// Eksik veya hatalı bilgiler nedeniyle Model örneği doğrulanamadıysa
// sayfada kalalım
if (!ModelState.IsValid)
{
return Page();
}
// Güncellenen kitap bilgilerini Context'e ilave edip durumunu Modified'e çektik
_context.Attach(BookData).State = EntityState.Modified;
try
{
// Değişiklikleri kaydetmeyi deniyoruz
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"{BookData.Id} numaralı kitabı bulamadık!");
}
// İşlemler başarılı ise tekrardan index'e(Anasayfa oluyor tabii) dönüyoruz
return RedirectToPage("/index");
}
}
}</pre>
<p><em>EditBook.cshtml</em></p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@page "{id:int}" // Sayfa direktifinde parametre bilidirmi söz konusu. Nitekim buraya güncellenmek istenen sayfanın id bilgisini almamız gerekiyor
@model MyBookStore.Pages.EditBookModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Kitap Bilgisi Güncelleme"; //Sayfa başlığını değiştirdik
}
<!--
Yeni bir kitap ekleme sayfasındakine benzer olacak şekilde bir formumuz var.
Form verisini Page Model sınıfındaki BindProperty'nin verisi ile dolduruyoruz.
Bunun için HTML kontrollerinin asp-for niteliklerini kullanmaktayız.
Submit özellikli Button'a basılması Sayfa model sınıfındaki OnPostAsync fonksiyonunun
tetiklenmesine neden olacaktır. Bu sayfa yüklenirken devreye giren OnGetAsync metodunun parametresi
Page direktifinde belirtilmiştir. Yani sayfa Id parametresi ile gelen talepleri karşıladığında
bunu ilgili metoda iletir. Tahmin edileceği üzere integer tipinden olmayan geçersiz bir Id değeri ile
sayfaya gelinmesi HTTP 404 etkisi yaratacaktır.
Bir sayfaya gelen router parametrelerinin opsiyonel olmasını istersek ? takısını kullanmak yeterlidir.
"{id:int?}" gibi
-->
<h3>@Model.BookData.Id numaralı kitabın bilgilerini günelleyebilirsiniz</h3>
<form method="post">
<input asp-for="BookData.Id" type="hidden" />
<div class="input-group mb-3">
<input type="text" asp-for="BookData.Title" placeholder="Başlığı" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<input type="text" asp-for="BookData.Authors" placeholder="Yazarları" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<input type="text" asp-for="BookData.PageCount" placeholder="Sayfa sayısı" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<input type="text" asp-for="BookData.ListPrice" placeholder="Liste fiyatı" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default">
</div>
<div class="input-group mb-3">
<textarea asp-for="BookData.Summary" placeholder="Kısa bir özeti" class="form-control" aria-label="Özet"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">Kaydet</button>
<div asp-validation-summary="All"></div>
</form></pre>
<p>Varsayılan olarak gelen Index.cshtml ve Index.cshtml.cs içeriklerinide aşağıdaki gibi değiştirelim.</p>
<p><em>Index.cshtml.cs</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.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyBookStore.Data;
namespace MyBookStore.Pages
{
public class IndexModel
: PageModel
{
private readonly StoreDataContext _context;
public IndexModel(StoreDataContext context)
{
// DbContext'i içeriye aldık
_context = context;
}
public IList<Book> Books { get; private set; }
// Kitap listesini çektiğimiz asenkron metodumuz
public async Task OnGetAsync()
{
Books = await _context.Books
.AsNoTracking()
.ToListAsync();
}
// Silme operasyonunu icra eden metodumuz
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
// Silme operasyonu için Identity alanından önce
// kitabı bul
var book=await _context.Books.FindAsync(id);
if(book!=null) //Kitabı bulduysan
{
_context.Books.Remove(book);
//Kitabı çıkart ve Context'i son haliyle kaydet
await _context.SaveChangesAsync();
}
return RedirectToPage(); // Scotty bizi o anki sayfaya döndür
}
}
}</pre>
<p><em>Index.cshtml</em></p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false">@page
@model IndexModel
@{
ViewData["Title"] = "Kitaplarım";
}
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h2>Güncel Liste</h2>
<form method="post">
<!-- Modeldeki Books özelliğinin işaret ettiği nesnelerin her biri için dönüyoruz -->
@foreach (var book in Model.Books)
{
<div class="card">
<div class="card-body">
<!--O anki book nesne örneğinin özelliklerine ulaşıp değerlerini basıyoruz -->
<h5 class="card-title">@book.Title (@book.PageCount sayfa)</h5>
<h6 class="card-subtitle mb-2 text-muted">@book.Authors</h6>
<p class="card-text">@book.Summary</p>
<p class="card-text">@book.ListPrice</p>
<!--Güncelleme başka bir Razor Page tarafından yapılacak -->
<a asp-page="./EditBook" asp-route-id="@book.Id" class="card-link">Düzenle</a>
<!--Silme işlemi ise bu sayfadan Post edilerek gerçekleşecek
asp-route-id ile silme ve güncelleme operasyonlarında gerekli identity
alanının nereden bağlanacağını belirtiyoruz
-->
<button type="submit" asp-page-handler="delete" asp-route-id="@book.Id" class="card-link">Sil</button>
</div>
</div>
}
<!--Yeni bir kitap eklemek için AddBook sayfasına yönlendiriyoruz-->
<a asp-page="./AddBook">Yeni Kitap</a>
</form></pre>
<p>Ayrıca shared klasöründe yer alan _Layout.cshtml dosyasınıda kurcalayıp navigasyon sekmesindeki linklerin bizim istediğimiz şekilde çıkmasını sağlayabiliriz.</p>
<pre class="brush:html;auto-links:false;toolbar:false" contenteditable="false"><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - MyBookStore</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
crossorigin="anonymous"
integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
</environment>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">Sevdiğim Kitaplar</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Lobi</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/AddBook">Yeni Kitap</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li> -->
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<partial name="_CookieConsentPartial" />
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2019 - MyBookStore - <a asp-area="" asp-page="/Privacy">Privacy</a>
</div>
</footer>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
</script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html></pre>
<h2>Çalışma Zamanı</h2>
<p>Kodlama tarafını tamamladıktan sonra uygulamayı aşağıdaki terminal komutu ile çalıştırıp deneme sürüşüne çıkabiliriz.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet run</pre>
<p>Eğer uygulama sorunsuz çalıştıysa http://localhost:5401/ adresi üzerinden hareket edebiliriz. İster üst bara eklediğimiz linkten ister http://localhost:5401/AddBook adresine giderek yeni kitap ekleme sayfasına ulaşabiliriz<em>(Razor için belirlenen varsayılan adres WestWorld sisteminde kullanıldığı için UseUrls metodu ile onu 5401e çektim. Program.cs'e bakınız)</em></p>
<blockquote>
<p>In Memory veritabanı kullandığımız versiyonda uygulama sonlandığında tüm kayıtlar uçacaktır. Kalıcı bir depolama için SQL, SQLite ve benzeri sistemleri içeriye enjekte edebiliriz. İlerleyen kısımda SQLite denememiz olacak.</p>
</blockquote>
<p>Uncle Bob temalı örnek bir kitap verisini ilk denemede kullanmak isterseniz diye aşağıya bilgilerini bırakıyorum ;)</p>
<pre class="brush:plain;auto-links:false;toolbar:false" contenteditable="false">Clean Architecture
Robert C. Martin (Uncle Bob)
393
34.99
"This is essential reading for every current of aspiring software architect..."</pre>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_1.png" alt="" /></p>
<p>Console logundan kitabın eklendiğini izleyebiliriz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_2.png" alt="" /></p>
<p>İşlemler sırasında veri doğrulama kontrolüne takılırsak aşağıdaki gibi bir görüntü ile karşılaşırız<em>(Bu kısmı daha şık bir hale getirmek gerekiyor. Belki popup'lar ile uyarı vermek daha güzel olabilir. Bunu yapmayı bir deneyin)</em></p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_3.png" alt="" /></p>
<p>Başarılı girişler sonrası gelinen Index sayfasının çıktısı ise aşağıdaki ekran görüntüsündekine benzer olacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_4.png" alt="" /></p>
<p>Bir kitabı düzenlemek için Düzenle başlıklı linke tıkladığımızda EditBook/{Id} şeklindeki bir yönlendirme çalışır. Bu tahmin edeceğiniz üzere EditBook.cshtml sayfasının işletilmesini sağlayacaktır.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_5.png" alt="" /></p>
<p>Düzenleme sonrası örnek sonuçlar da şöyle olabilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_6.png" alt="" /></p>
<h2>InMemory Veritabanını SQLite ile Değiştirme</h2>
<p>Örnekte kullandığımız veri merkezini SQLite tarafına dönüştürmek için EntityFramework Core'un ilgili NuGet paketini projeye eklemek lazım. Bunun için aşağıdaki terminal komutu kullanılabilir.</p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet add package Microsoft.EntityFrameworkCore.SQLite</pre>
<p>Ardından appsettings.json dosyasına bir Connection String bildirimi dahil edip, Startup sınıfındaki ConfigureServices metodunda minik bir ayarlama yapmak gerekiyor ki bunu yazının önceki kısımlarında not olarak belirtmiştik.</p>
<pre class="brush:js;auto-links:false;toolbar:false" contenteditable="false">{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"StoreDataContext": "Data Source=MyBookStore.db"
},
"AllowedHosts": "*"
}</pre>
<p>Bunlar başlangıç aşamasında yeterli değil. Çünkü ortada fiziki veri tabanı yok. Dolayısıyla SQLite veri tabanının da oluşturulması gerekiyor. </p>
<pre class="brush:bash;auto-links:false;toolbar:false" contenteditable="false">dotnet ef migrations add InitialCreate
dotnet ef database update</pre>
<p>Yukarıdaki terminal komutları sayesinde DataContext türevli sınıf baz alınarak migration planları çıkartılır. Planlar hazırlandıktan sonra ikinci komut ile update işlemi icra edilir.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_8.png" alt="" /></p>
<p>Eğer veri tabanını baştan hazırlamaz ve update planını çalıştırmazsak aşağıdakine benzer bir hata ile karşılaşabiliriz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_7.png" alt="" /></p>
<p>Artık verilerimiz SQLite ile fiziki olarak da kayıt altında. Hatta Visual Studio Code'a <a href="https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite" target="_blank">SQLite Explorer Extension</a> isimli aracı eklersek oluşan DB dosyasının içeriğini de görebiliriz.</p>
<p><img src="https://www.buraksenyurt.com/image.axd?picture=/2019/05/21/Cover_9.png" alt="" /></p>
<h2>Ben Neler Öğrendim?</h2>
<p>Bu çalışmanın da bana kattığı bir sürü şey oldu elbette. Üstünden tekrar geçmenin faydalarını gördüm ilk başta. Özetle öğrendiklerimi aşağıdaki gibi sıralayabilirim.</p>
<ul>
<li>Razor Page ve Page Model kavramlarının ne olduğunu</li>
<li>Razor'un temel çalışma prensiplerini</li>
<li>Yönlendirmelerin<em>(Routing)</em> nasıl işlediğini</li>
<li>Razor içinden model nesnelerine nasıl bağlanılabileceğini<em>(property binding)</em></li>
<li>Entity Framework Core'da InMemory veri tabanı kullanımını</li>
<li>DI ile ilgili servislerin nasıl enjekte edildiğini</li>
<li>Çeşitli DataAnnotations niteliklerini<em>(attributes)</em></li>
<li>InMemory veri tabanında SQLite kullanımına geçince yapılması gereken değişiklikleri ve Migration'ın ne işe yaradığını</li>
</ul>
<p>Böylece <a href="https://github.com/buraksenyurt/saturday-night-works" target="_blank">Saturday-Night-Works</a> çalışmalarının 21 numaralı örneğine ait derlemenin sonuna gelmiş olduk. Diğer çalışmalardan da gözüme kestirdiklerimi ele alıp bloğuma not olarak düşeceğim. Fark ettim ki Saturday-Night-Works çalışmaları kendimi kişisel olarak geliştirmek adına yeterli ama tamamlayıcılık açısından eksik. Yapılan her uygulamanın üstünden bir kere daha geçmek, kodları okumak ve notları daha derli toplu olarak bloguma koymak tamamlayıcı bir motivasyon olarak karşıma çıkıyor. Bir başka macera derlemesinde görüşmek ümidiyle hepinize mutlu günler dilerim.</p>
2019-05-17T12:53:00+00:00
razor
.net core
c#
html
cshtml
mode page
razor page
routing
entity framework
sqlite
inmemory database
data annotations
property binding
bootstrap
dotnet
viewdata
bsenyurt
Amacım, Microsoft'un Asp.Net Core MVC tarafında özellikle sayfa odaklı senaryolar için geliştirdiği Razor çatısını tanımak. Bu çatıda sayfalar doğrudan istemci taleplerini karşılayıp arada bir Controller'a uğramadan sayfa modeli _(PageModel)_ ile konuşabiliyorlar. Razor sayfaları SayfaAdı.cshtml benzeri olup kullandıkları sayfa modelleri SayfaAdi.cshtml.cs şeklinde oluşturuluyorlar. Genel hatları ile URL eşleştirmeleri aşağıdaki gibi oluyor.
https://www.buraksenyurt.com/pingback.axd
https://www.buraksenyurt.com/post.aspx?id=ff1e22ff-31b2-4ea2-a947-00163b13399c
1
https://www.buraksenyurt.com/trackback.axd?id=ff1e22ff-31b2-4ea2-a947-00163b13399c
https://www.buraksenyurt.com/post/razor-dunyasindaki-ilk-adimlarim#comment
https://www.buraksenyurt.com/syndication.axd?post=ff1e22ff-31b2-4ea2-a947-00163b13399c