Quantcast
Channel: Burak Selim ŞenyurtBurak Selim Şenyurt0.0000000.000000
Viewing all 525 articles
Browse latest View live

Kimdir Bu Travis?

$
0
0

Merhaba Arkadaşlar,

Geçen gün çalışma odamın artık ardiye haline gelmiş bir dolabını temizlemek üzere kolları sıvadım. Sayısız network kablosu, yazılabilir DVDler, müsvette notlar, bir kaç müzik CDsi, yüksek lisanstan kalma ders kitapları, kulaklıkları kayıp walkman, bataryası şişmiş Playstation Portable ve daha bir çok ıvır zıvır eşyayla doluydu. Hangileri gerekli hangileri gereksiz diye ayıklarken dönem dönem sebebsiz yere aldığım MP3 çalarlara denk geldim. Kocaman bir discman bile vardı. Ancak gözüm arkalarda köşeye sıkışmış 1 TBlık Harddisk'e takıldı. Zamanında E-book'lar, filmler ve müzikler için kullandığım bir disk olduğunu hayal mayal hatırlıyordum. 

Eşyaları ayıklamayı bırakıp diskin içinde ne var ne yok bakmak istedim. Daha yeni aldığım Mac Mini (ona ahch-to adını verdim) ile açmaya çalıştım. Sierra onu pek sevmedi diyebilirim. Bunun üzerine Westworld'e geçmeye karar verdim. Ubuntu'nun açamayacağı disk yoktu. Diskin içinde beklediğimden de geniş bir arşiv vardı. .Net 2.0/3.5 ile yazılmış projeler, makaleler için toplanmış belgeler, cv'min seksensekiz çeşidi, bloğun dönemsel yedekleri, fotoğrafçılık ile uğraştığım zamanlardan kalma klasörler ve diğer şeyler

Günün bir bölümünü o arşivleri tarayarak geçirdim. Sonra ID Taglerine kadar düzenlediğim MP3leri karışık sırada çalayım dedim. MP3 çalmayalı yıllar olmuştur. Hayatımız bulutlar üzerinde seyretmeye başladığından beri onu da Spotify gibi ortamlardan dinler olduk. Pek azımızın MP3 satın aldığını veya indirdiğini düşünüyorum. Hele ki mobil cihazlarda 1 kb yer bile tutmayan milyonlarca parçayı dinleme fırsatı varken. Şarkılar çalarken bende yeni araştırma konum olan Travis'le ilgili saturday-night-works çalışmalarından birisini yapıyordum. Hoş bir tesadüf olmalı ki çalışırken çalan parçalardan birisi de Travis'in 2001 yılında çıkarttığı The Invisible Band albümündeki Sing isimli şarkılarıydı. Konuyla ne alakası vardı bilemiyorum. Sadece isim benzerliği :) Aradan bir süre geçtikten sonra Travis-CI çalışmasını bloğuma not olarak düşmeye karar verdim.

Continuous Integration kaliteli ve sorunsuz kod çıkartmanın önemli safhalarından birisi. DevOps kültürü için değerli olan, Continuous Deployment/Delivery ile bir anılan CI'ın uygulanmasında en temel noktalar kodun sürekli test edilebilir olması ve ne kadarının kontrol altına alındığının bilinmesi. Başarılı bir Build için bu kriterlerin metrik olarak gerekli değerlerin üzerine çıkması şart. Ancak bu metriklere uyan bir Build, dağıtıma gönderilebilir bir aday sürüm haline gelebilir.

CI/CD hattını tesis ederken kullanılabilecek bir çok yardımcı ürün bulunuyor. Güncel olarak çalışmakta olduğum şirkette Microsoft'un VSTS'i kullanılmakta. Bunun muadili olabilecek Jenkins'de diğer bir alternatif olarak karşımıza çıkıyor. Benim öğrenmek istediğim ise Travis. Travis, Jenkins gibi kurulum ve bilgi maliyeti fazla olmayan, github ile kolayca entegre edilebilen, geliştirici dostu, ücretsiz bir CI ürünü olarak karşımıza çıkıyor. Amacım onu çok basit bir uygulama ile deneyimlemek.

İhtiyaçlar (Yapılacaklar)

İlk başta ihtiyaçları ve yapılacakları belirlemek lazım.

  • Öncelikle test edilebilir örnek bir uygulamaya ihtiyacımız var. Travis'in desteklediği dil ve platform yelpazesi oldukça geniş. (Ben .Net Core tabanlı bir kütüphaneyi ele almayı tercih ettim)
  • Uygulamayı github üzerindeki bir proje ile ilişkilendirmek gerekiyor. Nitekim Travis, code base olarak GitHub tarafını kullanmakta.
  • Travis'in Github entegrasyonu sayesinde code base üzerinde yapılan her Push sonrası otomatik olarak CI süreci başlayacak. Bu süreçte uygulamanın ihtiyaç duyduğu ortam paketleri yüklenip, build işlemi gerçekleştirilirken, aynı zamanda testler de koşulacak (CI süreci tamamen bulutta işleyecek)
  • Uygulama için belki de en kritik ihtiyaç .travis.yml dosyası ve içeriği. Docker çalışma dinamiklerine benzer şekilde Travis ortamı için gerekli bilgileri içeren bir dosya olarak düşünebiliriz.

Travis Tarafının Hazırlanması

Öncelikle Travis'in ilgili sayfasına gidip Github hesabımız ile kayıt olmamız gerekiyor. Sonrasında Acivate düğmesine basarak ilerliyoruz.

İzleyen adımda CI sürecine dahil etmek istediğimiz Github projesini seçiyoruz. Ben örnek için hello-travis isimli bir repo oluşturdum (Bu arada Travis'in yer yer çıkan logo'ları gerçekten çok tatlı)

Artık Travis ile Github projemiz birbirlerine bağlanmış durumdalar. Bunu Travis tarafındaki Repositories sekmesinden görebiliriz.

Projenin Geliştirilmesi

Örnek olarak .Net Core tabanlı bir sınıf kütüphanesi geliştirmeye karar verdim. İlk olarak Github projesini Westworld'e (Ubuntu 18.04, 64bit) klonladım.

git clone https://github.com/buraksenyurt/hello-travis.git

Ardından aşağıdaki adımları izleyerek bir .Net Core klasör ağacı oluşturdum.

dotnet new sln
mkdir MathService
cd MathService
dotnet new classlib
mv Class1.cs Common.cs
cd ..
dotnet sln add ./MathService/MathService.csproj
mkdir MathService.Tests
cd MathService.Tests
dotnet new xunit
dotnet add reference ../MathService/MathService.csproj
mv UnitTest1.cs CommonTest.cs
cd ..
dotnet sln add ./MathService.Tests/MathService.Tests.csproj
touch .travis.yml

Öncelikle klonlanan klasörde bir Solution oluşturuyoruz. İsim vermediğimiz için hello-travis isimli bir solution dosyası üretilecektir. Ardından MathService isimli bir sınıf kütüphanesi üretiyor ve Class1.cs dosyasının adını Common.cs olarak değiştiriyoruz. Projeyi, solution içeriğine de ekledikten sonra bu kez MathService.Tests isimli xUnit tipinden bir test projesi oluşturuyoruz. Bu projeye MathService kütüphanesini referans edip son olarak test projesini solution'a bildiriyoruz. En son adımda dikkat edeceğiniz üzere .travis.yml isimli yaml dosyasını oluşturmaktayız.

Kodları aşağıdaki gibi geliştirebiliriz.

Common.cs

using System;

namespace MathService
{
    public class Common
    {
        public bool IsNegative(int number)
        {
            return false;
        }

        public bool IsEven(int number)
        {
            return number % 2 == 0;
        }
    }
}

CommonTest.cs içeriği

using System;
using Xunit;
using MathService;

namespace MathService.Tests
{
    public class CommonTest
    {
        private Common _common;

        public CommonTest()
        {
            _common = new Common();
        }

        [Fact]
        public void Negative_Four_Is_Negative()
        {
            var result=_common.IsNegative(-4);

            Assert.True(result,"-4 is negative number");
        }


        [Fact]
        public void Four_Is_Even()
        {
            var result=_common.IsEven(4);

            Assert.True(result,"4 is an even number");
        }
    }
}

.travis.yml

Pek tabii Travis entegrasyonu için en kritik nokta bu dosya ve içeriği.

language: csharp
solution: hello-travis.sln
mono: none
dotnet: 2.1.502

script:
- dotnet build
- dotnet test MathService.Tests/MathService.Tests.csproj

Dosya içerisinde Travis'in çalışma zamanı ortamı için bir takım bilgiler yer alıyor. Bu bilgilere göre .Net Core 2.1.502 versiyonlu runtime üzerinde C# dilinin kullanıldığı bir uygulama söz konusu. Buna uygun bir makineyi Travis kendisi hazırlayacak (Travis'in log detaylarını incelemekte yarar var) script bloğunda yer alan ifadeler ise her push sonrası Travis tarafından icra edilecek olan işleri içeriyor. Önce build işlemi, sonrasında da test'in çalıştırılması. Örnekte kullanılan .Net çözümünün orjinal github adresi burasıdır.

Çalışma Zamanı

İlk olarak hatalı çalışan testi bulunduran bir geliştirme yapmayı tercih ettim. Local'de test sonuçları aşağıdaki şekilde görüldüğü gibiydi.

dotnet test

Hal böyleyken kodları commit edip github sunucusuna push ile gönderdim.

git add .
git commit -m "fonksiyonal eklendir ve test kodları yazıldı"
git status
git push

Travis'e gittiğimde otomatik bir Build işleminin başladığını fark ettim.

Bir süre sonra Fail eden test nedeniyle Build işlemi de hatalı olarak sonlandı (Bu zaten istediğimiz ve beklediğimiz durum)

Log raporu sonuçları da aşağıdaki gibi oluştu.

Sonrasında hata alan test kodunu düzelterek ilerledim.

using System;

namespace MathService
{
    public class Common
    {
        public bool IsNegative(int number)
        {
            return number<0;
        }

        public bool IsEven(int number)
        {
            return number % 2 == 0;
        }
    }
}

Westworld üzerinde dotnet test terminal komutu ile testlerin tamamının (sadece iki test var :P) başarılı olup olmadığını kontrol ettim. Ardından kodu commit edip tekrardan github'a push'ladım. Travis kısa süre içinde otomatik olarak yeni bir build işlemi başlattı. Bu sefer beklediğim gibi testler başarılı olduğundan build sonucu Passed olarak işaretlendi. İşte çalışma zamanına ait ekran görüntüleri.

Dikkat edileceği üzere tüm build işlemlerinin tarihçesini de görebiliyoruz. Bu tip loglar bizim için oldukça önemli.

Ben Neler Öğrendim

Pek tabii bu çalışma sırasında da öğrendiğim bir çok şey oldu. Kabaca öğrendiklerimi şöyle sıralayabilirim.

  • Travis'in CI sürecindeki yerini
  • Travis ile bir Github reposunun nasıl bağlanabileceğini
  • .travis.yml dosyasının içeriğinin nasıl olması gerektiğini ve içeriğindeki ifadelerin ne anlama geldiğini
  • .Net Core tarafında xUnit test ortamının nasıl oluşturulabileceğini
  • git push sonrası işletilen Build sürecinin izlenmesini

Böylece geldik bir maceranın daha sonuna. Sanırım Startup tadında bir projeye başlayacak olsak, takımın geliştirme sürecinde CI aracı olarak Travis'i alternatif olarak düşünebiliriz. Kullanımının kolay olması, github ile entegre çalışabilmesi ve bu nedenle push işlemleri sonrası build işlemlerinin otomatik olarak başlaması cezbedici özelliklerden. Tekrardan görüşünceye dek hepinize mutu günler dilerim.


http://www.buraksenyurt.com/post/Angular-ile-Basit-Bir-Tahmin-Oyunu-YazmakAngular ile Basit Bir Tahmin Oyunu Yazmak

$
0
0

Merhaba Arkadaşlar

Commodore 64 sahibi olduğum günlerde beni çok etkileyen bir Futbol oyunu vardı. Üstelik yerli malıydı. Görsel bir arabirimi yoktu. Komut satırından size sorulan sorulara verdiğiniz cevaplara göre Türkiye birinci futbol liginde maçlar yapıyordunuz. Açılışta takımınızı ve rakibinizi seçtikten sonra yazı tura sorusu ile başlıyordu her şey. Kazandıysanız da "top mu, kale mi" sorusuyla devam ediyordu. Maçın süresi ilerledikçe komut satırından sorular gelmeye devam ediyordu. "Rakip ceza sahasının gerisinde şut çekti. Kaleciniz ne yapacak?" Ve seçenekler geliyordu. "Plonjon, out'a çelme vs" Yapılan seçime göre gol yiyebilir, topu çelebilir veya tutabilirdiniz. İsmini bir türlü hatırlayamadığım ama komut satırından olsa bile beni saatlerce monitör başına kitleyen bir oyundu. Zaten o devrin Commodore 64 oyunlarındaki yaratıcılık, programlama kabiliyetleri bir başkaydı. Bu düşünceler ışığında günlerden bir gün Angular tarafı ile ilgili saturday-night-works çalışmalarımı yapmaktayken bende basit ama bana keyif verecek bir oyun yazayım istedim.

Esasında Angular tarafında çok deneyimli değildim. Eksiğim çoktu. Onu daha iyi tanımak için bol bol örnek yapmam gerekiyordu. Bilgilerimi pekiştirmek için farklı öğretileri uygulamaya devam ediyordum. Bu kez temelleri basit şekilde anlamak adına bir şehir tahmin oyunu yazmaya karar verdim. Uygulama havanın rastsal durumuna göre kullanıcısına bir soru soracak ve hangi şehirde olduğunun bulmasını isteyecek. Kabaca şu aşağıdaki cümleye benzer bir düşünce ile yola çıktığımı söyleyebilirim.

"Merhaba Burak. Bugün hava oldukça 'güneşli' ve ben kendimi bir yere ışınladım. Neresi olduğunu tahmin edebilir misin?"

'güneşli' yazan kısım rastgele gelecek bir kelime. Yağmurlu olabilir, sisli olabilir vb...Buna göre uygun şehirlerden rastgele birisine gidecek bilgisayar. Biz de bunu tahmin etmeye çalışacağız. Tabii tahmini kolaylaştırmak için minik bir ipucu vereceğiz. Baş harfini söyleyeceğiz(ki siz bunu daha da zenginleştirebilirsiniz. Tahmin sayısını tutup belli bir oranda hak tanıyabilir, tahmin edemedikçce daha fazla harf çıkarttırabilirsiniz)

Öyleyse vakit kaybetmeden işe koyulalım değil mi? Ben örneği artık sonbaharını yaşamakta olan WestWorld (Ubuntu 18.04, 64bit)üzerinde geliştirdim.

Ön Gereksinimler ve Kurulumlar

Sisteminizde angular CLI yüklü olursa iyi olur. Komut satırından angular projesi başlatmak için işimizi oldukça kolaylaştıracaktır. Sonrasında boilerplate etkisi ile uygulamayı oluşturabiliriz. Arayüzün şık görünmesini sağlamak için (ben ne kadar şıklaştırabilirsem artık :D ) bootstrap'i tercih edebiliriz. Aşağıdaki terminal komutları gerekli yükleme işlemlerini yapacaktır. İlk komutla angular CLI aracını yüklerken, ikinci komutla yeni bir angular projesi oluşturuyoruz. Son terminal komutuyla da bootstrap'i projemize dahil ediyoruz. Hepsi Node Package Manager yardımıyla gerçekleştirilmekte.

sudo npm install -g @angular/cli
ng new where-am-i --inlineTemplate
cd where-am-i
npm install bootstrap --save

Yapılan Değişiklikler

Uygulama kodlarında değişiklik yaptığım çok az yer var. Malum boilerplate etkisi ile zaten hazır bir proje şablonu üretilmiş durumda. Biz temel olarak bir bileşen oluşturup bunu ana sayfada kullanıyoruz. 

Bootstrap'i kullanabilmek için proje klasöründeki angular.json dosyasındaki styles elementine ilave bir bildirim yaptık. Buna ek olarak src/app klasöründeki app.component.html dosyasını aşağıdaki gibi değiştirdik (Size yardımcı olacak bilgiler kodların yorum satırlarında yer alıyor. Direkt copy-paste yapmadan önce okuyun)

<!--
  bootstrap css stilleri ile donattığımız basit bir arayüzümüz var.

  app.component sınıfındaki property'lere erişmek için {{propertyName}} notasyonu kullanılıyor.
  Yine bileşen üzerinde bir metod çağrısı yapmak ve bunu bir kontrol olayı ile ilişkilendirmek için 
  (eventName)="method name" şeklinde bir notasyon kullanılıyor.
  
  Angular direktiflerinde *ngIf komutunu kullanarak tahmine göre bir HTML elementinin gösterilmesi sağlanıyor.
--><div class="container"><h2>Bil bakalım hangi şehre gittim? :)</h2><div class="card bg-light mb-3"><div class="card-body"><p class="card-text">Bugün hava <b>{{currentWeather}}</b> ve ben ... şehrine gittim.</p></div></div><div><p><button class="btn btn-primary btn-sm" (click)="fullThrottle()">Hey Scotty. Beni yenidenışınla</button></p></div><div><label>Tahminin nedir?</label><input (input)="playersGuess=$event.target.value" type="text" /><button class="btn btn-primary btn-sm" (click)="checkMyGuess()">Dene</button></div><div><p *ngIf="guessIsCorrect" class="alert alert-success">Bravo! Yakaladın beni</p><p *ngIf="!guessIsCorrect" class="alert alert-warning">Tüh. Tekrar dener misin?</p><p class="text-info">İşte sana bir ipucu. {{hint}}</p></div></div>

Son olarak src/app klasöründeki app.component.ts typescript dosyasındaki bileşen sınıfının değiştirildiğini ifade edebilirim.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {
  title = 'Şimdi Hangi Şehirdeyim?';
  currentWeather: string; // Güncel hava durumu bilgisini tutan property
  computersLocation: string; //Bilgisayarın yerini tutacak property
  playersGuess: string; // Oyuncunun tahminini tutacak property
  guessIsCorrect: boolean; // Tahminin doğru olup olmadığını tuttuğumuz property
  hint:string; // Tahmini kolaylaştırmak için verdiğimiz ipucunu tutan property

  // Örnek veri dizileri. 
  // TODO: Daha uygun bir key-value dizisi bulunabilir mi?

  airConditions = ['güneşli', 'yağmurlu', 'karlı', 'sisli'];
  cities = [
    ['Barcelona', 'Madrid', 'Lima', 'Rio', 'Miami', 'Sydney', 'Antalya'],
    ['Prag', 'Paris', 'Tokyo', 'Dublin', 'Londra', 'Pekin'],
    ['Moskova', 'Montreal', 'Boston', 'Ağrı'],
    ['London', 'Glasgow', 'Mexico City', 'Frankfurt', 'İstanbul']
  ];

  /*
  Uygulama button bağımsız ilk başlatıldığında da hava tahmini yapılsın ve şehir tutulsun.
  */
  constructor() {
    this.hint = "";
    this.computersLocation="";
    this.currentWeather="";
    this.fullThrottle();
  }
  /*
  Bilgisayar için rastgele hava durumu üreten fonksiyon
  Random fonksiyonundan yararlanıp uygun aralıklarda rastgele sayı üretir
  ve buna göre rastgele bir şehir tutar.
  */
  fullThrottle() {
    // hava durumlarını tutan dizinin boyutuna göre rastgele sayı ürettik
    var rnd1 = Math.floor((Math.random() * this.airConditions.length));
    // rastgele bir hava durumu bilgisi aldık
    this.currentWeather = this.airConditions[rnd1];

    // şehirlerin tutulduğu dizide, hava durumu bilgisine uyan (örnekte indeks sırası) dizinin uzunluğunu aldık
    var arrayLength = this.cities[rnd1].length;
    // uzunluğuna göre rastgele bir sayı ürettik
    var rnd2 = Math.floor((Math.random() * arrayLength));
    // üretilen rastgele sayıya göre diziden bir şehir adı aldık
    this.computersLocation = this.cities[rnd1][rnd2];

    this.hint="Baş harfi "+this.computersLocation[0];

    console.log(this.computersLocation); // Şşşşttt. Kimseye söylemeyin. F12'ye basınca ışınlanan şehri görebilirsiniz.
  }

  /*
  Oyuncunun tahminini kontrol eden fonksiyon
  */
  checkMyGuess() {

    if (this.playersGuess == this.computersLocation)
      this.guessIsCorrect = true;
    else
      this.guessIsCorrect = false;
  }
}

Çalışma Zamanı

Uygulamayı çalıştırmak için terminalden aşağıdaki komutu vermek yeterlidir.

ng serve

Çalışma zamanına ait örnek ekran görüntülerimiz ise aşağıdakine benzer olacaktır. Mesela bir tahmin yaptık ve sonucu bulamadıysak şuna benzer bir sonuçla karşılaşırız.

Ama sonucu bilirsek de şöyle bir ekranla karşılaşırız.

Ben Neler Öğrendim

Pek tabii bu antrenmanla da bir çok şey öğrendim. Aklımda kaldığı kadarıyla onları şöyle özetleyebilirim.

  • Component bileşeni ile HTML arayüzünü, sınıf özellikleri üzerinden nasıl konuşturabileceğimi
  • Bootstrap temel elementlerini Angular bileşenlerinde nasıl kullanabileceğimi
  • ng serve komutu ile uygulamayı çalıştırdıktan sonra, bileşen ve arayüzde yapılan değişikliklerin, save sonrası uygulamayı tekrardan çalıştırmaya gerek kalmadan çalışma zamanına yansıtıldığını
  • Component arayüzünden, Typescript tarafındaki metodların bir olaya bağlı olarak nasıl tetiklenebileceklerini

Böylece geldik bir maceramızın daha sonuna. Saturday-Night-Works'ün 30 numaralı projesine ait blog notlarımı da tamamlamış oldum. Ben bu maceralar sırasında güzel şeyler araştırıyor ve öğreniyorum. Size de böyle bir macerayı tavsiye ederim. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Angular ile Basit Bir Tahmin Oyunu Yazmak

$
0
0

Merhaba Arkadaşlar

Commodore 64 sahibi olduğum günlerde beni çok etkileyen bir Futbol oyunu vardı. Üstelik yerli malıydı. Görsel bir arabirimi yoktu. Komut satırından size sorulan sorulara verdiğiniz cevaplara göre Türkiye birinci futbol liginde maçlar yapıyordunuz. Açılışta takımınızı ve rakibinizi seçtikten sonra yazı tura sorusu ile başlıyordu her şey. Kazandıysanız da "top mu, kale mi" sorusuyla devam ediyordu. Maçın süresi ilerledikçe komut satırından sorular gelmeye devam ediyordu. "Rakip ceza sahasının gerisinde şut çekti. Kaleciniz ne yapacak?" Ve seçenekler geliyordu. "Plonjon, out'a çelme vs" Yapılan seçime göre gol yiyebilir, topu çelebilir veya tutabilirdiniz. İsmini bir türlü hatırlayamadığım ama komut satırından olsa bile beni saatlerce monitör başına kitleyen bir oyundu. Zaten o devrin Commodore 64 oyunlarındaki yaratıcılık, programlama kabiliyetleri bir başkaydı. Bu düşünceler ışığında günlerden bir gün Angular tarafı ile ilgili saturday-night-works çalışmalarımı yapmaktayken bende basit ama bana keyif verecek bir oyun yazayım istedim.

Esasında Angular tarafında çok deneyimli değildim. Eksiğim çoktu. Onu daha iyi tanımak için bol bol örnek yapmam gerekiyordu. Bilgilerimi pekiştirmek için farklı öğretileri uygulamaya devam ediyordum. Bu kez temelleri basit şekilde anlamak adına bir şehir tahmin oyunu yazmaya karar verdim. Uygulama havanın rastsal durumuna göre kullanıcısına bir soru soracak ve hangi şehirde olduğunun bulmasını isteyecek. Kabaca şu aşağıdaki cümleye benzer bir düşünce ile yola çıktığımı söyleyebilirim.

"Merhaba Burak. Bugün hava oldukça 'güneşli' ve ben kendimi bir yere ışınladım. Neresi olduğunu tahmin edebilir misin?"

'güneşli' yazan kısım rastgele gelecek bir kelime. Yağmurlu olabilir, sisli olabilir vb...Buna göre uygun şehirlerden rastgele birisine gidecek bilgisayar. Biz de bunu tahmin etmeye çalışacağız. Tabii tahmini kolaylaştırmak için minik bir ipucu vereceğiz. Baş harfini söyleyeceğiz(ki siz bunu daha da zenginleştirebilirsiniz. Tahmin sayısını tutup belli bir oranda hak tanıyabilir, tahmin edemedikçce daha fazla harf çıkarttırabilirsiniz)

Öyleyse vakit kaybetmeden işe koyulalım değil mi? Ben örneği artık sonbaharını yaşamakta olan WestWorld (Ubuntu 18.04, 64bit)üzerinde geliştirdim.

Ön Gereksinimler ve Kurulumlar

Sisteminizde angular CLI yüklü olursa iyi olur. Komut satırından angular projesi başlatmak için işimizi oldukça kolaylaştıracaktır. Sonrasında boilerplate etkisi ile uygulamayı oluşturabiliriz. Arayüzün şık görünmesini sağlamak için (ben ne kadar şıklaştırabilirsem artık :D ) bootstrap'i tercih edebiliriz. Aşağıdaki terminal komutları gerekli yükleme işlemlerini yapacaktır. İlk komutla angular CLI aracını yüklerken, ikinci komutla yeni bir angular projesi oluşturuyoruz. Son terminal komutuyla da bootstrap'i projemize dahil ediyoruz. Hepsi Node Package Manager yardımıyla gerçekleştirilmekte.

sudo npm install -g @angular/cli
ng new where-am-i --inlineTemplate
cd where-am-i
npm install bootstrap --save

Yapılan Değişiklikler

Uygulama kodlarında değişiklik yaptığım çok az yer var. Malum boilerplate etkisi ile zaten hazır bir proje şablonu üretilmiş durumda. Biz temel olarak bir bileşen oluşturup bunu ana sayfada kullanıyoruz. 

Bootstrap'i kullanabilmek için proje klasöründeki angular.json dosyasındaki styles elementine ilave bir bildirim yaptık. Buna ek olarak src/app klasöründeki app.component.html dosyasını aşağıdaki gibi değiştirdik (Size yardımcı olacak bilgiler kodların yorum satırlarında yer alıyor. Direkt copy-paste yapmadan önce okuyun)

<!--
  bootstrap css stilleri ile donattığımız basit bir arayüzümüz var.

  app.component sınıfındaki property'lere erişmek için {{propertyName}} notasyonu kullanılıyor.
  Yine bileşen üzerinde bir metod çağrısı yapmak ve bunu bir kontrol olayı ile ilişkilendirmek için 
  (eventName)="method name" şeklinde bir notasyon kullanılıyor.
  
  Angular direktiflerinde *ngIf komutunu kullanarak tahmine göre bir HTML elementinin gösterilmesi sağlanıyor.
--><div class="container"><h2>Bil bakalım hangi şehre gittim? :)</h2><div class="card bg-light mb-3"><div class="card-body"><p class="card-text">Bugün hava <b>{{currentWeather}}</b> ve ben ... şehrine gittim.</p></div></div><div><p><button class="btn btn-primary btn-sm" (click)="fullThrottle()">Hey Scotty. Beni yenidenışınla</button></p></div><div><label>Tahminin nedir?</label><input (input)="playersGuess=$event.target.value" type="text" /><button class="btn btn-primary btn-sm" (click)="checkMyGuess()">Dene</button></div><div><p *ngIf="guessIsCorrect" class="alert alert-success">Bravo! Yakaladın beni</p><p *ngIf="!guessIsCorrect" class="alert alert-warning">Tüh. Tekrar dener misin?</p><p class="text-info">İşte sana bir ipucu. {{hint}}</p></div></div>

Son olarak src/app klasöründeki app.component.ts typescript dosyasındaki bileşen sınıfının değiştirildiğini ifade edebilirim.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {
  title = 'Şimdi Hangi Şehirdeyim?';
  currentWeather: string; // Güncel hava durumu bilgisini tutan property
  computersLocation: string; //Bilgisayarın yerini tutacak property
  playersGuess: string; // Oyuncunun tahminini tutacak property
  guessIsCorrect: boolean; // Tahminin doğru olup olmadığını tuttuğumuz property
  hint:string; // Tahmini kolaylaştırmak için verdiğimiz ipucunu tutan property

  // Örnek veri dizileri. 
  // TODO: Daha uygun bir key-value dizisi bulunabilir mi?

  airConditions = ['güneşli', 'yağmurlu', 'karlı', 'sisli'];
  cities = [
    ['Barcelona', 'Madrid', 'Lima', 'Rio', 'Miami', 'Sydney', 'Antalya'],
    ['Prag', 'Paris', 'Tokyo', 'Dublin', 'Londra', 'Pekin'],
    ['Moskova', 'Montreal', 'Boston', 'Ağrı'],
    ['London', 'Glasgow', 'Mexico City', 'Frankfurt', 'İstanbul']
  ];

  /*
  Uygulama button bağımsız ilk başlatıldığında da hava tahmini yapılsın ve şehir tutulsun.
  */
  constructor() {
    this.hint = "";
    this.computersLocation="";
    this.currentWeather="";
    this.fullThrottle();
  }
  /*
  Bilgisayar için rastgele hava durumu üreten fonksiyon
  Random fonksiyonundan yararlanıp uygun aralıklarda rastgele sayı üretir
  ve buna göre rastgele bir şehir tutar.
  */
  fullThrottle() {
    // hava durumlarını tutan dizinin boyutuna göre rastgele sayı ürettik
    var rnd1 = Math.floor((Math.random() * this.airConditions.length));
    // rastgele bir hava durumu bilgisi aldık
    this.currentWeather = this.airConditions[rnd1];

    // şehirlerin tutulduğu dizide, hava durumu bilgisine uyan (örnekte indeks sırası) dizinin uzunluğunu aldık
    var arrayLength = this.cities[rnd1].length;
    // uzunluğuna göre rastgele bir sayı ürettik
    var rnd2 = Math.floor((Math.random() * arrayLength));
    // üretilen rastgele sayıya göre diziden bir şehir adı aldık
    this.computersLocation = this.cities[rnd1][rnd2];

    this.hint="Baş harfi "+this.computersLocation[0];

    console.log(this.computersLocation); // Şşşşttt. Kimseye söylemeyin. F12'ye basınca ışınlanan şehri görebilirsiniz.
  }

  /*
  Oyuncunun tahminini kontrol eden fonksiyon
  */
  checkMyGuess() {

    if (this.playersGuess == this.computersLocation)
      this.guessIsCorrect = true;
    else
      this.guessIsCorrect = false;
  }
}

Çalışma Zamanı

Uygulamayı çalıştırmak için terminalden aşağıdaki komutu vermek yeterlidir.

ng serve

Çalışma zamanına ait örnek ekran görüntülerimiz ise aşağıdakine benzer olacaktır. Mesela bir tahmin yaptık ve sonucu bulamadıysak şuna benzer bir sonuçla karşılaşırız.

Ama sonucu bilirsek de şöyle bir ekranla karşılaşırız.

Ben Neler Öğrendim

Pek tabii bu antrenmanla da bir çok şey öğrendim. Aklımda kaldığı kadarıyla onları şöyle özetleyebilirim.

  • Component bileşeni ile HTML arayüzünü, sınıf özellikleri üzerinden nasıl konuşturabileceğimi
  • Bootstrap temel elementlerini Angular bileşenlerinde nasıl kullanabileceğimi
  • ng serve komutu ile uygulamayı çalıştırdıktan sonra, bileşen ve arayüzde yapılan değişikliklerin, save sonrası uygulamayı tekrardan çalıştırmaya gerek kalmadan çalışma zamanına yansıtıldığını
  • Component arayüzünden, Typescript tarafındaki metodların bir olaya bağlı olarak nasıl tetiklenebileceklerini

Böylece geldik bir maceramızın daha sonuna. Saturday-Night-Works'ün 30 numaralı projesine ait blog notlarımı da tamamlamış oldum. Ben bu maceralar sırasında güzel şeyler araştırıyor ve öğreniyorum. Size de böyle bir macerayı tavsiye ederim. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Peki ya Kong Kim?

$
0
0

Merhaba Arkadaşlar,

Kurumsal mimari ekibinin önerdiği çatılardan birisi üzerine kurulmuş yeni ürünümüzü test ortamına almaya çalıştığımız bir gündü. Local makinelerimizde çok az sorunla ayağa kaldırdığımız proje, test ortamında ne yazık ki daha fazla problem üretmişti. Ağırlıklı olarak web önyüzünden iş kurallarının yürütüldüğü Web API servislerine gidişlerde sorunlar yaşıyorduk.

CI/CD hattındaki parametreleri, veri tabanı nesnelerini, SSO ayarlarını kontrol edip Kibana loglarını incelemeye başladık. Tüm bu işler devam ederken DevOps ekibinden bize destek veren sevigili Yavuz, servisler üzerindeki trafiği monitör etmekteydi. Konuşmalarımız sırasında Docker Container'larının önünde yer alan KONG isimli bir arabirimden bahsetti. O an içimde bir merak uyanmış olsa da aslında sorunların bir an önce çözülmesini istiyordum. Bu yüzden merakımı birkaç hafta sonrasına bıraktım.

Derken artık cumartesi geceleri dışına da taşan saturday-night-worksçalışmalarımda ona yer verme fırsatı yakaladım. Kimdi bu Kong? Müzik grubu olan Kong'muydu yoksa Skull adasındaki iri olan mıydı? Belki de API Gateway'di. Onu Westworld üzerinde çalıştırabilir miydim? Öğrenmin yolu basitti. Sonunda macera başladı. Github çalışmaları tamamlandıktan uzun süre sonra da bloğuma not olarak düşmeye karar verdim.

Hali hazırda çalışmakta olduğum firmada, microservice'lerin orkestrasyonu için KONG kullanılıyor. Kabaca bir API Gateway rolünü üstlenen KONG mikro servislere gelen taleplerle ilgili olarak Load Balancing, Authentication, Rate Limiting, Caching, Logging gibi cross-cutting olarak tabir edebileceğimiz yapıları hazır olarak sunuyor(muş) Web, Mobil ve IoT gibi uygulamalar geliştirirken back-end servisleri çoğunlukla mikro servis formunda yaşamaktalar. Bunların orkestrasyonunda görev alan KONG, Lua dili ile geliştirilmiş, performansı ile ön plana çıkan NGINX üzerinde koşan açık kaynaklı bir proje olmasıyla da dikkat çekiyor.

Benim amacım ilk etapta KONG'u WestWorld(Ubuntu 18.04, 64bit)üzerine kurmak ve en az bir servis geliştirip ona gelen talepleri KONG üzerinden geçirmeye çalışmak(Kabaca proxy rolünü üstlenecek diyebiliriz) Normal şartlarda KONG'u sisteme tüm gereksinimleri ile kurabiliriz ancak denemeler için docker imajlarını kullanmak da yeterli olacaktır ki ben bu yolu tercih ediyorum.

Kobay REST servisleri

Çalışmada en azından bir Web API servisinin olması lazım. Bir tane .net core bir tane de node.js tabanlı servis geliştirmeye karar verdim. Projeler için WestWorld'de uyguladığım terminal komutları şöyle.

mkdir services
cd services
dotnet new webapi -o FabrikamApi
touch Dockerfile
touch .dockerignore
mkdir GameCenterApi
cd GameCenterApi
npm init
sudo npm i --save express body-parser
touch index.js
touch Dockerfile

.Net Core ile geliştirilmiş FabrikamApi servisindeki hazır kod dosyalarında bir kaç değişiklik yapıp, Node.js tabanlı GameCenterApi klasöründeki index.js'i sıfırdan geliştirmem gerekti (Servislerin normal kullanım örneklerine ait Postman dosyasını burada bulabilirsiniz) 

PlayerController içeriği;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using FabrikamApi.Models;

namespace FabrikamApi.Controllers
{
    /*
    PlayersController isimli Controller sınıfı Player türünden bir listeyle çalışıyor.
    Konumuz KONG'u tanımak olduğu için çok detalı bir servis değil.
    Temel Get, Post, Put ve Delete operasyonlarını içermekte.
    Listeyi static bir değişkende tutuyoruz. Dolayısıyla servis sonlandırıldığında bilgiler uçacaktır.
    Ancak isterseniz kalıcı bir repository ekleyebilirsiniz.
     */
    [Route("api/v1/[controller]")]
    [ApiController]
    public class PlayersController : ControllerBase
    {
        private static List<Player> playerList = new List<Player>{
            new Player{Id=1000,Nickname="Hatuta Matata",Level=100}
        };
        [HttpGet]
        public ActionResult<IEnumerable<Player>> Get()
        {
            return playerList;
        }

        [HttpGet("{id}")]
        public ActionResult<Player> Get(int id)
        {
            var p = playerList.Where(item => item.Id == id).FirstOrDefault();
            if (p != null)
            {
                return p;
            }
            else
            {
                return NotFound();
            }
        }

        [HttpPost]
        public void Post([FromBody] Player player)
        {
            playerList.Add(player);
        }

        [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody] Player player)
        {
            var p = playerList.Where(item => item.Id == id).FirstOrDefault();
            if (p != null)
            {
                p.Nickname = player.Nickname;
                p.Level = player.Level;
                return Ok();
            }
            else
            {
                return NotFound();
            }
        }

        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            var p = playerList.Where(item => item.Id == id).FirstOrDefault();
            if (p != null)
            {
                playerList.Remove(p);
                return Ok();
            }
            else
            {
                return NotFound();
            }
        }
    }
}

PlayerController tarafından kullanılan Player sınıfı içeriği;

namespace FabrikamApi.Models
{
    public class Player{
        public int Id { get; set; } 
        public string Nickname { get; set; }
        public int Level { get; set; }
    }
}

FabrikamAPI ye ait Docker ve .dockerignore içerikleri;

FROM microsoft/dotnet:sdk AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .

ENV ASPNETCORE_URLS=http://+:65001

ENTRYPOINT ["dotnet", "FabrikamApi.dll"]

Başlangıçta dotnet:sdk imajından yararlanılacağı bildiriliyor. Çalışma klasörü bildirildikten sonra proje dosyasının kopyalanıp paketlerin yüklenmesi için restore işlemi başlatılıyor. Diğer her şeyin kopylanamasını bir build işlemi takip ediyor ki burada release versiyonu da çıkılıyor. Çalışma zamanı imajı alındıktan sonra 65001 numaralı port yayın noktası olarak belirtiliyor. Son adımsa dll'i çalıştıran dotnet komutunu içermekte. Birde bin ve obj klasörlerinin docker ortamında yer almaması için .dockerignore isimli dosyamız var. İçeriği oldukça basit.

bin\
obj\

games isimli json veri dizisi ile ilgili basit get operasyonları içeren GameCenterApi uygulamasındaki kod içeriklerimiz ise şöyle.

index.js

/*
GameCenterApi'den yayına alınan bu dummy servis games isimli diziyi döndüren iki basit fonksiyonelliğe sahip.
*/
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

const games = [
    {
        id: 1,
        title: 'Red Dragons',
        maxPlayerCount: 10
    },
    {
        id: 2,
        title: 'Green Barrets',
        maxPlayerCount: 24
    },
    {
        id: 3,
        title: 'River Raid',
        maxPlayerCount: 4
    },
    {
        id: 4,
        title: 'A-Team',
        maxPlayerCount: 9
    },
];

app.use(bodyParser.json());

app.get('/api/v1/games', (req, res) => {
    res.json(games);
});

app.get('/api/v1/games/:id', (req, res) => {
    res.json(games[req.params.id]);
});

app.listen(65002, () => {
    console.log(`Oyun servisi aktif! http://localhost:65002/api/v1/games`);
});

DockerFile dosyası

FROM node:carbon

# create work directory
WORKDIR /usr/src/app

# copy package.json
COPY package.json ./
RUN npm install

# copy source code
COPY . .

EXPOSE 65002

CMD ["npm", "start"]

Dosya node.js ortamlarından birisini ifade eden carbon bildirimi ile başlıyor. İmaj buradan alınacak. Çalışma klasörünün oluşturulması, package.json dosyasının burayı alınıp proje bağımlılıklarının install edilmesi, uygulamanın 65002 numaralı porttan ayağa kaldırılması diğer bildirimler olarak karşımıza çıkıyor.

Geliştirme noktasında servislerin çalıştığını kontrol etmemiz gerekiyor. FabrikamAPI isimli .Net Core tabanlı servisi çalıştırmak için,

dotnet run

terminal komutunu verip http://localhost:65001/api/v1/players adresine gidebiliriz. GameCenterApi isimli Node.js tabanlı servisi çalıştırmak içinse package.json içerisine aldığımız start kod adlı script'i işlettirebiliriz.

npm run start

Sonrasında http://localhost:65002/api/v1/games adresi üzerinden bu servisi de test edebiliriz.

localhost bilgisi ilerleyen kısımlarda görüleceği gibi Docker'a geçildikten sonra değişmektedir.

Servislerin Dockerize Edilmesi

Dikkat edilmesi gereken noktalardan birisi de, her iki örneğin Dockerize edilebilecek şekilde Dockerfile dosyaları ile donatılmış olmalarıdır. İlaveten .Net Core uygulamasında .dockerignore dosyası vardır. Bunu build context'ini ufalamak için kullanıyoruz. Docker imajları KONG tarafından kullanılacakları için önemli. 

FabrikamApi uygulaması için Dockerize işlemleri aşağıdaki terminal komutuyla yapılabilir.

docker build -t fma_docker .

GameCenterApi isimli Node.js uygulaması içinse aşağıdaki gibi.

docker build -t gca_docker .

Dockerize işlemleri tamamlandıktan sonra container'ları çalıştırıp kontrol etmemizde yarar var. İlk iki komutla ayağa kaldırıp son komutla listede olup olmadıklarına bakıyoruz.

docker run -d --name=game_center_api gca_docker
docker run -d --name=fabrikam_api fma_docker
docker ps -a

WestWord'de durum aşağıdaki gibi.

Docker imajları çalışmaya başladıktan sonra servislere hangi IP adresi üzerinden gitmemiz gerektiğine bakmak için 'docker inspect game_center_api' ve 'docker inspect fabrikam_api' komutlarından yararlanabiliriz. Bize uzun bir Json içeriği dönecektir ancak son kısımda IPAddress bilgisini yakalayabiliriz. WestWorld için docker tabanlı adresler http://172.17.0.3:65001/api/v1/players ve http://172.17.0.2:65002/api/v1/games şeklinde oluştu. Sizin sisteminizde bu IP adresleri farklı olabilir.

Kong Kurulumları ve Docker Servislerinin Dahil Edilmesi

Tüm işlemleri Docker Container'lar üzerinde yapacağız. Bu nedenle kendimize yeni bir ağ oluşturarak işe başlamakta yarar var. Aşağıdaki terminal komutları ile devam edelim.

docker network create sphere-net

docker run -d --name kong-db --network=sphere-net -p 5555:5432 -e "POSTGRES_USER=kong" -e "POSTGRES_DB=kong" postgres:9.6

docker run --rm --network=sphere-net -e "KONG_DATABAE=postgres" -e "KONG_PG_HOST=kong-db" kong:latest kong migrations bootstrap

docker run -d --name kong --network=sphere-net -e "KONG_LOG_LEVEL=debug" -e "KONG_DATABASE=postgres" -e "KONG_PG_HOST=kong-db" -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" -e "KONG_PROXY_ERROR_LOG=/dev/stderr" -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" -p 9000:8000 -p 9443:8443 -p 9001:8001 -p 9444:8444 kong:latest
  • İlk komutla sphere-net isimli bir docker network'ü oluşturuyoruz.
  • İkinci uzun komutla Postgres veri tabanı için bir Container başlatıyoruz. sphere-net ağında çalışacak olan veri tabanını KONG kullanacak. KONG, veri tabanı olarak Postgres veya Cassandra sistemlerini destekliyor. Eğer yerel makinede Postgres imajı yoksa (ki örneği denediğim dönemde WestWorld'de yoktu) pull işlemi biraz zaman alabilir.
  • Üçüncü komutla Postgres veri tabanının KONG için hazırlanması söz konusu.
  • Dördüncü ve uzuuuuuun bir parametre listesine sahip komutla da KONG Container'ını çalıştırıyoruz (üşenmedim, kopyalamadan yazdım. Siz de öyle yapın)

Bu adımlardan sonra kong ve postgres ile ilgili Container'ların çalıştığını teyit etmeliyiz.

Hatta http://localhost:9001 adresine bir HTTP GET talebi attığımızda konfigurasyon ayarlarını da görebiliriz. 9001 portu (Normal kurulumda 8001 de olabilir) yönetsel işlemlerin bulunduğu servis katmanıdır. Service ekleme, silme, görüntüleme ve güncelleme gibi işlemler 9001 portundan ulaşılan servisçe yapılır (Route yönetimi içinde aynı şey söz konusudur)

Komutlar biter mi? Şimdi servislere ait Container'ları sphere-net üzerinde çalışacak şekilde ayağa kaldırmalıyız.

docker run -d --name=game_center_api --network=sphere-net gca_docker
docker run -d --name=fabrikam_api --network=sphere-net fma_docker
docker ps -a

KONG için bir Docker Network oluşturduk. Bu ağa dahil olan ne kadar Container varsa IP adresleri farklılık gösterecektir. sphere-net'e dahil olan Container'ların host edildiği IP adreslerini öğrenmek için terminalden 'docker inspect sphere-net' komutunu çalıştırabiliriz.

Çalışma Zamanı (Bir başka deyişle KONG üzerinde servislerin ayarlanması)

KONG, veri tabanı olarak kullanılan Postgres ve geliştirdiğimiz iki REST Servisine ait Docker Container'ları ayakta. WestWorld'deki güncel duruma göre

  • http://172.19.0.4:65002/api/v1/games adresinde Node.js tabanlı servisimiz yaşıyor.
  • http://172.19.0.5:65001/api/v1/players adresinde ise .Net Core Web API servisimiz bulunuyor.

Amacımız şu anda localhost:9000 adresli KONG servisine gelecek olan games ve players odaklı talepleri aslı servislere iletmek. Yani KONG ilk etapta bir Proxy servis şeklinde davranış gösterecek. Bunun için öncelikle servislerimizi KONG'a eklemeliyiz. KONG'a eklenen servisler http://localhost:9001/services adresinden izlenebilir ve hatta yönetilebilirler. Şimdi bu adrese aşağıdaki içeriğe sahip POST komutunu gönderelim (Postman ile yapabilir veya curl komutu ile terminalden icra edebiliriz)

URL : http://localhost:9001/services
Method : HTTP Post
Content-Type : application/json
Body :
{
"name":"api-v1-games",
"url":"http://172.19.0.4:65002/api/v1/games"
}

Bu işlemi FabrikamAPI içinde yaptıktan sonra http://localhost:9001/services adresine gidersek servis bilgilerini görebiliriz.

Servisleri eklemek yeterli değil. Route tanımlamalarını da yapmak gerekiyor (KONG tarafındaki entrypoint tanımlamaları için gerekli bir aksiyon olarak düşünebiliriz) KONG services'e aşağıdaki içeriğe sahip talepleri göndererek gerekli route tanımlamaları yapılabilir.

URL: http://localhost:9001/services/api-v1-players/routes
Method : HTTP Post
Content-Type : application/json
Body :
{
"hosts":["api.ct.id"],
"paths":["/api/v1/players"]
}
URL: http://localhost:9001/services/api-v1-games/routes
Method : HTTP Post
Content-Type : application/json
Body :
{
"hosts":["api.ct.id"],
"paths":["/api/v1/games"]
}

Oluşan route bilgilerini http://localhost:9001/routes adresinden görebiliriz. Her iki servis için gerekli route tanımlamaları başarılı bir şekilde yapıldıktan sonra KONG üzerinden GameCenterAPI ve FabrikamAPI servislerine erişebiliyor olmamız gerekir.

Yararlandığım Diğer Docker Komutları

Örneği geliştirirken yararlandığım bazı Docker komutları da oldu. Mesela çalışan Container'ları stop komutu sonrası durduramayınca,

sudo killall docker-containerd-shim

Container'larımı görmek için,

docker ps -a

Container'ları sık sık remove etmem gerektiğinden,

docker rm {ContainerID}

Container'ın tüm bilgilerini görmem gerektiğinde de(özellikle IP adresini)

docker inspect {container adı}
docker inspect {ağ adı}

Ben Neler Öğrendim

Doğruyu söylemek gerekirse Saturday-Night-Worksçalışmalarının herbirisi bana tahmin ettiğimden de çok şey öğretiyor. 33 numaralı örnekten yanıma kar olarak kalanları şöyle sıralayabilirim.

  • KONG'un temel olarak ne işe yaradığını
  • .Net Core ve Node.js tabanlı servis uygulamaları için Dockerfile dosyalarının nasıl hazırlanacağını
  • KONG a bir servis ve route bilgisinin nasıl eklenebileceğini
  • Bolca Docker terminal komutunu
  • Docker Container içine açılan uygulamaların asıl IP adreslerini nasıl görebileceğimi

Bu macerada API Gateway olarak kullanılabilen KONG isimli ürünü bir Linux platformunda docker imajları üzerinde deneyimlemeye çalıştık. Böylece geldik bir maceramızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/angular-ile-basit-bir-gorevler-listesi-uygulamasi-yazmakAngular ile Basit Bir Görevler Listesi Uygulaması Yazmak

$
0
0

Merhaba Arkadaşlar,

Bazen ne kadar basit olursa olsun üşenmeden bir örneğin üstüne gitmek gerekiyor. Çünkü çok basit örneklerle çalışıyor olsak bile gözümüzden kaçan önemli detaylar olabilir. Günümüzde kullanmakta olduğumuz pek çok geliştirme çatısı, belli ürünlere yönelik hazır şablonları kolayca üretebileceğimiz komut setleri sunmakta.

Boilerplate olarak da ifade edebileceğimiz bu enstrümanlar sayesinde bir anda işler halde karşımıza çıkan uygulamalarla karşılaşıyoruz. Ancak ürüne hakim olabilmek, rahatça sağını solunu bükebilmek için hazır gelen şablonları bile kurcalamak gerekiyor. Benim Saturday-Night-Works birinci fazında sıklıkla icra ettiğim bir eğitim süreci bu. Angular, Blazor, React ve benzeri konu başlıklarında hazır hello world şablonları ile sıklıkla karşılaştım. Onları eğip bükerek daha çok şey öğrenmeye çalıştım. Sonuçta tecrübe etmediğimiz sürece bilgi dağarcığımız genişleyemez, yanılıyor muyum? Öyleyse gelin 09 numaralı çalışmayı kayıt altına alalım.

Angular ürünü web, mobil ve masaüstü uygulamalar geliştirmek için kullanılan Javascript tabanlı açık kaynak bir web çatısı olarak karşımıza çıkıyor. Uzun zamandır hayatımızda olan ve endüstüriyel anlamda kendisini kanıtlamış bir ürün. Pek tabii sıklıkla Vue ve React ile karşılaştırıldığına da şahit oluyoruz. Ben Saturday-Night-Works çalışmaları kapsamında herbiriyle ilgili en temel seviyede örnekler geliştirmeye de çalıştım. Nitekim bırakın bunları birbirleriyle karşılaştırmayı, gözü kapalı Hello World uygulamaları nasıl yazılır bile bilmiyordum.

Hali hazırda çalıştığım şirketteki yeni nesil uygulamalarda ağırlıklı olarak Vue.js kullanılıyor olsa da yeni özellikler eklemek için var olan öğelere bakıyorduk. Dolayısıyla Angular tarafında sıcak kalmaya çalışmak adına basit bir örnekle başlamak yerinde bir karardı. Bende böyle yapmışım. Örnekte kendime bir görev listesi oluşturuyorum. Sadece yeni giriş ve silme fonksiyonu olsa da bir şeyler öğrendim diyebilirim("ToDo List" en yaygın Hello World örnekleri arasında yer alıyor) Uygulamayı her zaman ki gibi Visual Studio Code ile WestWorld(Ubuntu 18.04, 64bit)üzerinde icra etmekteyim.

Ön Gereklilikler

Tabii işin başında bize bir takım alet edevatlar gerekiyor. node ve npm sistemde olması gerekenler. WestWorld'de bu araçlar zaten var(Yani sizin sisteminizde yoksa edinmelisiniz) npm'i Angular için Command Line Interface(CLI) aracını yüklemek maksadıyla kullanıyoruz. Kurulum için gerekli terminal komutu şöyle, 

sudo npm install -g @angular/cli

Angular CLI ile projeyi oluşturmak oldukça basit. Önyüz tarafının görselliğini arttırmak adına Bootstrap kullanabiliriz. Tabii öncelikle ilgili bootstrap paketlerini sisteme dahil etmemiz gerekiyor. Bunu bower yöneticisinden yararlanarak aşağıdaki terminal komutu ile yapabiliriz.

bower i bootstrap

Angular projesini oluşturduktan sonra bootstrap'in CSS dosyalarını assets/css altına alıp orayı referans etmeyi tercih ettim(index.html sayfasına bakın) Lakin Bootstrap için CDN adreslerini de pekala kullanabiliriz.

Angular Uygulamasının Oluşturulması

Angular uygulamasını hazır şablonundan üretmek oldukça kolay ve sıklıkla tercih edilen yollardan birisi. Tek yapmamız gereken terminalden ng new komunutunu çalıştırmak. new sonrası gelen parametre tahmin edileceği üzere uygulamamızın adı olacak.

ng new life-pbi-app

ng new sonrası oluşan proje içerisinde çok fazla dosya bulunacaktır. Şu haliyle de uygulamayı çalıştırıp sonuçlarını görebiliriz ama başta da belirttiğim üzere biraz eğip bükmek lazım. Benim yaptığım değişiklikler son derece basit. Sonuçta tek bir arayüzüm olacak ki bu index.html. Önyüzde gösterilecek bileşenimiz ise yine şablon ile hazır olarak gelen app.component. Ona ait HTML içeriğini örnek için aşağıdaki gibi değiştirebiliriz.

app.component.html

<div class="container"><form><div class="form-group"><h1 class="text-center text-success">Çalışma Planım...</h1><p>Burada 1 haftalık kişisel görev planlarıma yer vermekteyim. Mesela <i>"bu hafta 10 km yürüyüş yapacağım"</i></p><div class="card input-group-prebend"><div class="card-body"><input type="text" #job class="form-control" placeholder="Salı günü 100 faul atışı çalışacağım..." name="job"
            ngModel><!-- addJob metodundaki job nesnesi üst kontroldeki #job niteliğidir. 
              value özelliğine giderek girilen bilgiyi addJob metoduna göndermiş oluyoruz. --><input type="button" class="btn btn-info" (click)="addJob(job.value)" value="Ekle" /></div></div><!-- ngFor ile jobs dizisinde dolaşıyoruz ve her bir eleman için 
        card stilinde birer div oluşturulmasını sağlıyoruz
      --><div *ngFor="let job of jobs" class="card"><div class="card-body"><div class="row"><div class="col-sm-10">
              {{job}} <!-- dizideki görevin bilgisini yazdırıyoruz--></div><div class="col-sm-2"><!-- Silme işlemi için removeJob fonksiyonu çağrılıyor. 
                Parametre ise dizinin o anki elemanı--><input type="button" class="btn btn-primary" (click)="removeJob(job)" value="Çıkart" /></div></div></div></div></div></form></div><!-- addJob, removeJob metodları ile jos dizisi app.component.ts dosyası içerisinde yer alıyor -->

component içerisinde basit bir form grubu var. İçinde iki adet bileşen gövdesi bulunuyor. Üst taraf yeni görev girmek için kullanılan kısım. Ekle başlıklı düğmeye basıldığındaysa Typescript tarafındaki addJob metodu çağırılıyor. Parametre olarak job isimli text kontrolünün içeriği gönderilmekte.

Alt tarafta yer alan gövde içindeyse bir for döngüsünden yararlanılarak tüm görev listenin basıldığı satırlar bulunuyor. Çıkart başlıklı düğmeye basıldığında devreye giren removeJob fonksiyonu parametre olarak döngünün o anki Job nesne örneğini almakta ki bunu silme işlemi için kullanıyoruz. Burada aslında güncelleme içinde bir şeyler yapmak gerekiyor. Ne var ki çalışma sırasında bunu atlamışım. Kuvvetle muhtemel üşendiğim içindir. Siz güncelleme için ayrı bir bileşene yönlendirmeyi deneyebilirsiniz(ki ben ilerleyen safhalarda Firebase ile ilgili bir kullanımı da denemişim. Magic Johnson numaralı örnek. Onu da bir ara bloğa kayıt altına almalıyım)

app.component.ts (typescript tabanlı bileşenimiz) 

import { Component } from '@angular/core';
import { isJsObject } from '@angular/core/src/change_detection/change_detection_util';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html', //Bu Typescript dosyasının hangi html ile ilişkili olduğu belirtiliyor
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  jobs = []; //görev listesinin tutulacağı dizi

  // yeni bir job eklemek için
  addJob(value) {
    if (value !== "") {
      this.jobs.push(value)
      // console.log(this.jobs)  // Tarayıcı console penceresine log düşürebiliriz
    } else {
      alert('Bir görev girmelisin... ;)')
    }
  }

  // bir görevi listeden çıkartmak için
  removeJob(job) {
    for (let i = 0; i <= this.jobs.length; i++) {
      if (job == this.jobs[i]) {
        this.jobs.splice(i, 1)
      }
    }
  }
}

Bileşenin Typescript tabanlı arka tarafı görev ekleme ve silme operasyonlarını içermekte. app-root ile ilişkilendirilmiş durumda(Bu, index.html sayfasındaki yerleşim için önemli bir bilgi)Örneğin basit olması amacıyla görev listesi uygulama çalıştığı sürece bellekte duran bir diziyi kullanıyor. Elbette bunu farklı bir veri kaynağına bağlayabiliriz. Mesela Azure Cosmos DB veya SQLite gibi veri kaynaklarının kullanılması tercih edilebilir. Son olarak ilgili bileşenin gösterildiği index.html içeriği de aşağıdaki gibi değiştirilebilir. 

<!doctype html><html><head><meta charset="utf-8"><title>Kişisel PBI Listem</title><base href="http://www.buraksenyurt.com/"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" type="image/x-icon" href="favicon.ico"><link rel="stylesheet" href="assets/css/bootstrap.min.css" /></head><body><app-root>Az sabır. Yükleniyor daaa!</app-root></body></html>

Çalışma zamanı

Uygulamayı çalıştırmak için aşağıdaki terminal komutunu vermek yeterli.

ng server

Buna göre http://localhost:4200 adresine talep gönderirsek uygulamamıza ulaşabiliriz(URL bilgisi javascript dosyalarından birisinde de parametrik olarak bulunuyor. Geliştirme ortamı için değiştirmek isteyebilirsiniz diye söylüyorum ;) ) Uygulamanın çalışma zamanına ait örnek bir görüntüde şöyle.

Ben Neler Öğrendim?

Saturday-Night-Works birinci fazındaki ilk acemilik uygulamalarımdan birisi olan 09 numaralı örneğin de bana kattığı bir takım şeyler oldu tabii. Bunları genişletirsek aşağıdaki gibi listeleyebilirim.

  • Typescript ile HTML tarafındaki Angular yapılarının nasıl anlaştığını
  • Bootstrap'i bir Angular projesinde nasıl kullanabileceğimi
  • component üzerindeki button kontrollerinden Typescript olaylarının nasıl tetiklendiğini
  • Temel ng terminal komutlarını

Böylece geldik bir maceramızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Angular ile Basit Bir Görevler Listesi Uygulaması Yazmak

$
0
0

Merhaba Arkadaşlar,

Bazen ne kadar basit olursa olsun üşenmeden bir örneğin üstüne gitmek gerekiyor. Çünkü çok basit örneklerle çalışıyor olsak bile gözümüzden kaçan önemli detaylar olabilir. Günümüzde kullanmakta olduğumuz pek çok geliştirme çatısı, belli ürünlere yönelik hazır şablonları kolayca üretebileceğimiz komut setleri sunmakta.

Boilerplate olarak da ifade edebileceğimiz bu enstrümanlar sayesinde bir anda işler halde karşımıza çıkan uygulamalarla karşılaşıyoruz. Ancak ürüne hakim olabilmek, rahatça sağını solunu bükebilmek için hazır gelen şablonları bile kurcalamak gerekiyor. Benim Saturday-Night-Works birinci fazında sıklıkla icra ettiğim bir eğitim süreci bu. Angular, Blazor, React ve benzeri konu başlıklarında hazır hello world şablonları ile sıklıkla karşılaştım. Onları eğip bükerek daha çok şey öğrenmeye çalıştım. Sonuçta tecrübe etmediğimiz sürece bilgi dağarcığımız genişleyemez, yanılıyor muyum? Öyleyse gelin 09 numaralı çalışmayı kayıt altına alalım.

Angular ürünü web, mobil ve masaüstü uygulamalar geliştirmek için kullanılan Javascript tabanlı açık kaynak bir web çatısı olarak karşımıza çıkıyor. Uzun zamandır hayatımızda olan ve endüstüriyel anlamda kendisini kanıtlamış bir ürün. Pek tabii sıklıkla Vue ve React ile karşılaştırıldığına da şahit oluyoruz. Ben Saturday-Night-Works çalışmaları kapsamında herbiriyle ilgili en temel seviyede örnekler geliştirmeye de çalıştım. Nitekim bırakın bunları birbirleriyle karşılaştırmayı, gözü kapalı Hello World uygulamaları nasıl yazılır bile bilmiyordum.

Hali hazırda çalıştığım şirketteki yeni nesil uygulamalarda ağırlıklı olarak Vue.js kullanılıyor olsa da yeni özellikler eklemek için var olan öğelere bakıyorduk. Dolayısıyla Angular tarafında sıcak kalmaya çalışmak adına basit bir örnekle başlamak yerinde bir karardı. Bende böyle yapmışım. Örnekte kendime bir görev listesi oluşturuyorum. Sadece yeni giriş ve silme fonksiyonu olsa da bir şeyler öğrendim diyebilirim("ToDo List" en yaygın Hello World örnekleri arasında yer alıyor) Uygulamayı her zaman ki gibi Visual Studio Code ile WestWorld(Ubuntu 18.04, 64bit)üzerinde icra etmekteyim.

Ön Gereklilikler

Tabii işin başında bize bir takım alet edevatlar gerekiyor. node ve npm sistemde olması gerekenler. WestWorld'de bu araçlar zaten var(Yani sizin sisteminizde yoksa edinmelisiniz) npm'i Angular için Command Line Interface(CLI) aracını yüklemek maksadıyla kullanıyoruz. Kurulum için gerekli terminal komutu şöyle, 

sudo npm install -g @angular/cli

Angular CLI ile projeyi oluşturmak oldukça basit. Önyüz tarafının görselliğini arttırmak adına Bootstrap kullanabiliriz. Tabii öncelikle ilgili bootstrap paketlerini sisteme dahil etmemiz gerekiyor. Bunu bower yöneticisinden yararlanarak aşağıdaki terminal komutu ile yapabiliriz.

bower i bootstrap

Angular projesini oluşturduktan sonra bootstrap'in CSS dosyalarını assets/css altına alıp orayı referans etmeyi tercih ettim(index.html sayfasına bakın) Lakin Bootstrap için CDN adreslerini de pekala kullanabiliriz.

Angular Uygulamasının Oluşturulması

Angular uygulamasını hazır şablonundan üretmek oldukça kolay ve sıklıkla tercih edilen yollardan birisi. Tek yapmamız gereken terminalden ng new komunutunu çalıştırmak. new sonrası gelen parametre tahmin edileceği üzere uygulamamızın adı olacak.

ng new life-pbi-app

ng new sonrası oluşan proje içerisinde çok fazla dosya bulunacaktır. Şu haliyle de uygulamayı çalıştırıp sonuçlarını görebiliriz ama başta da belirttiğim üzere biraz eğip bükmek lazım. Benim yaptığım değişiklikler son derece basit. Sonuçta tek bir arayüzüm olacak ki bu index.html. Önyüzde gösterilecek bileşenimiz ise yine şablon ile hazır olarak gelen app.component. Ona ait HTML içeriğini örnek için aşağıdaki gibi değiştirebiliriz.

app.component.html

<div class="container"><form><div class="form-group"><h1 class="text-center text-success">Çalışma Planım...</h1><p>Burada 1 haftalık kişisel görev planlarıma yer vermekteyim. Mesela <i>"bu hafta 10 km yürüyüş yapacağım"</i></p><div class="card input-group-prebend"><div class="card-body"><input type="text" #job class="form-control" placeholder="Salı günü 100 faul atışı çalışacağım..." name="job"
            ngModel><!-- addJob metodundaki job nesnesi üst kontroldeki #job niteliğidir. 
              value özelliğine giderek girilen bilgiyi addJob metoduna göndermiş oluyoruz. --><input type="button" class="btn btn-info" (click)="addJob(job.value)" value="Ekle" /></div></div><!-- ngFor ile jobs dizisinde dolaşıyoruz ve her bir eleman için 
        card stilinde birer div oluşturulmasını sağlıyoruz
      --><div *ngFor="let job of jobs" class="card"><div class="card-body"><div class="row"><div class="col-sm-10">
              {{job}} <!-- dizideki görevin bilgisini yazdırıyoruz--></div><div class="col-sm-2"><!-- Silme işlemi için removeJob fonksiyonu çağrılıyor. 
                Parametre ise dizinin o anki elemanı--><input type="button" class="btn btn-primary" (click)="removeJob(job)" value="Çıkart" /></div></div></div></div></div></form></div><!-- addJob, removeJob metodları ile jos dizisi app.component.ts dosyası içerisinde yer alıyor -->

component içerisinde basit bir form grubu var. İçinde iki adet bileşen gövdesi bulunuyor. Üst taraf yeni görev girmek için kullanılan kısım. Ekle başlıklı düğmeye basıldığındaysa Typescript tarafındaki addJob metodu çağırılıyor. Parametre olarak job isimli text kontrolünün içeriği gönderilmekte.

Alt tarafta yer alan gövde içindeyse bir for döngüsünden yararlanılarak tüm görev listenin basıldığı satırlar bulunuyor. Çıkart başlıklı düğmeye basıldığında devreye giren removeJob fonksiyonu parametre olarak döngünün o anki Job nesne örneğini almakta ki bunu silme işlemi için kullanıyoruz. Burada aslında güncelleme içinde bir şeyler yapmak gerekiyor. Ne var ki çalışma sırasında bunu atlamışım. Kuvvetle muhtemel üşendiğim içindir. Siz güncelleme için ayrı bir bileşene yönlendirmeyi deneyebilirsiniz(ki ben ilerleyen safhalarda Firebase ile ilgili bir kullanımı da denemişim. Magic Johnson numaralı örnek. Onu da bir ara bloğa kayıt altına almalıyım)

app.component.ts (typescript tabanlı bileşenimiz) 

import { Component } from '@angular/core';
import { isJsObject } from '@angular/core/src/change_detection/change_detection_util';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html', //Bu Typescript dosyasının hangi html ile ilişkili olduğu belirtiliyor
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  jobs = []; //görev listesinin tutulacağı dizi

  // yeni bir job eklemek için
  addJob(value) {
    if (value !== "") {
      this.jobs.push(value)
      // console.log(this.jobs)  // Tarayıcı console penceresine log düşürebiliriz
    } else {
      alert('Bir görev girmelisin... ;)')
    }
  }

  // bir görevi listeden çıkartmak için
  removeJob(job) {
    for (let i = 0; i <= this.jobs.length; i++) {
      if (job == this.jobs[i]) {
        this.jobs.splice(i, 1)
      }
    }
  }
}

Bileşenin Typescript tabanlı arka tarafı görev ekleme ve silme operasyonlarını içermekte. app-root ile ilişkilendirilmiş durumda(Bu, index.html sayfasındaki yerleşim için önemli bir bilgi)Örneğin basit olması amacıyla görev listesi uygulama çalıştığı sürece bellekte duran bir diziyi kullanıyor. Elbette bunu farklı bir veri kaynağına bağlayabiliriz. Mesela Azure Cosmos DB veya SQLite gibi veri kaynaklarının kullanılması tercih edilebilir. Son olarak ilgili bileşenin gösterildiği index.html içeriği de aşağıdaki gibi değiştirilebilir. 

<!doctype html><html><head><meta charset="utf-8"><title>Kişisel PBI Listem</title><base href="http://www.buraksenyurt.com/"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" type="image/x-icon" href="favicon.ico"><link rel="stylesheet" href="assets/css/bootstrap.min.css" /></head><body><app-root>Az sabır. Yükleniyor daaa!</app-root></body></html>

Çalışma zamanı

Uygulamayı çalıştırmak için aşağıdaki terminal komutunu vermek yeterli.

ng server

Buna göre http://localhost:4200 adresine talep gönderirsek uygulamamıza ulaşabiliriz(URL bilgisi javascript dosyalarından birisinde de parametrik olarak bulunuyor. Geliştirme ortamı için değiştirmek isteyebilirsiniz diye söylüyorum ;) ) Uygulamanın çalışma zamanına ait örnek bir görüntüde şöyle.

Ben Neler Öğrendim?

Saturday-Night-Works birinci fazındaki ilk acemilik uygulamalarımdan birisi olan 09 numaralı örneğin de bana kattığı bir takım şeyler oldu tabii. Bunları genişletirsek aşağıdaki gibi listeleyebilirim.

  • Typescript ile HTML tarafındaki Angular yapılarının nasıl anlaştığını
  • Bootstrap'i bir Angular projesinde nasıl kullanabileceğimi
  • component üzerindeki button kontrollerinden Typescript olaylarının nasıl tetiklendiğini
  • Temel ng terminal komutlarını

Böylece geldik bir maceramızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/bir-net-core-web-api-servisini-minikube-uzerinden-calistirmakBir .Net Core Web API Servisini Minikube Üzerinden Çalıştırmak

$
0
0

Merhaba Arkadaşlar,

Soğuk bir Şubat akşamı mıydı, dışarıda kar var mıydı, günün tam olarak hangi vakitleriydi tam olarak hatırlamıyorum ama github'a göre 24 numaralı örneğin son check-in işlemi 20 şubat Çarşamba günüydü.

Saturday-Night-Worksçalışmalarına başladığımda hedefim sadece Cumartesi geceleri olmasına rağmen içten gelen bir motivasyon konulara haftanın herhangi bir gününde bakmamı sağlıyordu. Genelde ilgi çekici konular seçtiğimden başta belirlediğim standart çalışma takviminin dışına çıkmıştım. Bu hevesli motivasyon birinci fazın(41 bölümlük ilk faz olarak ifade edebilirim) tamamı boyunca süregeldi.

İç motivasyon kişisel gelişim açısından bence çok önemli bir sürükleyici. Doğruyu söylemek gerekirse onu bulduğumuz anda bir çalışmanın peşinden koşturmamıza da gerek kalmıyor. Kendiliğinden gelen disiplin bizi zaten o alana odaklıyor ve sonrasında fırtınada gemisini ustalıkla kullanırken yüksek sesle şarkılar söyleyen mutlu kaptan misali zaman prüssüzce akıyor.

İşte o Şubat günü bu motivasyonla tamamladığım bir çalışmam olmuş. Minikube konusunu incelemişim. Şimdi notların üstünden geçip derleme ve öğrendiklerimi gözden geçirme sırası. Haydi başlayalım.

Birden fazla konteyner'ın bir araya geldiği(Docker container'larını düşünelim), yönetilmeleri(Manegement), kolayca dağıtılmaları(Deployment) küçülerek veya büyüyerek ölçeklenebilmeleri(Scaling) gerektiği durumlarda orkestrasyon işi çoğunlukla Kubernetes(k8s) tarafından sağlanmakta. Kubernetes bir konteyner kümeleme(Clustering) aracı olarak Google tarafından Go dili ile yazılmış bir ürün. Ancak bazen deneme amaçlı olarak geliştirdiğimiz enstrümanları k8s kurulumuna ihtiyaç duymadan tek küme(Cluster)üzerinde çalışacak şekilde kurgulamak isteyebiliriz. Bu noktada minikube oldukça işimize yaramaktadır.

Benim 24 numaralı bu Saturday-Night-Worksçalışmamdaki amacım Kubertenes'i WestWorld(Ubuntu 18.04, 64bit)üzerine kurmak yerine onu development ortamları için deneyimlememizi sağlayan Minikube'ü tanımaktı(Kubernetes'in tüm küme yapısının kurulumu çok da kolay değil. Üstelik sadece geliştirme noktasında onu denemek istersek bu maliyete girmeye gerek yok kanısındayım) Çalışma sırasında, .Net Core tabanlı bir Web API servisini içeren Docker konteynerının Minikube üzerinde koşturulması için gerekli işlemlere yer veriliyor.

Minikube sayesinde Kubernetes ortamını local bir makinede deneme şansımız oluyor(Tabii belirli kısıtlar dahilinde) Minikube, VirtualBox veya muadili bir sanal makine içinde tek node olarak çalışan bir Kubernetes kümesi sunmakta. Dolayısıyla geliştirme katmanı için ideal bir ortam.

İlk Kurulumlar

Linux ortamında Virtual Box isimli sanal makineye, Docker'a, Minikube ve onu komut satırından kontrol eden kubectl araçlarına ihtiyacımız var. WestWorld'de docker yüklü olduğu için diğerlerini kurarak ilerlemeye çalıştım. Virtual Box kurulumu için aşağıdaki terminal komutlarını kullanabiliriz.

sudo add-apt-repository multiverse
sudo apt-get update
sudo apt install virtualbox

Minukube kurulumunu içinse şöyle ilerleyebiliriz (Minikube ve bağımlılıklarının platforma göre farklı kurulumları için şu adrese bakabilirsiniz)

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.34.0/minikube-linux-amd64 && chmod +x minikube && sudo cp minikube /usr/local/bin/ && rm minikube

Kubernetes'i komut satırından yönetebilmek için kullanacağımız Kubectl aracını kurmak içinse aşağıdaki adımları takip edebiliriz.

sudo apt-get update && sudo apt-get install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubectl

Kurulum Sonrası Kontroller

Pek tabii kurulumlar sonrası bir sistem kontrolü yapmamızda yarar var. Docker, Virtual Box, Minikube ve kubectl gibi dört enstrümanın bir arada yaşayacağı bir geliştirme söz konusu. İlk olarak minikube servisini başlatmak lazım. Aşağıdaki terminal komutu ile bunu yapabiliriz.

minikube start

Hatta Minikube başarılı bir şekilde başladıktan sonra Virtual Box ortamından servis durumunu kontrol edebiliriz de. Aşağıdaki ekran görüntüsünde olduğu gibi minikube servisinin running modda görünmesi iyiye işarettir.

Çok doğal olarak servisi durdurma ve hatta silme ihtiyacımız da olabilir denemeler sırasında. Örneğin Minikube servisini durdurmak için,

minikube stop

silmek içinse,

minikube delete

komutlarından yararlanabiliriz.

Örnek .Net Core Web API Uygulamasının Geliştirilmesi

Minikube orkestrasyonunda yönetmek istediğimiz servis veya servisler olması gerekiyor. Ben çalışma kapsamında geliştirme noktasında daha rahat hareket edebildiğim için .Net Core platformunu tercih ettim. Servis uygulaması Docker üzerinde koşacak. Oluşturmak için aşağıdaki terminal komutuyla ilerleyebiliriz.

dotnet new webapi -o InstaceAPI

InstanceAPI isimli servisimiz rastgele isim dönen bir metod sunmakta ki servisin ne iş yaptığı bu örnek özelinde çok önemli değil aslında. Ama pek tabii kodsal olarak yaptıklarımızı özetleyerek ilerlemekte yarar var. Ben varsayılan olarak gelen ValuesController sınıfını NamesController olarak aşağıdaki gibi değiştirdim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace InstanceAPI.Controllers
{
    [Route("api/random/[controller]")]
    [ApiController]
    public class NamesController : ControllerBase
    {
        List<string> nameList=new List<string>{
            "Senaida","Armand","Yi","Tyra","Maud",
            "Dominque","Jayme","Amira","Salome","Anisa",
            "Spencer","Angelyn","Pete","Hoa","Cherelle",
            "Lavonne","Gladys","Adrianne","Gussie","Delmar"
        };
        // HTTP Get talebine cevap veren metodumuz.
        // nameList koleksiyonundan rastgele bir isim döndürüyor
        [HttpGet]
        public ActionResult<string> Get()
        {
            Random randomizer=new Random();
            var number=randomizer.Next(0,21);
            return nameList[number];
        }
    }
}

Web API uygulamasını Dockerize etmek için aşina olduğunuz üzere Dockerfile dosyasına ihtiyacımız var ki onu da aşağıdaki şekilde kodlayabiliriz.

# Microsoft'ın dotnet sdk imajını aldık
FROM microsoft/dotnet:sdk AS build-env
# takip eden komutları çalıştıracağımız klasörü set ettik
WORKDIR /app

# Gerekli dotnet kopyalamalarını yaptırıp
# Restore ve publish işlemlerini gerçekleştiriyoruz
COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

# Çalışma zamanı imajının oluşturulmasını istiyoruz
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
# Uygulamanın giriş noktasını belirtiyoruz
ENTRYPOINT [ "dotnet","InstanceAPI.dll" ]

Minikube içerisine neyin deploy edileceğini belirtmek için şimdilik deployment.yaml isimli dosyadan yararlanabiliriz. Bildirimlerden de görüldüğü üzere random-names-api-netcore isimli bir dağıtım söz konusu. Buna ait replica, label ve container gibi kubernetes odaklı bilgiler doküman içerisinde yazılmış durumda (Henüz bu konulara tam vakıf değilim. Çalışmaya devam)

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: random-names-api-netcore
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: random-names-api-netcore
    spec:
      containers:
        - name: random-names-api-netcore
          imagePullPolicy: Never
          image: random-names-api-netcore
          ports:
          - containerPort: 80

 

Docker Hazırlıkları

Dockerfile dosyası tamamlandıktan sonra Web API uygulamasının dockerize edilmesine başlanabilir. Sonuçta k8s ya da örnekte ele aldığımız Minikube'ün ana görevi dockerize edilmiş örneklerin orkestrasyonunun sağlanması. Dockerize işlemi için build komutunu aşağıdaki gibi kullanmamız yeterli olacaktır.

docker build -t random-names-api-netcore .

Minikube Deployment Hazırlıkları

Docker imajı hazır olduktan sonra artık minikube için gerekli dağıtım işlemine geçilebilir. Bu notkada kubectl komut satırı aracından yararlanmaktayız. kubectl, deployment.yaml dosyasının içeriğini kullanarak bir dağıtım işlemi icra edecektir. Aşağıdaki terminal komutları ile bu işlemleri gerçekleştirebiliriz. Bu işlemlere başlamadan önce minikube servisinin çalışır durumda olması gerektiğini hatırlatmak isterim.

kubectl create -f deployment.yaml
kubectl get deployments
kubectl get pods

create sonrasında kullanılan get komutları ile dağıtımı yapılan enstrümanı ve Podları görebiliriz(Pod = Aynı host üzerine dağıtımı yapılan bir veya daha fazla container olarak düşünülebilir ki senaryomuzda minikube için 3 pod söz konusudur) Lakin pod içeriklerine bakıldığında image durumlarının ErrImageNeverPull şeklinde kalmış olması gibi bir durum söz konusudur. En azından WestWorld'de böyle bir sorunla karşılaştığımı ifade edebilirim.

Sorun, minikube ile docker'ın birbirlerinden haberdar olmamalarından kaynaklanmaktaymış. Problemi aşmak için eval komutundan yararlanmak ve sonrasında docker imajını tekrar oluşturup minikube dağıtımını yeniden yapmak gerekiyor. Tabii önceki komutlar nedeniyle büyük ihtimalle sistemde duran dağıtımlar bulunacaktır. Önce onları silmek lazım. Aşağıaki ilk komutla dağıtım paketini bulup sonrasında silebiliriz.

kubectl get all
kubectl delete deployment.apps/random-names-api-netcore service/kubernetes

Temizlik tamamlandıktan sonra aşağıdaki terminak komutları ile ilerleyebiliriz. İlk komut docker'ı minikube örneği içerisinde çalıştırabilmek için gerekli yerel ortam parametrelerinin ayarlanmasını sağlıyor. Sonrasındaki komutlar tahmin edeceğiniz üzere docker imajının oluşturulması ve minikube ortamına dağıtım yapılması ile ilgili.

eval $(minikube docker-env)
docker build -t random-names-api-netcore .
kubectl create -f deployment.yaml
kubectl get deployments
kubectl get pods

Çalışma Zamanı

Gelelim çalışma zamanına. Dockerize edilmiş servisimiz şu anda Minikube ortamında yaşıyor. Servisi dışarıya açmak için nodePort tipinden yararlanılmakta. Şu terminal komutları ile işlemlerimize devam edelim.

kubectl expose deployment random-names-api-netcore --type=NodePort
minikube service random-names-api-netcore --url

İlk komut ile dağıtımı yapılmış random-names-api-netcore isimli paket dışarıya açılmakta. İkinci terminal komutu ile servisin hangi adresten açıldığını öğrenebiliriz. Örneği denediğim zaman WestWorld'de 192.168.99.100 adresi ve 30046 nolu porttan hizmet verilmişti. Sonuç olarak bu adres bilgisinden servise erişip rastgele bir isim çekebiliriz.

minikube aksini belirtmezsek 30000 ile 32767 port aralığını kullandırtmaktadır.

80 Numaralı Port

Daha yakın bir gerçek hayat senaryosu düşünüldüğünde ervisin 80 numaralı portan hizmet verebilecek şekilde çalıştırılması önemlidir. Bunu sağlamak minikube tarafında bir servis kurgusuna ihtiyacımız var. Servisleri bir cluster üzerinde çalışan pod grupları olarak düşünebiliriz. Dolayısıyla birden fazla pod'un tek bir servismiş gibi dışarıya sunulması söz konusudur. 80 numaralı port içinde buna benzer bir hazırlığa ihtiyacımız var. Bunun için uygulamaya services.yaml isimli bir dosyanın eklenmesi gerekiyor. Bu dosyada NodePort değeri 80 olarak belirtilmekte. Dosya içeriğimiz aşağıdaki gibidir (Pod ve Service konusu ile ilgili olarak daha fazla bilgi için şu yazıya bir göz atabilirsiniz)

apiVersion: v1
kind: Service
metadata:
  name: random-names-api-netcore
  labels:
    app: random-names-api-netcore
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    nodePort: 80
    protocol: TCP
  selector:
    app: random-names-api-netcore

Sonrasında sırasıyla dağıtımı yapılan varlıklar silinir(Belki de buna gerek yoktur, araştırmak lazım) minikube 80 ile 30000 aralığını baz alacak şekilde yeniden başlatılır ve servis tekrardan oluşturulur. Bu kez dikkat edileceği üzere kubectl create komutu deployment.yaml yerine services.yaml dosyasını kullanmaktadır.

kubectl delete service random-names-api-netcore
kubectl delete deployment random-names-api-netcore
minikube start --extra-config=apiserver.service-node-port-range=80-30000
kubectl create -f services.yaml

İşlemleri başarılı bir şekilde sonlandırdık diyebiliriz. Evden çıkmadan önce minikube stop komutunu vermek yararlı olabilir.

Ben Neler Öğrendim

Bu çalışmanın yarattığı eğlenceli dakikaları geride bırakırken aşağıdaki maddelerde yazılanları öğrendiğimi not olarak düşmüşüm. Bir kaç zaman sonra bu notlara baktığımda yeniden düşünüyorum. Gerçekten ne kadarı aklımda kalmış ne kadarını doğru hatırlıyorum... Sonuçta unuttuklarım da olmuş ve bunları yeniden ele almak Saturday-Night-Works çalışmasına başlamamın ne kadar isabetli bir karar olduğunu kendi adıma ispat ediyor.

  • Kubernetes kurulumları ile uğraşmak yerine development amaçlı olarak Minikube kullanılmasını
  • Temel kubectl komutlarını
  • Pod ve Service kavramlarının ne anlama geldiğini
  • .Net Core Web API uygulamasının basitçe Dockerize edilmesini
  • Minkube ortamının sunduğu port numarasının 80e nasıl çekileceğini
  • Dockerfile, deployment.yaml ve services.yaml içeriklerindeki kavramların ne anlama geldiklerini

Böylece geldik bir cumartesi gecesi macerasının daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Bir .Net Core Web API Servisini Minikube Üzerinden Çalıştırmak

$
0
0

Merhaba Arkadaşlar,

Soğuk bir Şubat akşamı mıydı, dışarıda kar var mıydı, günün tam olarak hangi vakitleriydi tam olarak hatırlamıyorum ama github'a göre 24 numaralı örneğin son check-in işlemi 20 şubat Çarşamba günüydü.

Saturday-Night-Worksçalışmalarına başladığımda hedefim sadece Cumartesi geceleri olmasına rağmen içten gelen bir motivasyon konulara haftanın herhangi bir gününde bakmamı sağlıyordu. Genelde ilgi çekici konular seçtiğimden başta belirlediğim standart çalışma takviminin dışına çıkmıştım. Bu hevesli motivasyon birinci fazın(41 bölümlük ilk faz olarak ifade edebilirim) tamamı boyunca süregeldi.

İç motivasyon kişisel gelişim açısından bence çok önemli bir sürükleyici. Doğruyu söylemek gerekirse onu bulduğumuz anda bir çalışmanın peşinden koşturmamıza da gerek kalmıyor. Kendiliğinden gelen disiplin bizi zaten o alana odaklıyor ve sonrasında fırtınada gemisini ustalıkla kullanırken yüksek sesle şarkılar söyleyen mutlu kaptan misali zaman prüssüzce akıyor.

İşte o Şubat günü bu motivasyonla tamamladığım bir çalışmam olmuş. Minikube konusunu incelemişim. Şimdi notların üstünden geçip derleme ve öğrendiklerimi gözden geçirme sırası. Haydi başlayalım.

Birden fazla konteyner'ın bir araya geldiği(Docker container'larını düşünelim), yönetilmeleri(Manegement), kolayca dağıtılmaları(Deployment) küçülerek veya büyüyerek ölçeklenebilmeleri(Scaling) gerektiği durumlarda orkestrasyon işi çoğunlukla Kubernetes(k8s) tarafından sağlanmakta. Kubernetes bir konteyner kümeleme(Clustering) aracı olarak Google tarafından Go dili ile yazılmış bir ürün. Ancak bazen deneme amaçlı olarak geliştirdiğimiz enstrümanları k8s kurulumuna ihtiyaç duymadan tek küme(Cluster)üzerinde çalışacak şekilde kurgulamak isteyebiliriz. Bu noktada minikube oldukça işimize yaramaktadır.

Benim 24 numaralı bu Saturday-Night-Worksçalışmamdaki amacım Kubertenes'i WestWorld(Ubuntu 18.04, 64bit)üzerine kurmak yerine onu development ortamları için deneyimlememizi sağlayan Minikube'ü tanımaktı(Kubernetes'in tüm küme yapısının kurulumu çok da kolay değil. Üstelik sadece geliştirme noktasında onu denemek istersek bu maliyete girmeye gerek yok kanısındayım) Çalışma sırasında, .Net Core tabanlı bir Web API servisini içeren Docker konteynerının Minikube üzerinde koşturulması için gerekli işlemlere yer veriliyor.

Minikube sayesinde Kubernetes ortamını local bir makinede deneme şansımız oluyor(Tabii belirli kısıtlar dahilinde) Minikube, VirtualBox veya muadili bir sanal makine içinde tek node olarak çalışan bir Kubernetes kümesi sunmakta. Dolayısıyla geliştirme katmanı için ideal bir ortam.

İlk Kurulumlar

Linux ortamında Virtual Box isimli sanal makineye, Docker'a, Minikube ve onu komut satırından kontrol eden kubectl araçlarına ihtiyacımız var. WestWorld'de docker yüklü olduğu için diğerlerini kurarak ilerlemeye çalıştım. Virtual Box kurulumu için aşağıdaki terminal komutlarını kullanabiliriz.

sudo add-apt-repository multiverse
sudo apt-get update
sudo apt install virtualbox

Minukube kurulumunu içinse şöyle ilerleyebiliriz (Minikube ve bağımlılıklarının platforma göre farklı kurulumları için şu adrese bakabilirsiniz)

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.34.0/minikube-linux-amd64 && chmod +x minikube && sudo cp minikube /usr/local/bin/ && rm minikube

Kubernetes'i komut satırından yönetebilmek için kullanacağımız Kubectl aracını kurmak içinse aşağıdaki adımları takip edebiliriz.

sudo apt-get update && sudo apt-get install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubectl

Kurulum Sonrası Kontroller

Pek tabii kurulumlar sonrası bir sistem kontrolü yapmamızda yarar var. Docker, Virtual Box, Minikube ve kubectl gibi dört enstrümanın bir arada yaşayacağı bir geliştirme söz konusu. İlk olarak minikube servisini başlatmak lazım. Aşağıdaki terminal komutu ile bunu yapabiliriz.

minikube start

Hatta Minikube başarılı bir şekilde başladıktan sonra Virtual Box ortamından servis durumunu kontrol edebiliriz de. Aşağıdaki ekran görüntüsünde olduğu gibi minikube servisinin running modda görünmesi iyiye işarettir.

Çok doğal olarak servisi durdurma ve hatta silme ihtiyacımız da olabilir denemeler sırasında. Örneğin Minikube servisini durdurmak için,

minikube stop

silmek içinse,

minikube delete

komutlarından yararlanabiliriz.

Örnek .Net Core Web API Uygulamasının Geliştirilmesi

Minikube orkestrasyonunda yönetmek istediğimiz servis veya servisler olması gerekiyor. Ben çalışma kapsamında geliştirme noktasında daha rahat hareket edebildiğim için .Net Core platformunu tercih ettim. Servis uygulaması Docker üzerinde koşacak. Oluşturmak için aşağıdaki terminal komutuyla ilerleyebiliriz.

dotnet new webapi -o InstaceAPI

InstanceAPI isimli servisimiz rastgele isim dönen bir metod sunmakta ki servisin ne iş yaptığı bu örnek özelinde çok önemli değil aslında. Ama pek tabii kodsal olarak yaptıklarımızı özetleyerek ilerlemekte yarar var. Ben varsayılan olarak gelen ValuesController sınıfını NamesController olarak aşağıdaki gibi değiştirdim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace InstanceAPI.Controllers
{
    [Route("api/random/[controller]")]
    [ApiController]
    public class NamesController : ControllerBase
    {
        List<string> nameList=new List<string>{
            "Senaida","Armand","Yi","Tyra","Maud",
            "Dominque","Jayme","Amira","Salome","Anisa",
            "Spencer","Angelyn","Pete","Hoa","Cherelle",
            "Lavonne","Gladys","Adrianne","Gussie","Delmar"
        };
        // HTTP Get talebine cevap veren metodumuz.
        // nameList koleksiyonundan rastgele bir isim döndürüyor
        [HttpGet]
        public ActionResult<string> Get()
        {
            Random randomizer=new Random();
            var number=randomizer.Next(0,21);
            return nameList[number];
        }
    }
}

Web API uygulamasını Dockerize etmek için aşina olduğunuz üzere Dockerfile dosyasına ihtiyacımız var ki onu da aşağıdaki şekilde kodlayabiliriz.

# Microsoft'ın dotnet sdk imajını aldık
FROM microsoft/dotnet:sdk AS build-env
# takip eden komutları çalıştıracağımız klasörü set ettik
WORKDIR /app

# Gerekli dotnet kopyalamalarını yaptırıp
# Restore ve publish işlemlerini gerçekleştiriyoruz
COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

# Çalışma zamanı imajının oluşturulmasını istiyoruz
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
# Uygulamanın giriş noktasını belirtiyoruz
ENTRYPOINT [ "dotnet","InstanceAPI.dll" ]

Minikube içerisine neyin deploy edileceğini belirtmek için şimdilik deployment.yaml isimli dosyadan yararlanabiliriz. Bildirimlerden de görüldüğü üzere random-names-api-netcore isimli bir dağıtım söz konusu. Buna ait replica, label ve container gibi kubernetes odaklı bilgiler doküman içerisinde yazılmış durumda (Henüz bu konulara tam vakıf değilim. Çalışmaya devam)

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: random-names-api-netcore
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: random-names-api-netcore
    spec:
      containers:
        - name: random-names-api-netcore
          imagePullPolicy: Never
          image: random-names-api-netcore
          ports:
          - containerPort: 80

 

Docker Hazırlıkları

Dockerfile dosyası tamamlandıktan sonra Web API uygulamasının dockerize edilmesine başlanabilir. Sonuçta k8s ya da örnekte ele aldığımız Minikube'ün ana görevi dockerize edilmiş örneklerin orkestrasyonunun sağlanması. Dockerize işlemi için build komutunu aşağıdaki gibi kullanmamız yeterli olacaktır.

docker build -t random-names-api-netcore .

Minikube Deployment Hazırlıkları

Docker imajı hazır olduktan sonra artık minikube için gerekli dağıtım işlemine geçilebilir. Bu notkada kubectl komut satırı aracından yararlanmaktayız. kubectl, deployment.yaml dosyasının içeriğini kullanarak bir dağıtım işlemi icra edecektir. Aşağıdaki terminal komutları ile bu işlemleri gerçekleştirebiliriz. Bu işlemlere başlamadan önce minikube servisinin çalışır durumda olması gerektiğini hatırlatmak isterim.

kubectl create -f deployment.yaml
kubectl get deployments
kubectl get pods

create sonrasında kullanılan get komutları ile dağıtımı yapılan enstrümanı ve Podları görebiliriz(Pod = Aynı host üzerine dağıtımı yapılan bir veya daha fazla container olarak düşünülebilir ki senaryomuzda minikube için 3 pod söz konusudur) Lakin pod içeriklerine bakıldığında image durumlarının ErrImageNeverPull şeklinde kalmış olması gibi bir durum söz konusudur. En azından WestWorld'de böyle bir sorunla karşılaştığımı ifade edebilirim.

Sorun, minikube ile docker'ın birbirlerinden haberdar olmamalarından kaynaklanmaktaymış. Problemi aşmak için eval komutundan yararlanmak ve sonrasında docker imajını tekrar oluşturup minikube dağıtımını yeniden yapmak gerekiyor. Tabii önceki komutlar nedeniyle büyük ihtimalle sistemde duran dağıtımlar bulunacaktır. Önce onları silmek lazım. Aşağıaki ilk komutla dağıtım paketini bulup sonrasında silebiliriz.

kubectl get all
kubectl delete deployment.apps/random-names-api-netcore service/kubernetes

Temizlik tamamlandıktan sonra aşağıdaki terminak komutları ile ilerleyebiliriz. İlk komut docker'ı minikube örneği içerisinde çalıştırabilmek için gerekli yerel ortam parametrelerinin ayarlanmasını sağlıyor. Sonrasındaki komutlar tahmin edeceğiniz üzere docker imajının oluşturulması ve minikube ortamına dağıtım yapılması ile ilgili.

eval $(minikube docker-env)
docker build -t random-names-api-netcore .
kubectl create -f deployment.yaml
kubectl get deployments
kubectl get pods

Çalışma Zamanı

Gelelim çalışma zamanına. Dockerize edilmiş servisimiz şu anda Minikube ortamında yaşıyor. Servisi dışarıya açmak için nodePort tipinden yararlanılmakta. Şu terminal komutları ile işlemlerimize devam edelim.

kubectl expose deployment random-names-api-netcore --type=NodePort
minikube service random-names-api-netcore --url

İlk komut ile dağıtımı yapılmış random-names-api-netcore isimli paket dışarıya açılmakta. İkinci terminal komutu ile servisin hangi adresten açıldığını öğrenebiliriz. Örneği denediğim zaman WestWorld'de 192.168.99.100 adresi ve 30046 nolu porttan hizmet verilmişti. Sonuç olarak bu adres bilgisinden servise erişip rastgele bir isim çekebiliriz.

minikube aksini belirtmezsek 30000 ile 32767 port aralığını kullandırtmaktadır.

80 Numaralı Port

Daha yakın bir gerçek hayat senaryosu düşünüldüğünde ervisin 80 numaralı portan hizmet verebilecek şekilde çalıştırılması önemlidir. Bunu sağlamak minikube tarafında bir servis kurgusuna ihtiyacımız var. Servisleri bir cluster üzerinde çalışan pod grupları olarak düşünebiliriz. Dolayısıyla birden fazla pod'un tek bir servismiş gibi dışarıya sunulması söz konusudur. 80 numaralı port içinde buna benzer bir hazırlığa ihtiyacımız var. Bunun için uygulamaya services.yaml isimli bir dosyanın eklenmesi gerekiyor. Bu dosyada NodePort değeri 80 olarak belirtilmekte. Dosya içeriğimiz aşağıdaki gibidir (Pod ve Service konusu ile ilgili olarak daha fazla bilgi için şu yazıya bir göz atabilirsiniz)

apiVersion: v1
kind: Service
metadata:
  name: random-names-api-netcore
  labels:
    app: random-names-api-netcore
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    nodePort: 80
    protocol: TCP
  selector:
    app: random-names-api-netcore

Sonrasında sırasıyla dağıtımı yapılan varlıklar silinir(Belki de buna gerek yoktur, araştırmak lazım) minikube 80 ile 30000 aralığını baz alacak şekilde yeniden başlatılır ve servis tekrardan oluşturulur. Bu kez dikkat edileceği üzere kubectl create komutu deployment.yaml yerine services.yaml dosyasını kullanmaktadır.

kubectl delete service random-names-api-netcore
kubectl delete deployment random-names-api-netcore
minikube start --extra-config=apiserver.service-node-port-range=80-30000
kubectl create -f services.yaml

İşlemleri başarılı bir şekilde sonlandırdık diyebiliriz. Evden çıkmadan önce minikube stop komutunu vermek yararlı olabilir.

Ben Neler Öğrendim

Bu çalışmanın yarattığı eğlenceli dakikaları geride bırakırken aşağıdaki maddelerde yazılanları öğrendiğimi not olarak düşmüşüm. Bir kaç zaman sonra bu notlara baktığımda yeniden düşünüyorum. Gerçekten ne kadarı aklımda kalmış ne kadarını doğru hatırlıyorum... Sonuçta unuttuklarım da olmuş ve bunları yeniden ele almak Saturday-Night-Works çalışmasına başlamamın ne kadar isabetli bir karar olduğunu kendi adıma ispat ediyor.

  • Kubernetes kurulumları ile uğraşmak yerine development amaçlı olarak Minikube kullanılmasını
  • Temel kubectl komutlarını
  • Pod ve Service kavramlarının ne anlama geldiğini
  • .Net Core Web API uygulamasının basitçe Dockerize edilmesini
  • Minkube ortamının sunduğu port numarasının 80e nasıl çekileceğini
  • Dockerfile, deployment.yaml ve services.yaml içeriklerindeki kavramların ne anlama geldiklerini

Böylece geldik bir cumartesi gecesi macerasının daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


http://www.buraksenyurt.com/post/node-js-mongodb-fastify-ve-swagger-kullanilan-web-api-servisi-gelistirmekNode.js, MongoDB, Fastify ve Swagger Kullanılan Web API Servisi Geliştirmek

$
0
0

Merhaba Arkadaşlar,

Yazılım tarafında yeni bir şeyler öğrenmeye çalışmak hayatımın standart ritüelleri arasında. Bu döngü içerisinde yaşamak en büyük keyiflerimden birisi. Tabii bu döngünün en önemli parçalarından birisi masabaşında yapılan kodlama çalışmaları. WestWorld ve son zamanlardaki gözdem Ahch-To başlıca yardımcılarım. Çalışmalar değişik diyarlardan geliyor. Bazen konular arasında keskin geçişler yapıyorum. Bir gün Node.js dünyasında debelenirken bir başka gün daha aşina olduğum .Net Core kıyılarında yürüyüşe çıkıyorum. 

Ancak konular ne kadar değişirse değişsin bazı şeyler hep aynı kalıyor. Bu sebepten kullandığım örneklerdeki veri odaklı varlıklar zamanla tekrar önüme geliyor. Star Wars gezegenleri, ünlü düşünürlerin özlü sözleri, yapılacaklar listesindeki maddeler, emektar Northwind ve AdventureWorks veri tabanları, müzik gruplarının sevilen albümleri, Marvel karakterleri, basketbol yıldızları ve Minion'lar :) İşte yine onlarla karşı karşıyayım. Bu sefer eski örneklerden birisini masaya yatırmaya karar verdiğimde rastladım onlara.

Cumartesi geceleri çalışmaları kapsamında ele aldığım 07 numaralı örnekteki amacım MongoDB kullanan Node.Js tabanlı basit bir Web API servisi geliştirmekti. Ancak bunu yaparken web framework olarak sıklıkla kullandığım express yerine fastify paketini tercih etmiştim. Ayrcıa web api tarafından sunulan operayonların geliştirici dostu bir arayüzle sunulması için Swagger'dan yararlandım (Web API geliştiricilerinin artık olmazsa olmazlarından diyebiliriz)Örneği Visual Studio Code yardımıyla geliştirdiğim WestWorld'de (Ubuntu 18.04 64bit) Node.js, npm(Node paket yönetimi aracı) ve MongoDB(NoSQL veri tabanımız) yüklüydü. Bu örneğe ait notların üstünden bir kez daha geçerek bilgilerimi yeniden hatırlama fırsatı bulmuş oldum.

MongoDB'yi Ubuntu sistemine kurmak için şu adresteki bilgilerden yararlanabiliriz. Ama isterseniz MongoDB'nin konu ile ilgili docker imajını da ele alabilirsiniz.

Klasör Ağacı ve Paketler

Uygulamanın klasör yapısını ilk etapta aşağıdaki gibi kurguladım. Çok basit anlamda bir MVC(Model View Controller) deseni olduğunu varsayabiliriz. Her ne kadar ortada view isimli bir klasör olmasa da, yönlendirme işlemlerinin ele alındığı routes bu anlamda düşünülebilir. Son satırda yer alan npm init komutu ile node operasyonu başlatılmış oluyor.

mkdir Minion-API
cd Minion-API
mkdir src
cd src
mkdir models
mkdir controllers
mkdir routes
mkdir config
touch index.js
npm init

Uygulamanın pek tabii ihtiyaç duyduğu belli başlı paketler var. Bunları npm aracı ile aşağıdaki terminal komutu yardımıyla yükleyebiliriz.

npm i nodemon mongoose fastify fastify-swagger boom

nodemon'u kod dosyalarından birisinde değişiklik olduğunda node sunucusunu otomatik olarak yeniden başlatmak için kullanıyoruz. Özellikle geliştirme safhasında çok işe yarayan bir monitoring fonksiyonelliği olduğunu ifade edebilirim. Sürekli uygulamayı sonlandırıp yeniden başlatmaya gerek bırakmayan bir özellik. Bu arada kullanımı için package.json dosyasındaki start komutunu aşağıdaki gibi değiştirmemiz gerekiyor.

"start": "./node_modules/nodemon/bin/nodemon.js ./src/index.js"

mongoose, mongodb ile konuşabilmek için gereken paketimiz. Fastify, Hapi ve Express'ten ilham alınarak yazılmış oldukça hızlı bir web framework olarak ifade edilmekte. İlk kez bu örnek çalışma kapsamında tanıştığımı itiraf edeyim. API dokümantasyonu için Fastify'a Swagger desteği veren Fastify-swagger modülü kullanılıyor. Fastify route tanımlamaları Swagger ile otomatik olarak ilişkilendirilecekler(Koddaki izleri takip edin) HTTP hata mesajlarını göstermek için boom isimli utility paketinden yararlanılıyor(Bu arada ilgili paket bir süre önce devre dışı bırakılmış. Şu adresten güncel sürümüne ulaşabiliriz)

Kod Tarafı

Uygulama veri odaklı bir REST servis olarak özetlenebilir. Verinin tutulduğu taraf MongoDB. Popüler bir doküman bazlı NoSQL sistemi olduğunu biliyoruz. Verinin kod tarafında şemalar yardımıyla modellenmesi mümkün. Örneğe göre mongodb dokümanlarına ait şemaları models klasöründe tutuyoruz (minion.js) Veri ile ilgili ekleme, güncelleme, silme veya okuma gibi CRUD operasyonlarını controllers içerisinde karşılıyoruz. minioncontroller.js minion modeli ile ilgili Controller tipimiz. HTTP taleplerini ele aldığımız yer ise routes klasöründeki index.js dosyası. Bu dosya, HTTP taleplerini aldığında (örneğin yeni bir satır eklenmesi veya tüm listenin çekilmesi gibi) bunları Controller sınıfına iletmekte. Controller sınıfı da esasen MongoDb ve model sınıfı ile işbirliği içerisinde ilgili talepleri karşılamakta.

minion.js;

const mongoose = require('mongoose')

// mini isimli şemayı tanımladık. 
// Minion filmindeki bir karakteri temsil ediyor
const minionSchema = new mongoose.Schema({
    nickname: String,
    age: Number,
    gender: String
})

module.exports = mongoose.model('Minion', minionSchema)

minioncontroller.js;

const boom = require('boom') //bomba gibi bir hata mesajı yöneticisi
const Minion = require('../models/minion')

// yeni bir Minion karakteri eklemek için
exports.add = async (req, res) => {
    try {
        // Minion bilgilerini request'in body'sinden aldık
        const mini = new Minion(req.body)
        return mini.save() //kaydedip sonucu geriye döndürdük
    } catch (err) {
        throw boom.boomify(err)
    }
}

// bir Minion karakterini güncellemek için
exports.update = async (req, res) => {
    try {
        // güncelleme işlemini gerçekleştir
        const result = await Minion.findByIdAndUpdate(req.params.id, req.body, { new: true })
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

// bir Minion karakterini silmek için
exports.delete = async (req, res) => {
    try {
        // query parametresi olarak gelen id'den ilgili Minion bul ve kaldır
        const result = await Minion.findByIdAndRemove(req.params.id)
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

// id bilgisinden Minion bul
exports.getSingle = async (req, res) => {
    try {
        const result = await Minion.findById(req.params.id)
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

// ne kadar Minion varsa geriye döndür
exports.getAll = async (req, res) => {
    try {
        const result = await Minion.find()
        console.log(result)
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

routes/index.js;

// controller tipini içeriye tanımladık
const minionController = require('../controllers/minionController')
const help = require('./swagger-help/minionApi') // swagger yardım dokümanının yeri söylendi

// HTTP Get, Post, Put, Delete tanımlamalarını yapıyoruz
const handlers = [
    {
        method: 'GET', // alt satırdaki adrese HTTP Get talebi gelirse
        url: '/api/minions',
        handler: minionController.getAll, //controller'daki getAll metoduna yönlendir
        schema: help.getAllMinionSchema
    },
    {
        method: 'GET', //alt satırdaki adrese HTTP Get talebi gelirse
        url: '/api/minions/:id',
        handler: minionController.getSingle //controller'daki getSingle metoduna yönlendir
    },
    {
        method: 'POST', //alttaki adres için POST talebi gelirse
        url: '/api/minions',
        handler: minionController.add, // yeni bir mini ekleme isteği nedeniyle controller'daki add metoduna yönlendir
        schema: help.addMinionSchema
    },
    {
        method: 'PUT', //aşağıdaki adres için PUT talebi gelirse
        url: '/api/minions/:id',
        handler: minionController.update //güncelleme sebebiyle update metoduna yönlendir
    },
    {
        method: 'DELETE', //aşağıdaki adres için HTTP Delete talebi gelirse
        url: '/api/minions/:id',
        handler: minionController.delete //miniyi silmek için controller'daki delete metodunu çağır
    }
]

module.exports = handlers // handlers isimli array'deki metodları modül dışına aç

Web API fonksiyonelliklerini hoş bir şekilde göstermek ve daha kullanışlı testler yaptırabilmek için Swagger ile ilgili ayarlamalar yapmak yerinde olur. Bunun için config klasöründeki swagger.js dosyasını kullanabiliriz. 

exports.options = {
    routePrefix: '/help',
    exposeRoute: true,
    swagger: {
      info: {
        title: 'Minions API',
        description: 'Minion ailesi ile ilgili yönetsel işlemler...',
        version: '1.0.0'
      },
      externalDocs: {
        url: 'https://swagger.io',
        description: 'Daha fazla bilgi için buraya gidin'
      },
      host: 'localhost',
      schemes: ['http'],
      consumes: ['application/json'],
      produces: ['application/json']
    }
  }

Dikkat edileceği üzere help bir adres öneki olarak belirtilmiş durumda (ama değiştirebilirsiniz) Yardım sayfasına ait başlık, açıklama ve servisin versiyon bilgileri info elementinde belirtiliyor. İstenirse servisle ilgili harici dokümantasyonlara yönlendirmelerde de bulunulabilinir. Bu, externalDocs isimli kısımda tanımlanmakta. Takip eden bölümlerde host, schema ve content type bilgileri belirtilmekte. Servisin ilgili operayonlarına yapılacak GET ve POST gibi çağrılara ait yardımcı bilgilerse routes/swagger-help klasöründeki js dosyası içerisinde yazıyor. Aşağıdaki örnek kod parçasına göre POST ve GET kullanımları için bazı tanımlamalar yapılmış durumda. Bu tanımlamalar yardım sayfasının önyüzüne yansıtılmakta.

exports.addMinionSchema = {
    description: 'Yeni minionlar ekle',
    tags: ['minions'],
    summary: 'Minionlar ailesine yeni bir mini eklemek için',
    body: {
        type: 'object',
        properties: {
            nickname: { type: 'string' },
            age: { type: 'number' },
            gender: { type: 'string' }
        }
    },
    response: {
        200: {
            description: 'Eklendi',
            type: 'object',
            properties: {
                _id: { type: 'string' },
                nickname: { type: 'string' },
                age: { type: 'number' },
                gender: { type: 'string' },
                __v: { type: 'number' }
            }
        }
    }
}

exports.getAllMinionSchema = {
    description: 'Tüm minionlar',
    tags: ['minions'],
    summary: 'Tüm minionları getirmek için kullanılır',
    response: {
        200: {
            description: 'Liste başarılı bir şekilde çekilir',
            type: 'object',
            properties: {
                _id: { type: 'string' },
                nickname: { type: 'string' },
                age: { type: 'number' },
                gender: { type: 'string' },
                __v: { type: 'number' }
            }
        }
    }
}

Dosya bilgilerine göre localhost:4005/help adresine talepte bulunduğumuzda ekran görüntüsünde yer alan yardım sayfası ile karşılaşırız. Tam bir geliştirici dostu öyle değil mi?

Pek tabii node.js uygulamasını ayağa kaldıran ana modüle ait kodlarımız da oldukça önemli. Proje iskeletine göre routes klasörü altındaki modülleri Fastify ile ilişkilendirmek gerekiyor. Bunun için bir forEach döngüsü kullanılmakta (Fastify'ın Swagger ile ilişkilendirildiği yeri görebildiniz mi?)

//gerekli modüller yüklenir
const fastify = require('fastify')({ logger: true })
const routes = require('./routes') //route modüllerinin yeri söylendi
const swagger = require('./config/swagger') //swager konfigurasyonunun yeri söylendi
fastify.register(require('fastify-swagger'), swagger.options) // swagger, fastify için kayıt edildi
const mongoose = require('mongoose')

// routes klasöründeki tüm modülleri fastify ile ilişkilendiriyoruz
routes.forEach((route, index) => {
    fastify.route(route)
})

// mongodb'ye bağlanılıyor. minions isimli veritabanı yoksa oluşturulacaktır
mongoose.connect('mongodb://localhost/animation', { useNewUrlParser: true })
    .then(() => console.log('MongoDB ile iletişim kuruldu'))
    .catch(err => console.log(err))

// sunucu 4005 nolu porttan yayın yapacak.
// asenkron çalışır
const online = async () => {
    try {
        await fastify.listen(4005)
        fastify.swagger()
        fastify.log.info(`Sunucu ${fastify.server.address().port} adresi üzerinden dinlemede`)
    } catch (err) {
        fastify.log.error(err)
        process.exit(1)
    }
}
online()

Kodun devam eden kısmında mongodb ile bağlantı sağlanıyor. Sonrasındaysa online isimli bir fonksiyonun asenkron olarak çağırıldığını görüyoruz. listen metoduna yapılan isteğe göre uygulamamız sonlandırılıncaya kadar 4005 numaralı port üzerinden dinlemede kalacak. Herhangibir hata olması ihtimaline karşın bir try...catch bloğu kullanılıyor. Gelelim çalışma zamanına.

Çalışma Zamanı Testleri

Elbette ilk olarak mongodb servisini çalıştırmak lazım. Ardından node uygulaması ayağa kaldırılabilir. İki ayrı terminal penceresi açılarak ilerlenebilir ki ben örneği bu şekilde denemiştim.

mongod
npm start

Dikkat edileceği üzere ekrana gayet hoş log'lar da düşüyor. Testler için curl veya popüler araçlardan olan Postman kullanılabilir. Ben bu tip çalışmalarda servis çalışabilirliğini hızlı ve kolay bir şekilde test etmek için Postman veya SoapUI gibi araçlardan yararlanıyorum.

Örneğimize yeniden odaklanırsak;

Yeni bir minion eklemek için http://localhost:4005/api/minions adresine gövdesinde JSON formatında içeriğe sahip bir talep göndermek yeterli.

{
"nickname":"Agnes Gru",
"age":5,
"gender":"Female"
}

Eklenen kayıtlara ait benzersiz ID değerleri tahmin edileceği üzere MongoDB tarafından otomatik olarak üretilmekte. ID değerleri veri silme ve güncelleme operasyonları için önemli arama kriterlerinden. Aşağıdaki ekran görüntüsünde üstteki çağrı sonuçlarını görebiliriz. Agnes başarılı bir şekilde eklenmiş durumda.

Bir kaç minion daha ekledikten sonra bunların güncel listesini elde etmek için http://localhost:4005/api/minions adresine HTTP Get talebini yollamak yeterli. Belli bir minion'u elde etmek içinse MongoDb'nin verdiği ID bilgisini kullanabiliriz. Örneğin, http://localhost:4005/api/minions/5c1581e579140d6969b5951f talebi için şöyle bir sonuç dönebilir.

Benzer şekilde aynı adresi PUT metodu ile kullanıp BODY kısmında yeni minion bilgilerini JSON formatında göndererek güncelleme işlemini de gerçekleştirebiliriz. Bu ve silme operasyonlarını örneği tamamlayıp denemenizi öneririm.

Ben Neler Öğrendim?

Bu çalışmaya tekrardan dönmek benim için faydalı oldu. Sonuçta sürekli gelişen yazılım dünyasında bir şeylerin ucundan tutabilmek için geriye dönük çalışmaları arada bir hatırlamak gerekiyor. Ben bu yazı için aşağıdaki kazanımları elde ettiğimi not almışım.

  • Web çatısı için express yerine Fastify'ı nasıl kullanabileceğimi
  • nodemon'un çalışma zamanına getirdiği rahatlığı
  • mongodb'de temel veri işlemlerinin node.js tarafında mongoose ile nasıl kodlanacağını
  • Swagger ile API arayüzünün geliştirici dostu hale getirilmesini
  • Postman ile basit REST testlerinin yapılmasını

Böylece geldik bir Saturday Night Works macerasının daha sonuna. Bu sefer eski maceralardan birisini bloguma not olarak düşmeye çalıştım. Birkaç ay öncesinden kalma bir çalışma olsa da örneğin üstünden bir kere daha geçmek, kodları yeniden çalıştırmayı denemek ve yazılanları incelemek unuttuklarımı hatırlamama yardımcı oldu. Sonuç olarak bu çalışma kapsamında node.js ile MongoDB bazlı bir CRUD API servisi geliştirmeye çalıştığımızı özetleyebiliriz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Node.js, MongoDB, Fastify ve Swagger Kullanılan Web API Servisi Geliştirmek

$
0
0

Merhaba Arkadaşlar,

Yazılım tarafında yeni bir şeyler öğrenmeye çalışmak hayatımın standart ritüelleri arasında. Bu döngü içerisinde yaşamak en büyük keyiflerimden birisi. Tabii bu döngünün en önemli parçalarından birisi masabaşında yapılan kodlama çalışmaları. WestWorld ve son zamanlardaki gözdem Ahch-To başlıca yardımcılarım. Çalışmalar değişik diyarlardan geliyor. Bazen konular arasında keskin geçişler yapıyorum. Bir gün Node.js dünyasında debelenirken bir başka gün daha aşina olduğum .Net Core kıyılarında yürüyüşe çıkıyorum. 

Ancak konular ne kadar değişirse değişsin bazı şeyler hep aynı kalıyor. Bu sebepten kullandığım örneklerdeki veri odaklı varlıklar zamanla tekrar önüme geliyor. Star Wars gezegenleri, ünlü düşünürlerin özlü sözleri, yapılacaklar listesindeki maddeler, emektar Northwind ve AdventureWorks veri tabanları, müzik gruplarının sevilen albümleri, Marvel karakterleri, basketbol yıldızları ve Minion'lar :) İşte yine onlarla karşı karşıyayım. Bu sefer eski örneklerden birisini masaya yatırmaya karar verdiğimde rastladım onlara.

Cumartesi geceleri çalışmaları kapsamında ele aldığım 07 numaralı örnekteki amacım MongoDB kullanan Node.Js tabanlı basit bir Web API servisi geliştirmekti. Ancak bunu yaparken web framework olarak sıklıkla kullandığım express yerine fastify paketini tercih etmiştim. Ayrcıa web api tarafından sunulan operayonların geliştirici dostu bir arayüzle sunulması için Swagger'dan yararlandım (Web API geliştiricilerinin artık olmazsa olmazlarından diyebiliriz)Örneği Visual Studio Code yardımıyla geliştirdiğim WestWorld'de (Ubuntu 18.04 64bit) Node.js, npm(Node paket yönetimi aracı) ve MongoDB(NoSQL veri tabanımız) yüklüydü. Bu örneğe ait notların üstünden bir kez daha geçerek bilgilerimi yeniden hatırlama fırsatı bulmuş oldum.

MongoDB'yi Ubuntu sistemine kurmak için şu adresteki bilgilerden yararlanabiliriz. Ama isterseniz MongoDB'nin konu ile ilgili docker imajını da ele alabilirsiniz.

Klasör Ağacı ve Paketler

Uygulamanın klasör yapısını ilk etapta aşağıdaki gibi kurguladım. Çok basit anlamda bir MVC(Model View Controller) deseni olduğunu varsayabiliriz. Her ne kadar ortada view isimli bir klasör olmasa da, yönlendirme işlemlerinin ele alındığı routes bu anlamda düşünülebilir. Son satırda yer alan npm init komutu ile node operasyonu başlatılmış oluyor.

mkdir Minion-API
cd Minion-API
mkdir src
cd src
mkdir models
mkdir controllers
mkdir routes
mkdir config
touch index.js
npm init

Uygulamanın pek tabii ihtiyaç duyduğu belli başlı paketler var. Bunları npm aracı ile aşağıdaki terminal komutu yardımıyla yükleyebiliriz.

npm i nodemon mongoose fastify fastify-swagger boom

nodemon'u kod dosyalarından birisinde değişiklik olduğunda node sunucusunu otomatik olarak yeniden başlatmak için kullanıyoruz. Özellikle geliştirme safhasında çok işe yarayan bir monitoring fonksiyonelliği olduğunu ifade edebilirim. Sürekli uygulamayı sonlandırıp yeniden başlatmaya gerek bırakmayan bir özellik. Bu arada kullanımı için package.json dosyasındaki start komutunu aşağıdaki gibi değiştirmemiz gerekiyor.

"start": "./node_modules/nodemon/bin/nodemon.js ./src/index.js"

mongoose, mongodb ile konuşabilmek için gereken paketimiz. Fastify, Hapi ve Express'ten ilham alınarak yazılmış oldukça hızlı bir web framework olarak ifade edilmekte. İlk kez bu örnek çalışma kapsamında tanıştığımı itiraf edeyim. API dokümantasyonu için Fastify'a Swagger desteği veren Fastify-swagger modülü kullanılıyor. Fastify route tanımlamaları Swagger ile otomatik olarak ilişkilendirilecekler(Koddaki izleri takip edin) HTTP hata mesajlarını göstermek için boom isimli utility paketinden yararlanılıyor(Bu arada ilgili paket bir süre önce devre dışı bırakılmış. Şu adresten güncel sürümüne ulaşabiliriz)

Kod Tarafı

Uygulama veri odaklı bir REST servis olarak özetlenebilir. Verinin tutulduğu taraf MongoDB. Popüler bir doküman bazlı NoSQL sistemi olduğunu biliyoruz. Verinin kod tarafında şemalar yardımıyla modellenmesi mümkün. Örneğe göre mongodb dokümanlarına ait şemaları models klasöründe tutuyoruz (minion.js) Veri ile ilgili ekleme, güncelleme, silme veya okuma gibi CRUD operasyonlarını controllers içerisinde karşılıyoruz. minioncontroller.js minion modeli ile ilgili Controller tipimiz. HTTP taleplerini ele aldığımız yer ise routes klasöründeki index.js dosyası. Bu dosya, HTTP taleplerini aldığında (örneğin yeni bir satır eklenmesi veya tüm listenin çekilmesi gibi) bunları Controller sınıfına iletmekte. Controller sınıfı da esasen MongoDb ve model sınıfı ile işbirliği içerisinde ilgili talepleri karşılamakta.

minion.js;

const mongoose = require('mongoose')

// mini isimli şemayı tanımladık. 
// Minion filmindeki bir karakteri temsil ediyor
const minionSchema = new mongoose.Schema({
    nickname: String,
    age: Number,
    gender: String
})

module.exports = mongoose.model('Minion', minionSchema)

minioncontroller.js;

const boom = require('boom') //bomba gibi bir hata mesajı yöneticisi
const Minion = require('../models/minion')

// yeni bir Minion karakteri eklemek için
exports.add = async (req, res) => {
    try {
        // Minion bilgilerini request'in body'sinden aldık
        const mini = new Minion(req.body)
        return mini.save() //kaydedip sonucu geriye döndürdük
    } catch (err) {
        throw boom.boomify(err)
    }
}

// bir Minion karakterini güncellemek için
exports.update = async (req, res) => {
    try {
        // güncelleme işlemini gerçekleştir
        const result = await Minion.findByIdAndUpdate(req.params.id, req.body, { new: true })
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

// bir Minion karakterini silmek için
exports.delete = async (req, res) => {
    try {
        // query parametresi olarak gelen id'den ilgili Minion bul ve kaldır
        const result = await Minion.findByIdAndRemove(req.params.id)
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

// id bilgisinden Minion bul
exports.getSingle = async (req, res) => {
    try {
        const result = await Minion.findById(req.params.id)
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

// ne kadar Minion varsa geriye döndür
exports.getAll = async (req, res) => {
    try {
        const result = await Minion.find()
        console.log(result)
        return result
    } catch (err) {
        throw boom.boomify(err)
    }
}

routes/index.js;

// controller tipini içeriye tanımladık
const minionController = require('../controllers/minionController')
const help = require('./swagger-help/minionApi') // swagger yardım dokümanının yeri söylendi

// HTTP Get, Post, Put, Delete tanımlamalarını yapıyoruz
const handlers = [
    {
        method: 'GET', // alt satırdaki adrese HTTP Get talebi gelirse
        url: '/api/minions',
        handler: minionController.getAll, //controller'daki getAll metoduna yönlendir
        schema: help.getAllMinionSchema
    },
    {
        method: 'GET', //alt satırdaki adrese HTTP Get talebi gelirse
        url: '/api/minions/:id',
        handler: minionController.getSingle //controller'daki getSingle metoduna yönlendir
    },
    {
        method: 'POST', //alttaki adres için POST talebi gelirse
        url: '/api/minions',
        handler: minionController.add, // yeni bir mini ekleme isteği nedeniyle controller'daki add metoduna yönlendir
        schema: help.addMinionSchema
    },
    {
        method: 'PUT', //aşağıdaki adres için PUT talebi gelirse
        url: '/api/minions/:id',
        handler: minionController.update //güncelleme sebebiyle update metoduna yönlendir
    },
    {
        method: 'DELETE', //aşağıdaki adres için HTTP Delete talebi gelirse
        url: '/api/minions/:id',
        handler: minionController.delete //miniyi silmek için controller'daki delete metodunu çağır
    }
]

module.exports = handlers // handlers isimli array'deki metodları modül dışına aç

Web API fonksiyonelliklerini hoş bir şekilde göstermek ve daha kullanışlı testler yaptırabilmek için Swagger ile ilgili ayarlamalar yapmak yerinde olur. Bunun için config klasöründeki swagger.js dosyasını kullanabiliriz. 

exports.options = {
    routePrefix: '/help',
    exposeRoute: true,
    swagger: {
      info: {
        title: 'Minions API',
        description: 'Minion ailesi ile ilgili yönetsel işlemler...',
        version: '1.0.0'
      },
      externalDocs: {
        url: 'https://swagger.io',
        description: 'Daha fazla bilgi için buraya gidin'
      },
      host: 'localhost',
      schemes: ['http'],
      consumes: ['application/json'],
      produces: ['application/json']
    }
  }

Dikkat edileceği üzere help bir adres öneki olarak belirtilmiş durumda (ama değiştirebilirsiniz) Yardım sayfasına ait başlık, açıklama ve servisin versiyon bilgileri info elementinde belirtiliyor. İstenirse servisle ilgili harici dokümantasyonlara yönlendirmelerde de bulunulabilinir. Bu, externalDocs isimli kısımda tanımlanmakta. Takip eden bölümlerde host, schema ve content type bilgileri belirtilmekte. Servisin ilgili operayonlarına yapılacak GET ve POST gibi çağrılara ait yardımcı bilgilerse routes/swagger-help klasöründeki js dosyası içerisinde yazıyor. Aşağıdaki örnek kod parçasına göre POST ve GET kullanımları için bazı tanımlamalar yapılmış durumda. Bu tanımlamalar yardım sayfasının önyüzüne yansıtılmakta.

exports.addMinionSchema = {
    description: 'Yeni minionlar ekle',
    tags: ['minions'],
    summary: 'Minionlar ailesine yeni bir mini eklemek için',
    body: {
        type: 'object',
        properties: {
            nickname: { type: 'string' },
            age: { type: 'number' },
            gender: { type: 'string' }
        }
    },
    response: {
        200: {
            description: 'Eklendi',
            type: 'object',
            properties: {
                _id: { type: 'string' },
                nickname: { type: 'string' },
                age: { type: 'number' },
                gender: { type: 'string' },
                __v: { type: 'number' }
            }
        }
    }
}

exports.getAllMinionSchema = {
    description: 'Tüm minionlar',
    tags: ['minions'],
    summary: 'Tüm minionları getirmek için kullanılır',
    response: {
        200: {
            description: 'Liste başarılı bir şekilde çekilir',
            type: 'object',
            properties: {
                _id: { type: 'string' },
                nickname: { type: 'string' },
                age: { type: 'number' },
                gender: { type: 'string' },
                __v: { type: 'number' }
            }
        }
    }
}

Dosya bilgilerine göre localhost:4005/help adresine talepte bulunduğumuzda ekran görüntüsünde yer alan yardım sayfası ile karşılaşırız. Tam bir geliştirici dostu öyle değil mi?

Pek tabii node.js uygulamasını ayağa kaldıran ana modüle ait kodlarımız da oldukça önemli. Proje iskeletine göre routes klasörü altındaki modülleri Fastify ile ilişkilendirmek gerekiyor. Bunun için bir forEach döngüsü kullanılmakta (Fastify'ın Swagger ile ilişkilendirildiği yeri görebildiniz mi?)

//gerekli modüller yüklenir
const fastify = require('fastify')({ logger: true })
const routes = require('./routes') //route modüllerinin yeri söylendi
const swagger = require('./config/swagger') //swager konfigurasyonunun yeri söylendi
fastify.register(require('fastify-swagger'), swagger.options) // swagger, fastify için kayıt edildi
const mongoose = require('mongoose')

// routes klasöründeki tüm modülleri fastify ile ilişkilendiriyoruz
routes.forEach((route, index) => {
    fastify.route(route)
})

// mongodb'ye bağlanılıyor. minions isimli veritabanı yoksa oluşturulacaktır
mongoose.connect('mongodb://localhost/animation', { useNewUrlParser: true })
    .then(() => console.log('MongoDB ile iletişim kuruldu'))
    .catch(err => console.log(err))

// sunucu 4005 nolu porttan yayın yapacak.
// asenkron çalışır
const online = async () => {
    try {
        await fastify.listen(4005)
        fastify.swagger()
        fastify.log.info(`Sunucu ${fastify.server.address().port} adresi üzerinden dinlemede`)
    } catch (err) {
        fastify.log.error(err)
        process.exit(1)
    }
}
online()

Kodun devam eden kısmında mongodb ile bağlantı sağlanıyor. Sonrasındaysa online isimli bir fonksiyonun asenkron olarak çağırıldığını görüyoruz. listen metoduna yapılan isteğe göre uygulamamız sonlandırılıncaya kadar 4005 numaralı port üzerinden dinlemede kalacak. Herhangibir hata olması ihtimaline karşın bir try...catch bloğu kullanılıyor. Gelelim çalışma zamanına.

Çalışma Zamanı Testleri

Elbette ilk olarak mongodb servisini çalıştırmak lazım. Ardından node uygulaması ayağa kaldırılabilir. İki ayrı terminal penceresi açılarak ilerlenebilir ki ben örneği bu şekilde denemiştim.

mongod
npm start

Dikkat edileceği üzere ekrana gayet hoş log'lar da düşüyor. Testler için curl veya popüler araçlardan olan Postman kullanılabilir. Ben bu tip çalışmalarda servis çalışabilirliğini hızlı ve kolay bir şekilde test etmek için Postman veya SoapUI gibi araçlardan yararlanıyorum.

Örneğimize yeniden odaklanırsak;

Yeni bir minion eklemek için http://localhost:4005/api/minions adresine gövdesinde JSON formatında içeriğe sahip bir talep göndermek yeterli.

{
"nickname":"Agnes Gru",
"age":5,
"gender":"Female"
}

Eklenen kayıtlara ait benzersiz ID değerleri tahmin edileceği üzere MongoDB tarafından otomatik olarak üretilmekte. ID değerleri veri silme ve güncelleme operasyonları için önemli arama kriterlerinden. Aşağıdaki ekran görüntüsünde üstteki çağrı sonuçlarını görebiliriz. Agnes başarılı bir şekilde eklenmiş durumda.

Bir kaç minion daha ekledikten sonra bunların güncel listesini elde etmek için http://localhost:4005/api/minions adresine HTTP Get talebini yollamak yeterli. Belli bir minion'u elde etmek içinse MongoDb'nin verdiği ID bilgisini kullanabiliriz. Örneğin, http://localhost:4005/api/minions/5c1581e579140d6969b5951f talebi için şöyle bir sonuç dönebilir.

Benzer şekilde aynı adresi PUT metodu ile kullanıp BODY kısmında yeni minion bilgilerini JSON formatında göndererek güncelleme işlemini de gerçekleştirebiliriz. Bu ve silme operasyonlarını örneği tamamlayıp denemenizi öneririm.

Ben Neler Öğrendim?

Bu çalışmaya tekrardan dönmek benim için faydalı oldu. Sonuçta sürekli gelişen yazılım dünyasında bir şeylerin ucundan tutabilmek için geriye dönük çalışmaları arada bir hatırlamak gerekiyor. Ben bu yazı için aşağıdaki kazanımları elde ettiğimi not almışım.

  • Web çatısı için express yerine Fastify'ı nasıl kullanabileceğimi
  • nodemon'un çalışma zamanına getirdiği rahatlığı
  • mongodb'de temel veri işlemlerinin node.js tarafında mongoose ile nasıl kodlanacağını
  • Swagger ile API arayüzünün geliştirici dostu hale getirilmesini
  • Postman ile basit REST testlerinin yapılmasını

Böylece geldik bir Saturday Night Works macerasının daha sonuna. Bu sefer eski maceralardan birisini bloguma not olarak düşmeye çalıştım. Birkaç ay öncesinden kalma bir çalışma olsa da örneğin üstünden bir kere daha geçmek, kodları yeniden çalıştırmayı denemek ve yazılanları incelemek unuttuklarımı hatırlamama yardımcı oldu. Sonuç olarak bu çalışma kapsamında node.js ile MongoDB bazlı bir CRUD API servisi geliştirmeye çalıştığımızı özetleyebiliriz. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/razor-dunyasindaki-ilk-adimlarimRazor Dünyasındaki İlk Adımlarım

$
0
0

Merhaba Arkadaşlar,

Bizim servisin dönüş yolculuğu bir başkadır. Her gün yaklaşık git gel neredeyse seksen kilometrelik yol teperiz(Daha ne kadar teperim bilemiyorum tabii) 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.

Ş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.

Ş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 ;)

Saturday-Night-Works çalışmalarımdaki 21 numaralı örnekteki 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(PageModel) 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.

 Örnek URL Adresi   Karşılayan Razor Sayfası Model Nesnesi
 /Book pages/Book.cshtml pages/book.cshtml.cs
 /Category/Product pages/Category/Product.cshtml   pages/Category/Product.cshtml.cs  
 /Category pages/Category/Index.cshtml pages/Category/Index.cshtml.cs
 /Category/Index pages/Category/Index.cshtml pages/Category/Index.cshtml.cs
 /Index pages/Index.cshtml pages/Index.cshtml.cs
 / pages/Index.cshtml pages/Index.cshtml.cs

Ç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.

Başlangıç

Hazırsanız ilk adımlarımızla işe başlayalım. Ben diğer pek çok örnekte olduğu gibi kodlamayı WestWorld(Ubuntu 18.04, 64bit)ü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.

dotnet new webapp -o MyBookStore

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.

Geliştirme Safhası

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.

StoreDataContext.cs

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 
    }
}

Book.cs

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
    }
}

Ö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.

// InMemory veritabanı kullanacağımız DbContext'imizi DI ile ekledik
services.AddDbContext<StoreDataContext>(options=>options.UseInMemoryDatabase("StoreLook"));

SQLite kullanımına geçildiğindeyse buradaki servis entegrasyonu şöyle olmalıdır.

// 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));

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. 

AddBook.cshtml

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
        }
    }
}

AddBook.cshtml.cs

@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>

Kitap bilgilerini düzenlemek içinse EditBook.cshtml ve EditBook.cshtml.cs isimli tipleri kullanmaktayız.

EditBook.cshtml.cs

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");
        }
    }
}

EditBook.cshtml

@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>

Varsayılan olarak gelen Index.cshtml ve Index.cshtml.cs içeriklerinide aşağıdaki gibi değiştirelim.

Index.cshtml.cs

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
        }
    }
}

Index.cshtml

@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>

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.

<!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>

Çalışma Zamanı

Kodlama tarafını tamamladıktan sonra uygulamayı aşağıdaki terminal komutu ile çalıştırıp deneme sürüşüne çıkabiliriz.

dotnet run

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(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)

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.

Uncle Bob temalı örnek bir kitap verisini ilk denemede kullanmak isterseniz diye aşağıya bilgilerini bırakıyorum ;)

Clean Architecture
Robert C. Martin (Uncle Bob)
393
34.99
"This is essential reading for every current of aspiring software architect..."

Console logundan kitabın eklendiğini izleyebiliriz.

İş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(Bu kısmı daha şık bir hale getirmek gerekiyor. Belki popup'lar ile uyarı vermek daha güzel olabilir. Bunu yapmayı bir deneyin)

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.

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.

Düzenleme sonrası örnek sonuçlar da şöyle olabilir.

InMemory Veritabanını SQLite ile Değiştirme

Ö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.

dotnet add package Microsoft.EntityFrameworkCore.SQLite

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.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionStrings": {
    "StoreDataContext": "Data Source=MyBookStore.db"
  },
  "AllowedHosts": "*"
}

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. 

dotnet ef migrations add InitialCreate
dotnet ef database update

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.

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.

Artık verilerimiz SQLite ile fiziki olarak da kayıt altında. Hatta Visual Studio Code'a SQLite Explorer Extension isimli aracı eklersek oluşan DB dosyasının içeriğini de görebiliriz.

Ben Neler Öğrendim?

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.

  • Razor Page ve Page Model kavramlarının ne olduğunu
  • Razor'un temel çalışma prensiplerini
  • Yönlendirmelerin(Routing) nasıl işlediğini
  • Razor içinden model nesnelerine nasıl bağlanılabileceğini(property binding)
  • Entity Framework Core'da InMemory veri tabanı kullanımını
  • DI ile ilgili servislerin nasıl enjekte edildiğini
  • Çeşitli DataAnnotations niteliklerini(attributes)
  • InMemory veri tabanında SQLite kullanımına geçince yapılması gereken değişiklikleri ve Migration'ın ne işe yaradığını

Böylece Saturday-Night-Worksç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.

Razor Dünyasındaki İlk Adımlarım

$
0
0

Merhaba Arkadaşlar,

Bizim servisin dönüş yolculuğu bir başkadır. Her gün yaklaşık git gel neredeyse seksen kilometrelik yol teperiz(Daha ne kadar teperim bilemiyorum tabii) 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.

Ş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.

Ş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 ;)

Saturday-Night-Works çalışmalarımdaki 21 numaralı örnekteki 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(PageModel) 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.

 Örnek URL Adresi   Karşılayan Razor Sayfası Model Nesnesi
 /Book pages/Book.cshtml pages/book.cshtml.cs
 /Category/Product pages/Category/Product.cshtml   pages/Category/Product.cshtml.cs  
 /Category pages/Category/Index.cshtml pages/Category/Index.cshtml.cs
 /Category/Index pages/Category/Index.cshtml pages/Category/Index.cshtml.cs
 /Index pages/Index.cshtml pages/Index.cshtml.cs
 / pages/Index.cshtml pages/Index.cshtml.cs

Ç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.

Başlangıç

Hazırsanız ilk adımlarımızla işe başlayalım. Ben diğer pek çok örnekte olduğu gibi kodlamayı WestWorld(Ubuntu 18.04, 64bit)ü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.

dotnet new webapp -o MyBookStore

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.

Geliştirme Safhası

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.

StoreDataContext.cs

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 
    }
}

Book.cs

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
    }
}

Ö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.

// InMemory veritabanı kullanacağımız DbContext'imizi DI ile ekledik
services.AddDbContext<StoreDataContext>(options=>options.UseInMemoryDatabase("StoreLook"));

SQLite kullanımına geçildiğindeyse buradaki servis entegrasyonu şöyle olmalıdır.

// 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));

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. 

AddBook.cshtml

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
        }
    }
}

AddBook.cshtml.cs

@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>

Kitap bilgilerini düzenlemek içinse EditBook.cshtml ve EditBook.cshtml.cs isimli tipleri kullanmaktayız.

EditBook.cshtml.cs

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");
        }
    }
}

EditBook.cshtml

@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>

Varsayılan olarak gelen Index.cshtml ve Index.cshtml.cs içeriklerinide aşağıdaki gibi değiştirelim.

Index.cshtml.cs

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
        }
    }
}

Index.cshtml

@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>

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.

<!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>

Çalışma Zamanı

Kodlama tarafını tamamladıktan sonra uygulamayı aşağıdaki terminal komutu ile çalıştırıp deneme sürüşüne çıkabiliriz.

dotnet run

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(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)

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.

Uncle Bob temalı örnek bir kitap verisini ilk denemede kullanmak isterseniz diye aşağıya bilgilerini bırakıyorum ;)

Clean Architecture
Robert C. Martin (Uncle Bob)
393
34.99
"This is essential reading for every current of aspiring software architect..."

Console logundan kitabın eklendiğini izleyebiliriz.

İş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(Bu kısmı daha şık bir hale getirmek gerekiyor. Belki popup'lar ile uyarı vermek daha güzel olabilir. Bunu yapmayı bir deneyin)

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.

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.

Düzenleme sonrası örnek sonuçlar da şöyle olabilir.

InMemory Veritabanını SQLite ile Değiştirme

Ö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.

dotnet add package Microsoft.EntityFrameworkCore.SQLite

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.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionStrings": {
    "StoreDataContext": "Data Source=MyBookStore.db"
  },
  "AllowedHosts": "*"
}

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. 

dotnet ef migrations add InitialCreate
dotnet ef database update

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.

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.

Artık verilerimiz SQLite ile fiziki olarak da kayıt altında. Hatta Visual Studio Code'a SQLite Explorer Extension isimli aracı eklersek oluşan DB dosyasının içeriğini de görebiliriz.

Ben Neler Öğrendim?

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.

  • Razor Page ve Page Model kavramlarının ne olduğunu
  • Razor'un temel çalışma prensiplerini
  • Yönlendirmelerin(Routing) nasıl işlediğini
  • Razor içinden model nesnelerine nasıl bağlanılabileceğini(property binding)
  • Entity Framework Core'da InMemory veri tabanı kullanımını
  • DI ile ilgili servislerin nasıl enjekte edildiğini
  • Çeşitli DataAnnotations niteliklerini(attributes)
  • InMemory veri tabanında SQLite kullanımına geçince yapılması gereken değişiklikleri ve Migration'ın ne işe yaradığını

Böylece Saturday-Night-Worksç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.

http://www.buraksenyurt.com/post/cloud-firestore-ile-angular-kullanimiCloud Firestore ile Angular Kullanımı

$
0
0

Merhaba Arkadaşlar,

Earvin (Magic) Johnson. Michael Jordan'la geçen gençlik yıllarımın henüz başlarında rastladığım NBA'in ve Los Angles Lakers'ın 2.06lık unutulmaz oyun kurucusu. O dönemlerde yaptığı inanılmaz assistler ve oyun zekası hala aranır nitelikte. Aslında sadece oyun kurucu değil zaman zaman şutör gard ve uzun forvet pozisyonlarında da oynamıştır.

Lakers tarafından 1979 yılında birinci sırada draft edilen Johnson toplamda 5 NBA şampiyonluğu yaşamış efsanelerden birisi. NBA istatistiklerine göre oynadığı 906 maçta 19.5 sayı ve 11.2 assist ortalamaları ile double double yapmıştır. Toplamda 10141 asist ile tüm zamanların en çok asist yapan 5nci oyuncusu durumunda. 32 numaralı formasıyla 12 sezon Lakers'da görev alan oyun kurucunun hayatını sevgili Murat Murathanoğlu'nun eşsiz anlatımıyla dinlemek isterseniz şöyle buyrun. Onun Saturday-Night-Works çalıştayımla olan tek ilgisi ise forma numarası. Hoş bir giriş olsun istedim de :[]

Gelelim derleyip toparladığım blog notlarıma.

Angular tarafına yavaş yavaş alışmaya başlamıştım. Yine de fazladan idman yapmaktan ve tekrar etmekten zarar gelmez diye düşünüp farklı örnekleri uygulamaya çalışıyordum. Bu sefer temel CRUD(Create Read Update Delete) operasyonlarını Cloud Firestore üzerinden icra ederken Angular'da koşmaya çalışmışım. Amaçlarımdan birisi servis tarafında Form kontrolü kullanabilmek. Örnekte ikinci el eşya satışı yapmak üzere kurgulanan basit bir web arayüzü söz konusu. Programı her zaman olduğu gibi WestWorld(Ubuntu 18.04, 64bit)üzerinde yazdım.

Google bilindiği üzere 2014 yılında bir bulut servis sağlayıcı olan Firebase'i satın almıştı. Sonrasında bu servisin Web ve Mobil tarafı için kullanılabilen Firestore isimli NoSQL tabanlı veri tabanını kullanıma açtı. Firestore, Realtime Database alternatifi olarak kullanıma sunuldu. Realtime Database'e göre bazı farklılıkları var. Örneğin sadece mobil değil web tarafı için de offline kullanım imkanı sağlıyor. Ölçekleme bulut sisteminde otomatik olarak yapılıyor. Realtime Database'e göre karmaşık sorgu performansının daha iyi olduğu belirtiliyor. Ücretlendirme politikası uygulamanın büyüklüğüne göre Realtime Database'e göre daha ekonomik olabiliyor. Dolayısıyla mobil tarafta ilerleyen Startup projelerinin MVP modelleri için ideal bir çözüm gibi duruyor.

İlk Hazırlıklar

Tabii konumuz esas itibariyle Angular deneyimini arttırmak. Cloud Firestore bu noktada bir veri sağlayıcısı rolünü üstlenecek. İşe Angular projesini oluşturarak başlayabiliriz. Bir Angular projesini kolayca oluşturmanın en etkili yolu bildiğiniz üzere CLI(Command-Line Interface) aracından yararlanmak. Dolayısıyla sistemimizde Angular CLI yüklü olmalı. Eğer yüklü değilse aşağıdaki ilk terminal komutunu bu amaçla kullanabiliriz.

sudo npm install -g @angular/cli
ng new quick-auction
npm i --save bootstrap firebase @angular/fire
cd speed-sell
ng g c products
ng g c product-list
ng g s shared/products

Takip eden komutlara gelirsek...

ng new ile quick-action isimli yeni bir Angular projesi oluşturmaktayız(Sorulan sorularda Routing seçeneğine No dedim ve Style olarak CSS'i seçili bıraktım. Ancak bunun yerine bootstrap kullanacağız)npm i ile başlayan komutlarda stil için bootstrap, Google'ın Cloud Firestore tarafı ile konuşabilmek içinde firebase ve anglular'ın firebase ile konuşabilmesi içinse @angular/fire paketlerini ekliyoruz. ng g ile başlayan komutlarda iki bileşen(component) ve her iki bileşen için ortaklaşa kullanılacak bir servis nesnesi oluşturuyoruz. Bu servis temel olarak firestore veri tabanı ile olan iletişim görevlerini üstlenecek.

Firebase Tarafı(Cloud Firestore)

Google Cloud tarafında yapacağımız bazı hazırlıklar var. Firebase tarafında yeni bir proje açıp içerisinde test amaçlı bir Firestore veri tabanı oluşturacağız. Öncelikle bu adresten Firebase Console'a gidelim ve örnek bir proje üretelim. Ben aşağıdaki ekran görüntüsüneki gibi quict-auctions-project isimli bir uygulama oluşturdum(Esasen quick demek istemiştim ama dikkatsizlik olsa gerek quict demişim, olsun. Özgün bir isim olmuş :P )

Sonrasında Database menüsünden veya kocaman turuncu kutucuk içerisindeki Cloud Firestorm bölümünden hareket ederek yeni bir veri tabanı oluşturalım. Aşağıdaki ekran görüntüsünde olduğu gibi veri tabanını Test modunda açabiliriz.

Şimdi Angular uyguaması ile Firebase servis tarafını tanıştırmalıyız. Project Overview kısmından hareket ederek

kırmızı kutucuktaki düğmeye basalım. Gerekli ortam değişkenleri otomatik olarak üretilecektir. Karşımıza gelen ekrandaki config içeriğini uygulamanın environment.ts dosyası içerisine almamız yeterli.

Kod Tarafı

Gelelim kod tarafında yaptığımız değişikliklere. Arayüz tarafını daha şık hale getirmek için bootstrap kullanıyoruz. Bu nedenle angular.json dosyasındaki style elementini değiştirdik.

"styles": [
	"node_modules/bootstrap/dist/css/bootstrap.min.css",
        "src/styles.css"
        ],

Uygulama, satılacak ürünlerin yönetimi ilgili iki bileşen kullanıyor. Hatırlayacağınız üzere bunları terminalden üretmiştik(products ve product-list) Birisi tipik listeleme diğeri ise ekleme işlemi için kullanılacak. Bu bileşenlere ait HTML ve Typescript kodlarını aşağıdaki gibi geliştirebiliriz. Kodların anlaşılması adına mümkün mertebe yorum satırları kullandım.

products.component.ts

import { Component, OnInit } from '@angular/core';
import { ProductsService } from '../shared/products.service'; // ProductsService modülünü bildiriyoruz

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {

  constructor(private productsService: ProductsService) { } // Constructor injection ile ProductsService tipini içeriye alıyoruz
  auctions = []; // açık artırma verilerini tutacağımız array
  ngOnInit() {
  }

  // Bileşendeki button'a basıldığında (click) niteliğine atanan olay bildirimi nedeniyle bu metod çalışacaktır
  onSubmit() {
    let formData = this.productsService.productForm.value; // aslında servis tarafındaki form kontrolü bileşenle ilişkilendirildiğinden girilen değerler oraya da yansır
    console.log(formData); // F12 ile tarayıcı Console penceresinden bu çıktıya bakabiliriz
    this.productsService.addProduct(formData);
  }
}

products.component.html

<form [formGroup]="this.productsService.productForm"><div class="form-group"><label for="lblTitle">Tanıtım başlığı</label><input type="text" formControlName="title" class="form-control" id="txtTitle" placeholder="Tanıtım başlığını giriniz"></div><div class="form-group"><label for="lblSummary">Açıklaması</label><input type="text" formControlName="summary" class="form-control" id="txtSummary" placeholder="Ne satıyorsunuz az biraz bilgi..."></div><div class="form-group"><label for="lblPrice">Fiyat</label><input type="number" formControlName="price" class="form-control" id="txtPrice" placeholder="10"></div><div class="form-group form-check"><input type="checkbox" formControlName="bargain" class="form-check-input" id="chkBargain"><label class="form-check-label" for="chkBargain">Pazarlık olur mu?</label></div><button class="btn btn-primary" (click)="onSubmit()">Yolla</button></form><!--
  form elementindeki [formGroup] niteliğine dikkat edelim. Buraya atanan değer,
  bileşene enjekte edilen ProductsService nesnesine ait form özelliğidir.
  Servis tipinin productForm değişkenindeki alanlar bu bileşen üzerindeki kontrollere
  formControlName niteliği yardımıyla bağlanırlar.
-->

product-list.component.ts

import { Component, OnInit } from '@angular/core';
import { ProductsService } from '../shared/products.service'; // ProductService modülünü bildiriyoruz

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  constructor(private productService: ProductsService) { } // servisi constructor üzerinden enjekte ettik
  allProducts; // Firestore koleksiyonundaki tüm dokümanları temsil edecen değişkenimiz
  ngOnInit() {
    /*
    Bileşen initialize aşamasındayken servisten tüm ürünleri çekiyoruz.
    Subscribe metoduyla da servisin getProducts metodundan dönen sonuç kümesini,
    allProducts isimli değişkene bağlıyoruz ki bunu bileşenin ön yüzü kullanıyor
    */
    this.productService
      .getProducts()
      .subscribe(res => this.allProducts = res);
  }

  /*
    Bir ürünü silmek için kullandığımız metod. 
    Servis tarafındaki deleteProduct çağrılıyor.
    Parametre olarak o anki product içeriği gönderilmekte
  */
  delete = p => this.productService.deleteProduct(p).then(r => {
    //alert('silindi');
  });

  /*
    Ürünün sadece bargain özelliğini update eden bir metod 
    olarak düşünelim. Senaryoda pazarlık payı olup olmadığını belirten
    checkbox'ın durumunu güncelletiyoruz
  */

  // Güncelleme örneği (fiyatı 10 birim arttırıyoruz)
  increasePrice = p => this.productService.updateProduct(p, 10);

  // Güncelleme örneği (fiyatı 10 birim düşürüyoruz)
  decreasePrice = p => this.productService.updateProduct(p, -10);
}

product-list.component.html

<table class="table"><thead class="thead-dark"><tr><th>Açıklama</th><th>Başlık</th><th>Fiyat</th><th>Pazarlık?</th><th></th></tr></thead><tbody><tr *ngFor="let product of allProducts"><td>{{product.payload.doc.data().summary}}</td><td>{{product.payload.doc.data().title}}</td><td>{{product.payload.doc.data().price}}</td><td>
        {{product.payload.doc.data().bargain?'Var':'Yok'}}</td><td><div class="btn-group-sm"><button class="primary btn-default btn-block" (click)="increasePrice(product)">+</button><button class="primary btn-default btn-block" (click)="decreasePrice(product)">-</button><button class="primary btn-danger btn-block" (click)="delete(product)">Sil</button></div></td></tr></tbody></table><!--
  Klasik bir Grid tasarımı söz konusu.
  *ngFor ile bileşenin init metodunda doldurulan allProducts dizisini dönüyoruz.
  Firestore'dan gelen her bir dokümanın elemanlarına ulaşmak için,
  payload.doc.data().[özellik adı] notasyonunu kullandık.

  Sil başlıklı button'a basıldığında bileşendeki delete metodunu çağrılmış oluyor.

  Checkbox kontrolünün click olayında bileşendeki güncelleme metodunu ve dolayısıyla
  servis tarafındaki versiyonunu çağırmış oluyoruz.
-->

CRUD operasyonları her iki bileşen içinde ortaklaşa kullanılabilecek fonksiyonellikler. Bu nedenle Shared klasörü altında konuşlandırdığımız products.service.ts isimli bir tip mevcut.

import { Injectable } from '@angular/core';
import { FormControl, FormGroup } from "@angular/forms"; // FormGroup ve FormControl tiplerini kullanabilmek için eklemeliyiz
import { AngularFirestore } from "@angular/fire/firestore"; // Firestore tarafı ile konuşmamızı sağlayacak modül. Servisini constructor'da enjekte ediyoruz

@Injectable({
  providedIn: 'root'
})
export class ProductsService {

  constructor(private firestore: AngularFirestore) { }

  /* 
  Yeni bir FormGroup nesnesi örnekliyoruz.
  title, summary, price ve online isimli FormControl nesneleri içeriyor.
  bu özelliklere atanan değerleri Firebase tarafına yazacağız.
  element adları arayüz tarafında da birebir kullanılacaklar
  */
  productForm = new FormGroup({
    title: new FormControl(''),
    summary: new FormControl(''),
    price: new FormControl(100),
    bargain: new FormControl(false),
  })

  /*
  Firestore veritabanına yeni bir Product verisi eklemek için kullanılan servis metodu.
  collection ile Firestore tarafındaki koleksiyonu işaret ediyoruz.
  Gelen json içeriği products isimli koleksiyona yazılıyor.
  */
  addProduct(p) {
    return new Promise<any>((resolve, reject) => {
      this.firestore.collection("products").add(p).
        then(res => { }, err => reject(err));
    });
  }

  getProducts() {
    /*
     Firestore veri tabanındaki products koleksiyonu içerisinde yer alan tüm dokümanları alıyoruz.
     snapshotChanges çağrısı değişikliklerin kontrol altında olmasını sağlar. 
     Bizim değişiklikleri yakalayıp güncellemeler yapmamıza gerek kalmaz.
    */

    return this.firestore.collection("products").snapshotChanges();
  }

  // silme işlemini üstlenen servis metodumuz
  deleteProduct(p) {
    return this.firestore
      .collection("products")
      .doc(p.payload.doc.id) // firestrore tarafındaki id bilgisini kullanacak.
      .delete();
  }

  // Güncelleme operasyonu. rate değişkenine gelen değere göre price değerini değiştiriyoruz
  updateProduct(p, rate) {
    // Önce üzerinde çalışılan veriyi alalım.
    var prd=p.payload.doc.data();
    if(prd.price==10 && rate<0) // fiyatı sıfırın altına indirmek istemeyiz çünkü
      return;    
    // Üst limit kontrolü de konulabilir belki

    // fiyat arttırımı veya azaltımı uygunsa yeni değeri alıyoruz ve firestore üzerinden güncelleme yapıyoruz
    var newPrice=prd.price+rate;
    return this.firestore
      .collection("products")
      .doc(p.payload.doc.id)
      .set({ price: newPrice }, { merge: true });
    // merge özelliğine atanan true değeri, tüm entity değerlerinin güncellenmesi yerine sadece metoda ilk parametre ile gelenlerin ele alınmasını söyler.
  }
}

Eklediğimiz bileşenleri kullandığımız yer app nesnesi. Angular tarafındaki ana bileşenimiz olarak düşünebiliriz. Dolayısıyla modül bildirimleri ve bileşenlerin HTML yerleşimleri için app.module.ts ve app.component.html dosyalarını da kodlamamız gerekiyor.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { environment } from "src/environments/environment"; //environment.ts içerisindeki firebaseConfig sekmesinin anlaşılabilmesi için gerekli modül
import { AngularFireModule } from "@angular/fire";
import { AngularFirestoreModule } from "@angular/fire/firestore";

import { AppComponent } from './app.component';
import { ProductsComponent } from './products/products.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductsService } from './shared/products.service'; // Tüm bileşenlerde kullanabilmek için ProductsService modülünü bildirip alttaki providers özelliğine de ekledik
import {ReactiveFormsModule} from '@angular/forms'; // Service tarafında FormControl ve FormGroup modüllerini kullanabilmek için bildirdik ve aşağıdaki import kısmında ekledik

@NgModule({
  declarations: [
    AppComponent,
    ProductsComponent,
    ProductListComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    AngularFireModule.initializeApp(environment.firebaseConfig), // AngularFireModule' ü environment.ts içerisindeki firebaseConfig ayarları ile başlatmış olduk
    AngularFirestoreModule
  ],
  providers: [ProductsService], 
  bootstrap: [AppComponent]
})
export class AppModule { }

ve app.component.html

<!-- 
  Uygulama hizmete alındığında render edilecek olan ana bileşenimiz.
  ng g c komutları ile oluşturduğumuz products ve product-list bileşenlerini bootstrap grid sistemini kullanarak ekrana yerleştiriyoruz.
  class niteliklerinde kullandığımzı değerler ile ortamı biraz renklendirmeye çalıştım

--><div class="container px-lg-5"><div class="row"><h1>Hızlı Satış?</h1></div><div class="row border border-primary rounded"><div class="col py-3 px-lg-3 col-md-12"><app-products></app-products></div></div><div class="row border border-dark rounded"><div class="col py-3 px-lg-3 col-md-12"><app-product-list></app-product-list></div></div></div>

Son olarak Firestore tarafı için gerekli apiKey, databaseUrl, senderId, projectId gibi bilgilerin environment.ts dosyasına eklenmesi lazım ki çalışma zamanında kullanılabilsinler. Bu bilgileri Google Cloud tarafı otomatik olarak üretmişti hatırlayacağınız üzere.

export const environment = {
  production: false,
  // Firebase'in orada oluşturduğumuz projemiz için bize verdiği ayarlar
  firebaseConfig: {
    apiKey: ".....", //Burası sizin projenizin Api Key değeri olmalı
    authDomain: "quict-auctions-project.firebaseapp.com",
    databaseURL: "https://quict-auctions-project.firebaseio.com",
    projectId: "quict-auctions-project",
    storageBucket: "quict-auctions-project.appspot.com",
    messagingSenderId: "....." // Bu da sizin projeniz için verilen senderId değeri olmalı
  },
};

Hepsi bu kadar :) Artık uygulamayı çalıştırıp sonuçlarına bakabiliriz.

Çalışma Zamanı

Uygulamayı çalıştırmak için terminalden

ng serve

komutunu vermemiz yeterli. 4200 numaralı port üzerinden web arayüzüne erişebiliriz. WestWorld testlerinde benim aldığım örnek bir ekran görüntüsünü aşağıda bulabilirsiniz(Hani ispatı olsun da sonra çalışmıyor bu filan demeyelim)

Örneğin ilgi çekici yanlarından birisi, önyüz ve Firestore taraflarının eş zamanlı olarak güncel kalabilmeleridir. Firestore web konsolunda eriştiğimiz dokümanlarda yapacağımız değişiklikler anında önyüz tarafına push edilir, önyüzde yaptığımız değişiklikler de benzer şekilde Firestore tarafına yansır. Bunu denemenizi öneriririm. + ve - düğmeleri ile güncel fiyat bilgisini arttırma veya azaltma işlemlerini yapabiliriz. Sil düğmesi tahmin edileceği üzere satışa çıkarttığımız ürünü repository'den kaldırmak içindir. Güncelleme oparasyonunu sadece fiyat ayarlamaları için yaptık lakin ürün bilgilerinin düzenlenmesi ihtiyacı da var. Bunu uygulamaya nasıl ekleyebiliriz bir düşünün ;)

Ben Neler Öğrendim

Bu örnek çalışma ile Angular bilgilerimi biraz daha pekiştirmiş ama daha da önemlisi veri kaynağı olarak Google Cloud Platform'un bir ürününü kullanmış oldum. Genel hatlarıyla öğrendiklerimi şöyle özetleyebilirim.

  • Bir component üzerindeki element değerlerinin formControlName niteliği yardımıyla servis tarafındaki FormControl nesnelerine bağlanabileceğini
  • Firebase üzerinde Cloud Firestore veri tabanının nasıl oluşturulabileceğini
  • Uygulamanın Firebase tarafı ile haberleşebilmesi için gerekli konfigurasyon ayarlarının nereye konulması gerektiğini ve nasıl çağırılabildiğini
  • Cloud Firestore ve önyüzün birbirlerinin değişikliklerini anında görebildiklerini
  • Bileşenlerdeki kontrollere olay metodlarının nasıl bağlanabileceğini
  • Firestore paketinin temel CRUD(Create Read Update Delete) komutlarını

Böylece geldik bir maceranın daha sonuna. 32 numaralı Saturday-Night-Works çalışmasının kodlarına buradan ulaşabilirsiniz. Yeni bir gözden geçirme yazısında buluşuncaya dek hepinize mutlu günler dilerim.

Cloud Firestore ile Angular Kullanımı

$
0
0

Merhaba Arkadaşlar,

Earvin (Magic) Johnson. Michael Jordan'la geçen gençlik yıllarımın henüz başlarında rastladığım NBA'in ve Los Angles Lakers'ın 2.06lık unutulmaz oyun kurucusu. O dönemlerde yaptığı inanılmaz assistler ve oyun zekası hala aranır nitelikte. Aslında sadece oyun kurucu değil zaman zaman şutör gard ve uzun forvet pozisyonlarında da oynamıştır.

Lakers tarafından 1979 yılında birinci sırada draft edilen Johnson toplamda 5 NBA şampiyonluğu yaşamış efsanelerden birisi. NBA istatistiklerine göre oynadığı 906 maçta 19.5 sayı ve 11.2 assist ortalamaları ile double double yapmıştır. Toplamda 10141 asist ile tüm zamanların en çok asist yapan 5nci oyuncusu durumunda. 32 numaralı formasıyla 12 sezon Lakers'da görev alan oyun kurucunun hayatını sevgili Murat Murathanoğlu'nun eşsiz anlatımıyla dinlemek isterseniz şöyle buyrun. Onun Saturday-Night-Works çalıştayımla olan tek ilgisi ise forma numarası. Hoş bir giriş olsun istedim de :[]

Gelelim derleyip toparladığım blog notlarıma.

Angular tarafına yavaş yavaş alışmaya başlamıştım. Yine de fazladan idman yapmaktan ve tekrar etmekten zarar gelmez diye düşünüp farklı örnekleri uygulamaya çalışıyordum. Bu sefer temel CRUD(Create Read Update Delete) operasyonlarını Cloud Firestore üzerinden icra ederken Angular'da koşmaya çalışmışım. Amaçlarımdan birisi servis tarafında Form kontrolü kullanabilmek. Örnekte ikinci el eşya satışı yapmak üzere kurgulanan basit bir web arayüzü söz konusu. Programı her zaman olduğu gibi WestWorld(Ubuntu 18.04, 64bit)üzerinde yazdım.

Google bilindiği üzere 2014 yılında bir bulut servis sağlayıcı olan Firebase'i satın almıştı. Sonrasında bu servisin Web ve Mobil tarafı için kullanılabilen Firestore isimli NoSQL tabanlı veri tabanını kullanıma açtı. Firestore, Realtime Database alternatifi olarak kullanıma sunuldu. Realtime Database'e göre bazı farklılıkları var. Örneğin sadece mobil değil web tarafı için de offline kullanım imkanı sağlıyor. Ölçekleme bulut sisteminde otomatik olarak yapılıyor. Realtime Database'e göre karmaşık sorgu performansının daha iyi olduğu belirtiliyor. Ücretlendirme politikası uygulamanın büyüklüğüne göre Realtime Database'e göre daha ekonomik olabiliyor. Dolayısıyla mobil tarafta ilerleyen Startup projelerinin MVP modelleri için ideal bir çözüm gibi duruyor.

İlk Hazırlıklar

Tabii konumuz esas itibariyle Angular deneyimini arttırmak. Cloud Firestore bu noktada bir veri sağlayıcısı rolünü üstlenecek. İşe Angular projesini oluşturarak başlayabiliriz. Bir Angular projesini kolayca oluşturmanın en etkili yolu bildiğiniz üzere CLI(Command-Line Interface) aracından yararlanmak. Dolayısıyla sistemimizde Angular CLI yüklü olmalı. Eğer yüklü değilse aşağıdaki ilk terminal komutunu bu amaçla kullanabiliriz.

sudo npm install -g @angular/cli
ng new quick-auction
npm i --save bootstrap firebase @angular/fire
cd speed-sell
ng g c products
ng g c product-list
ng g s shared/products

Takip eden komutlara gelirsek...

ng new ile quick-action isimli yeni bir Angular projesi oluşturmaktayız(Sorulan sorularda Routing seçeneğine No dedim ve Style olarak CSS'i seçili bıraktım. Ancak bunun yerine bootstrap kullanacağız)npm i ile başlayan komutlarda stil için bootstrap, Google'ın Cloud Firestore tarafı ile konuşabilmek içinde firebase ve anglular'ın firebase ile konuşabilmesi içinse @angular/fire paketlerini ekliyoruz. ng g ile başlayan komutlarda iki bileşen(component) ve her iki bileşen için ortaklaşa kullanılacak bir servis nesnesi oluşturuyoruz. Bu servis temel olarak firestore veri tabanı ile olan iletişim görevlerini üstlenecek.

Firebase Tarafı(Cloud Firestore)

Google Cloud tarafında yapacağımız bazı hazırlıklar var. Firebase tarafında yeni bir proje açıp içerisinde test amaçlı bir Firestore veri tabanı oluşturacağız. Öncelikle bu adresten Firebase Console'a gidelim ve örnek bir proje üretelim. Ben aşağıdaki ekran görüntüsüneki gibi quict-auctions-project isimli bir uygulama oluşturdum(Esasen quick demek istemiştim ama dikkatsizlik olsa gerek quict demişim, olsun. Özgün bir isim olmuş :P )

Sonrasında Database menüsünden veya kocaman turuncu kutucuk içerisindeki Cloud Firestorm bölümünden hareket ederek yeni bir veri tabanı oluşturalım. Aşağıdaki ekran görüntüsünde olduğu gibi veri tabanını Test modunda açabiliriz.

Şimdi Angular uyguaması ile Firebase servis tarafını tanıştırmalıyız. Project Overview kısmından hareket ederek

kırmızı kutucuktaki düğmeye basalım. Gerekli ortam değişkenleri otomatik olarak üretilecektir. Karşımıza gelen ekrandaki config içeriğini uygulamanın environment.ts dosyası içerisine almamız yeterli.

Kod Tarafı

Gelelim kod tarafında yaptığımız değişikliklere. Arayüz tarafını daha şık hale getirmek için bootstrap kullanıyoruz. Bu nedenle angular.json dosyasındaki style elementini değiştirdik.

"styles": [
	"node_modules/bootstrap/dist/css/bootstrap.min.css",
        "src/styles.css"
        ],

Uygulama, satılacak ürünlerin yönetimi ilgili iki bileşen kullanıyor. Hatırlayacağınız üzere bunları terminalden üretmiştik(products ve product-list) Birisi tipik listeleme diğeri ise ekleme işlemi için kullanılacak. Bu bileşenlere ait HTML ve Typescript kodlarını aşağıdaki gibi geliştirebiliriz. Kodların anlaşılması adına mümkün mertebe yorum satırları kullandım.

products.component.ts

import { Component, OnInit } from '@angular/core';
import { ProductsService } from '../shared/products.service'; // ProductsService modülünü bildiriyoruz

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {

  constructor(private productsService: ProductsService) { } // Constructor injection ile ProductsService tipini içeriye alıyoruz
  auctions = []; // açık artırma verilerini tutacağımız array
  ngOnInit() {
  }

  // Bileşendeki button'a basıldığında (click) niteliğine atanan olay bildirimi nedeniyle bu metod çalışacaktır
  onSubmit() {
    let formData = this.productsService.productForm.value; // aslında servis tarafındaki form kontrolü bileşenle ilişkilendirildiğinden girilen değerler oraya da yansır
    console.log(formData); // F12 ile tarayıcı Console penceresinden bu çıktıya bakabiliriz
    this.productsService.addProduct(formData);
  }
}

products.component.html

<form [formGroup]="this.productsService.productForm"><div class="form-group"><label for="lblTitle">Tanıtım başlığı</label><input type="text" formControlName="title" class="form-control" id="txtTitle" placeholder="Tanıtım başlığını giriniz"></div><div class="form-group"><label for="lblSummary">Açıklaması</label><input type="text" formControlName="summary" class="form-control" id="txtSummary" placeholder="Ne satıyorsunuz az biraz bilgi..."></div><div class="form-group"><label for="lblPrice">Fiyat</label><input type="number" formControlName="price" class="form-control" id="txtPrice" placeholder="10"></div><div class="form-group form-check"><input type="checkbox" formControlName="bargain" class="form-check-input" id="chkBargain"><label class="form-check-label" for="chkBargain">Pazarlık olur mu?</label></div><button class="btn btn-primary" (click)="onSubmit()">Yolla</button></form><!--
  form elementindeki [formGroup] niteliğine dikkat edelim. Buraya atanan değer,
  bileşene enjekte edilen ProductsService nesnesine ait form özelliğidir.
  Servis tipinin productForm değişkenindeki alanlar bu bileşen üzerindeki kontrollere
  formControlName niteliği yardımıyla bağlanırlar.
-->

product-list.component.ts

import { Component, OnInit } from '@angular/core';
import { ProductsService } from '../shared/products.service'; // ProductService modülünü bildiriyoruz

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  constructor(private productService: ProductsService) { } // servisi constructor üzerinden enjekte ettik
  allProducts; // Firestore koleksiyonundaki tüm dokümanları temsil edecen değişkenimiz
  ngOnInit() {
    /*
    Bileşen initialize aşamasındayken servisten tüm ürünleri çekiyoruz.
    Subscribe metoduyla da servisin getProducts metodundan dönen sonuç kümesini,
    allProducts isimli değişkene bağlıyoruz ki bunu bileşenin ön yüzü kullanıyor
    */
    this.productService
      .getProducts()
      .subscribe(res => this.allProducts = res);
  }

  /*
    Bir ürünü silmek için kullandığımız metod. 
    Servis tarafındaki deleteProduct çağrılıyor.
    Parametre olarak o anki product içeriği gönderilmekte
  */
  delete = p => this.productService.deleteProduct(p).then(r => {
    //alert('silindi');
  });

  /*
    Ürünün sadece bargain özelliğini update eden bir metod 
    olarak düşünelim. Senaryoda pazarlık payı olup olmadığını belirten
    checkbox'ın durumunu güncelletiyoruz
  */

  // Güncelleme örneği (fiyatı 10 birim arttırıyoruz)
  increasePrice = p => this.productService.updateProduct(p, 10);

  // Güncelleme örneği (fiyatı 10 birim düşürüyoruz)
  decreasePrice = p => this.productService.updateProduct(p, -10);
}

product-list.component.html

<table class="table"><thead class="thead-dark"><tr><th>Açıklama</th><th>Başlık</th><th>Fiyat</th><th>Pazarlık?</th><th></th></tr></thead><tbody><tr *ngFor="let product of allProducts"><td>{{product.payload.doc.data().summary}}</td><td>{{product.payload.doc.data().title}}</td><td>{{product.payload.doc.data().price}}</td><td>
        {{product.payload.doc.data().bargain?'Var':'Yok'}}</td><td><div class="btn-group-sm"><button class="primary btn-default btn-block" (click)="increasePrice(product)">+</button><button class="primary btn-default btn-block" (click)="decreasePrice(product)">-</button><button class="primary btn-danger btn-block" (click)="delete(product)">Sil</button></div></td></tr></tbody></table><!--
  Klasik bir Grid tasarımı söz konusu.
  *ngFor ile bileşenin init metodunda doldurulan allProducts dizisini dönüyoruz.
  Firestore'dan gelen her bir dokümanın elemanlarına ulaşmak için,
  payload.doc.data().[özellik adı] notasyonunu kullandık.

  Sil başlıklı button'a basıldığında bileşendeki delete metodunu çağrılmış oluyor.

  Checkbox kontrolünün click olayında bileşendeki güncelleme metodunu ve dolayısıyla
  servis tarafındaki versiyonunu çağırmış oluyoruz.
-->

CRUD operasyonları her iki bileşen içinde ortaklaşa kullanılabilecek fonksiyonellikler. Bu nedenle Shared klasörü altında konuşlandırdığımız products.service.ts isimli bir tip mevcut.

import { Injectable } from '@angular/core';
import { FormControl, FormGroup } from "@angular/forms"; // FormGroup ve FormControl tiplerini kullanabilmek için eklemeliyiz
import { AngularFirestore } from "@angular/fire/firestore"; // Firestore tarafı ile konuşmamızı sağlayacak modül. Servisini constructor'da enjekte ediyoruz

@Injectable({
  providedIn: 'root'
})
export class ProductsService {

  constructor(private firestore: AngularFirestore) { }

  /* 
  Yeni bir FormGroup nesnesi örnekliyoruz.
  title, summary, price ve online isimli FormControl nesneleri içeriyor.
  bu özelliklere atanan değerleri Firebase tarafına yazacağız.
  element adları arayüz tarafında da birebir kullanılacaklar
  */
  productForm = new FormGroup({
    title: new FormControl(''),
    summary: new FormControl(''),
    price: new FormControl(100),
    bargain: new FormControl(false),
  })

  /*
  Firestore veritabanına yeni bir Product verisi eklemek için kullanılan servis metodu.
  collection ile Firestore tarafındaki koleksiyonu işaret ediyoruz.
  Gelen json içeriği products isimli koleksiyona yazılıyor.
  */
  addProduct(p) {
    return new Promise<any>((resolve, reject) => {
      this.firestore.collection("products").add(p).
        then(res => { }, err => reject(err));
    });
  }

  getProducts() {
    /*
     Firestore veri tabanındaki products koleksiyonu içerisinde yer alan tüm dokümanları alıyoruz.
     snapshotChanges çağrısı değişikliklerin kontrol altında olmasını sağlar. 
     Bizim değişiklikleri yakalayıp güncellemeler yapmamıza gerek kalmaz.
    */

    return this.firestore.collection("products").snapshotChanges();
  }

  // silme işlemini üstlenen servis metodumuz
  deleteProduct(p) {
    return this.firestore
      .collection("products")
      .doc(p.payload.doc.id) // firestrore tarafındaki id bilgisini kullanacak.
      .delete();
  }

  // Güncelleme operasyonu. rate değişkenine gelen değere göre price değerini değiştiriyoruz
  updateProduct(p, rate) {
    // Önce üzerinde çalışılan veriyi alalım.
    var prd=p.payload.doc.data();
    if(prd.price==10 && rate<0) // fiyatı sıfırın altına indirmek istemeyiz çünkü
      return;    
    // Üst limit kontrolü de konulabilir belki

    // fiyat arttırımı veya azaltımı uygunsa yeni değeri alıyoruz ve firestore üzerinden güncelleme yapıyoruz
    var newPrice=prd.price+rate;
    return this.firestore
      .collection("products")
      .doc(p.payload.doc.id)
      .set({ price: newPrice }, { merge: true });
    // merge özelliğine atanan true değeri, tüm entity değerlerinin güncellenmesi yerine sadece metoda ilk parametre ile gelenlerin ele alınmasını söyler.
  }
}

Eklediğimiz bileşenleri kullandığımız yer app nesnesi. Angular tarafındaki ana bileşenimiz olarak düşünebiliriz. Dolayısıyla modül bildirimleri ve bileşenlerin HTML yerleşimleri için app.module.ts ve app.component.html dosyalarını da kodlamamız gerekiyor.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { environment } from "src/environments/environment"; //environment.ts içerisindeki firebaseConfig sekmesinin anlaşılabilmesi için gerekli modül
import { AngularFireModule } from "@angular/fire";
import { AngularFirestoreModule } from "@angular/fire/firestore";

import { AppComponent } from './app.component';
import { ProductsComponent } from './products/products.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductsService } from './shared/products.service'; // Tüm bileşenlerde kullanabilmek için ProductsService modülünü bildirip alttaki providers özelliğine de ekledik
import {ReactiveFormsModule} from '@angular/forms'; // Service tarafında FormControl ve FormGroup modüllerini kullanabilmek için bildirdik ve aşağıdaki import kısmında ekledik

@NgModule({
  declarations: [
    AppComponent,
    ProductsComponent,
    ProductListComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    AngularFireModule.initializeApp(environment.firebaseConfig), // AngularFireModule' ü environment.ts içerisindeki firebaseConfig ayarları ile başlatmış olduk
    AngularFirestoreModule
  ],
  providers: [ProductsService], 
  bootstrap: [AppComponent]
})
export class AppModule { }

ve app.component.html

<!-- 
  Uygulama hizmete alındığında render edilecek olan ana bileşenimiz.
  ng g c komutları ile oluşturduğumuz products ve product-list bileşenlerini bootstrap grid sistemini kullanarak ekrana yerleştiriyoruz.
  class niteliklerinde kullandığımzı değerler ile ortamı biraz renklendirmeye çalıştım

--><div class="container px-lg-5"><div class="row"><h1>Hızlı Satış?</h1></div><div class="row border border-primary rounded"><div class="col py-3 px-lg-3 col-md-12"><app-products></app-products></div></div><div class="row border border-dark rounded"><div class="col py-3 px-lg-3 col-md-12"><app-product-list></app-product-list></div></div></div>

Son olarak Firestore tarafı için gerekli apiKey, databaseUrl, senderId, projectId gibi bilgilerin environment.ts dosyasına eklenmesi lazım ki çalışma zamanında kullanılabilsinler. Bu bilgileri Google Cloud tarafı otomatik olarak üretmişti hatırlayacağınız üzere.

export const environment = {
  production: false,
  // Firebase'in orada oluşturduğumuz projemiz için bize verdiği ayarlar
  firebaseConfig: {
    apiKey: ".....", //Burası sizin projenizin Api Key değeri olmalı
    authDomain: "quict-auctions-project.firebaseapp.com",
    databaseURL: "https://quict-auctions-project.firebaseio.com",
    projectId: "quict-auctions-project",
    storageBucket: "quict-auctions-project.appspot.com",
    messagingSenderId: "....." // Bu da sizin projeniz için verilen senderId değeri olmalı
  },
};

Hepsi bu kadar :) Artık uygulamayı çalıştırıp sonuçlarına bakabiliriz.

Çalışma Zamanı

Uygulamayı çalıştırmak için terminalden

ng serve

komutunu vermemiz yeterli. 4200 numaralı port üzerinden web arayüzüne erişebiliriz. WestWorld testlerinde benim aldığım örnek bir ekran görüntüsünü aşağıda bulabilirsiniz(Hani ispatı olsun da sonra çalışmıyor bu filan demeyelim)

Örneğin ilgi çekici yanlarından birisi, önyüz ve Firestore taraflarının eş zamanlı olarak güncel kalabilmeleridir. Firestore web konsolunda eriştiğimiz dokümanlarda yapacağımız değişiklikler anında önyüz tarafına push edilir, önyüzde yaptığımız değişiklikler de benzer şekilde Firestore tarafına yansır. Bunu denemenizi öneriririm. + ve - düğmeleri ile güncel fiyat bilgisini arttırma veya azaltma işlemlerini yapabiliriz. Sil düğmesi tahmin edileceği üzere satışa çıkarttığımız ürünü repository'den kaldırmak içindir. Güncelleme oparasyonunu sadece fiyat ayarlamaları için yaptık lakin ürün bilgilerinin düzenlenmesi ihtiyacı da var. Bunu uygulamaya nasıl ekleyebiliriz bir düşünün ;)

Ben Neler Öğrendim

Bu örnek çalışma ile Angular bilgilerimi biraz daha pekiştirmiş ama daha da önemlisi veri kaynağı olarak Google Cloud Platform'un bir ürününü kullanmış oldum. Genel hatlarıyla öğrendiklerimi şöyle özetleyebilirim.

  • Bir component üzerindeki element değerlerinin formControlName niteliği yardımıyla servis tarafındaki FormControl nesnelerine bağlanabileceğini
  • Firebase üzerinde Cloud Firestore veri tabanının nasıl oluşturulabileceğini
  • Uygulamanın Firebase tarafı ile haberleşebilmesi için gerekli konfigurasyon ayarlarının nereye konulması gerektiğini ve nasıl çağırılabildiğini
  • Cloud Firestore ve önyüzün birbirlerinin değişikliklerini anında görebildiklerini
  • Bileşenlerdeki kontrollere olay metodlarının nasıl bağlanabileceğini
  • Firestore paketinin temel CRUD(Create Read Update Delete) komutlarını

Böylece geldik bir maceranın daha sonuna. 32 numaralı Saturday-Night-Works çalışmasının kodlarına buradan ulaşabilirsiniz. Yeni bir gözden geçirme yazısında buluşuncaya dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/blazor-ile-hello-world-uygulamasi-gelistirmekBlazor ile Hello World Uygulaması Geliştirmek

$
0
0

Merhaba Arkadaşlar,

Oturduğunuz yerden göründüğü gibi çok karikatür okuyan biri değilimdir. Ama bazen kendimi sevgili Yiğit Özgür'ün kaleminden çıkan bir Huni Kafa karakteri gibi hissettiğim olur. Bir sebepten ne olduğunu tam olarak anlayamadığım konular üzerinde debelenir dururum. O kaynaktan bu kaynağa geçerken de kaybolurum. Lakin her zaman elle tutulur bir şeylere ulaşma şansı da bulurum.

Blazor'da bu standart anlayamama sürecime takılan konulardan birisiydi. Ona olan merakım çevremde konuşulanlarla başlamıştı. Çok yakın dostum Bora Kaşmer'in konu ile ilgili yazıları ve şirketteki deneyimli yazılımcıların tariflemelerine rağmen zihnimde onu tanımlayacak iyi bir cümleyi bir türlü kuramıyordum. Neden kullanacaktım ki onu? Hangi problemi çözüyordu? Ne gibi kolaylıklar getiriyordu? Bunları tam olarak niteleyemediğimi görünce 19ncu bölüm ortaya çıktı. Öyleyse notlarımı derlemeye başlayalım.

Saturday-Night-Works'ün 19ncu bölümündeki amacım Microsoft'un deneysel olarak geliştirdiği Blazorçatısı(Web Framework) ile C#/Razor(Razor HTML markup ve C#'ın bir arada kullanılabildiği syntax olarak düşünülebilir. Bu sayede C# ve HTML kodlamasını aynı dosyada intellisense desteği ile ele alabiliriz), HTML ve WebAssembly tabanlı uygulamaların nasıl geliştirilebileceğini Hello World diyerek deneyimlemekti. 

Aslında uzun süredir hayatımızda olan ve Windows, macOS, Linux gibi platformlarda C# tabanlı Client Web uyulamalarının geliştirilmesine odaklanan Blazor, bu idealini gerçekleştirirken WebAssembly desteğinden yararlanıyor. WebAssembly, yüksek performanslı web uygulamalarının geliştirilmesinde kullanılan öncü akımlardan. Felsefe olarak C, C++, Rust gibi düşük seviyeli dillerle yazılmış kodların derlenerek browser(tüm tarayıcılar destekliyor)üzerinde çalıştırılabilmesi ilkesini benimsiyor. İşte bu noktada yorumlamalı dillerden olan ve web tarafında çok kullanılan Javascript'in önüne geçiyor. Bunun en büyük sebebi derlemenin getirdiği performans ve hız kazanımı. Blazor işte bu avantajı C# tarafında kullanabilmemize olanak sağlayan bir çatı. Konu kafamda hala muallakta olmakla birlikte en azından .Net Core cephesinde bir Blazor uygulaması nasıl geliştirilir bilmem gerekiyor. İlk hedef basit bir uygulamayı inşa edip ayağa kaldırmak ve temel bileşenleri anlamaya çalışmak.

Blazor, .Net ile geliştirilmiş Single Page Application'ların WebAssembly desteği yardımıyla tarayıcı üzerinde çalışmalarına olanak sağlayan bir Web Framework olarak düşünülebilir.

Blazor cephesinde Client Side ve Server Side Hosting modelleri söz konusu. Client-Side modelinde C#/Razor ile geliştirilip derlenen .Net Assembly'ları, .Net Runtime ile birlikte tarayıcıya indiriliyor. Sunucu bazlı modele bakıldığındaysa, Razor bileşenlerinin sunucu tarafında konuşlandığını UI, Javascript ve olay(event)çağrıları içinse SignalR odaklı iletişimin devreye girdiğini görüyoruz. Esasında uygulamalar Component bazlı geliştirilmekte. Bir component bir C# sınıfıdır ve Blazor açısından bakıldığında genellikle bir cshtml dosyasıdır(Elbette bir C# dosyası da olabilir)

WebAssembly koduna derlenen uygulamalar herhangi bir tarayıcıda yüksek performansla çalışabilirler.

Nelere İhtiyacımız Var?

Pek çok kaynak konuyu Visual Studio üzerinde incelemekte. Bu profesyonel IDE üzerinde bir Web projesi açarken şablon kısmından Blazor'u seçmek yeterli. Ancak ben yabancı topraklardayım ve WestWorld'de Linux ile en yakın arkadaşı Visual Studio Code yaşamakta. Bu nedenle işe aşağıdaki terminal komutları ile başlamak gerekiyor.

dotnet new --install "Microsoft.AspNetCore.Blazor.Templates"
dotnet new blazor -o HelloWorld

Öncelikle blazor için gerekli proje şablonunu indiriyoruz. Ardından blazor tipinden hazır bir proje iskeletini oluşturuyoruz. Hemen ilgili klasöre girip dotnet run komutu ile programı çalıştırıp deneyebiliriz. Uyguluma, localhost:5000 numaralı porttan hizmet verecektir.

Oluşturulan ilk örneği didiklemekte fayda var. Index, Counter ve FetchData(Dependency Injection kullanılan örnek) yönlendirmeleri sonrası çalışan aynı isimli cshtml içeriklerine odaklanmak gerekiyor. Söz gelimi Counter sayfasında düğmeye bastıkça sayaç değeri artmakta. Ancak bu gerçekleşirken sayfa yeniden yüklenmiyor ki bunun için normalde Client-Side Javascript kodunun yazılması gerekir. Olaya Blazor açısından baktığımızda, kodlamanın Javascript değil de C# ile yapıldığını fark etmemiz lazım. İlgili sayfada oynayarak farklı sonuçlar elde etmeye çalışabiliriz. Ben Counter sayfasını biraz kurcalayıp kod tarafını aşağıdaki gibi ele almaya çalışmıştım

@page "/counter"<h1>Rastgele Toplamlar</h1><p>Blazor'a geçişten önce bu tarafı anlamaya çalışıyorum...</p><p>Güncel rastgele toplam: @currentCount</p><p>Arttırım miktarı: @incraseValue </p><button class="btn btn-primary" onclick="@IncrementValue">Arttırmak için bas!</button>

@functions {
    // Değişken değerlerini HTML tarafında @ operatörü ile kolayca kullanabiliriz
    int currentCount = 0;
    int incraseValue=0;
    Random random=new Random();

    void IncrementValue() // button'un onclick metodunda @ operatörü ile erişiyoruz
    {        
        incraseValue=random.Next(1,100); //1 ile 100 arasında rastgele değer ürettirdik
        currentCount+=incraseValue; 
    }
}

ki çalışma zamanı çıktısı aşağıdakine benzerdi.

Arayüz mutlaka dikkatinizi çekmiştir. Hoş bir tasarımı var. En azından benim için öyle. Blazor proje şablonuna göre CSS tarafı için bootstrap hazır olarak geliyor. Sol taraftaki navigation menu'yü kurcalamak istersek, Shared klasöründeki NavMenu.cshtml ile oynamak yeterli ki örneğin son kısmında burayı değiştirmiş olacağız. Her şeyin giriş noktası olan index.html sayfasında blazor.webassembly.js isimli javascript dosyası için bir referans bulunuyor.

Dependency Injection Kullanımı

Blazor dahili bir DI mekanizmasını destekliyor ve built-in olanlar haricinde kendi servislerimizin de içeriye bu mekanizma yardımıyla alınmasına olanak sağlıyor(hatta buna zorluyor) Söz gelimi HttpClient gibi bir built-in servisi client-side Razor tarafına enjekte edip kullanabiliriz. IJSRuntime, IUriHelper gibi bir çok yararlı built-in servis bulunmakta. Kendi servislerimizi de(söz gelimi bir data repository için kullanılabilecek tipleri) DI ile sisteme dahil etmemiz mümkün. Aynen .Net Core'da olduğu gibi ConfigureServices metoduna gelen IServicesCollection arayüzünden yararlanarak bunu sağlayabiliriz (WorldPopulation sayfasında built-in servis kullanımına dair bir örnek bulunuyor)

services.AddSingleton<IMessenger, SMSMessenger>();

Kod Tarafının Geliştirilmesi

Şimdi Blazor tarafındaki kodlamayı anlayabilmek için iki basit bileşen tasarımı yapalım. Bunlardan ilkinde kobay olarak kitaplarımızı konu alacağız. Bir listeye kitap eklenmesi ve bu listenin gösterilmesi işlerini yapmaya çalışacağız. Bir kitabı kod tarafında temsil emtek için book isimli aşağıdaki sınıftan yararlanabiliriz. I know, I know... Bir kitabı birden fazla yazar yazmış olabilir ve bir yazarın birden fazla kitabı da olabilir. Hani nerede nesneler arası many-to-many ilişki? Motivasyonum Blazor tarafında Hello World demek olduğu için bu kısmı tamamen örtpas etmiş durumdayım.

public class Book
{
    public string Title { get; set; }
    public string Summary { get; set; }
    public int PageCount { get; set; }
    public string Authors { get; set; }
}

Kitaplar ile ilgili işlemler için Pages klasörüne Book.cshtml isimli bir dosya ekleyip aşağıdaki şekilde kodlayabiliriz. Çok basit olarak kitap listesinin gösterilmesi ve yeni bir kitabın eklenebilmesi için gerekli fonksiyonelliklerin sunulduğu bir arayüzümüz var. HTML tarafı ile kod bir arada kullanılmakta.

@page "/bookList"<h1>Okuduğum Kitaplar (Toplam @books.Count() kitabım var) </h1> <!--Toplam kitap sayısını da başlığa ekledik --><blockquote class="blockquote">
    Burada okumaktan keyif aldığım kitaplar yer alıyor.
</blockquote><ul><!-- Tüm kitapları dolaşıp örnek olarak başlıklarını listeliyor ve hemen alt kısmına özet bilgilerini yerleştiriyoruz-->
    @foreach(var book in books){<li aria-describedby="bookTitle">@book.Title</li><small id="bookTitle" class="form-text text-muted">@book.Summary</small> 
    }</ul><!-- Yeni bir kitap bilgisinin girişi için Bootstrap ile zenginleştirilmiş basit bir formumuz var --><div class="form-group"><input class="form-control" id="txtTitle" placeholder="Kitabın adı" bind="@newBook.Title"/><br/> <!--bind attribute'una atanan değer ile Title özelliğine bağladık --><input class="form-control" id="txtAuthors" placeholder="Yazarlar" bind="@newBook.Authors" /><br/><input class="form-control" id="txtPageCount" placeholder="Sayfa sayısı" bind="@newBook.PageCount" /><br/><input class="form-control" id="txtSummary" aria-describedby="summaryHelp" placeholder="Özet" bind="@newBook.Summary" /><small id="summaryHelp" class="form-text text-muted">Lütfen bir cümleyle kitabın neyle ilgili olduğunu anlat</small> <!-- yardımcı bilgi veren metin için koyduk --></div><button onclick="@AddNewBook" class="btn btn-primary">Listeye ekleyelim</button> <!-- onclick attribute'unda AddNewBook metoduna bağladık --><!-- Fonksiyonlarımız -->
@functions{
    // Tüm kitap listemizi ifade eden koleksiyonumuz
    IList<Book> books=new List<Book>();
    Book newBook=new Book();
    // Yeni bir kitap eklemek için kullanıyoruz.
    void AddNewBook(){
        books.Add(newBook); // Kitabı listeye ekledik
        newBook=new Book(); // Eğer newBook nesnesini sıfırlamassak büyük ihtimalle koleksiyona hep aynı nesne örneği eklenecektir.
    }
}

Ekleyeceğimiz bir diğer örnek Dependency Injection kullanımı ile ilgili. Built-in olarak gelen HttpClient servisini cshtml tarafında nasıl kullanabileceğimizi görebilmek için WorldPopulation.cshtml isimli bir dosya geliştiriyoruz. Yine Pages klasörüne konuşlandıracağımız dosya içeriği aşağıdaki gibi yazılabilir. @page direktifine göre /population adresine gelen taleplere karşılık bu sayfa işletilecektir. @inject kısmında httpClient servisinin koda enjekte edilmesi söz konusudur.

@page "/population"
@inject HttpClient httpClient<!-- built-in servislerden olan HttpClient servisini buraya enjekte ettik. 
httpClient değişken adıyla kullanabiliriz --><h2>Güncel 3 Günlük Dünya Nüfusu Bilgileri</h2><blockquote class="blockquote">
    Bilgiler api.population.io sitesinden alınmıştır.
</blockquote>

@if (values == null) // Henüz veriler gelmemiş olabilir.
{
    <p><em>Bilgiler alınıyor...</em></p>
}
else
{<div class="card" style="width: 18rem;"><ul class="list-group list-group-flush">
            @foreach (var currentData in @values) // Tüm değerleri dolaşıp güncel nüfus verisini ekrana basıyoruz            
            {
                <li class="list-group-item">@string.Format("{0:#,0}",@currentData.Value) - @currentData.Date.ToShortDateString() </li>
            }</ul></div>
}

@functions{
    Population[] values; // istatistik bilgilerin dizisi

    // Sayfamızın başlangıç aşamasında çalışan asenkron olay metodumuz
    protected override async Task OnInitAsync()
    {
        // GetJsonAsync metodunu kullanarak bir talep gönderiyoruz ve sunucu tarafından json dosyasını alıyoruz
        // Burada harici bir servis adresine de çıkılabilir
        //TODO: world.json içeriğini veren bir .net web api dahil edelim
        values = await httpClient.GetJsonAsync<Population[]>("db/world.json");
    }

    // Nüfus bilgilerini tutan sınıfımız
    class Population
    {
        public DateTime Date { get; set; }
        public Int64 Value { get; set; }
    }
}

Bu sayfa sembolik olarak üç günlük dünya nüfusu bilgilerini paylaşıyor. Tamamen kafadan uydurma bir örnek. Normal şartlarda nüfus bilgileri bir servis aracılığıyla çekilmekte. Bu noktada HttpClient hizmetinden yararlanmalıyız. Biz veri kaynağı olarak gerçek bir servisi kullanmak yerine sahte bir json içeriğini ele alıyoruz. wwwroot altında oluşturacağımız db klasöründe yer alan world.json dosyası bu noktada devreye giriyor. Ancak TODO kısmında belirttiğimiz üzere siz örneği geliştirirken bir Web API servisini kullanmayı deneyebilirsiniz.

[
    {
        "date": "2019-02-01",
        "value": 7644991666
    },
    {
        "date": "2019-02-02",
        "value": 7645213391
    },
    {
        "date": "2019-02-03",
        "value": 7645435108
    }
]

Pek tabii boilerplate etkisi ile üretilen projenin menüsü hazır şablona göre tesis edilmiş durumda. Burayı yeni eklediğimiz kendi sayfalarımıza göre düzenleyebiliriz. Tek yapmamız gereken NavMenu.cshtml dosyasını kurcalayarak aşağıdaki kıvama getirmektir. NavLink elementlerinde yeni eklediğimiz bileşenlerdeki @page direktiflerinde belirtilen URL adresleri kullanılmaktadır.

<div class="top-row pl-4 navbar navbar-dark"><a class="navbar-brand" href="">HelloWorld</a><button class="navbar-toggler" onclick=@ToggleNavMenu><span class="navbar-toggler-icon"></span></button></div><div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu><ul class="nav flex-column"><li class="nav-item px-3"><NavLink class="nav-link" href="" Match=NavLinkMatch.All><span class="oi oi-home" aria-hidden="true"></span> Başlangıç :P</NavLink></li><li class="nav-item px-3"><NavLink class="nav-link" href="population"><span class="oi oi-home" aria-hidden="true"></span> Dünya Nüfusu</NavLink></li><!-- <li class="nav-item px-3"><NavLink class="nav-link" href="counter"><span class="oi oi-plus" aria-hidden="true"></span> Sayaç</NavLink></li>
        --><!-- Yeni eklediğimiz book sayfası için link. href değerine göre bookList.cshtml sayfasına yönlendirileceğiz --><li class="nav-item px-3"><NavLink class="nav-link" href="bookList"><span class="oi oi-list-rich" aria-hidden="true"></span> Kitaplar</NavLink></li></ul></div>

@functions {
    bool collapseNavMenu = true;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Çalışma Zamanı

Artık bir deneme sürüşüne çıkabiliriz. Uygulamayı terminalden aşağıdaki komutu vererek çalıştırmamız mümkün.

dotnet run

Örnek olarak bir iki kitap girip sonuçları inceleyebiliriz. Ben aşağıdakine benzer bir ekran görüntüsü yakalamışım.

Çalışma zamanını incelerken F12 ile debug moda geçmekte yarar var. Söz gelimi booklist üzerinde çalışırken kitap ekleme ve listeleme gibi operasyonların gerçekleştirilmesine karşılık oluşan HTML kaynağı aşağıdaki gibidir. Standart üretilen HTML çıktılarından biraz farklı değil mi? MVC'de, eski nesil Server Side Web Forms'larda veya saf HTML ile yazdıklarımızda üretilen içerikleri düşünelim. Bir takım elementleri source üzerinde göremiyoruz gibi. Yine de sayfamız kanlı canlı bir şeyler yürütüyor. Derlenmiş bir uygulamanın tarayıcıda koştuğunu ifade edebiliriz.

Built-In HttpClient servisini enjekte ettiğimiz dünya nüfus verileri sayfası ise şöyle görünecektir (Ekranı daraltmamıza rağmen UX deneyiminin bozulmadığını görmüşsünüzdür)

Ürünün Paketlenmesi

Bir Blazor uygulamasının dağıtımı için publish işlemine ihtiyacımız var. Visual Studio tarafında bu iş oldukça kolay. Microsoft Azure platformuna servis olarak da alabiliriz. WestWorld gibi Ubuntu tabanlı ortamlardaysa dağıtım işlemini dotnet komut satırı aracını kullanarak aşağıdaki terminal komutuyla gerçekleştirebiliriz.

dotnet publish -c Release

Oluşan dosya içeriklerini incelemekte yarar var. publish operasyonu sırasında gereksiz kütüphaneler çıkartılıp paket boyutu mümkün mertebe küçültülüyor. Dikkat çekici nokta C# kodunun çalışması için gerekli ne kadar runtime bileşeni(mscorlib, mono runtime, c libraries vb) varsa mono.wasm içine konulması. WestWorld'teki örnek için bu 2.1 mb'lık dosya anlamına geldi.

Bunun sonucu olarak bin/Release/netstandard2.0/publish/ klasörü altına gerekli tüm proje dosyaları atılır. Bu dosyaları web sunucusuna veya bir host service'e alarak(manuel veya otomatik araçlar yardımıyla) uygulamayı canlı(production) ortama taşıyabiliriz.

Ben Neler Öğrendim?

Blazor benim yeni yeni keşfetmeye, öğrenmeye ve anlamaya çalıştığım konulardan birisi. Yer yer huni takmama sebep olan iç mimarisi sebebiyle üstüne daha çok kafa patlatmam gerektiğiyse aşikar. Buna rağmen bu basit Hello World denemesi sırasında bile öğrendiğim bir kaç şey oldu. Bunları şöyle maddeleştirebilirim.

  • Bir Blazor proje şablonunun temel bileşenlerinin ne olduğunu
  • Blazor tarafında Bootstrap kullanarak daha şık tasarımlar yapılmasını
  • Razor'da sayfa bileşenleri ile fonksiyonların nasıl etkileşebileceğini
  • Blazor'daki Dependency Injection mekanizmasının nasıl ele alınabileceğini
  • Bileşen odaklı bir geliştirme ortamı olduğunu
  • Kabaca WASM terimini
  • Blazor uygulamasının canlı ortamlar için publish edilmesini

Ve böylece geldik bir Saturday-Night-Works derlemesinin daha sonuna. Bir başka macerada görüşmek üzere hepinize mutlu günler dilerim.


Blazor ile Hello World Uygulaması Geliştirmek

$
0
0

Oturduğunuz yerden göründüğü gibi çok karikatür okuyan biri değilimdir. Ama bazen kendimi sevgili Yiğit Özgür'ün kaleminden çıkan bir Huni Kafa karakteri gibi hissettiğim olur. Bir sebepten ne olduğunu tam olarak anlayamadığım konular üzerinde debelenir dururum. O kaynaktan bu kaynağa geçerken de kaybolurum. Lakin her zaman elle tutulur bir şeylere ulaşma şansı da bulurum.

Blazor'da bu standart anlayamama sürecime takılan konulardan birisiydi. Ona olan merakım çevremde konuşulanlarla başlamıştı. Çok yakın dostum Bora Kaşmer'in konu ile ilgili yazıları ve şirketteki deneyimli yazılımcıların tariflemelerine rağmen zihnimde onu tanımlayacak iyi bir cümleyi bir türlü kuramıyordum. Neden kullanacaktım ki onu? Hangi problemi çözüyordu? Ne gibi kolaylıklar getiriyordu? Bunları tam olarak niteleyemediğimi görünce 19ncu bölüm ortaya çıktı. Öyleyse notlarımı derlemeye başlayalım.

Saturday-Night-Works'ün 19ncu bölümündeki amacım Microsoft'un deneysel olarak geliştirdiği Blazorçatısı(Web Framework) ile C#/Razor(Razor HTML markup ve C#'ın bir arada kullanılabildiği syntax olarak düşünülebilir. Bu sayede C# ve HTML kodlamasını aynı dosyada intellisense desteği ile ele alabiliriz), HTML ve WebAssembly tabanlı uygulamaların nasıl geliştirilebileceğini Hello World diyerek deneyimlemekti. 

Aslında uzun süredir hayatımızda olan ve Windows, macOS, Linux gibi platformlarda C# tabanlı Client Web uyulamalarının geliştirilmesine odaklanan Blazor, bu idealini gerçekleştirirken WebAssembly desteğinden yararlanıyor. WebAssembly, yüksek performanslı web uygulamalarının geliştirilmesinde kullanılan öncü akımlardan. Felsefe olarak C, C++, Rust gibi düşük seviyeli dillerle yazılmış kodların derlenerek browser(tüm tarayıcılar destekliyor)üzerinde çalıştırılabilmesi ilkesini benimsiyor. İşte bu noktada yorumlamalı dillerden olan ve web tarafında çok kullanılan Javascript'in önüne geçiyor. Bunun en büyük sebebi derlemenin getirdiği performans ve hız kazanımı. Blazor işte bu avantajı C# tarafında kullanabilmemize olanak sağlayan bir çatı. Konu kafamda hala muallakta olmakla birlikte en azından .Net Core cephesinde bir Blazor uygulaması nasıl geliştirilir bilmem gerekiyor. İlk hedef basit bir uygulamayı inşa edip ayağa kaldırmak ve temel bileşenleri anlamaya çalışmak.

Blazor, .Net ile geliştirilmiş Single Page Application'ların WebAssembly desteği yardımıyla tarayıcı üzerinde çalışmalarına olanak sağlayan bir Web Framework olarak düşünülebilir.

Blazor cephesinde Client Side ve Server Side Hosting modelleri söz konusu. Client-Side modelinde C#/Razor ile geliştirilip derlenen .Net Assembly'ları, .Net Runtime ile birlikte tarayıcıya indiriliyor. Sunucu bazlı modele bakıldığındaysa, Razor bileşenlerinin sunucu tarafında konuşlandığını UI, Javascript ve olay(event)çağrıları içinse SignalR odaklı iletişimin devreye girdiğini görüyoruz. Esasında uygulamalar Component bazlı geliştirilmekte. Bir component bir C# sınıfıdır ve Blazor açısından bakıldığında genellikle bir cshtml dosyasıdır(Elbette bir C# dosyası da olabilir)

WebAssembly koduna derlenen uygulamalar herhangi bir tarayıcıda yüksek performansla çalışabilirler.

Nelere İhtiyacımız Var?

Pek çok kaynak konuyu Visual Studio üzerinde incelemekte. Bu profesyonel IDE üzerinde bir Web projesi açarken şablon kısmından Blazor'u seçmek yeterli. Ancak ben yabancı topraklardayım ve WestWorld'de Linux ile en yakın arkadaşı Visual Studio Code yaşamakta. Bu nedenle işe aşağıdaki terminal komutları ile başlamak gerekiyor.

dotnet new --install "Microsoft.AspNetCore.Blazor.Templates"
dotnet new blazor -o HelloWorld

Öncelikle blazor için gerekli proje şablonunu indiriyoruz. Ardından blazor tipinden hazır bir proje iskeletini oluşturuyoruz. Hemen ilgili klasöre girip dotnet run komutu ile programı çalıştırıp deneyebiliriz. Uyguluma, localhost:5000 numaralı porttan hizmet verecektir.

Oluşturulan ilk örneği didiklemekte fayda var. Index, Counter ve FetchData(Dependency Injection kullanılan örnek) yönlendirmeleri sonrası çalışan aynı isimli cshtml içeriklerine odaklanmak gerekiyor. Söz gelimi Counter sayfasında düğmeye bastıkça sayaç değeri artmakta. Ancak bu gerçekleşirken sayfa yeniden yüklenmiyor ki bunun için normalde Client-Side Javascript kodunun yazılması gerekir. Olaya Blazor açısından baktığımızda, kodlamanın Javascript değil de C# ile yapıldığını fark etmemiz lazım. İlgili sayfada oynayarak farklı sonuçlar elde etmeye çalışabiliriz. Ben Counter sayfasını biraz kurcalayıp kod tarafını aşağıdaki gibi ele almaya çalışmıştım

@page "/counter"<h1>Rastgele Toplamlar</h1><p>Blazor'a geçişten önce bu tarafı anlamaya çalışıyorum...</p><p>Güncel rastgele toplam: @currentCount</p><p>Arttırım miktarı: @incraseValue </p><button class="btn btn-primary" onclick="@IncrementValue">Arttırmak için bas!</button>

@functions {
    // Değişken değerlerini HTML tarafında @ operatörü ile kolayca kullanabiliriz
    int currentCount = 0;
    int incraseValue=0;
    Random random=new Random();

    void IncrementValue() // button'un onclick metodunda @ operatörü ile erişiyoruz
    {        
        incraseValue=random.Next(1,100); //1 ile 100 arasında rastgele değer ürettirdik
        currentCount+=incraseValue; 
    }
}

ki çalışma zamanı çıktısı aşağıdakine benzerdi.

Arayüz mutlaka dikkatinizi çekmiştir. Hoş bir tasarımı var. En azından benim için öyle. Blazor proje şablonuna göre CSS tarafı için bootstrap hazır olarak geliyor. Sol taraftaki navigation menu'yü kurcalamak istersek, Shared klasöründeki NavMenu.cshtml ile oynamak yeterli ki örneğin son kısmında burayı değiştirmiş olacağız. Her şeyin giriş noktası olan index.html sayfasında blazor.webassembly.js isimli javascript dosyası için bir referans bulunuyor.

Dependency Injection Kullanımı

Blazor dahili bir DI mekanizmasını destekliyor ve built-in olanlar haricinde kendi servislerimizin de içeriye bu mekanizma yardımıyla alınmasına olanak sağlıyor(hatta buna zorluyor) Söz gelimi HttpClient gibi bir built-in servisi client-side Razor tarafına enjekte edip kullanabiliriz. IJSRuntime, IUriHelper gibi bir çok yararlı built-in servis bulunmakta. Kendi servislerimizi de(söz gelimi bir data repository için kullanılabilecek tipleri) DI ile sisteme dahil etmemiz mümkün. Aynen .Net Core'da olduğu gibi ConfigureServices metoduna gelen IServicesCollection arayüzünden yararlanarak bunu sağlayabiliriz (WorldPopulation sayfasında built-in servis kullanımına dair bir örnek bulunuyor)

services.AddSingleton<IMessenger, SMSMessenger>();

Kod Tarafının Geliştirilmesi

Şimdi Blazor tarafındaki kodlamayı anlayabilmek için iki basit bileşen tasarımı yapalım. Bunlardan ilkinde kobay olarak kitaplarımızı konu alacağız. Bir listeye kitap eklenmesi ve bu listenin gösterilmesi işlerini yapmaya çalışacağız. Bir kitabı kod tarafında temsil emtek için book isimli aşağıdaki sınıftan yararlanabiliriz. I know, I know... Bir kitabı birden fazla yazar yazmış olabilir ve bir yazarın birden fazla kitabı da olabilir. Hani nerede nesneler arası many-to-many ilişki? Motivasyonum Blazor tarafında Hello World demek olduğu için bu kısmı tamamen örtpas etmiş durumdayım.

public class Book
{
    public string Title { get; set; }
    public string Summary { get; set; }
    public int PageCount { get; set; }
    public string Authors { get; set; }
}

Kitaplar ile ilgili işlemler için Pages klasörüne Book.cshtml isimli bir dosya ekleyip aşağıdaki şekilde kodlayabiliriz. Çok basit olarak kitap listesinin gösterilmesi ve yeni bir kitabın eklenebilmesi için gerekli fonksiyonelliklerin sunulduğu bir arayüzümüz var. HTML tarafı ile kod bir arada kullanılmakta.

@page "/bookList"<h1>Okuduğum Kitaplar (Toplam @books.Count() kitabım var) </h1> <!--Toplam kitap sayısını da başlığa ekledik --><blockquote class="blockquote">
    Burada okumaktan keyif aldığım kitaplar yer alıyor.
</blockquote><ul><!-- Tüm kitapları dolaşıp örnek olarak başlıklarını listeliyor ve hemen alt kısmına özet bilgilerini yerleştiriyoruz-->
    @foreach(var book in books){<li aria-describedby="bookTitle">@book.Title</li><small id="bookTitle" class="form-text text-muted">@book.Summary</small> 
    }</ul><!-- Yeni bir kitap bilgisinin girişi için Bootstrap ile zenginleştirilmiş basit bir formumuz var --><div class="form-group"><input class="form-control" id="txtTitle" placeholder="Kitabın adı" bind="@newBook.Title"/><br/> <!--bind attribute'una atanan değer ile Title özelliğine bağladık --><input class="form-control" id="txtAuthors" placeholder="Yazarlar" bind="@newBook.Authors" /><br/><input class="form-control" id="txtPageCount" placeholder="Sayfa sayısı" bind="@newBook.PageCount" /><br/><input class="form-control" id="txtSummary" aria-describedby="summaryHelp" placeholder="Özet" bind="@newBook.Summary" /><small id="summaryHelp" class="form-text text-muted">Lütfen bir cümleyle kitabın neyle ilgili olduğunu anlat</small> <!-- yardımcı bilgi veren metin için koyduk --></div><button onclick="@AddNewBook" class="btn btn-primary">Listeye ekleyelim</button> <!-- onclick attribute'unda AddNewBook metoduna bağladık --><!-- Fonksiyonlarımız -->
@functions{
    // Tüm kitap listemizi ifade eden koleksiyonumuz
    IList<Book> books=new List<Book>();
    Book newBook=new Book();
    // Yeni bir kitap eklemek için kullanıyoruz.
    void AddNewBook(){
        books.Add(newBook); // Kitabı listeye ekledik
        newBook=new Book(); // Eğer newBook nesnesini sıfırlamassak büyük ihtimalle koleksiyona hep aynı nesne örneği eklenecektir.
    }
}

Ekleyeceğimiz bir diğer örnek Dependency Injection kullanımı ile ilgili. Built-in olarak gelen HttpClient servisini cshtml tarafında nasıl kullanabileceğimizi görebilmek için WorldPopulation.cshtml isimli bir dosya geliştiriyoruz. Yine Pages klasörüne konuşlandıracağımız dosya içeriği aşağıdaki gibi yazılabilir. @page direktifine göre /population adresine gelen taleplere karşılık bu sayfa işletilecektir. @inject kısmında httpClient servisinin koda enjekte edilmesi söz konusudur.

@page "/population"
@inject HttpClient httpClient<!-- built-in servislerden olan HttpClient servisini buraya enjekte ettik. 
httpClient değişken adıyla kullanabiliriz --><h2>Güncel 3 Günlük Dünya Nüfusu Bilgileri</h2><blockquote class="blockquote">
    Bilgiler api.population.io sitesinden alınmıştır.
</blockquote>

@if (values == null) // Henüz veriler gelmemiş olabilir.
{
    <p><em>Bilgiler alınıyor...</em></p>
}
else
{<div class="card" style="width: 18rem;"><ul class="list-group list-group-flush">
            @foreach (var currentData in @values) // Tüm değerleri dolaşıp güncel nüfus verisini ekrana basıyoruz            
            {
                <li class="list-group-item">@string.Format("{0:#,0}",@currentData.Value) - @currentData.Date.ToShortDateString() </li>
            }</ul></div>
}

@functions{
    Population[] values; // istatistik bilgilerin dizisi

    // Sayfamızın başlangıç aşamasında çalışan asenkron olay metodumuz
    protected override async Task OnInitAsync()
    {
        // GetJsonAsync metodunu kullanarak bir talep gönderiyoruz ve sunucu tarafından json dosyasını alıyoruz
        // Burada harici bir servis adresine de çıkılabilir
        //TODO: world.json içeriğini veren bir .net web api dahil edelim
        values = await httpClient.GetJsonAsync<Population[]>("db/world.json");
    }

    // Nüfus bilgilerini tutan sınıfımız
    class Population
    {
        public DateTime Date { get; set; }
        public Int64 Value { get; set; }
    }
}

Bu sayfa sembolik olarak üç günlük dünya nüfusu bilgilerini paylaşıyor. Tamamen kafadan uydurma bir örnek. Normal şartlarda nüfus bilgileri bir servis aracılığıyla çekilmekte. Bu noktada HttpClient hizmetinden yararlanmalıyız. Biz veri kaynağı olarak gerçek bir servisi kullanmak yerine sahte bir json içeriğini ele alıyoruz. wwwroot altında oluşturacağımız db klasöründe yer alan world.json dosyası bu noktada devreye giriyor. Ancak TODO kısmında belirttiğimiz üzere siz örneği geliştirirken bir Web API servisini kullanmayı deneyebilirsiniz.

[
    {
        "date": "2019-02-01",
        "value": 7644991666
    },
    {
        "date": "2019-02-02",
        "value": 7645213391
    },
    {
        "date": "2019-02-03",
        "value": 7645435108
    }
]

Pek tabii boilerplate etkisi ile üretilen projenin menüsü hazır şablona göre tesis edilmiş durumda. Burayı yeni eklediğimiz kendi sayfalarımıza göre düzenleyebiliriz. Tek yapmamız gereken NavMenu.cshtml dosyasını kurcalayarak aşağıdaki kıvama getirmektir. NavLink elementlerinde yeni eklediğimiz bileşenlerdeki @page direktiflerinde belirtilen URL adresleri kullanılmaktadır.

<div class="top-row pl-4 navbar navbar-dark"><a class="navbar-brand" href="">HelloWorld</a><button class="navbar-toggler" onclick=@ToggleNavMenu><span class="navbar-toggler-icon"></span></button></div><div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu><ul class="nav flex-column"><li class="nav-item px-3"><NavLink class="nav-link" href="" Match=NavLinkMatch.All><span class="oi oi-home" aria-hidden="true"></span> Başlangıç :P</NavLink></li><li class="nav-item px-3"><NavLink class="nav-link" href="population"><span class="oi oi-home" aria-hidden="true"></span> Dünya Nüfusu</NavLink></li><!-- <li class="nav-item px-3"><NavLink class="nav-link" href="counter"><span class="oi oi-plus" aria-hidden="true"></span> Sayaç</NavLink></li>
        --><!-- Yeni eklediğimiz book sayfası için link. href değerine göre bookList.cshtml sayfasına yönlendirileceğiz --><li class="nav-item px-3"><NavLink class="nav-link" href="bookList"><span class="oi oi-list-rich" aria-hidden="true"></span> Kitaplar</NavLink></li></ul></div>

@functions {
    bool collapseNavMenu = true;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Çalışma Zamanı

Artık bir deneme sürüşüne çıkabiliriz. Uygulamayı terminalden aşağıdaki komutu vererek çalıştırmamız mümkün.

dotnet run

Örnek olarak bir iki kitap girip sonuçları inceleyebiliriz. Ben aşağıdakine benzer bir ekran görüntüsü yakalamışım.

Çalışma zamanını incelerken F12 ile debug moda geçmekte yarar var. Söz gelimi booklist üzerinde çalışırken kitap ekleme ve listeleme gibi operasyonların gerçekleştirilmesine karşılık oluşan HTML kaynağı aşağıdaki gibidir. Standart üretilen HTML çıktılarından biraz farklı değil mi? MVC'de, eski nesil Server Side Web Forms'larda veya saf HTML ile yazdıklarımızda üretilen içerikleri düşünelim. Bir takım elementleri source üzerinde göremiyoruz gibi. Yine de sayfamız kanlı canlı bir şeyler yürütüyor. Derlenmiş bir uygulamanın tarayıcıda koştuğunu ifade edebiliriz.

Built-In HttpClient servisini enjekte ettiğimiz dünya nüfus verileri sayfası ise şöyle görünecektir (Ekranı daraltmamıza rağmen UX deneyiminin bozulmadığını görmüşsünüzdür)

Ürünün Paketlenmesi

Bir Blazor uygulamasının dağıtımı için publish işlemine ihtiyacımız var. Visual Studio tarafında bu iş oldukça kolay. Microsoft Azure platformuna servis olarak da alabiliriz. WestWorld gibi Ubuntu tabanlı ortamlardaysa dağıtım işlemini dotnet komut satırı aracını kullanarak aşağıdaki terminal komutuyla gerçekleştirebiliriz.

dotnet publish -c Release

Oluşan dosya içeriklerini incelemekte yarar var. publish operasyonu sırasında gereksiz kütüphaneler çıkartılıp paket boyutu mümkün mertebe küçültülüyor. Dikkat çekici nokta C# kodunun çalışması için gerekli ne kadar runtime bileşeni(mscorlib, mono runtime, c libraries vb) varsa mono.wasm içine konulması. WestWorld'teki örnek için bu 2.1 mb'lık dosya anlamına geldi.

Bunun sonucu olarak bin/Release/netstandard2.0/publish/ klasörü altına gerekli tüm proje dosyaları atılır. Bu dosyaları web sunucusuna veya bir host service'e alarak(manuel veya otomatik araçlar yardımıyla) uygulamayı canlı(production) ortama taşıyabiliriz.

Ben Neler Öğrendim?

Blazor benim yeni yeni keşfetmeye, öğrenmeye ve anlamaya çalıştığım konulardan birisi. Yer yer huni takmama sebep olan iç mimarisi sebebiyle üstüne daha çok kafa patlatmam gerektiğiyse aşikar. Buna rağmen bu basit Hello World denemesi sırasında bile öğrendiğim bir kaç şey oldu. Bunları şöyle maddeleştirebilirim.

  • Bir Blazor proje şablonunun temel bileşenlerinin ne olduğunu
  • Blazor tarafında Bootstrap kullanarak daha şık tasarımlar yapılmasını
  • Razor'da sayfa bileşenleri ile fonksiyonların nasıl etkileşebileceğini
  • Blazor'daki Dependency Injection mekanizmasının nasıl ele alınabileceğini
  • Bileşen odaklı bir geliştirme ortamı olduğunu
  • Kabaca WASM terimini
  • Blazor uygulamasının canlı ortamlar için publish edilmesini

Ve böylece geldik bir Saturday-Night-Works derlemesinin daha sonuna. Bir başka macerada görüşmek üzere hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/vue-ve-nw-js-ile-desktop-uygulamasi-gelistirmekVue ve NW.js ile Desktop Uygulaması Geliştirmek

$
0
0

Geçen gün fark ettim ki yaş ilerleyince blogumdaki yazıların girişinde kullanabileceğim malzeme sayısı da artmış. Söz gelimi şu anda lise son yıllarıma yani seksenlerin sonu doksanların başına doğru gitmiş durumdayım. O dönemlerde kısa Amerikan dizileri popüler. Hatta Arjantin menşeeli diziler de çok yaygın. Sanıyorum Mariana isimli popüler bir dizi vardı. Kısa boylu, siyah kıvırcık saçlı, buğday tenli ve hayatı acılar içinde geçen bir Latin kadının hikayesiydi. Lakin ben hayatı toz pembe görmemize vesile olan komedileri tercih ediyordum. Hatta en çok sevdiğim komedi dizisi Perfect Strangers'dı.

Mipos isimli Yunan köyünden Chicago'daki kuzeni Larry Appleton'ın yanına yerleşip "Komik olma kuzen" repliği ile zihnime kazınan Balki Bartokomous bizleri epeyce güldürürdü. Aradan çeyrek asır geçmiş olsa da aptal kutunun bizleri ekrana bağlayan bazı alışkanlıkları değişmiyor. Platformlar belki ama yine komedi dizileri, yine Arjantin dizileri ve yine aklımıza kazınan Balki'ler var. Saturday-Night-Works'ün 16 numaralı çalışmasına konu olan Big Bang Theory'de işte bana bu çağrışımları yapmış durumda. Öyleyse gelin başlayalım.

Daha önceden Electron ile cross platform desktop uygulamalarının geliştirilmesi üzerine çalışmıştım(github repo istatistiklerine göre kimsenin ilgisini çekmemişti ama malum çok eski bir desktop programıcısı olduğumdan ilgilenmiştim) Bu kez eskiden node-webkit olarak bilinen NW.js kullanarak WestWorld üzerinde desktop uygulaması geliştirmek istedim. NW.js cephesinde de aynen Electron'da olduğu gibi Chromium, Node.js, HTML, CSS ve javascript kullanılmakta. Lakin ufak tefek farklılıklar var. Electron'da entry point yeri Javascript script'i iken NW.js tarafında script haricinde bir web sayfası da giriş noktası olabiliyor. Build süreçlerinde de bir takım farklılıklar var.

Peki bu çalışma kapsamında ne yapacağız? Uygulama çok basit bir arayüze sahip olacak. Ekrandaki metin kutusuna bir isim girilecek ve Big Bang Theory'nin ilgili bölümüne ait bazı bilgiler ekrana bastırılacak(Akıllı bir arama ekranı değil çok şey beklemeyin) Bölüm bilgisini ise bigbangapi isimli ve .net core ile yazılmış bir web api servisi sağlayacak.

Başlangıç

WestWorld'de(Ubuntu 18.04 64bit) bu örnek için Vue CLI'a(Vue'nun Command Language Interface aracı olarak düşünebiliriz) ihtiyaç var. Önce versiyonu kontrol edip yoksa yüklemek lazım. Ayrıca projeyi oluşturduktan sonra NW paketini de eklemek gerekiyor. axios'u servis haberleşmesi için kullanacağız. Bunun için terminalden aşağıdaki adımlarla ilerleyebiliriz. vue create ile başlayan satır bbtheory isimli hazır bir Vue uygulaması inşa edecek. npm install satırlarında da bu uygulama için gerekli paketlerin yüklenmesi sağlanıyor. Nw sdk ve axios bu anlamda önemli.

vue --version
sudo npm install -g @vue/cli
vue create bbtheory
cd bbtheory
sudo npm install --save-dev nwjs-builder-phoenix nw@sdk
sudo npm install axios

Vue projesi varsayılan kurulum ayarları ile oluşturulmuştur.

Kod Tarafı

Gelelim kodlama tarafına. Uygulamanın masaüstü arayüzü olan App bileşeni app.vue dosyasında kodlanıyor. Bu dosyayı aşağıdaki gibi değiştirerek ilerleyebiliriz. Sonuçta HTML tabanlı bir ortam var. Elbette Vue'ya özgü bir sentaks da söz konusu. Söz gelimi bileşendeki bir kontrolü model tarafına bağlamak için v-model direktifinden yararlanılıyor. Bir section elementinin görünürlüğünü koşullandıracaksak v-if direktifini kullanabiliyoruz. Button kontrolündeki olayları betikteki bir fonksiyonla ilişkilendirirken @click şeklindeki element adı ele alınıyor. Modeldeki özellikleri kontrollerde gösterirkense {{propertyName}} notasyonuna başvuruyoruz.

Örneğimizdeki bileşen, önyüz tasarımı ve kodu aynı dosya içerisinde barındırmakta. Ancak hazır olarak gelen şablonu incelerseniz Components klasöründe bir bileşen geldiğini de görebilirsiniz. Yani alt bileşenleri bu klasör altında da toplayabiliriz. Bu arada kodlarda yakaladığınız yorum satırlarını okumayı unutmayın. Destekleyici bilgiler görebilirsiniz.

<template><div id="app"><h2>Bölüm adını yazar mısın?</h2><section class="input-Section"><input type="text" v-model="query"><button :disabled="!query.length" @click="findEpisode">Göster</button><!-- butona basılınca findEpisode metodu çağırılacak --></section><section v-if="error"><!-- error değişkeni true olarak set edilmişse bir şeyler ters gitmiştir --><i>Sanırım bölüm bulunamadı ya da bir şeyler ters gitti</i></section><section v-if="!error"><!-- Aranan veri bulunduysa --><h1>{{name}} ({{season}}/{{number}}) - {{ airdate }}</h1><div><p>{{summary}}</p></div><div><img :src="imageLink"/></div></section></div></template><script>
export default {
  name: "Pilot",
  data() {
    // data modelimiz api servisinden dönen tipe göre düzenlendi
    return {
      query: "",
      error: false,
      id: null,
      name: "",
      airdate:"",
      season: null,
      number: null,
      summary: "",
      imageLink: ""
    };
  },
  methods: {
    findEpisode() {
      // api servisine talep gönderen metod
      this.$http
        .get(`/episode/${this.query}`) // sorguyu tamamlıyoruz. parametre olarak input kontrolüne girilen değer alınıyor. query değişkeni üzerinden.
        .then(response => {
          this.error = false;
          this.name = response.data.name; // servisten gelen cevabın içindeki alanların, vue data modelindeki karşılıklarına ataması yapılıyor
          this.season = response.data.season;
          this.number = response.data.number;
          this.summary = response.data.summary; 
          this.airdate=response.data.airDate;
          this.imageLink=response.data.imageLink;
          console.log(response.data); //control amaçlı
        })
        .catch(() => {
          // hata alınması durumu
          this.error = true;
          this.name = "";
        });
    }
  }
};
</script><style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  padding:10px;
  text-align: center;
  color: #2c3e50;
  margin-top: 10px;
}
input {
  width: 75%;
  outline: none;
  height: 20px;
  font-size: 1em;
}

button{
  display: block;
  width: 25%;
  height: 25px;
  outline: none;
  border-radius: 4px;
  white-space: nowrap; 
  margin:0 10px;
  font-size: 1rem;
}

.input-Section {
  display: flex;
  align-items: center;
  padding: 20px 0;
}

</style>

App bileşeninde dikkat edileceği üzere $http ile yapılan bir servis çağrısı var. Bu axios tarafından sağlanacak bir hizmet. Bu nedenle main.js dosyasında gerekli hazırlıkların yapılması lazım. Dikkat edileceği üzere Vue çalışma zamanının axios'u $http özelliği üzerinden kullanabilmesini sağlayacak bir enjekte işlemi söz konusu.

import Vue from 'vue'
import App from './App.vue'
import axios from 'axios' // API servisine HTTP talebini göndermek için kullandığımız modül

axios.defaults.baseURL = 'http://localhost:4001/api/'; // base url adresini atadık
Vue.http = Vue.prototype.$http = axios;
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Bu konu kapsamı dışında ancak .Net Core tabanlı bir Web API hizmetimiz de bulunuyor. Bu servis dizinin bölümlerini aramak amacıyla kodladığımız sahte bir program. Konumuzla doğrudan ilintili olmadığı için detayına girmemize gerek yok ama en azından Controller sınıfında neler yaptığımıza bir bakalım derim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;

namespace bigbangapi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EpisodeController : ControllerBase
    {

        [HttpGet("{name}")]
        public ActionResult<Episode> Get(string name)
        {
            try
            {
                string db = System.IO.File.ReadAllText("db/content.json");
                JObject json = JObject.Parse(db);
                JArray episodes = (JArray)json["episodes"];
                var all = episodes
                            .Select(e => new Episode
                            {
                                Id = (int)e["id"],
                                Name = (string)e["name"],
                                Season = (int)e["season"],
                                Number = (int)e["number"],
                                Summary = (string)e["summary"],
                                ImageLink = (string)e["image"]["medium"],
                                AirDate=(string)e["airdate"]
                            });
                var result = all.Where(e => e.Name == name).FirstOrDefault();
                return new ActionResult<Episode>(result);
            }
            catch
            {
                return NotFound();
            }
        }
    }
}

Örneğin basitliği açısından yalın bir Get operasyonu sunuyoruz. Parametre olarak gelen bölüm adını fiziki olarak tuttuğumuz content.json içeriğinde arayarak bir sonuç döndürmekteyiz. Pek tabii bu sahte bir servis. Veri kaynağı olarak fiziki dosya yerine veri tabanı kullanılan bir moda da geçebiliriz. Hatta film bilgileri sunan bir gerçek hayat API'sini de tercih edebiliriz. Tercih size kalmış.

Ah unutmadan! Geliştirme safhasında kuvvetle muhtemel CORS(Cross Origin Resource Sharing) ile ilgili bir sorun yaşayabilirsiniz. Bu nedenle Startup.cs içerisinde CORS özelliğini etkinleştirmemiz ve masaüstünden gelecek cevapları kabul edebileceğimizi belirtmemiz gerekiyor.

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
	// Diğer uygulamanın node.js servisinin buraya axios üzerinden
	// talep atabilmesi için Cors desteği eklenmiştir
	// Configure metodu içerisinde de 8080 kaynağından gelecek
	// tüm metodlar için izin yetkisi bildirilmiştir.
	services.AddCors();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	app.UseCors(
		options=>options.WithOrigins("http://localhost:8080").AllowAnyMethod()
	);
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseHsts();
	}

	//app.UseHttpsRedirection();
	app.UseMvc();
}

Tekrar Vue tarafına dönerek ilerleyelim. Uygulamanın giriş noktasını belirtmek için package.json dosyasına main özelliğini eklememiz ve bir adres yönlendirmesi yapmamız gerekiyor. Bu sayede uygulama kodunda yapılan her değişiklik anında çalışma zamanına da yansıyacaktır(Program çalıştıktan sonra önyüz bileşeni olan App.vue dosyasında değişiklikler yapmayı deneyin)

"main": "http://localhost:8080",

Çalışma Zamanı

Normalde desktop uygulamasını çalıştırmak için proje klasöründeyken birinci terminalden

npm run serve

ile sunucuyu etkinleştirmek ve ardından ikinci bir terminal penceresinden

./node_modules/.bin/run .

yazmak gerekiyor. Lakin bu durumda NW.js'in ilgili SDK'sı indirilip development ortamı ayağa kalkıyor. Bunu otomatikleştirmek için nw@sdk isimli paketi yüklemek ve package.json dosyasındaki script bölümüne örneğin desktop isimli yeni bir çalışma zamanı parametresi dahil etmemiz yeterli.

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "desktop": "nw ."
  },

Desktop uygulaması çalıştıktan sonra tarayıcının Development Tools'unu kullanarak debug yapılması mümkün. Masaüstü tarafından yapılan API çağrılarını ve dönen sonuçları buradan izleme şansımız var. Tabii tüm bunların başında yazdığımız web api servisinin de çalışır durumda olması gerekiyor öyle değil mi? Sonrasında Node.js server ve desktop uygulaması çalıştırılarak ilerlersek yerinde olacaktır. Bunları üç ayrı terminal penceresinden yürütebiliriz ama temel olarak aşağıdaki komutları kullanmamız lazım.

dotnet run
npm run serve
npm run desktop

Eğer bir sorun olmazsa uygulama ayağa kalktıktan sonra Big Bang Theory'den örnek bir bölümü aratabiliriz. Ben aşağıdaki gibi bir sonuca ulaşmışım.

Paketleme

Uygulamayı paketlemek çok daha mantıklı ve gerekli elbette. Sonuçta dağıtımını(Deployment) yapmak isteyeceğiz. Bunun için packages.json içerisine build bölümünü aşağıdaki gibi eklememiz lazım.

  "build": {
    "nwVersion": "0.35.5"
  }

Dikkat edileceği üzere nw paketinin hangi versiyonunu kullanacağımızı belirtiyoruz(Güncel sürümüne bakmanızda yarar var) bbtheory isimli uygulamanın root klasöründe aşağıdaki komut ile 64bit linux platformu için gerekli paketin üretilmesi sağlanabiliyor.

./node_modules/.bin/build --tasks linux-x64 .

Paket boyutu oldukça yüksek görüldüğü üzere! Zaten cross-platform masaüstü uygulamaları için en rahatsız edici konuların başında da dosya boyutları geliyor. Ancak küçültmek için çeşitli yollar olduğu ifade edilmekte. Bunu henüz araştırma fırsatım olmadı ancak yakın tarihli şu yazıda bir takım bilgiler mevcut.

Ben Neler Öğrendim?

Elbette aptal kutunun başında saatlerimi geçirdiğim Perfect Strangers dizisinin bana alttan alttan verdiği mesajlar gibi bu örnek çalışma sonrasında öğrendiğim bazı şeyler de olmadı değil. Bunları aşağıdaki gibi özetlemeye çalışayım.

  • Vue tarafında ön yüz nasıl geliştirilir
  • v-model, v-if, {{ }}, @click gibi Vue ilişkili ifadeler ne işe yarar
  • Bileşen ile model özellikleri nasıl kullanılır
  • axios ile node.js tarafından servis talepleri nasıl gönderilir
  • newtonsoft.json ile bir json dizisinde nasıl linq sorgusu çalıştırılır
  • CORS ne işe yarar

Ne yazık ki Vue konusunda uzman değilim. Aslında onu şirketteki yeni nesil projelerde kullanıyoruz lakin iyi bir başlangıcım yok. Belki de ahch-to(macOS High Sierra)üzerinde yapacağım ikinci faz çalışmaları kapsamında ona daha fazla zaman ayırabilirim. Böylece geldik neşeli bir cumartesi gecesinin 16ncı bölümüne ait derlemelerin de sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Vue ve NW.js ile Desktop Uygulaması Geliştirmek

$
0
0

Geçen gün fark ettim ki yaş ilerleyince blogumdaki yazıların girişinde kullanabileceğim malzeme sayısı da artmış. Söz gelimi şu anda lise son yıllarıma yani seksenlerin sonu doksanların başına doğru gitmiş durumdayım. O dönemlerde kısa Amerikan dizileri popüler. Hatta Arjantin menşeeli diziler de çok yaygın. Sanıyorum Mariana isimli popüler bir dizi vardı. Kısa boylu, siyah kıvırcık saçlı, buğday tenli ve hayatı acılar içinde geçen bir Latin kadının hikayesiydi. Lakin ben hayatı toz pembe görmemize vesile olan komedileri tercih ediyordum. Hatta en çok sevdiğim komedi dizisi Perfect Strangers'dı.

Mipos isimli Yunan köyünden Chicago'daki kuzeni Larry Appleton'ın yanına yerleşip "Komik olma kuzen" repliği ile zihnime kazınan Balki Bartokomous bizleri epeyce güldürürdü. Aradan çeyrek asır geçmiş olsa da aptal kutunun bizleri ekrana bağlayan bazı alışkanlıkları değişmiyor. Platformlar belki ama yine komedi dizileri, yine Arjantin dizileri ve yine aklımıza kazınan Balki'ler var. Saturday-Night-Works'ün 16 numaralı çalışmasına konu olan Big Bang Theory'de işte bana bu çağrışımları yapmış durumda. Öyleyse gelin başlayalım.

Daha önceden Electron ile cross platform desktop uygulamalarının geliştirilmesi üzerine çalışmıştım(github repo istatistiklerine göre kimsenin ilgisini çekmemişti ama malum çok eski bir desktop programıcısı olduğumdan ilgilenmiştim) Bu kez eskiden node-webkit olarak bilinen NW.js kullanarak WestWorld üzerinde desktop uygulaması geliştirmek istedim. NW.js cephesinde de aynen Electron'da olduğu gibi Chromium, Node.js, HTML, CSS ve javascript kullanılmakta. Lakin ufak tefek farklılıklar var. Electron'da entry point yeri Javascript script'i iken NW.js tarafında script haricinde bir web sayfası da giriş noktası olabiliyor. Build süreçlerinde de bir takım farklılıklar var.

Peki bu çalışma kapsamında ne yapacağız? Uygulama çok basit bir arayüze sahip olacak. Ekrandaki metin kutusuna bir isim girilecek ve Big Bang Theory'nin ilgili bölümüne ait bazı bilgiler ekrana bastırılacak(Akıllı bir arama ekranı değil çok şey beklemeyin) Bölüm bilgisini ise bigbangapi isimli ve .net core ile yazılmış bir web api servisi sağlayacak.

Başlangıç

WestWorld'de(Ubuntu 18.04 64bit) bu örnek için Vue CLI'a(Vue'nun Command Language Interface aracı olarak düşünebiliriz) ihtiyaç var. Önce versiyonu kontrol edip yoksa yüklemek lazım. Ayrıca projeyi oluşturduktan sonra NW paketini de eklemek gerekiyor. axios'u servis haberleşmesi için kullanacağız. Bunun için terminalden aşağıdaki adımlarla ilerleyebiliriz. vue create ile başlayan satır bbtheory isimli hazır bir Vue uygulaması inşa edecek. npm install satırlarında da bu uygulama için gerekli paketlerin yüklenmesi sağlanıyor. Nw sdk ve axios bu anlamda önemli.

vue --version
sudo npm install -g @vue/cli
vue create bbtheory
cd bbtheory
sudo npm install --save-dev nwjs-builder-phoenix nw@sdk
sudo npm install axios

Vue projesi varsayılan kurulum ayarları ile oluşturulmuştur.

Kod Tarafı

Gelelim kodlama tarafına. Uygulamanın masaüstü arayüzü olan App bileşeni app.vue dosyasında kodlanıyor. Bu dosyayı aşağıdaki gibi değiştirerek ilerleyebiliriz. Sonuçta HTML tabanlı bir ortam var. Elbette Vue'ya özgü bir sentaks da söz konusu. Söz gelimi bileşendeki bir kontrolü model tarafına bağlamak için v-model direktifinden yararlanılıyor. Bir section elementinin görünürlüğünü koşullandıracaksak v-if direktifini kullanabiliyoruz. Button kontrolündeki olayları betikteki bir fonksiyonla ilişkilendirirken @click şeklindeki element adı ele alınıyor. Modeldeki özellikleri kontrollerde gösterirkense {{propertyName}} notasyonuna başvuruyoruz.

Örneğimizdeki bileşen, önyüz tasarımı ve kodu aynı dosya içerisinde barındırmakta. Ancak hazır olarak gelen şablonu incelerseniz Components klasöründe bir bileşen geldiğini de görebilirsiniz. Yani alt bileşenleri bu klasör altında da toplayabiliriz. Bu arada kodlarda yakaladığınız yorum satırlarını okumayı unutmayın. Destekleyici bilgiler görebilirsiniz.

<template><div id="app"><h2>Bölüm adını yazar mısın?</h2><section class="input-Section"><input type="text" v-model="query"><button :disabled="!query.length" @click="findEpisode">Göster</button><!-- butona basılınca findEpisode metodu çağırılacak --></section><section v-if="error"><!-- error değişkeni true olarak set edilmişse bir şeyler ters gitmiştir --><i>Sanırım bölüm bulunamadı ya da bir şeyler ters gitti</i></section><section v-if="!error"><!-- Aranan veri bulunduysa --><h1>{{name}} ({{season}}/{{number}}) - {{ airdate }}</h1><div><p>{{summary}}</p></div><div><img :src="imageLink"/></div></section></div></template><script>
export default {
  name: "Pilot",
  data() {
    // data modelimiz api servisinden dönen tipe göre düzenlendi
    return {
      query: "",
      error: false,
      id: null,
      name: "",
      airdate:"",
      season: null,
      number: null,
      summary: "",
      imageLink: ""
    };
  },
  methods: {
    findEpisode() {
      // api servisine talep gönderen metod
      this.$http
        .get(`/episode/${this.query}`) // sorguyu tamamlıyoruz. parametre olarak input kontrolüne girilen değer alınıyor. query değişkeni üzerinden.
        .then(response => {
          this.error = false;
          this.name = response.data.name; // servisten gelen cevabın içindeki alanların, vue data modelindeki karşılıklarına ataması yapılıyor
          this.season = response.data.season;
          this.number = response.data.number;
          this.summary = response.data.summary; 
          this.airdate=response.data.airDate;
          this.imageLink=response.data.imageLink;
          console.log(response.data); //control amaçlı
        })
        .catch(() => {
          // hata alınması durumu
          this.error = true;
          this.name = "";
        });
    }
  }
};
</script><style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  padding:10px;
  text-align: center;
  color: #2c3e50;
  margin-top: 10px;
}
input {
  width: 75%;
  outline: none;
  height: 20px;
  font-size: 1em;
}

button{
  display: block;
  width: 25%;
  height: 25px;
  outline: none;
  border-radius: 4px;
  white-space: nowrap; 
  margin:0 10px;
  font-size: 1rem;
}

.input-Section {
  display: flex;
  align-items: center;
  padding: 20px 0;
}

</style>

App bileşeninde dikkat edileceği üzere $http ile yapılan bir servis çağrısı var. Bu axios tarafından sağlanacak bir hizmet. Bu nedenle main.js dosyasında gerekli hazırlıkların yapılması lazım. Dikkat edileceği üzere Vue çalışma zamanının axios'u $http özelliği üzerinden kullanabilmesini sağlayacak bir enjekte işlemi söz konusu.

import Vue from 'vue'
import App from './App.vue'
import axios from 'axios' // API servisine HTTP talebini göndermek için kullandığımız modül

axios.defaults.baseURL = 'http://localhost:4001/api/'; // base url adresini atadık
Vue.http = Vue.prototype.$http = axios;
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Bu konu kapsamı dışında ancak .Net Core tabanlı bir Web API hizmetimiz de bulunuyor. Bu servis dizinin bölümlerini aramak amacıyla kodladığımız sahte bir program. Konumuzla doğrudan ilintili olmadığı için detayına girmemize gerek yok ama en azından Controller sınıfında neler yaptığımıza bir bakalım derim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;

namespace bigbangapi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EpisodeController : ControllerBase
    {

        [HttpGet("{name}")]
        public ActionResult<Episode> Get(string name)
        {
            try
            {
                string db = System.IO.File.ReadAllText("db/content.json");
                JObject json = JObject.Parse(db);
                JArray episodes = (JArray)json["episodes"];
                var all = episodes
                            .Select(e => new Episode
                            {
                                Id = (int)e["id"],
                                Name = (string)e["name"],
                                Season = (int)e["season"],
                                Number = (int)e["number"],
                                Summary = (string)e["summary"],
                                ImageLink = (string)e["image"]["medium"],
                                AirDate=(string)e["airdate"]
                            });
                var result = all.Where(e => e.Name == name).FirstOrDefault();
                return new ActionResult<Episode>(result);
            }
            catch
            {
                return NotFound();
            }
        }
    }
}

Örneğin basitliği açısından yalın bir Get operasyonu sunuyoruz. Parametre olarak gelen bölüm adını fiziki olarak tuttuğumuz content.json içeriğinde arayarak bir sonuç döndürmekteyiz. Pek tabii bu sahte bir servis. Veri kaynağı olarak fiziki dosya yerine veri tabanı kullanılan bir moda da geçebiliriz. Hatta film bilgileri sunan bir gerçek hayat API'sini de tercih edebiliriz. Tercih size kalmış.

Ah unutmadan! Geliştirme safhasında kuvvetle muhtemel CORS(Cross Origin Resource Sharing) ile ilgili bir sorun yaşayabilirsiniz. Bu nedenle Startup.cs içerisinde CORS özelliğini etkinleştirmemiz ve masaüstünden gelecek cevapları kabul edebileceğimizi belirtmemiz gerekiyor.

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
	// Diğer uygulamanın node.js servisinin buraya axios üzerinden
	// talep atabilmesi için Cors desteği eklenmiştir
	// Configure metodu içerisinde de 8080 kaynağından gelecek
	// tüm metodlar için izin yetkisi bildirilmiştir.
	services.AddCors();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	app.UseCors(
		options=>options.WithOrigins("http://localhost:8080").AllowAnyMethod()
	);
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseHsts();
	}

	//app.UseHttpsRedirection();
	app.UseMvc();
}

Tekrar Vue tarafına dönerek ilerleyelim. Uygulamanın giriş noktasını belirtmek için package.json dosyasına main özelliğini eklememiz ve bir adres yönlendirmesi yapmamız gerekiyor. Bu sayede uygulama kodunda yapılan her değişiklik anında çalışma zamanına da yansıyacaktır(Program çalıştıktan sonra önyüz bileşeni olan App.vue dosyasında değişiklikler yapmayı deneyin)

"main": "http://localhost:8080",

Çalışma Zamanı

Normalde desktop uygulamasını çalıştırmak için proje klasöründeyken birinci terminalden

npm run serve

ile sunucuyu etkinleştirmek ve ardından ikinci bir terminal penceresinden

./node_modules/.bin/run .

yazmak gerekiyor. Lakin bu durumda NW.js'in ilgili SDK'sı indirilip development ortamı ayağa kalkıyor. Bunu otomatikleştirmek için nw@sdk isimli paketi yüklemek ve package.json dosyasındaki script bölümüne örneğin desktop isimli yeni bir çalışma zamanı parametresi dahil etmemiz yeterli.

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "desktop": "nw ."
  },

Desktop uygulaması çalıştıktan sonra tarayıcının Development Tools'unu kullanarak debug yapılması mümkün. Masaüstü tarafından yapılan API çağrılarını ve dönen sonuçları buradan izleme şansımız var. Tabii tüm bunların başında yazdığımız web api servisinin de çalışır durumda olması gerekiyor öyle değil mi? Sonrasında Node.js server ve desktop uygulaması çalıştırılarak ilerlersek yerinde olacaktır. Bunları üç ayrı terminal penceresinden yürütebiliriz ama temel olarak aşağıdaki komutları kullanmamız lazım.

dotnet run
npm run serve
npm run desktop

Eğer bir sorun olmazsa uygulama ayağa kalktıktan sonra Big Bang Theory'den örnek bir bölümü aratabiliriz. Ben aşağıdaki gibi bir sonuca ulaşmışım.

Paketleme

Uygulamayı paketlemek çok daha mantıklı ve gerekli elbette. Sonuçta dağıtımını(Deployment) yapmak isteyeceğiz. Bunun için packages.json içerisine build bölümünü aşağıdaki gibi eklememiz lazım.

  "build": {
    "nwVersion": "0.35.5"
  }

Dikkat edileceği üzere nw paketinin hangi versiyonunu kullanacağımızı belirtiyoruz(Güncel sürümüne bakmanızda yarar var) bbtheory isimli uygulamanın root klasöründe aşağıdaki komut ile 64bit linux platformu için gerekli paketin üretilmesi sağlanabiliyor.

./node_modules/.bin/build --tasks linux-x64 .

Paket boyutu oldukça yüksek görüldüğü üzere! Zaten cross-platform masaüstü uygulamaları için en rahatsız edici konuların başında da dosya boyutları geliyor. Ancak küçültmek için çeşitli yollar olduğu ifade edilmekte. Bunu henüz araştırma fırsatım olmadı ancak yakın tarihli şu yazıda bir takım bilgiler mevcut.

Ben Neler Öğrendim?

Elbette aptal kutunun başında saatlerimi geçirdiğim Perfect Strangers dizisinin bana alttan alttan verdiği mesajlar gibi bu örnek çalışma sonrasında öğrendiğim bazı şeyler de olmadı değil. Bunları aşağıdaki gibi özetlemeye çalışayım.

  • Vue tarafında ön yüz nasıl geliştirilir
  • v-model, v-if, {{ }}, @click gibi Vue ilişkili ifadeler ne işe yarar
  • Bileşen ile model özellikleri nasıl kullanılır
  • axios ile node.js tarafından servis talepleri nasıl gönderilir
  • newtonsoft.json ile bir json dizisinde nasıl linq sorgusu çalıştırılır
  • CORS ne işe yarar

Ne yazık ki Vue konusunda uzman değilim. Aslında onu şirketteki yeni nesil projelerde kullanıyoruz lakin iyi bir başlangıcım yok. Belki de ahch-to(macOS High Sierra)üzerinde yapacağım ikinci faz çalışmaları kapsamında ona daha fazla zaman ayırabilirim. Böylece geldik neşeli bir cumartesi gecesinin 16ncı bölümüne ait derlemelerin de sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/google-cloud-fonksiyonlarini-firebase-ile-birlikte-kullanmakGoogle Cloud Fonksiyonlarını Firebase ile Birlikte Kullanmak

$
0
0

Google'ın Doodle hizmetini takip ediyor musunuz bilemiyorum ancak ben zaman zaman orada hazırlanmış ikonik görsellerden harika hikayelere gidiyorum. Bu seferki yazının derlemesi sırasında da yolum bir şekilde onunla kesişti ve girişte kimden bahsedebilirim derken havacılılk tarihinin en önemli isimlerinden olan Türkiye'nin ilk kadın pilotu Sabiha Gökçen'i(22 Mart 1913 - 22 Mart 2001) anmaya karar verdim.

Amerikan Hava Kurmay Koleji'nin 1996 yılında Maxwell Hava Üssünde yapılan töreninde Dünya tarihine adını yazdıran 20 havacıdan birisi olarak ödül alan Sabiha Gökçen'in tarihde iz bırakan başarıları saymakla bitmez elbette. Lakin diğer pek çok başarısının yanında bu en çok dikkatimi çekenlerden birisiydi. İçindeki uçma arzusu ve sevgisi öyle büyük olmalı ki Fransız pilot Daniel Acton ile son uçuşunu yaptığında 83 yaşındaydı. Türk Hava Kurumu Türkkuşu'nda Başöğretmen olarak görev aldı ve 1955 yılına kadar bir çok değerli pilotun yetişmesine ön ayak oldu.

Arada bir sizde doodlelayın derim. Bazen çok değerli bilgilere ulaşabiliyoruz. Gelelim Google ile ne işimiz olduğuna(Hoş onsuz hareket ettiğimiz bir günümüz de yok) Bu kez Saturday-Night-Works birinci fazdan 27 numaralı örneğin derlemesi ile karşınızdayım. Konumuz Google Cloud Platform üzerinden Firebase tabanlı bir bulut fonksiyon sunmak.

Bulut çözümlerin sunduğu imkanlardan birisi de sunucu oluşturma, barındırma, yönetme gibi etkenleri düşünmemize gerek kalmayacak şekilde uygulama geliştirme ortamları sağlamaları. Bazen bulut platform üzerinde tutulan bir veri tabanı ile konuşan servis kodlarını yine o platformun sunucularında barındırmak suretiyle hizmet sunarız. Söz gelimi Google'ın Firebase veri tabanı ve onu kullanan servis tabanlı fonksiyonları Google Cloud Platform üzerinde konuşlandırabiliriz. Bu örnekteki amacımsa Firebase ile ilişkili bir uygulama servisini Google Cloud Platform üzerinde fonksiyonlaştırabilmekmiş. Her zaman olduğu gibi örneği WestWorld(Ubuntu 18.04, 64bit)üzerinde geliştirmişim. Öyleyse gelin notlarımızı derlemeye başlayalım.

Örnekte Firebase'in Realtime Database seçeneği kullanılmakta. Veriyi JSON tipinde tutan bir NoSQL sistemi olarak düşünülebilir. Veri, bağlı olan tüm istemciler için gerçek zamanlı(realtime) olarak eşlenir. Dahası, istemci uygulama kapansa bile veriyi hatırlar. Cloud-Hosted bir veri tabanıdır. Bir başka deyişle veri tabanı sunucusu google üzerinde durmaktadır. Özellikle Cross-Platform tipinden uygulamalar söz konusuysa(iOS, Android, Javascript veya Typescript fark etmez) tüm bağlı istemcilerle aynı verinin senkronize olarak paylaşılmasını sağlamak gibi önemli bir özelliği vardır. Diğer yandan söz konusu Realtime Database ürünü dışında Cloud Firestore isimli daha önceden üzerinden durup düşündüğümüz bir veri tabanı modeli daha vardır. Firebase'in orjinal veri tabanı olan Realtime modelinin daha geliştirilmiş bir versiyonu olarak düşünebiliriz. Her iki ürün arasındaki farklılıkları kabaca aşağıdaki gibi özetleyebiliriz.

  • Realtime modelinde veri JSON ağaç yapısı şeklinde saklanırken Firestore'da koleksiyon biçiminde organize edilmiş dokümanlar söz konusudur(Firestore, Mongo'yu hatırlattı burada bana)
  • Firestore özellikle karmaşık ve hiyerarşik veri kümelerini ölçeklerken Realtime modele göre daha başarılıdır.
  • Realtime veri tabanı iOS ve Android gibi mobil platformlar için çevrim dışı(offline)çalışma desteği sunar. Firestore buna ek olarak Web tabanlı istemciler için de offline çalışma desteği sağlar.
  • Sıralama ve filtreleme imkanları Cloud Firestore'da Realtime modeline göre çok daha geniştir.
  • Firestore'da bir transaction tamamlanıncaya kadar otomatik olarak tekrar ve tekrar denenir.
  • Realtime veri tabanı modelinde ölçekleme için Sharding uygulanması gerekirken Firestore'da bu iş otomatik olarak yapılır.

Ben uygulaması çok daha basit olduğundan Realtime Database modelini tercih ettim.

İlk Hazırlıklar

Her şeyden önce Google Cloud Platform üzerinde bir hesabımızın olması lazım. Hesabımız ile login olduktan sonra Firebase Console adresine gidip bir proje oluşturacağız. Söz gelimi project-new-hope gibi bir isimle...

Projeyi komut satırından yönetebilmek önemli. Nitekim yazdığımız kodları kolayca deploy edebilmeliyiz. Bu nedenle Firebase CLI(Command Line Interface) aracına ihtiyacımız var. Kendisini npm ile aşağıdaki gibi yükleyebiliriz(Dolayısıyla sistemimizde node ve npm yüklü olmalıdır)

npm install -g firebase-tools

Yükleme işlemi başarılı olduktan sonra proje ile aynı isimde bir klasör oluşturup, içerisinde sırasıyla login ve functions komutlarını kullanarak ilerleyebiliriz. Bu komutlarla Firebase ortamına giriş yapma ve projenin başlangıç iskeletinin oluşturulması işlemleri yapılmaktadır.

mkdir project-new-hope
cd project-new-hope
firebase login
firebase init functions

Login işlemi sonrası arabirim bizi tarayıcıya yönlendirecek ve platform için giriş yapmamız istenecektir. Başarılı login sonrası tekrardan console ekranına dönüş yapmış oluruz.

init functions çağrısı ile yeni bir google cloud function oluşturma işlemine başlanır. Dört soru sorulacaktır(En azından çalışmanın yapıldığı tarih itibariyle böyleydi) Projeyi zaten Firebase Console'unda oluşturmuştuk. Klasör adını aynı verdiğimiz için varsayılan olarak onu kullanacağını belirtebiliriz. Dil olarak Typescript ve Javascript desteği sorulmakta ki ben ikincisi tercih ettim. Üçüncü adımda ESLint kullanıp kullanmayacağımız soruluyor. Şimdilik 'No' seçeneğini işaretleyerek ilerlenebilir ancak gerçek hayat senaryolarında etkinleştirmek iyi bir fikirdir. (İleriye yönelik problem yaratabilecek olası kod hatalarının önceden tespitinin kritikliği sebebiyle) Projenin bağımlılık duyduğu npm paketleri varsa bunların install edilmesini de istediğimizden son soruda 'Yes' seçminini yapmalıyız.

Komut çalışmasını tamamladıktan sonra aşağıdaki klasör yapısının oluştuğunu görebiliriz.

Bundan sonra index.js dosyası ile oynayıp örnek bir dağıtım(deployment) işlemi gerçekleştirebiliriz de. Index sayfasında yorum satırı içerisine alınmış bir kod parçası bulunmaktadır. Bu kısmı açarak hemen Hello World sürecini işletmemiz mümkün. Ama bunun için, yapılan değişiklikleri platforma almamız lazım. Aşağıdaki terminal komutu ile bunu sağlayabiliriz. Örnekteki amacımıza göre sadece fonksiyonların taşınması söz konusudur.

firebase deploy --only functions

Sorun Yaşayabiliriz

Yukarıdaki terminal komutunu denediğimde aktif bir proje olmadığına dair bir hata mesajı aldım ve deployment işlemi başarısız oldu. Bunun üzerine önce aktif proje listesine baktım(firebase list) ve sonrasında use --add ile tekrardan proje seçimi yaptım. Bir alias tanımladıktan sonra(ki her nedense proje adının aynısını vermişim :S) tekrardan deploy işlemini denedim. Bu seferde sadece fonksiyon olarak dağıtım yapmak istediğimi belirtmediğim için başka bir hata aldım. Nihayetinde çalıştırdığım terminal komutu işe yaradı ve proje GCP'a deploy edildi.

firebase list
firebase use --add
firebase deploy --only functions

Firebase Dashboard'una gittiğimizde helloworld isimli API fonksiyonunun(ki index.js dosyasından export edilen metodumuzdur) eklenmiş olduğunu görebiliriz.

Çalışmanın bu ilk yalın versiyonunda Google'ın index.js içerisine koyduğu yorum satırları kaldırılarak bir deneme yapılmıştır. Bu taşıma işlemi sonrası Firebase tarafında üretilen fonksiyona ait API adresini aşağıdaki gibi curl ile çağırdığımızda 'Hello from Firebase!' yazısını görebiliriz.

curl -get https://us-central1-project-new-hope.cloudfunctions.net/helloWorld

Kod Tarafı

Asıl işi yapan örneğimiz ise basit bir REST hizmeti. POST ve GET mesajlarını destekleyen metotlar içeriyor ve temel olarak veri ekleme ve listeleme fonksiyonelliklerini sağlıyor. Arka planda Firebase veri tabanının Realtime çalışan modelini kullanıyor. Arka plandan kastımız GCP üzerindeki Firebase veri tabanı. Yani kendi makinemizde geliştirdiğimiz bir API servisini, firebase veri tabanını kullanacak şekilde GCP üzerinde konuşlandırmış oluyoruz. İkinci örnek için gerekli bir kaç npm paketi var. REST(Representational State Transfer) modelini node tarafında kolayca kullanabilmek için express ve CORS(Cross-Origin Resource Sharing) etkisini rahatça yönetebilmek için cors :D Aşağıdaki terminal komutları ile onları projemize ekleyebiliriz.

npm install --save express cors

İlgili paketleri functions klasöründeyken yüklememiz gerekiyor. Nitekim deploy sırasında bu JSON dosyasındaki paket bilgileri, GCP tarafında da yüklenmeye çalışacak. Dolayısıyla GCP'nin, kendi ortamında kullanacağı paketlerin neler olduğunu bilmesi lazım.

index.js dosyasına ait kod içeriğini aşağıdaki gibi geliştirebiliriz.

const functions = require('firebase-functions');
const express = require('express');
const cors = require('cors');
const admin = require('firebase-admin');
admin.initializeApp();

const app = express();
app.use(cors()); //CORS özelliğini express nesnesi içine enjekte ettik

// HTTP Get çağrısı gelmesi halinde çalışacak metodumuz
app.get("/", (req, res) => {

    return admin.database().ref('/somedata').on("value", snapshot => {
        // HTTP 200 Ok cevabı ile birlikte somedata içeriğini döndürüyoruz
        return res.status(200).send(snapshot.val());
    }, err => {
        // Bir hata varsa HTTP Internal Server Error mesajı ile birlikte içeriğini döndürüyoruz
        return res.status(500).send('There is something go wrong ' + err);
    });
});

// HTTP Post çağrısını veritabanına veri eklemek için kullanacağız
app.post("/", (req, res) => {
    const payload = req.body; // gelen içeriği bir alalım
    // push metodu ile veriyi yazıyoruz.
    // işlem başarılı olursa then bloğu devreye girecektir
    // bir hata oluşması halinde catch bloğu çalışır
    return admin.database().ref('/somedata').push(payload)
        .then(() => {
            // HTTP 200 Ok - yani işlem başarılı oldu diyoruz
            return res.status(200).send('Eklendi');
        }).catch(err => {
            // İşlem başarısız oldu
            // HTTP 500 Internal server error ile hata mesajını yollayabiliriz
            return res.status(500).send('There is something go wrong ' + err);
        });
});

// Servisten dışarıya açtığımız fonksiyonlar
// somedata fonksiyonumuz için app isimli express nesnemiz ve doğal olarak Get, Post metodları ele alınacak
exports.somedata = functions.https.onRequest(app);

// Servis hayatta mı metodumuz :P
// Ping'e Pong dönüyorsa yaşıyordur deriz en kısa yoldan.
exports.ping = functions.https.onRequest((request, response) => {
    response.send("Pong!");
});

Kod nihai halini aldıktan sonra tekrardan dağıtım işlemi yapılmalıdır.

firebase deploy --only functions

Dağıtım işlemi sonrasında somedata ve ping referans adresli endpoint bilgilerini dashboard üzerinde görebilmemiz gerekiyor.

Şimdi somedata fonksiyonunun Post metodunu kullanarak bir kaç örnek veri girişi yapalım. Postman gibi bir araçtan  yararlanarak bu işlemleri kolayca gerçekleştirebiliriz.

Hızlıca bir test yapmak için ping fonksiyonunu da çağırabilirsiniz. https://us-central1-project-new-hope.cloudfunctions.net/ping adresine talep göndermeniz yeterlidir.

Adres : https://us-central1-project-new-hope.cloudfunctions.net/somedata/
Metod : HTTP Post
Body : JSONÖrnek Veri : {
"Id":1000,
"Quote":"Let’s go invent tomorrow rather than worrying about what happened yesterday.",
"Owner":"Steve Jobs"
}

Bir kaç deneme girişi yaparak veriyi çoğaltabiliriz. JSON formatlı olmak suretiyle istediğimiz şema yapısında veriler yollamamız mümkün. Firebase sayfasındaki Database kısmına baktığımıza aşağıdakine benzer sonuçları görürürüz.

Pek tabii HTTP Get çağrıları sonuncunda da aktardığımız tüm verileri çekebiliriz. Bunun için aşağıdaki adrese talepte bulunmak yeterlidir.

Adres : https://us-central1-project-new-hope.cloudfunctions.net/somedata/
Metod : HTTP Get

Başka Neler Yapılabilir?

Ben bir an önce deneyimlemenin heyecanından olsa gerek bu tip Hello World örneklerinde Get, Post harici fonksiyonları uygulamayı çoğunlukla atlıyorum. Dolayısıyla siz tembellik etmeyerek Put, Delete ve filtre bazlı Get metodlarını da örneğe katabilirsiniz. Hatta bu örneğin aksine Realtime Database yerine Cloud Firestore modelini kullanmayı denemenizi de şiddetle öneririm. Ayrıca şema olarak daha düzgün bir veri modeli üzerinden ilerlenebilir.

Malum bulut hizmetleri belli bir noktadan sonra kullanımlarımıza göre ücret alıyorlar. Bu nedenle yukarıdaki servislere an itibariyle ulaşamayabilirsiniz. Nitekim Azure, Google Cloud Platform veya Amazon Web Services gibi ortamlarda hazırladığım kaynakları işim bittikten bir süre sonra mutlaka kaldırıyorum. Daha önceden yaşadığım bazı acı tecrübeler nedeniyle...

Ben Neler Öğrendim?

Bu çalışma kapsamında daha çok GCP üzerinde bulut fonksiyon barındırma ve veri tabanı ile ilişkilendirme konularını inceleme fırsatı bulmuş oldum. Pek tabii bu çalışmanın da bana kattığı bir şeyler oldu. Bunları aşağıdaki maddeler halinde özetleyebilirim.

  • Firebase üzerinde bir projenin nasıl oluşturulacağını
  • firebase-tools ile proje başlatma, fonksiyon dağıtma gibi temel işlemlerin terminalden nasıl yapılacağını
  • Kendi geliştirme ortamımızda yazılan node.js tabanlı bir API hizmetini Function olarak Firebase'e nasıl deploy edeceğimi
  • Realtime veri tabanı modelinin kabaca ne olduğunu

Böylece geldik bir cumartesi gecesi macerasının daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Google Cloud Fonksiyonlarını Firebase ile Birlikte Kullanmak

$
0
0

Google'ın Doodle hizmetini takip ediyor musunuz bilemiyorum ancak ben zaman zaman orada hazırlanmış ikonik görsellerden harika hikayelere gidiyorum. Bu seferki yazının derlemesi sırasında da yolum bir şekilde onunla kesişti ve girişte kimden bahsedebilirim derken havacılılk tarihinin en önemli isimlerinden olan Türkiye'nin ilk kadın pilotu Sabiha Gökçen'i(22 Mart 1913 - 22 Mart 2001) anmaya karar verdim.

Amerikan Hava Kurmay Koleji'nin 1996 yılında Maxwell Hava Üssünde yapılan töreninde Dünya tarihine adını yazdıran 20 havacıdan birisi olarak ödül alan Sabiha Gökçen'in tarihde iz bırakan başarıları saymakla bitmez elbette. Lakin diğer pek çok başarısının yanında bu en çok dikkatimi çekenlerden birisiydi. İçindeki uçma arzusu ve sevgisi öyle büyük olmalı ki Fransız pilot Daniel Acton ile son uçuşunu yaptığında 83 yaşındaydı. Türk Hava Kurumu Türkkuşu'nda Başöğretmen olarak görev aldı ve 1955 yılına kadar bir çok değerli pilotun yetişmesine ön ayak oldu.

Arada bir sizde doodlelayın derim. Bazen çok değerli bilgilere ulaşabiliyoruz. Gelelim Google ile ne işimiz olduğuna(Hoş onsuz hareket ettiğimiz bir günümüz de yok) Bu kez Saturday-Night-Works birinci fazdan 27 numaralı örneğin derlemesi ile karşınızdayım. Konumuz Google Cloud Platform üzerinden Firebase tabanlı bir bulut fonksiyon sunmak.

Bulut çözümlerin sunduğu imkanlardan birisi de sunucu oluşturma, barındırma, yönetme gibi etkenleri düşünmemize gerek kalmayacak şekilde uygulama geliştirme ortamları sağlamaları. Bazen bulut platform üzerinde tutulan bir veri tabanı ile konuşan servis kodlarını yine o platformun sunucularında barındırmak suretiyle hizmet sunarız. Söz gelimi Google'ın Firebase veri tabanı ve onu kullanan servis tabanlı fonksiyonları Google Cloud Platform üzerinde konuşlandırabiliriz. Bu örnekteki amacımsa Firebase ile ilişkili bir uygulama servisini Google Cloud Platform üzerinde fonksiyonlaştırabilmekmiş. Her zaman olduğu gibi örneği WestWorld(Ubuntu 18.04, 64bit)üzerinde geliştirmişim. Öyleyse gelin notlarımızı derlemeye başlayalım.

Örnekte Firebase'in Realtime Database seçeneği kullanılmakta. Veriyi JSON tipinde tutan bir NoSQL sistemi olarak düşünülebilir. Veri, bağlı olan tüm istemciler için gerçek zamanlı(realtime) olarak eşlenir. Dahası, istemci uygulama kapansa bile veriyi hatırlar. Cloud-Hosted bir veri tabanıdır. Bir başka deyişle veri tabanı sunucusu google üzerinde durmaktadır. Özellikle Cross-Platform tipinden uygulamalar söz konusuysa(iOS, Android, Javascript veya Typescript fark etmez) tüm bağlı istemcilerle aynı verinin senkronize olarak paylaşılmasını sağlamak gibi önemli bir özelliği vardır. Diğer yandan söz konusu Realtime Database ürünü dışında Cloud Firestore isimli daha önceden üzerinden durup düşündüğümüz bir veri tabanı modeli daha vardır. Firebase'in orjinal veri tabanı olan Realtime modelinin daha geliştirilmiş bir versiyonu olarak düşünebiliriz. Her iki ürün arasındaki farklılıkları kabaca aşağıdaki gibi özetleyebiliriz.

  • Realtime modelinde veri JSON ağaç yapısı şeklinde saklanırken Firestore'da koleksiyon biçiminde organize edilmiş dokümanlar söz konusudur(Firestore, Mongo'yu hatırlattı burada bana)
  • Firestore özellikle karmaşık ve hiyerarşik veri kümelerini ölçeklerken Realtime modele göre daha başarılıdır.
  • Realtime veri tabanı iOS ve Android gibi mobil platformlar için çevrim dışı(offline)çalışma desteği sunar. Firestore buna ek olarak Web tabanlı istemciler için de offline çalışma desteği sağlar.
  • Sıralama ve filtreleme imkanları Cloud Firestore'da Realtime modeline göre çok daha geniştir.
  • Firestore'da bir transaction tamamlanıncaya kadar otomatik olarak tekrar ve tekrar denenir.
  • Realtime veri tabanı modelinde ölçekleme için Sharding uygulanması gerekirken Firestore'da bu iş otomatik olarak yapılır.

Ben uygulaması çok daha basit olduğundan Realtime Database modelini tercih ettim.

İlk Hazırlıklar

Her şeyden önce Google Cloud Platform üzerinde bir hesabımızın olması lazım. Hesabımız ile login olduktan sonra Firebase Console adresine gidip bir proje oluşturacağız. Söz gelimi project-new-hope gibi bir isimle...

Projeyi komut satırından yönetebilmek önemli. Nitekim yazdığımız kodları kolayca deploy edebilmeliyiz. Bu nedenle Firebase CLI(Command Line Interface) aracına ihtiyacımız var. Kendisini npm ile aşağıdaki gibi yükleyebiliriz(Dolayısıyla sistemimizde node ve npm yüklü olmalıdır)

npm install -g firebase-tools

Yükleme işlemi başarılı olduktan sonra proje ile aynı isimde bir klasör oluşturup, içerisinde sırasıyla login ve functions komutlarını kullanarak ilerleyebiliriz. Bu komutlarla Firebase ortamına giriş yapma ve projenin başlangıç iskeletinin oluşturulması işlemleri yapılmaktadır.

mkdir project-new-hope
cd project-new-hope
firebase login
firebase init functions

Login işlemi sonrası arabirim bizi tarayıcıya yönlendirecek ve platform için giriş yapmamız istenecektir. Başarılı login sonrası tekrardan console ekranına dönüş yapmış oluruz.

init functions çağrısı ile yeni bir google cloud function oluşturma işlemine başlanır. Dört soru sorulacaktır(En azından çalışmanın yapıldığı tarih itibariyle böyleydi) Projeyi zaten Firebase Console'unda oluşturmuştuk. Klasör adını aynı verdiğimiz için varsayılan olarak onu kullanacağını belirtebiliriz. Dil olarak Typescript ve Javascript desteği sorulmakta ki ben ikincisi tercih ettim. Üçüncü adımda ESLint kullanıp kullanmayacağımız soruluyor. Şimdilik 'No' seçeneğini işaretleyerek ilerlenebilir ancak gerçek hayat senaryolarında etkinleştirmek iyi bir fikirdir. (İleriye yönelik problem yaratabilecek olası kod hatalarının önceden tespitinin kritikliği sebebiyle) Projenin bağımlılık duyduğu npm paketleri varsa bunların install edilmesini de istediğimizden son soruda 'Yes' seçminini yapmalıyız.

Komut çalışmasını tamamladıktan sonra aşağıdaki klasör yapısının oluştuğunu görebiliriz.

Bundan sonra index.js dosyası ile oynayıp örnek bir dağıtım(deployment) işlemi gerçekleştirebiliriz de. Index sayfasında yorum satırı içerisine alınmış bir kod parçası bulunmaktadır. Bu kısmı açarak hemen Hello World sürecini işletmemiz mümkün. Ama bunun için, yapılan değişiklikleri platforma almamız lazım. Aşağıdaki terminal komutu ile bunu sağlayabiliriz. Örnekteki amacımıza göre sadece fonksiyonların taşınması söz konusudur.

firebase deploy --only functions

Sorun Yaşayabiliriz

Yukarıdaki terminal komutunu denediğimde aktif bir proje olmadığına dair bir hata mesajı aldım ve deployment işlemi başarısız oldu. Bunun üzerine önce aktif proje listesine baktım(firebase list) ve sonrasında use --add ile tekrardan proje seçimi yaptım. Bir alias tanımladıktan sonra(ki her nedense proje adının aynısını vermişim :S) tekrardan deploy işlemini denedim. Bu seferde sadece fonksiyon olarak dağıtım yapmak istediğimi belirtmediğim için başka bir hata aldım. Nihayetinde çalıştırdığım terminal komutu işe yaradı ve proje GCP'a deploy edildi.

firebase list
firebase use --add
firebase deploy --only functions

Firebase Dashboard'una gittiğimizde helloworld isimli API fonksiyonunun(ki index.js dosyasından export edilen metodumuzdur) eklenmiş olduğunu görebiliriz.

Çalışmanın bu ilk yalın versiyonunda Google'ın index.js içerisine koyduğu yorum satırları kaldırılarak bir deneme yapılmıştır. Bu taşıma işlemi sonrası Firebase tarafında üretilen fonksiyona ait API adresini aşağıdaki gibi curl ile çağırdığımızda 'Hello from Firebase!' yazısını görebiliriz.

curl -get https://us-central1-project-new-hope.cloudfunctions.net/helloWorld

Kod Tarafı

Asıl işi yapan örneğimiz ise basit bir REST hizmeti. POST ve GET mesajlarını destekleyen metotlar içeriyor ve temel olarak veri ekleme ve listeleme fonksiyonelliklerini sağlıyor. Arka planda Firebase veri tabanının Realtime çalışan modelini kullanıyor. Arka plandan kastımız GCP üzerindeki Firebase veri tabanı. Yani kendi makinemizde geliştirdiğimiz bir API servisini, firebase veri tabanını kullanacak şekilde GCP üzerinde konuşlandırmış oluyoruz. İkinci örnek için gerekli bir kaç npm paketi var. REST(Representational State Transfer) modelini node tarafında kolayca kullanabilmek için express ve CORS(Cross-Origin Resource Sharing) etkisini rahatça yönetebilmek için cors :D Aşağıdaki terminal komutları ile onları projemize ekleyebiliriz.

npm install --save express cors

İlgili paketleri functions klasöründeyken yüklememiz gerekiyor. Nitekim deploy sırasında bu JSON dosyasındaki paket bilgileri, GCP tarafında da yüklenmeye çalışacak. Dolayısıyla GCP'nin, kendi ortamında kullanacağı paketlerin neler olduğunu bilmesi lazım.

index.js dosyasına ait kod içeriğini aşağıdaki gibi geliştirebiliriz.

const functions = require('firebase-functions');
const express = require('express');
const cors = require('cors');
const admin = require('firebase-admin');
admin.initializeApp();

const app = express();
app.use(cors()); //CORS özelliğini express nesnesi içine enjekte ettik

// HTTP Get çağrısı gelmesi halinde çalışacak metodumuz
app.get("/", (req, res) => {

    return admin.database().ref('/somedata').on("value", snapshot => {
        // HTTP 200 Ok cevabı ile birlikte somedata içeriğini döndürüyoruz
        return res.status(200).send(snapshot.val());
    }, err => {
        // Bir hata varsa HTTP Internal Server Error mesajı ile birlikte içeriğini döndürüyoruz
        return res.status(500).send('There is something go wrong ' + err);
    });
});

// HTTP Post çağrısını veritabanına veri eklemek için kullanacağız
app.post("/", (req, res) => {
    const payload = req.body; // gelen içeriği bir alalım
    // push metodu ile veriyi yazıyoruz.
    // işlem başarılı olursa then bloğu devreye girecektir
    // bir hata oluşması halinde catch bloğu çalışır
    return admin.database().ref('/somedata').push(payload)
        .then(() => {
            // HTTP 200 Ok - yani işlem başarılı oldu diyoruz
            return res.status(200).send('Eklendi');
        }).catch(err => {
            // İşlem başarısız oldu
            // HTTP 500 Internal server error ile hata mesajını yollayabiliriz
            return res.status(500).send('There is something go wrong ' + err);
        });
});

// Servisten dışarıya açtığımız fonksiyonlar
// somedata fonksiyonumuz için app isimli express nesnemiz ve doğal olarak Get, Post metodları ele alınacak
exports.somedata = functions.https.onRequest(app);

// Servis hayatta mı metodumuz :P
// Ping'e Pong dönüyorsa yaşıyordur deriz en kısa yoldan.
exports.ping = functions.https.onRequest((request, response) => {
    response.send("Pong!");
});

Kod nihai halini aldıktan sonra tekrardan dağıtım işlemi yapılmalıdır.

firebase deploy --only functions

Dağıtım işlemi sonrasında somedata ve ping referans adresli endpoint bilgilerini dashboard üzerinde görebilmemiz gerekiyor.

Şimdi somedata fonksiyonunun Post metodunu kullanarak bir kaç örnek veri girişi yapalım. Postman gibi bir araçtan  yararlanarak bu işlemleri kolayca gerçekleştirebiliriz.

Hızlıca bir test yapmak için ping fonksiyonunu da çağırabilirsiniz. https://us-central1-project-new-hope.cloudfunctions.net/ping adresine talep göndermeniz yeterlidir.

Adres : https://us-central1-project-new-hope.cloudfunctions.net/somedata/
Metod : HTTP Post
Body : JSONÖrnek Veri : {
"Id":1000,
"Quote":"Let’s go invent tomorrow rather than worrying about what happened yesterday.",
"Owner":"Steve Jobs"
}

Bir kaç deneme girişi yaparak veriyi çoğaltabiliriz. JSON formatlı olmak suretiyle istediğimiz şema yapısında veriler yollamamız mümkün. Firebase sayfasındaki Database kısmına baktığımıza aşağıdakine benzer sonuçları görürürüz.

Pek tabii HTTP Get çağrıları sonuncunda da aktardığımız tüm verileri çekebiliriz. Bunun için aşağıdaki adrese talepte bulunmak yeterlidir.

Adres : https://us-central1-project-new-hope.cloudfunctions.net/somedata/
Metod : HTTP Get

Başka Neler Yapılabilir?

Ben bir an önce deneyimlemenin heyecanından olsa gerek bu tip Hello World örneklerinde Get, Post harici fonksiyonları uygulamayı çoğunlukla atlıyorum. Dolayısıyla siz tembellik etmeyerek Put, Delete ve filtre bazlı Get metodlarını da örneğe katabilirsiniz. Hatta bu örneğin aksine Realtime Database yerine Cloud Firestore modelini kullanmayı denemenizi de şiddetle öneririm. Ayrıca şema olarak daha düzgün bir veri modeli üzerinden ilerlenebilir.

Malum bulut hizmetleri belli bir noktadan sonra kullanımlarımıza göre ücret alıyorlar. Bu nedenle yukarıdaki servislere an itibariyle ulaşamayabilirsiniz. Nitekim Azure, Google Cloud Platform veya Amazon Web Services gibi ortamlarda hazırladığım kaynakları işim bittikten bir süre sonra mutlaka kaldırıyorum. Daha önceden yaşadığım bazı acı tecrübeler nedeniyle...

Ben Neler Öğrendim?

Bu çalışma kapsamında daha çok GCP üzerinde bulut fonksiyon barındırma ve veri tabanı ile ilişkilendirme konularını inceleme fırsatı bulmuş oldum. Pek tabii bu çalışmanın da bana kattığı bir şeyler oldu. Bunları aşağıdaki maddeler halinde özetleyebilirim.

  • Firebase üzerinde bir projenin nasıl oluşturulacağını
  • firebase-tools ile proje başlatma, fonksiyon dağıtma gibi temel işlemlerin terminalden nasıl yapılacağını
  • Kendi geliştirme ortamımızda yazılan node.js tabanlı bir API hizmetini Function olarak Firebase'e nasıl deploy edeceğimi
  • Realtime veri tabanı modelinin kabaca ne olduğunu

Böylece geldik bir cumartesi gecesi macerasının daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Viewing all 525 articles
Browse latest View live