Quantcast
Channel: Burak Selim Şenyurt
Viewing all 351 articles
Browse latest View live

Code Coverage

$
0
0

Merhaba Arkadaşlar,

Basketbol en sevdiğim spor dalı. Takım ayırt etmeksizin izlemeyi çok seviyorum. 40lı yaşların ortalarına doğru geliyor olsam da hala büyük bir keyifle oynuyorum da. Bazen saatlerce. En büyük rakibimse beş yaşından beri basketbol eğitim alan S(h)arp Efe. Son beş yıldır düzenli olarak idman yaptığı için kendisini epey geliştirmiş durumda. Bire birlerde acımıyor ve doğru basketbol oynamaya çalışıyor. Doğru basketbol için temellerin de doğru atılması gerekiyor. İlk koçu dahil şu an oynadığı kulüpteki koçu da Ona ve diğer çocuklara doğru alışkanlıkları öğretmeye çalışıyorlar. Çok zaman potaya şut bile atmadan tamamladıkları idmanlar vardır. Bunun yanına disiplinli çalışmayı da ekleyince çocuklar ilerleyen yaşlarında temel hareketlerde güçlü birer basketbolcu adayı haline geliyorlar.

Konumuz basketbol değil ama onun çocuklar için düşünülen felsefesine yakın bir konu. Şimdi biraz geriye gidelim. 80lerin sonlarına. Eğer yazılıma ilk başladığım o yıllarda her şeyi Test Driven Development(ki NASA hariç sanırım pek çoğumuz o zamanlar bundan bihaberdi) prensiplerine göre geliştiriyor olsaydım...Şimdi ne kadar da kaliteli kodlar çıkartırdım diye düşünmeden edemiyorum. Çocuk değildim belki ama programlamaya ilk başladığım yıllardı ve işte o yıllarda temelleri de iyi atmak gerekiyordu.

Yeni yazılımcı adaylarının işte bu noktayı gözden kaçırmaması gerekiyor. Bugün yazılım geliştiren bir çok firma var. Ancak ürün kalitesi ile öne çıkanlar, pazara çok çabuk değer katan özellikler sunanlar diğerlerinden sürekli bir adım önde oluyorlar. Peki bunu nasıl oluyor da başarıyorlar!? En başından sonuna kadar sürekli olarak kendini iyileştiren, geri bildirimler ile devamlı beslenen, pek çok işin otomatikleştirildiği, yazılımcılar ile operasyon arasındaki bariyerlerin ortadan kaldırıldığı, çevik metodolojilere göre hareket edildiği kültür ile. Bu kültürün bir çok dayanak noktası var. Birisi de kodun ne kadar güvenilir ya da itibarının ne kadar yüksek olduğuyla alakalı. Bu genellikle statik ve dinamik kod analizleri ile mümkün kılınabilen bir senaryo. 

Kaliteli kodun önemli belirtilerinden birisi de test edilmiş olmasıdır. Satır satır, fonksiyon fonksiyon ne kadarının test edilmiş olduğu onun itibarını doğrudan etkileyen bir faktördür. Bütünüyle testten geçirilmiş bir kod parçası takdir edersiniz ki kabus görmemizi engeller ve geceleri rahat bir uyku çekmemizi sağlar. Peki bir kodun ne kadarının test edilmiş olduğunu nasıl ölçebiliriz? Terminolojide Code Coverage olarak da adlandırılan bu durum Continuous Integration hattı için de büyük öneme sahiptir. Nitekim kodun %99.9 testten geçmiş olmasını bir kalite kriteri olarak kabul edebilir ve CI Server'ın buna göre deployment süreçlerini yürütmesini sağlayabiliriz.

Esasında test güdümlü geliştirme(test driven development) esaslarına bağlı kalarak uygulama geliştirirsek kodun yazılan her fonksiyonunu test ederek ilerliyoruz demektir. Bu, doğal olarak Code Coverage değerinin yüksek çıkmasını sağlayacaktır. 

Code Coverage ölçümlemesi için bir çok araçtan yararlanmamız mümkün. Örneğin platform bağımsız olarak çalışan ve .Net core desteği de bulunan Coverlet paketi bunlardan birisi. Bu yazımızda da Coverlet'ten yararlanarak söz konusu ölçümlemeleri nasıl yapabileceğimizi basitçe incelemeye çalışacağız. Hatta sonlara doğru statik kod analiz araçlarının en iyilerinden diyebileceğimiz SonarQube üzerine sonuçları aktarmaya çalışacağız. Örneklerimizi West-World(Ubuntu 16.04) üzerinde ve Visual Studio Code kullanarak geliştireceğiz. Haydi başlayalım.

Öncelikli olarak örnek solution kurgusunu oluşturarak işe başlamakta yarar var. West-World' de bunun için aşağıdaki komutlardan yararlanabiliriz.

mkdir CodeCoverage
cd CodeCoverage
dotnet new sln
mkdir MathService
cd MathService
dotnet new classlib
cd ..
dotnet sln add MathService/MathService.csproj
mkdir MathService.Tests
cd MathService.Tests
dotnet new mstest
dotnet add reference ../MathService/MathService.csproj
cd ..
dotnet sln add MathService.Tests/MathService.Tests.csproj
dotnet build

Şu haliyle aşağıdaki şekilde görülen klasör yapısına sahibiz.

Bize tabii ki bir demet C# ve bir tutam da test kodu gerekiyor. MathService ve MathService.Tests projelerine aşağıdaki kod parçalarını serpiştirebiliriz. Buradaki temel hedefimiz Code Coverage için Visual Studio Code tarafında neler yapabileceğimize bakmak olduğundan basit bir iki deneme metodu yeterli olacaktır. MathService projesindeki Fundamental projesinde dörtgen çevresini bulmak için yararlanabileceğimiz bir fonksiyon yer alıyor.

using System;

namespace MathService
{
    public class Fundamental
    {
        public double SquarePerimeter(double a,double b)
        {
            if(a==b)
                return 4*a;
            else
                return 2*(a+b);
        }
    }
}

MathService.Tests projesinde de FundamentalTests isimli bir test sınıfımız olacak.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MathService.Tests
{
    [TestClass]
    public class FundamentalTests
    {
        private Fundamental _service;

        public FundamentalTests()
        {
            _service=new Fundamental();
        }
        [TestMethod]
        public void Should_Square_Area_Is_8_For_2()
        {
            var excepted=8;
            var result=_service.SquarePerimeter(2,2);
            Assert.AreEqual(excepted,result);

        }
        [TestMethod]
        public void Should_Rectangle_Area_Is_14_For_2_and_5()
        {
            var excepted=14;
            var result=_service.SquarePerimeter(2,5);
            Assert.AreEqual(excepted,result);
        }
    }
}

Şu anda iki test kabulümüz var. İlkinde kare ikincisinde de dikdörtgen için beklediğimiz değerler var. Şimdi Code Coverage işini kolaylaştırmak için Coverlet kütüphanesini test projemize eklemeliyiz. Bunun için test projesinin olduğu klasörde aşağıdaki terminal komutunu çalıştırmamız yeterli.

dotnet add package coverlet.msbuild

Coverlet destekli olacak şekilde test kodlarımızı çalıştırmak için root klasördeyken aşağıdaki komutla devam etmemiz gerekiyor.

dotnet test MathService.Tests/MathService.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover

Bu durumda çalışma zamanının tepkisi aşağıdaki ekran görüntüsündeki gibi olacaktır.

İki test başarılı şekilde çalışmış görünüyor. Bunun dışında kodlarımız satır, branch ve metod bazında yüzde yüz sigortalanmış durumda diyebiliriz. Söz konusu veri çıktıları bu örnek için CodeCoverage.opencover.xml isimli dosyaya da yansıtılmış durumdadır. Şimdi Fundamental sınıfına yeni bir fonksiyon daha ekleyelim.

public double Average(params double[] numbers)
{
    double total=0;
    for(int i=0;i<numbers.Length;i++){
        total+=numbers[i];
    }
    return total/numbers.Length;
}

n sayılı bir dizinin ortalama değerini bulan bir fonksiyon söz konusu. Ancak bu kez ilgili fonksiyon için herhangi bir test yazmayalım ve testimizi yeniden başlatalım.

Hımmm...Güzelllll...Sonuçlar değişti. Satır, branch ve metod bazında MathService projesinin bir kısmına güvenebileceğimizi söyleyebiliriz. Nitekim kodun neredeyse %50si testten geçirilmemiş ve kontrol edilmemiş durumda. Peki test metodlarından en az birisinin hatalı sonlandığı bir durum söz konusuysa ne olur? Bunun için şöyle bir test metodunu ekleyerek ilerleyelim.

[TestMethod]
public void Should_Average_Is_2_For_Some_Array()
{
    var excepted=2;
    Assert.AreEqual(excepted,_service.Average(1,2,5,7,19));
}

Tekrar testimizi çalıştırırsak aşağıdaki sonuçlarla karşılaşırız.

Code Coverage adımına geçemedik bile sanki :)) Zaten testlerinden en az birisi hatalı sonuçlanan kodun güvenilirliği de tartışılır ve Continuous Integration sunucusunda çalışan SonarQube gibi statik kod analiz araçları bu durumu affetmeyecektir. Genellikle bu gibi durumlarda build edilen parçaların dağıtım adımına geçirilmemesi söz konusudur. SonarQube demişken...Aslında uygulama ile ilgili CodeCoverage sonuçlarını gözlemlemek için SonarQube'dan da yararlanabiliriz. Coverlet'in ürettiği çıktılar SonarQube ile de uyumludur. West-World üzerinde SonraQube yüklü değil ancak docker imajından yararlanabilirim. Bunun için terminalden aşağıdaki komutu vermek yeterli.

sudo docker run -d --name sonarqube -p 9000:9000 -p 9092:9092 sonarqube

Coverlet sonuçlarını SonarQube'a alabilmek için SonarScanner aracına da ihtiyacımız olacak. Bunu West-World'e kurmak için aşağıdaki terminal komutundan yararlandım.

sudo dotnet tool install --global dotnet-sonarscanner

Artık uygulamıza ait Code Coverage değerlerini toplamak ve SonarQube üzerinden analiz etmek için hazırız. Yine terminalden aşağıdaki komutları kullanarak ilerlememiz yeterli(Tabii öncesinde tüm testleri yeşile çekmeyi ihmal etmemek lazım)

dotnet test MathService.Tests/MathService.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
dotnet sonarscanner begin /k:"ProjectCodeCoverage"
dotnet build
dotnet sonarscanner end

Komutlar işletildikten sonra docker üzerinden çalışan ve localhost:9000 nolu porttan hizmet veren SonarQube servisine gidebiliriz. Bu durumda aşağıdakine benzer bir ekran görüntüsü ile karşılaşmamız gerekir.

Tek kelime ile A kalite bir ürün söz konusu :P Ama tabii gerçek hayat pek de böyle olmuyor. Özellikle yaşlı ve aceleyle geliştirilmiş,acele geliştirildiği için de stratejik olarak teknik borçlanılmış ürünler söz konusu olduğunda tablo aşağıdaki grafikte görüldüğü gibi de olabilir. FAILED!!!

Kaliteli kod geliştirmek elimizde. Bunun için test odaklı düşünmeli ve kodun tepeden tırnağa her parçasının çalışır olduğundan emin olmalıyız. Statik kod analizi yapan araçlara güvenmeli ve uyarılarını dikkate almalıyız. CI/CD(Continuous Integration/Continuous Delivery,Continuous Deployment) hatlarını doğru kurgulayıp sağlam ve emin adımlarla ilerlemeliyiz. En başında birey olarak temellerimizi sağlam atmalıyız. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


EF Core : Testlerde InMemory Context Kullanımı

$
0
0

Ablamla rahmetli babamız bir önceki sabah olduğu gibi o günde terastaki ahşap yemek masasının üzerine kurdukları fileyi karşılıklı sabitlemekle meşgullerdi. Normal ebatlarına göre çok daha dar ve kısa olan yemek masası, benim gibi orta okul çağlarındaki birisi için ideal bir ping pong sahasıydı esasında. Son bir kaç yazdır en büyük eğlencelerimizden birisi haline gelmişti. Kuzenlerle dolup taşan kalabalık yaz akşamlarında bir çok aile ferdini çevresine sığdıran Alman ahşapından yapılma o sağlam masa, prüzsüz yüzeyiyle sabahları çekişmeli ping pong maçlarına ev sahipliği yapıyordu. Güzel anıları ile birlikte rahmetli babamı zaman zaman kızdıran vakitlere de tanıklık etmişti. Bir keresinde raketi tutan kolumu tavana doğru öyle bir açmıştım ki florasan lambayı tuzla buz etmiştim. O günden sonra tavandan sarkan değil zeminine sabit bir lamba tercih etmiştik. Lakin bir diğer sefer daha büyük bir sorun yaşamıştık.

Evin zemin katındaki terasta hemen bahçe giriş kapısının önünde topraktan elli santimetre kadar yüksekte olan taş zemin üzerinde duran yemek masası, sokağa bakan tarafı boydan boya cam olan mutfağın da yanı başındaydı. Her ne kadar masa ile mutfak camı arasında bir metrelik mesafe olsa da büyüyen ben yıllar içerisinde aradaki kol mesafesini de azaltmıştım. Ve bir gün lise çağına geldiğimde olan olmuştu. Kolumu sağa doğru koşarken öyle geniş ve sert açmıştım ki, kırmızı yüzeyi ile göz göze geldiğim raket elimden fırlayıvermişti. Bahçe yerine mutfak camına doğru. Koca cam ortadan büyük bir yarıkla kırıldı. Eh tabii o zamanlar bugünkü gibi minicik parçalara ayrılıp kimseye zarar vermeyen camlara sahip değildik. Annemin "Aman oğlum iyi ki size bir şey olmadı" deyişinin yanında rahmetlinin o en meşhur bakışı saplanmıştı gözlerimden içeriye. Telepatik olarak mesaj alınmıştı. Sonraki gün ve yaz tatillerinin ilerleyen yıllarında, panayır yerindeki ping pong masasını kiralamanın çok daha ucuz olacağını anlamıştık.

Ortaokul çağlarında başlayan masa tenisi sevdam üniversite yıllarında da devam etti. Pek tabii bir alanda çok iyi olmak için gerçekten de çok çalışmak gerekiyor. İyi masa tenisi oynamak, müsabakalara katılıp derece yapabilmek her gün saatlerce masa tenisi oynamayı gerektiriyor. Ben hep amatör altı seviyede kalsam da dönem dönem derece almış ya da bu oyunu çok sevmiş arkadaşlara da sahip oldum. Yazlıktaki Sinan, üniversitedeki Emre, ellibeş yaşında üst kattaki teraslarına hakiki masa tenisi kurup benden ders alan hevesli Erdal Amca ve diğerleri. Gel zaman git zaman kırklı yaşlarıma geldim. Derken son girdiğim iş yerinde yemekhaneye çıktığım o ilk gün...Uzaktaki bir dinlenme alanında masa tenisi oynayan insanlar...Ve tekrar oynamaya başladım. Ah bu arada masa tenisi demişken, dünyanın en iyi oyuncularının listesini uluslararası masa tenisi federasyonunun şu sayfasında bulabilirsiniz. Ben onlardan birisinin ismini bir Entity nesnesini örnekleyip InMemory çalışan veritabanına yazmak için kullanacağım.

Entity Framework ile çalışırken test süreçlerini zorlaştırabilecek bağımlılıklardan birisi de uzak veritabanı bağlantısıdır. Genellikle bir SQL sunucusu ile çalışıldığından connectionString bilgisinde belirtilen adrese birim testlerin çalıştırılması sırasında da gidiliyor olması beklenir. Ancak bu şu anki durumda şart değil. EF context'ini bellekte çalışacak şekilde o anki process içerisinde de kullanabiliriz. Bunun için şu adreste yayınlanan Nuget paketinden yararlanıyoruz. Ancak bellekte çalışan bu veritabanı modelini ilişkisel olan versiyonları ile karıştırmamak lazım. Nitekim InMemory veritabanı bir SQL Server veritabanını taklit edemiyor(O amaçla geliştirilmemiş) Bu sebepten genel amaçlı veritabanı operasyonları için kullanılması daha doğru diyebiliriz. MSDN dokümanlarına göre ilişkisel veritabanı modelinin yerine kullanılacak test amaçlı bir araç gerekiyorsa, SQLite'ın InMemory çalışan verisyonunu göz önüne alabiliriz. Şimdilik amacımız basit veritabanı operasyonları sunan bir servise ait birim testlerde hakiki SQL sunucusuna gitmeden fonksiyonellikleri deneyimleyebilmek.

Gelin adım adım ilerleyerek söz konusu testleri nasıl yazabileceğimize bir bakalım. Öncelikle üzerinde çalışacağımız Solution'ı hazırlayalım. Bunun için terminalden aşağıdaki komutlarla ilerleyebilir ve bir proje ağacı oluşturabiliriz.

mkdir Testing
cd Testing
dotnet new sln
mkdir CustomerService
cd CustomerService
dotnet new classlib
cd ..
dotnet sln add CustomerService/CustomerService.csproj
mkdir CustomerService.Tests
cd CustomerService.Tests
dotnet new mstest
dotnet add reference ../CustomerService/CustomerService.csproj
cd ..
dotnet sln add CustomerService.Tests/CustomerService.Tests.csproj
cd CustomerService.Tests

Testing isimli solution'ımız içerisinde iki tip proje yer alıyor. CustomerService isimli sınıf kütüphanesinde(class library) Entity Framework tabanlı çalışan içeriklere yer vereceğiz. Test fonksiyonlarını ise CustomerService.Tests isimli mstest şablonundaki projede yazacağız. Kabaca aşağıdaki şekilde görülen ağacı oluşturmamız başlangıç için yeterli.

Pek tabii ihtiyacımız olan paketleri de kurmamız lazım. EntityFrameworkCore, SqlServer ve InMemory paketlerini CustomerService projesine eklemek için aşağıdaki terminal komutları ile çalışmamıza devam edelim.

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.InMemory

Artık DbContext türevli CustomerContext sınıfını ve diğerlerini kodlayabiliriz. Örneği basit bir şekilde almak için Customer isimli tek bir Entity sınıfı kullanacağız.

namespace CustomerService
{
    public class Customer
    {
        public int CustomerID { get; set; }
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        public string Title { get; set; }
    }
}

Sadece isim, soyisim ve ünvana yer verdiğimiz Customer tipinden sonra CustomerContext sınıfını yazarak devam edelim.

using Microsoft.EntityFrameworkCore;

namespace CustomerService
{
    public class CustomerContext
    : DbContext
    {
        public DbSet<Customer> Customers { get; set; }

        public CustomerContext()
        { }

        public CustomerContext(DbContextOptions<CustomerContext> options)
            : base(options)
        { }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=PDOSVIST01;Database=ATPMasters.InMemory;Trusted_Connection=True;ConnectRetryCount=0");
            }
        }
    }
}

CustomerContext, Customer tipinden bir DbSet ile çalışıyor. DbContext türevli olan bu sınıfın içerisinde iki yapıcı metoda(Constructor) yer veriyoruz. Varsayılan yapıcı değil ama DbContextOptions<T> türünden parametre alan ikinci verisyon önemli. Nitekim bu parametreye vereceğimiz bilgilerle test projesinde CustomerContext nesnesini oluştururken InMemory veritabanı kullanılacağını belirteceğiz. Override edilen OnConfiguring metodunda kullandığımız hakiki bir SQL Server bağlantı bilgisi olduğu dikkatinizden kaçmamış olsa gerek. Yani testler sırasında InMemory ilerlenirken, Context'in orjinal kullanımında aksi belirtilmediği sürece ilişkisel veritabanı ile konuşulacağını garanti etmiş oluyoruz.

Temel işlemleri içeren AddingService sınıfını da aşağıdaki gibi geliştirebiliriz. 

using System.Collections.Generic;
using System.Linq;

namespace CustomerService
{
    public class AddingService
    {
        private CustomerContext _context;

        public AddingService(CustomerContext context)
        {
            _context = context;
        }
        public Customer CreateCustomer(Customer customer)
        {
            var newCustomer=_context.Customers.Add(customer);
            _context.SaveChanges();
            return newCustomer.Entity;
        }
        public void UpdateCustomer(Customer customer)
        {
            var cust = _context.Customers.FirstOrDefault(c => c.CustomerID == customer.CustomerID);
            if (cust != null)
            {
                cust.Firstname = customer.Firstname;
                cust.Lastname = customer.Lastname;
                cust.Title = customer.Title;
                _context.SaveChanges();
            }
        }
        public IEnumerable<Customer> FindByLastname(string lastName)
        {
            return _context.Customers
                .Where(c => c.Lastname.Contains(lastName))
                .ToList();
        }

        public Customer FindById(int customerID)
        {
            return _context.Customers.FirstOrDefault(c=>c.CustomerID==customerID);
        }
    }
}

CreateCustomer ile yeni bir müşteri oluşturma, UpdateCustomer ile bilgilerini güncelleme, FindById ile belli bir CustomerID'ye göre kişi bulma ve FindByLastName ile de soyadına göre listeleme operasyonlarını üstlenen fonksiyonlarımız var. Tipik LINQ işlemlerine yer verdiğimizi düşünebiliriz. Tüm metodlarda CustomerContext örneğini kullanıyoruz. Bu nesneyi servis sınıfımıza yine yapıcı metod üzerinden geçirmekteyiz. Dolayısıyla hangi veri sağlayıcısını kullanacaksak buradaki fonksiyonlar ona göre işlem yapacaklar.

Servis tarafındaki ihtiyaçlarımızı tamamladığımıza göre artık test metodlarını geliştirmeye başlayabiliriz. Bunun için Unit Test projesine geçelim ve AddingTests sınıfını aşağıdaki gibi geliştirelim.

using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CustomerService.Tests
{
    [TestClass]
    public class AddingTests
    {
        [TestMethod]
        public void Create_Single_Customer_In_Memory()
        {
            var options = new DbContextOptionsBuilder<CustomerContext>()
                .UseInMemoryDatabase(databaseName: "TT100")
                .Options;

            using (var context = new CustomerContext(options))
            {
                var service = new AddingService(context);
                var nadal = new Customer
                {
                    Firstname = "Dimitrij",
                    Lastname = "OVTCHAROV",
                    Title = "Mr"
                };
                service.CreateCustomer(nadal);
            }

            using (var context = new CustomerContext(options))
            {
                Assert.AreEqual(1, context.Customers.Count());
                var added = context.Customers.Single();
                Assert.AreEqual("Dimitrij", added.Firstname);
                Assert.AreEqual("OVTCHAROV", added.Lastname);
                Assert.AreEqual("Mr", added.Title);
            }
        }

        [TestMethod]
        public void Find_Customers_By_Lastname()
        {
            var options = new DbContextOptionsBuilder<CustomerContext>()
                .UseInMemoryDatabase(databaseName: "TT50")
                .Options;

            using (var context = new CustomerContext(options))
            {
                context.Customers.Add(new Customer { Firstname = "Kim Hing", Lastname = "Yong", Title = "Mr" });
                context.Customers.Add(new Customer { Firstname = "Burak Selim", Lastname = "Yong", Title = "Mr" });
                context.Customers.Add(new Customer { Firstname = "Su Han", Lastname = "Yong", Title = "Ms" });
                context.Customers.Add(new Customer { Firstname = "Kim Hing", Lastname = "Yang", Title = "Mr" });
                context.Customers.Add(new Customer { Firstname = "Koki", Lastname = "Niwa", Title = "Ms" });
                context.Customers.Add(new Customer { Firstname = "Fun Sun", Lastname = "Kim", Title = "Ms" });
                context.SaveChanges();
            }

            using (var context = new CustomerContext(options))
            {
                var service = new AddingService(context);
                var result = service.FindByLastname("Yong");
                Assert.AreEqual(3, result.Count());
            }
        }

        [TestMethod]
        public void Update_Single_Customer()
        {
            var options = new DbContextOptionsBuilder<CustomerContext>()
                .UseInMemoryDatabase(databaseName: "TT50")
                .Options;
            var id = 0;
            using (var context = new CustomerContext(options))
            {
                var service = new AddingService(context);

                var kimHing = service.CreateCustomer(new Customer { Firstname = "Kim Hing", Lastname = "Yong", Title = "Mr" });
                context.SaveChanges();
                id = kimHing.CustomerID;

                service.UpdateCustomer(new Customer
                {
                    CustomerID = id,
                    Firstname = "Kim Kim",
                    Lastname = "Yong",
                    Title = "Mr"
                });
            }

            using (var context = new CustomerContext(options))
            {
                var service = new AddingService(context);
                var founded = service.FindById(id);
                Assert.AreEqual("Kim Kim", founded.Firstname);
            }
        }
    }
}

Üç test metodumuz var. Tek bir müşterinin oluşturulması, n sayıda müşteriden aynı soyada sahip olanlarının çekilmesi ve belli bir müşterinin verisinin değiştirilmesi işlerini deneyimliyoruz. Buna uygun olacak bir kaç Assert kullanımımız var. Tüm test metodlarının yazımız açısından en önemli ortak noktası ise DbContextOptionsBuilder<T> nesnesi örneklenirken UseInMemoryDatabase fonksiyonunun kullanılmış olması. Bu sayede sonraki satırlarda oluşturulan CustomerContext örneklerinin hangi tür veritabanı ile çalışacağını belirtmiş oluyoruz.

Test metodlarına ait çalışma zamanı sonuçlarını görmek için 

dotnet test

terminal komutunu vermemiz yeterli olacaktır. Ben denemelerimde aşağıdaki ekran görüntüsünde yer alan sonuçlara ulaştım. Tüm testler başarılı bir şekilde ilerletildi. Dolayısıyla operasyonun InMemory veritabanı kullanılarak icra edildiğini söyleyebiliriz.

InMemory veritabanı kullanımı görüldüğü gibi oldukça basit ancak başlarda da belirttiğimiz üzere her veritabanı özelliği desteklenmiyor. Örneğin transaction desteği yok ve bu veritabanı üzerinden SQL sorgularını çalıştıramıyoruz. Bu tip bir durumda SQLite veritabanının bellekte çalışacak şekilde kullanılması öneriliyor. Amaç yine SQL Server'a ihtiyaç duymadan genel Entity Framework işlevlerini test edebilmek. Ufak bir kaç kod değişikliği ile testlerimizi SQLite'ın InMemory modda çalışan versiyonuna çekebiliriz. İlk etapta SQLite paketinin projeye dahil edilmesi gerekiyor.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

Örnek olması açısından bir test metodunda aşağıdaki değişiklikleri yaparak ilerleyebiliriz.

[TestMethod]
public void Create_Single_Customer_In_Memory()
{
    SqliteConnection connection = new SqliteConnection("DataSource=:memory:");
    connection.Open();
    var options = new DbContextOptionsBuilder<CustomerContext>()
        //.UseInMemoryDatabase(databaseName: "TT100")
        .UseSqlite(connection)
        .Options;

    using (var context = new CustomerContext(options))
    {
        context.Database.EnsureCreated();
        var service = new AddingService(context);
	// Diğer kod satırları aynen devam ediyor

SqliteConnection tipinden bir nesne oluşturuyor ve parametre olarak verdiğimiz değerle bellekte çalışacağını belirtmiş oluyoruz. UseSqlite fonksiyonuna yapılan çağrıya bu connection bilgisini verdiğimiz için CustomerContext değişkeni artık Sqlite tipinden bir veritabanını kullanacak(Hemde bellekte çalışan sürümünü) Bir ihtimal ilgili veritabanının oluşmaması ihtimaline karşılık context üzerinden EnsureCreated metodunu çağırmamız da gerekebilir. Testleri bu şekilde çalıştırdığımızda bir öncekiler ile aynı sonuçları elde edeceğimizi görebilirsiniz. 

Kuvvetle muhtemel ilerleyen dönemlerde özellikle kolay test yapabilmek için farklı opsiyonlarda karşımıza çıkabilir. Şu an için genel amaçlı kullanılan ve belli başlı CRUD operasyonlarını içeren Entity Framework tabanlı servislere ait test senaryolarında değerlendirebileceğimiz iki önemli seçenek var. InMemory veya ilişkisel modele biraz daha yakın durabilen SQLite'ın InMemory versiyonu. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örnek kodlara github üzerinden erişebilirsiniz

Scala ile Tanışmak

$
0
0

Merhaba Arkadaşlar,

Yazılım geliştirmek için kullanabileceğimiz bir çok programlama dili var. Hatta bir ürünün geliştirilmesi demek farklı platformların bir araya getirilmesi anlamına da geldiği için, bir firmanın kendi ekosisteminde çeşitli dilleri bir arada kullandığını görmek mümkün. Scala'da bu çark içerisinde kendisine yer edinmiş ve son zamanlarda dikkatimi çeken programlama dillerden birisi. Bunun en büyük sebebi daha önceden çalıştığım turuncu bankanın Hollanda kanadında servis odaklı süreçlerin orkestrasyonu için onu backend tarafında kullanıyor(veya deneyimliyor) olması. Hatta şuradaki github adresinden açık kaynak olan Baker isimli ürünü inceleme şansımız da var. Scala ve Java kullanılarak yazılmış bir çatı.

Bunu bir konuşma sırasında öğrenmiştim ancak inceleme fırsatını bir türlü bulamamıştım. O vaktiler süreç akışlarının yönetimi için .Net ve Tibco işbirliğinden yararlanılıyordu. Hollanda neden daha farklı bir yol izlemişti? Scala'yı tercih etme sebepleri neydi? Hatta Scala programcıları aradıklarına dair iş ilanları var. Bu sorular bir süre kafamı meşgul etmişti. Derken zaman geçti, hayatlar değişti, teknolojiler farklılaştı ve araştırılacak konu listesinde sıra ona geldi. Bir bakalım neymiş bu Scala dedikleri programlama dili.

Scala 2001 yılında Martin Odersky tarafından geliştirilmeye başlanmış ve resmi olarak 2004'te yayınlanmış. Java dilinin bir uzantısı gibi görünüyor ama kesinlikle değil. Scala ile yazılan kodlar Java Bytecode'a dönüştürülüp JVM üzerinde çalıştırılabiliyor ama aynı zamanda REPL modeline göre yorumlatılarak da yürütülebiliyorlar. Hal böyle olunca Java kütüphanelerinin kullanılabilir olduğunu söylemek şaşırtıcı olmaz sanıyorum ki. Dolayısıyla Scala içinden Java kullanımı ve tam tersi durum mümkün. Esasında onu Java ile birlikte sıklıkla anıyorlar. Java ile yapılan işlerin aynısını daha az kod satırı ile halledebileceğimizi söylüyorlar. Söz gelimi Java ile yazılmış Vehicle isimli aşağıdaki sınıfı düşünelim.

public class Vehicle{
   private final String title;
   public Vehicle(String title){
      this.title=title;
   }
   public String getTitle(){
      return title;
   }
}

Bunu Scala tarafında şu haliyle yazmamız mümkün.

case class Vehicle(title: String)

Scala ile ilgili övgüler bununla sınırlı değil tabii. Çok zarif bir şekilde nesne yönelimlilik(Object Oriented) ve fonksiyonel programlama paradigmalarını bir araya getirdiği belirtiliyor. Yani nesne yönelimli ilerlerken fonksiyonel olmanın kolaylıklarını da ele abiliriz gibime geliyor. Bu sebeptendir ki Scala'daki her şey birer nesnedir. Buna sayılar gibi tipler haricinde fonksiyonlar da dahildir. Genel amaçlı bu dilin pek çok fanatiği var. Söz gelimi Twitter'ın şu adreste yayınlanmış repo'larında Scala sıklıkla geçiyor.

Eğer yeni mezunsanız veya halen öğrenciyseniz ve yeni bir programlama dili öğrenmek istiyorsanız size "Scala'yı mutlaka öğrenin" diyemem. Size C#'ı yutun, Java'da efsane olun da diyemem. Ancak size sıklıkla kullandığınız Facebook, Twitter, Instagram, Linkedin, Netflix, Spotify gibi devlerin Github repolarına bir bakın derim. Neyi çözmek için hangi ürünlerinde neleri kullanmışlar, sizlere birçok fikir verecektir. Asla tek bir dilin veya platformun fanatiği olmamak lazım. Bunların hepsi birer araç. Aşağıdaki görselde yazıyı hazırladığım tarih itibariyle baker projesinin içerisindeki dil kullanım oranlarını görüyorsunuz. Scala ve Java bir arada ele alınarak geliştirilmiş bir çatı.

Benim şu anki amacım dili temel özellikleri ile tanımaya çalışmak, bir kaç satır kod yazıp el alışkanlığı kazanmak. Java ile yazılım geliştirenler için öğrenirken yazım stiline alışmakta zorluk yaşandığına dair söylentiler var. Java ile yıllardır uğraşmamış bir .Net geliştiricisi olarak beni de epey zorlayacak diye düşünüyorum. Haydi gelin West-World'de(Ubuntu 16.04'ün 64 Bit sürümü olduğunu biliyorsunuzdur artık), Visual Studio Code kullanarak dili tanımaya çalışalım. İşe terminalden bağzı kurulumları yaparak başlamak gerekiyor elbette. Öncelikle sistemde JDK 8(v 1.8 olarak da biliniyor) yüklü olmalı. West-World'de yüklüydü ki bunu versiyonu kontrol ederek teyid ettim. Sonrasında Scala'nın kurulumunu yaptım. İşte kullanabileceğimiz terminal komutları.

## Java SDK Kurulumu
sudo apt-get update
sudo apt-get install default-jdk

## Scala Runtime Kurulumu
sudo apt-get remove scala-library scala
sudo wget http://scala-lang.org/files/archive/scala-2.12.6.deb
sudo dpkg -i scala-2.12.6.deb
sudo apt-get update
sudo apt-get install scala

Muhtemelen sisteminizde JDK yüklüdür ama her ihtimale karşı genel bir güncelleme ve sonrasında JDK kurulumu ile işe başlanabilir. Ardından var olan bir scala sürümü varsa bunu kaldırmanızı öneririm. Öğrenmeye başlarken son stabil sürüm ile ilerlemekte yarar var. deb paketini indirdikten sonra bunu açıp kuruyoruz. Bu işlemler başarılı bir şekilde gerçekleştiyse terminalden scala yazarak yeni bir ufka doğru yelken açmaya başlayabiliriz. Örneğin aşağıdaki ifadeleri deneyebiliriz.

scala
nickname="persival"
var nickname="persival"
nickname
nickname="perZival"
nickname
val default_point=50,45
val default_point=50.45
default_point
default_point=60.50

Scala arabirimine ulaştıktan sonra ilk önce bir değişken tanımlayayım istedim. Tür belirtmeden bodoslama yazdım. Tabii ki hata aldım. Bunun üzerine en başta yapmam gereken şeyi yaptım. Dokümanı okumaya başladım. Sonrasında var ile val şeklinde bir kullanıma rastladım. var ile değeri değiştirilebilir(mutable) bir değişken tanımlanabildiğini anladım. val komutuyla da değeri değiştirilemeyen(immutable) değişkenlerin tanımlanabileceğini öğrendim ki default_point'in değerini bu sebeple değiştiremedim. Aralarda ufak yazım hatalarım da oldu. double bir değişken için virgül kullanımı hata verdi örneğin. Genel olarak ilk sonuçlar aşağıdaki ekran görüntüsündeki gibi oldu.

İlerlemeye devam ettim ve değişken tanımlarken kullanılabilen def ifadesiyle karşılaştım. Önce mutable bir değişkenle, sonradan immutable bir tanesiyle denedim.

var city="istanbul"
def label_city=city
label_city
city="Istanbul"
label_city
label_city="istanbul"

Burada dikkat çekici bir nokta var. city isimli değişken için def ile bir başka değişken tanımlanıyor. Ancak label_city değişkeninin içeriği o çağrılana kadar alınmıyor. Bir başka deyişle değer ataması çağrım yapıldığı zaman gerçekleştiriliyor(Oysa ki yukarıdaki var ve val kullanımlarında atama yapılır yapılmaz değişkenler atanan değerlerine sahip olmuşlardı) Bunun henüz ne işe yarayacağını bilemiyorum ancak dili tasarlayanların mutlaka bir amacı vardır. Ayrıca city değişkeninin değerinde yapılacak değişiklikler label_city'yi de etkiliyor. Aynı referansa bakan değişkenler olduklarını ifade edebiliriz. Nitekim label_city değişkenine değer ataması yapamıyoruz.

Bu birkaç kod parçasında String ve Double tipleri ile tanışmış oldum. Elbette başka tipler de mevcut. Boolean, Byte, Short, Int, Long, Float, Char diğer veri tiplerinden. Aslında tip ağacının tepesinde Any yer alıyor. Any'den türeyen AnyVal ve AnyRef isimli iki ana alt tip daha var. Tüm tiplerin bu ikisinden türediğini söyleyebiliriz. AnyVal değer türleri için AnyRef ise referans türleri için ata tip olarak ele alınmakta. Yeri gelmişken Scala'nın case-sensitive bir dil olduğunu belirtelim. Şunları bir deneyin mesela;

var isExist=true
isExist=False
VAR name="what"
val point:int=40
val point:Int=40

Komut satırından Scala dilinin özelliklerini öğrenmeye çalışırken ekranı temizlemek için CTRL+L tuş kombinasyonundan yararlanabileceğimizi öğrendim. Ayrıca :help ile kullanabileceğimiz önemli terminal komutlarını da görebiliriz. Söz gelimi :reset ile o ana kadar ki tüm REPL işlemlerini sıfırlamış olur ve başlangıç konumuna döneriz.

Diğer dillerde olduğu gibi Scala içerisinde de if-else if-else kullanımları mevcut. Nam-ı diğer koşullu ifadeler. Ancak bunun yerine aşağıdaki kod parçasına baksak daha güzel olur.

def isEven(number:Int) = if (number%2==0) "Yes" else "No"

Burada isEven isimli bir değişken tanımlanmış durumda ki aslen kendisi bir metod ve eşitliğin sağ tarafındaki ifadenin sonucunu taşıyor. Bu örneği daha çok Scala'da ternary(?:) operatörünün olmayışına karşılık gösteriyorlar. Yukarıdaki fonksiyona ait örnek bir kullanımı aşağıdaki ekran görüntüsünde bulabilirsiniz.

Tabii if-else if-else kullanımını görünce insan switch case gibi bir bloğu da arıyor. Tam olarak aynı isimde olmasa da Pattern Matching özelliği kullanılarak bu mümkün kılınabiliyor. Bir anlamda bir değeri bir desenle kıyaslayıp kod akışını yönlendiriyoruz. Örneğin şu kod parçasında olduğu gibi.

def gradeValue(point:Int):String = point match {
     case 1 => "A"
     case 2 => "B"
     case 3 => "C"
     case _ => "you should work more"
     }
gradeValue(1)
gradeValue(3)
gradeValue(5)

gradeValue fonksiyonu için Pattern Matching kullanılıyor. point değişkeninin değerine göre case ifadelerinden birisi çalışıyor. case _ kısmı, Alternatives olarak isimlendirilen bölüm. point değişkeninin 1,2 ve 3 dışındaki tüm değerleri için çalışıyor.

Ancak dahası var. Sınıflar ile birlikte kullanıldığı bir senaryo. Bu senaryoyu denemek için terminali terketmek gerektiğini düşünüyorum. Visual Studio Code ile ilerlemek daha doğru olacaktır. O zaman burada kısa bir es verelim derim. Visual Studio Code'da scala kodu nasıl yazılabilir öğrenelim isterim. Öncelikle bugün ve sonrası için West-World üzerinde bir klasör açtım. Sonrasında scala uzantılı HelloWorld isimli aşağıdaki kod içeriğine sahip dosyayı oluşturdum. Code dosyayı tanıyarak bana hemen bir uzantı(extension)önerdi. Onu yükleyerek devam ettim ve aşağıdaki içeriği oluşturdum.

object HelloWorld {
  def main(args: Array[String]) {
    println("Hello from Scala")
  }
}

Sonrasında aşağıdaki terminal komutları ile kod dosyasını derleyip çalıştırdım!

scalac HelloWorld.scala
scala -classpath . HelloWorld

Aslında burada Scala kodunun derlenerek çalıştırılması söz konusu. Zaten kod içeriğine bakılacak olursa C#,C, C++ ve Java gibi dünyalardan pekala aşina olduğumuz main metodumuz var. Ekrandan parametre alabilen ve bunları bir String dizi üzerinden içeriye alan bu metod program çalıştırıldığındaki giriş noktası görevini üstleniyor. Scala'nın aynı zamanda yorumlamalı olarak da çalışabileceğinden bahsetmiştik. Yani aşağıdaki gibi bir kullanım da söz konusu olabilir.

println("Merhaba benim adım Burak")
println("Bugün",java.time.LocalDate.now)
val pi=3.14
var r=2
var result=pi*r*r
println(result)

Tutorial_1.scala ismiyle kaydettiğim dosyayı çalıştırmak için, 

scala Tutorial_1.scala

terminal komutunu vermek yeterliydi. Kod içeriği yorumlanarak çalıştırılacaktı.

Satır satır yorumlanarak kodun çalıştırıldığını görüyoruz. İstersek derleyerek, istersek yorumlatarak çalışabileceğimizi görmüş olduk. Hımmm. Etkileyici ;)

Artık scala terminalini terk etmenin zamanı gelmişti. Dili öğrenmek için Visual Studio Code arabiriminden devam edebilirdim. Tekrar Pattern Matching konusuna döndüm ve bu kez aşağıdaki kod içeriğini yazdım.

abstract class Messenger

case class Human(to: String, title: String, message: String) extends Messenger
case class Computer(to: String, message: String) extends Messenger
case class Broadcast(message: String) extends Messenger

def sendAlert(messenger: Messenger): String = {
  messenger match {
    case Human(to, title, message) =>
      s"This message for you dear $to. Title : $title. Message : $message"
    case Computer(to, message) =>
      s"This message for you dear $to. Message : $message"
    case Broadcast(message) =>
      s"To all units '$message'"
  }
}
val jordi = Human("Peter", "Dude. What's up?","I am going to go there next weekend body :)")
val microServiceCenter = Computer("Sam", "The CPU service is not responding at this moment")
var westWorldCon=Broadcast("Emergency drop. Delete everything")

println(sendAlert(jordi))
println(sendAlert(microServiceCenter))
println(sendAlert(westWorldCon))

Messenger soyut bir sınıf ve diğer üç case class bu tipten türüyor. Tipik bir kalıtım söz konusu diyebiliriz sanırım. Human, Computer ve Broadcast sınıfları pek normal sınıf formatında değiller aslında değil mi? Dikkat ederseniz case class bildirimi ile tanımlanmış durumdalar ve hatta doğrudan parametre alıyorlar. Bakmayın ben direkt sınıf olduklarını ifade ettim. sendAlert isimli metod parametre olarak Messenger tipinden bir değişken alıyor. Buna göre ilgili metod Human, Computer ve Broadcast tipleri ile çalışabilir ki çok biçimli(Polymorphism) bir yapıda olduğunu ifade etsek yanlış olmaz. Eşitliğin sağ tarafına göre metod String değer dönürecek. Metodun kullanımı sırasında case class'lara ait birer değişkenin parametre olarak verildiğini görebilirsiniz. jordi, microServiceCenter ve westWorldCon bu kod parçasındaki kahramanlarımız. sendAlert metodu kendisine gelen case class kimse eşleşmeye göre ona ait kod bloğu çalıştırılmakta. s ile çift tırnaklı ifadeyi formatlıyoruz ve $ ile başlayan değişken adları aslında parametreleri işaret ediyor.

İşler büyümeye başladı değil mi? if-else if-else derken pattern matching gibi değişik bir konuya denk geldik. Üstelik Pattern Matching bu kadarla da sınırlı değil. Guard, sealed class denilen kavramlar söz konusu. Hele ki case class olgusu. Normal sınıf olarak düşünebileceğimiz case class'lar aslında immutable veri tiplerinin modellenmesinde kullanılmak üzere düşünülmüşler. Onu da merak edip devam etmek istiyorum ancak oraya gelmek için başka şeyleri de öğrenmem gerekiyor. Mixin, trait, case class vs Sakin olmanın tam sırası. Böylesine ciddi bir dil çat diye öğrenilemez. Basit adımlarla dili tanımaya devam etmem lazım. Nitekim gözden kaçırdığım detaylar var. Söz gelimi fonksiyon ve metod aslında iki ayrı kavram. Aşağıdaki kod parçasını göz önüne alarak ilerleyelim.

var sayHello= (name:String) => println(s"Merhaba $name")
sayHello("Burak")

val sum = (x:Int,y:Int) => x+y
println(sum(4,5))

def diff(a:Int,b:Int):Int = a-b
println(diff(6,7))

def writeAndCombine(name:String)(message:String):String = {
    var output=s"$name! Sana bir mesaj var. '$message'"
    return output
}

println(writeAndCombine("Persival")("Bugün nasılsın?"))

def getState: String = "All is well"
var state=getState
println(state)

Örnekte basit fonksiyon ve metod kullanımları var. Metodları def anahtar kelimesinden ayırt edebilirsiniz. sayHello ve sum birer fonksiyon olarak karşımıza çıkıyorlar. Metodlar, fonksiyonlardan farklı olarak geri dönüş değerleri varsa tipinin ne olduğunu belirtmekte. Fonksiyonlar için bu durum söz konusu değil. Ayrıca bir metodun çoklu(birden fazladan farklı bir durum) parametre listesine sahip olması da söz konusu. writeAndCombine metoduna dikkat ederseniz bu durumu göreceksiniz. Fonksiyonları var ve val kelimeleri ile tanımlayabiliyoruz. Metodlar ise mutlak suretle def ile tanımlanmalılar. Her ne kadar writeAndCombine metodunda return kelimesini kullanmış olsak da bu mecburi değil. Fonksiyonlar aslında => operatörünün sağ tarafındaki ifadenin bir değişkene atanmış hali gibi düşünülebilirler. Fonksiyonları isimsiz olarak tanımlamak da mümkün(Anonymous Function) Arada kullanım yerleri göz önüne alındığında başka farklılıklar olduğuna da eminim. Nitekim hangi durumlarda fonksiyon hangi durumlarda metod tercih edilir bilmek lazım. Bendeki çalışma zamanı çıktıları aşağıdaki gibi oldu.

Aslında metodları bir sınıf içerisinde deneyimlesek güzel olabilir değil mi? O zaman bir sınıf tanımlayalım.

class ContextManager(conStr:String)
{
    def GetActorCount(query:String):Int={
        println("Sistemdeki aktor sayısı bulunacak")
        println(s"Sorgumu '$query'")
        148
    }

    def Ping():Unit = println("Pong")

    val ConnectionString = () => conStr
}

var morinyo=new ContextManager("server=manchester;db=players;u_id=admin,pwd=1234")
morinyo.Ping()
var actorCount=morinyo.GetActorCount("select * from players where state='actor'")
println(actorCount)
println(morinyo.ConnectionString())

ContextManager isimli bir sınıfımız var. Sınıf tanımı sırasında parametre verebildiğimizi fark etmişsinizdir. conStr aslında yapıcı metod parametresi gibi düşünülebilir. İçerideki ConnectionString fonksiyonu onu doğrudan kullanmaktadır da ;) Ortada görünen bir yapıcı metod ya da parametrelerini aktardığımız özellikler yok ancak kullanabiliyoruz. Bir şeylerin basitleştirildiği kesin. GetActorCount bir metod ve parametre olarak gelen sorguyu çalıştırıp Integer bir değer dönüyor. Hayali olarak elbette. Ping isimli metodumuzun dönüş tipi ise dikkatinizi çekmiş olmalı. Unit, void gibi anlamlandırılan bir dönüş tipiymiş. Şimdilik bunu öğrendim. ContextManager sınıfına ait nesne örneği oluşturmak için new operatöründen yararlanılıyor. Sonrasında ise bildiğimiz metod ve fonksiyon çağrımları söz konusu. İşte çalışma zamanı sonuçları.

Yazının bu kısmında bıraksam mı yoksa hazır sınıflara değinmişken kısaca Case Class türüne bir baksam mı düşünmeye başladım. Filtre kahvem bitmişti zaten. Yenilerken soluklanır ve sonra tam gaz dokümanlar üzerinden araştırmaya devam ederim diye düşündüm. Öyle de yaptım :) Case Class aslında adı üzerinde "Kasa sınıf" olarak düşünülmeli. Bu özel tip immutable sınıflar tanımlamamıza olanak sağlıyor. Bu sebeple de değer bazlı(Compare by Value) karşılaştırmalar söz konusu. Aslında sınıfları referans, kasa sınıflarını da değer türü gibi düşünebiliriz(class vs struct gibi. Bu dillerde ne çok birbirlerine benziyorlar değil mi?) Aşağıdaki kod parçasını kullanarak bu durumu anlamaya çalışabiliriz.

case class Dimension(x:Double,y:Double,z:Double)
class Dim(x:Double,y:Double,z:Double)

val location1=Dimension(3.4,5.2,9.1)
val location2=Dimension(3.4,5.2,9.1)

println(location1 == location2)

val loc1=new Dim(3.4,5.2,9.1)
val loc2=new Dim(3.4,5.2,9.1)

println(loc1 == loc2)

Dimension bir case class. Dim ise normal bir sınıf olarak tanımlandı. location1 ve location2 birer Case Class örneği ve aynı x,y,z değerlerine sahipler. loc1 ve loc2 ise birer Dim sınıf örneği ve x,y,z bazında yine aynı değerlere sahipler. Ancak karşılaştırılma durumları farklı. Case Class'lar değer bazlı olarak karşılaştırıldığından sonuç true. Sınıflar içinse tam aksi durum söz konusu ki bu normal.

Scala'yı anlamak için bulduğum dokümanları kurcalarken dilin temel özellikleri içerisindeki Object ve Trait kavramları da ilgimi çekti. Bir sınıfın sadece tek bir nesne örneğine sahip olmasını istiyorsak Object tipinden yararlanabiliriz. Çalışma zamanı ona ihtiyaç duyulduğu yerde oluşturacaktır(Lazy creation) Bir nevi Singleton nesne tanımlamak için kullanılan basit bir tip olarak düşünebiliriz. Aşağıdaki kod parçasında object tipinin kullanımına ilişkin basit bir deneme var.

object Listener {
  def Start:Boolean = { 
    println("listening...") 
    true
    }
  def Stop:Boolean = { 
    println("stoped!") 
    true
    }
}

Listener.Start
Listener.Stop

Pek tabii Singleton nesnelere ihtiyaç duyulan senaryolara bakmak lazım buradaki durumu anlamak için. Şu anda sadece yazım stili ve kavramsal olarak farkındalık sahibi oldum diyebilirim. Gelin birde şu Trait mevzusuna bakalım. Trait'ler belirlenmiş alan ve metodları taşıyorlar. Örneklenemeyen bir tür ancak başka sınıf veya nesneleri genişletmekte kullanılabiliyorlar. Java 8 tarafındaki Interface tipine benzetiliyorlar(İnceleyince çok uzun zamandır C# tarafında var olan interface'lerden ne farkı var anlayamadım tabii ama Scala çalıştıkça fark edeceğim diye düşünüyorum) Generic tipleri de Trait'ler ile değerlendirmek mümkün. Aşağıdaki kod parçası onun kullanımı ile ilgili bana temel bir fikir verdi aslında.

trait Capability {
    def Walk(stepCont:Int)
    def Stop:Boolean
    def Turn(location:String)
}

class Truck(title:String) extends Capability{
    override def Walk(stepCount:Int) = println(s"$stepCount walk")
    override def Stop:Boolean=true
    override def Turn(location:String) = println(s"go to $location")
}

trait SpecialCapability extends Capability{
    def Fire(target:String):Boolean
}

val v40=new Truck("Volvi v40")
v40.Walk(10)
v40.Turn("Montreal")

Capability içerisinde Walk, Stop, Turn isimli metod tanımlamaları var. Capability'den genişletilen Truck sınıfı bu metodları uygulamak zorunda. Diğer yandan SpecialCapability isimli Trait ek bir metod tanımı daha getiriyor. Yani Capability Trait'ini genişletmiş olduk.

Scala dili tabii ki birkaç sayfada anlatılamaz. Ben merak ettiğim için bu dili incelemeye çalışıyorum. Daha fazla hakim olmak için gerçek hayat senaryolarında kullanmak ve en azından bir kurumsal çaplı projede değerlendirmek lazım. Yine de ilk yazı için onun hakkında bir takım fikirler elde edebildiğimi düşünüyorum. Başlarda da belirtiğim üzere büyük oyuncularının dikkatini çeken ve ürünlerinde kullandıkları bir programlama dili olarak karşımıza çıkıyor. Scala ile ilgili yeni veya ilginç şeyler öğrendikçe buradan paylaşmaya devam etmeyi düşünüyorum. Scala'yı çalışmak için ilk kaynak olarak resmi sitesinden yararlanıyorum ancak Doğuş Teknoloji sağolsun Pluralsight eğitimleri de epey yardımcı oluyor. Ayrıca Martin Odersky'nin Programming in Scala kitabını da şiddetle tavsiye ederim. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Servis Çıktılarını Plotly.js ile Grafikleştirmek

$
0
0

West-World'de eğlence tüm hızı ile devam ediyor. Geçen ay gerçekleştirdiğimiz "C64 Retro" partisinden sonra sıra bu geceki "Easy Graphics of new Era" adlı eğlenceye geldi. Onur konuğumuz açık kaynak Javascript dünyasının son zamanlardaki yükselek yıldızı olarak görülen grafik kütüphanesi Plotly. Oldukça renkli bir kişiliğe sahip olan Plotly, GitHub şehrinin de en sevilen karakterlerinden birisi haline gelmiş durumda. Şehrin devasa enerji santrallerinin ürettiği verilerle çalışan çılgın istatistikçileri arasında da çok popüler bir karakter. Kendisini West-World'e getiren en yakın destekçileri D3.js ve WebGL'de partiye renk katanlar arasındalar.

Ona West-World sakinleri adına bir soru yönelttik ve izleyicilerini nasıl böylesine inanılaz şekilde büyülediğini sorduk. Her zaman ki enerjisi ve içten uslübuyla "dans figürlerimi çalışırken çoğunlukla JSON ve CSV melodilerinden ilham alıyorum. Kareografide uzun zamandır Mr. jQuery ile ilerliyorum. Ayrıca Node'un bana sağladığı içsel motivasyondan bahsetmeden geçemem. Her isteğimi bekletmeden ve hızla karşılıyor. Hepsi içimde harika bir karmanın oluşmasına neden olmakta. Sonuç, gülümseyen ve ritmime uymaya çalışan insanların ortaya çıkarttığı müthiş bir dans gösterisi..." diyor. 

Pek hayalimdeki gibi bir West-World partisi olmasa da sonuçta aşağıdaki çıktıya ulaşmak istediğimi ifade edebilirim esasında. Plotly.js kütüphanesi ile çok fazla çalışmışlığım yok. Projemizdeki belirli ihtiyaçlar nedeniyle javascript tabanlı bir grafik kütüphanesi araştırırken onunla karşılaştım. Özellikle basitliği, geniş grafik yelpazesi ve WebGL desteği dikkat çekici geldi. Ayrıca R, Python ve Matlab dilleriyle de kullanılabiliyor. Data Scientest rolündekilerin grafiksel raporlama ihtiyaçlarında bu oldukça kıymetli diye düşünüyorum. Nitekim veriyi tarayıcıda grafikselleştirmek birazdan göreceğiniz üzere gayet kolay.

Tabii öncesinde onu en yalın haliyle kullanabilmem gerekiyordu. Kafamda basit bir kurgu hazırladım. Node.js ve express'i kullanarak, üç sunucunun son yedi günlük talep karşılama değerlerini JSON formatında döndüren bir servis yazacaktım. HTTP Rest modelinde çalışmasını planladığım bu servisi tamamladıktan sonra ona talepte bulunup gelen çıktıyı plotly sayesinde ekrana çizen bir HTML sayfası geliştirecektim. İşe FunnyGraphics isimli bir klasör açıp gerekli ön hazırlıkları yaparak başladım.

npm init
npm install express --save

npm başlangıcını yapıp, express modülünü yükledikten sonra package.json dosyasının son hali aşağıdaki gibiydi.

{
  "name": "funnygraphics",
  "version": "1.0.0",
  "description": "simple plotly.js sample",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node app.js"
  },
  "author": "buraks",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4"
  }
}

Sonrasında node.js sunucusu olarak görev yapıp talepleri karşılayacak app.js dosyasını yazmaya başladım.

var express = require("express");
var app = express();
var path = require('path');

app.get("/", (req, res, next) => {
    res.sendFile(path.join(__dirname + '/index.html'));
});

app.get("/report", (req, res, next) => {

    var days = ['Day01', 'Day02', 'Day03', 'Day04', 'Day05', 'Day06', 'Day07'];
    var seri01 = {
        x: days,
        y: [5, 7, 9, 14, 12, 10, 9],
        name: 'dcist01',
        mode: 'lines+markers',
        type: 'scatter'        
    };
    var seri02 = {
        x: days,
        y: [5, 3, 8, 10, 12, 6, 3],
        mode: 'lines+markers',
        name: 'dcizm03',
        type: 'scatter'
    };
    var seri03 = {
        x: days,
        y: [0, 3, 5, 8, 8, 8, 7],
        mode: 'lines+markers',
        name: 'dclnd07',
        type: 'scatter'
    };
    var data = [seri01, seri02, seri03];
    res.json(data);
});

app.listen(6701, () => {
    console.log("Raporlama sunucusu aktif!");
});

express modülüne ait değişkenimiz 6701 nolu yerel porttan dinleme yapacak şekilde kullanılıyor. /report adresine gelen talepler için devreye giren fonksiyonumuzda aslında index.html içerisindeki plotly için önem arz edem bir JSON içeriği döndürülmekte. Burada gün bazlı olacak şekilde bir takım rastsal sayılar bulunduran üç farklı seri söz konusu. Her biri sembolik olarak bir sunucuyu belirtmekte. Ayrıca grafiklerin tipine ilişkin bir takım bilgiler de yolluyoruz. Burada bir kararsızlık yaşadığımı ifade edebilirim. Acaba servisten, plotly ile alakalı özellik bilgilerini göndermekle servisi ve görsel kütüphaneyi çok mu bağımlı hale getirdik? Peki sadece veriyi göndersek de bunun ayrıştırma ve gösterme kısmını HTML içerisine bıraksak nasıl olur du? Doğruyu söylemek gerekirse grafiğin ihtiyaç duyduğu veriyi aynı web site içerisinde çalıştığım için bu şekilde göndermek daha kolayıma geldi :| Servisi yazdıktan sonra küçük bir test yaptım. Önce

npm start

ile sunucuyu başlattım ve ardından http://localhost:6701/report adresine Postman'den HTTP Get talebi gönderdim. Sonuçlar başarılıydı.

Sunucuya göre kök adrese gelen talepler doğrudan index.html sayfasının istemciye gönderilmesi ile sonuçlanmakta. Grafiğin çizildiği asıl yer index.html dosyasındaki script bloğu. Onu da aşağıdaki gibi tasarladım. 

<head><script src="https://cdn.plot.ly/plotly-latest.min.js"></script><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script></head><body><div id="divStatistics"></div><script>

        $(document).ready(function () {
            $.ajax({
                url: '/report',
                type: "GET",
                success: function (result) {
                    var layout = {
                        xaxis: { autorange: true },
                        yaxis: { autorange: true },
                        legend: {
                            x: 0,
                            y:-0.5,
                            yref: 'paper',
                            font: {
                                family: 'Tahoma',
                                size: 16
                            }
                        },
                        title: 'Haftalık sunucu istatistikleri (Akfit Servis/Gün)'
                    };

                    Plotly.newPlot('divStatistics', result, layout);
                },
                error: function (error) {
                    console.log('error ${error}')
                }
            })
        });

    </script></body>

Plotly ve jQuery kütüphanelerini dokümanın başındaki script bloklarında referans olarak bildiriyoruz(Local dosya olarak da kullanabiliriz tabii) /report adresine HTTP Get talebi yapmak için tek yöntem jQuery değil. ES6'nın fetch fonksiyonunu, XmlHttpRequest nesnesini veya bir başka çözümü de değerlendirebiliriz. Lakin benim kolayıma gelen jQuery oldu diyebilirim. Ajax çağrısı ile gerçekleştirilen işlem başarılı ise success bloğundaki kod parçası devreye giriyor. Grafiği çizdiren fonksiyon Plotly.newPlot isimli metod. İlk parametre ile grafiğin çizileceği div elementini belirtiyoruz. İkinci parametre ile grafiğin kaynak veri serileri gönderiliyor. Son parametre ile de grafiğe ait bir takım özellikler yollanıyor(legend'ın yeri, grafiğin başlığı, x ve y eksenlerinin otomatik olarak boyutlandırılacağı vs) Aslında hepsi bu kadar ;)

Kütüphanenin kullanım alanı tabii ki çok daha geniş. Söz gelimi CSV içerikleri ile de kolayca çalışılabiliyor. Sadece veri serilerini doğru şekilde eşleştirmek gerekiyor. Aslında bu konuda şu adresteki veri kümesini kullanarak grafik örnekleri deneyebilirsiniz. Resmi kaynakta konu ile ilgili detaylı bilgiler mevcut. Bir bakmakta yarar var ancak başlangıç basit senaryolarda okuduğunuz örnek yeterli olacaktır. Böylece geldik West-World'deki çılgın bir partinin daha sonuna. Sabah ışıklarını çok şükür bugün de gördük. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örneğe GitHub adresinden ulaşabilirsiniz.

Json, Protobuf ve MessagePack Serileştirme Performansları

$
0
0

Merhaba Arkadaşlar,

Süper salyangoz Turbo'nun hikayesini bilir misiniz? Hani şu sırtında roket takılı olan Turbo'nun. Peki ya oyununu oynadınız mı? Ben epey süre önce S(h)arp ile oynamış ve oldukça eğlenmiştim. Tabii cevaplar kişiden kişiye değişir lakin ondan esinlenilen bir logo'nun günümüz .Net uygulamalarında performans ölçümü için kullanılan meşhur BenchmarkDotNet'e ait olduğu kesin diyebilirim. Aslında ciddi anlamda düşünürsek yazdığımız uygulamaların bütün olarak veya parça halinde çalışma zamanı performanslarını metrik olarak ölçümlemek pek de kolay olmayan konulardan birisidir.

Bazen fonksiyonelliklerin hızı öne çıkarken bazen de alansal büyüklükler değer kazanabilir. Ama başka kriterler de vardır. Eş zamanlı yüklenmelerin artması sonrası oluşan hatalı sonuçlanma değerleri, standart sapmalar bunlara örnek olarak verilebilir. Tabii ölçümlemeyi yaparken kullanılan teknikler de önemlidir. Çıktıların yorumlanması titizlikle yapılmalıdır. Çünkü hatalı yorumlamalar tercihleri olumsuz yönde etkileyebilir.

Peki ne oldu da bu konuya geldim dersiniz?

Geçtiğimiz günlerde West-World'ün başında otururken hız ve alansal büyüklük açısından ölçümlemem gereken bir konuyla karşılaştım. MessagePack isimli bir ikili(binary) serileştirme formatı. İlk kez karşılaştığım bir konuydu(cahilliğin bu kadarı) Bildiğimiz JSON içeriklerine uygulanabilen bir teknik olarak karşımıza çıkıyor aslında ama daha hızlı ve daha az yer kapladığı ifade ediliyor. Bir ikili serileşme söz konusu ve örneğin

{"fullName":"burak selim şenyurt","city":"istanbul","salar":1000}

şeklindeki bir JSON içeriği, MessagePack formatında

83 a8 66 75 6c 6c 4e 61 6d 65 b4 62 75 72 61 6b 20 73 65 6c 69 
6d 20 c5 9f 65 6e 79 75 72 74 a4 63 69 74 79 a8 69 73 74 
61 6e 62 75 6c a5 73 61 6c 61 72 cd 03 e8

gibi üretiliyor ve resmi siteye göre %18 kadar yer kazanımı sağlıyor(Bu örnek için tabii)

Bu arada MessagePack serileştirmesi binary formatta olmak zorunda da değil. İnsan gözüyle okunabilir bir formatta da dönüşebiliyor ve bu haliyle de daha az yer tuttuğu daha hızlı serileştiği belirtiliyor.

[["Burak Selim Şenyurt,"istanbul",1000]]

Performans açısından söylenenlerin doğruluğundan pek şüphem yok ama yine de ölçümlemek lazım. Malum JSON dışında Protobuf(Google'ın veri değiş tokuş formatı) isimli başka bir serileştirme formatı daha var ortada. Peki bu üç farklı serileştirme formatını performans açısından kıyaslamak için ne yapabiliriz? Benim ilk aklıma gelen senaryo generic bir Entity listesinin farklı boyutlardaki örneklerini bu üç formata göre serileştirmek oldu. Bu deneyi .Net Core tarafında uygulayabilir ve ölçümleme için meşhur BenchmarkDotnet kütüphanesinden yararlanabiliriz. Gelin birlikte basit bir örnek geliştirelim ve hem yeni serileştirme tekniklerini nasıl kullanacağımızı hem de performanslarını nasıl ölçümleyeceğimizi öğrenelim.

İlk olarak bir Console projesi oluşturarak işe başlayabiliriz. Sonrasında bize yardımcı olacak paketleri projeye eklemekte yarar var.

dotnet add package protobuf-net
dotnet add package MessagePack
dotnet add package Newtonsoft.Json
dotnet add package BenchmarkDotnet

Profobuf, MessagePack ve Json serileştirme işlerinde kullanacağımız paketlere ek olarak ölçümleme için BenchmarkDotNet paketini de yüklememiz gerekiyor. Deneysel amaçlı kullanacağımız sınıf aşağıdaki gibi tasarlanabilir.

using MessagePack;
using ProtoBuf;

[MessagePackObject, ProtoContract]
public class Book{
    [Key(0),ProtoMember(1)]
    public int Id { get; set; }
    [Key(1),ProtoMember(2)]
    public string Title { get; set; }
    [Key(2),ProtoMember(3)]
    public double Price { get; set; }
}

MessagePack ve Protobuf serileştirmeleri için bir kaç nitelik(attribute) ile zengineştirilmiş bir sınıf olduğunu görebilirsiniz. Şimdi Benchmark işlerini üstlenecek Serializers isimli bir başka sınıfı yazalım.

using System.Collections.Generic;
using System.IO;
using System.Threading;
using BenchmarkDotNet.Attributes;
using MessagePack;
using Newtonsoft.Json;
using ProtoBuf;

[MarkdownExporter, AsciiDocExporter, HtmlExporter, CsvExporter, RPlotExporter, CoreJob, MaxWarmupCount(8),MinIterationCount(3), MaxIterationCount(5)] // DryCoreJob
public class Serializers
{
    [Params(1,10,1000,10000,100000)]
    public int BookCount { get; set; }
    public List<Book> books = new List<Book>();
    private string rootPath="c:\\projects\\data\\";

    [GlobalSetup]
    public void LoadDataset()
    {
        for (int i = 1; i < BookCount; i++)
        {
            books.Add(new Book
            {
                Id = i,
                Title = $"Book_{i}",
                Price = 10
            });
        }
    }

    [Benchmark]
    public void ToJson()
    {
        var result = JsonConvert.SerializeObject(books);
        WriteToFile($"json_sample_{BookCount}.json",result);
    }
    [Benchmark]
    public void ToMessagePack()
    {
        var result = MessagePackSerializer.Serialize(books);
        WriteToFile($"mPack_sample_{BookCount}.bin",result);
    }

    [Benchmark]
    public void ToMessagePackJson()
    {
        var content = MessagePackSerializer.Serialize(books);
        var result = MessagePackSerializer.ToJson(content);
        WriteToFile($"mPack_Json_sample_{BookCount}.bin",result);
    }

    [Benchmark]
    public void ToProtobuf()
    {
        using (FileStream fs = new FileStream($"{rootPath}protobuf_sample_{BookCount}.bin", FileMode.Create))
        {
            Serializer.Serialize(fs, books);
        }
    }

    public void WriteToFile(string fileName,string content)
    {
        using (FileStream fs = new FileStream(Path.Combine(rootPath,fileName), FileMode.Create))
        {
            using (StreamWriter writer = new StreamWriter(fs))
            {
                writer.Write(content);
            }
        }
    }

    public void WriteToFile(string fileName,byte[] content)
    {
        using (FileStream fs = new FileStream(Path.Combine(rootPath,fileName), FileMode.Create))
        {
            using (StreamWriter writer = new StreamWriter(fs))
            {
                writer.Write(content);
            }
        }
    }
}

Teorimize göre 1,10,1000,10000 ve 100000 adetlik Book nesne koleksiyonları ile çalışılacak. Benchmark ortamına bu parametreleri ele alacağını söylemek için BookCount özelliğinin başına konan Params niteliğinden yararlanılıyor. Veri kümesini bu parametrelere göre her ölçüm için oluşturacak metod ise LoadDataset ki onun bu görevi üstlenmesi için GlobalSetup niteliği ile işaretlenmesi gerekiyor. Sınıf içerisinde ölçümlenmesini istediğimiz metodların her biri Benchmark niteliğine sahip olmalı. Ölçümlemek istediğimiz dört farklı senaryo var. Kitap listesini JSON, Protobuf, MessagePack ve insan gözüyle okunabilir MessagePack formatında serileştirip fiziki bir dosyaya çıkmak(Aslında serileştirme ve serileşen içeriği dosyaya yazma ölçümlenmesi gereken iki farklı operasyon gibi lakin burada tek bir atomikmiş gibi düşünebiliriz) Tabii bu dört senaryo her bir parametre için oluşan veri setlerinde deneyimlenecek.

Sınıfın başında belirtilen başka nitelikler de var. Standart ölçümlemede kullanılan aşırı yüklenme ve tekrar etme değerleri yüksek olduğu için MaxWarmupCount, MinIterationCount ve MaxIterationCount değerleri ile oynayabiliriz ki ben öyle yaptım. Tabii burada daha fazla ince ayar yapmak mümkün. Bunun için ürünü biraz daha tanımak ve pratiklerin neler olduğunu öğrenmek gerekiyor. CoreJob niteliği, .Net Core çalışma zamanına yönelik bir ölçümleme yapmak istediğimizi ifade ediyor. Önden gelen diğer nitelikler ise ölçüm sonuçlarının hangi formatlarda çıktı olarak sunulacağını ifade etmekte. Buna göre CSV, HTML, MD(markdown language. Alın ürününüz için direkt github'a performans raporu olarak koyun mesela), ASCII gibi formatlarda rapor üretilecek.

Serileştirme işlemlerinde ilgili tiplerin basit fonksiyonelliklerinden yararlanıyoruz. MessagePack serileştirmesi için MessagePackSerializer sınıfının statik Serialize metodu kullanılıyor. Buradan çıkan byte içeriği okunabilir formata çevirmek içinse ToJson fonksiyonundan yararlanılmakta. Protobuf serileştirme doğrudan bir Stream üzerine yapılabiliyor. Serializer sınıfının Serialize metodu bu işi üstlenmekte. Artık aşina olduğumuz JSON serileştirmesi içinse JsonConvert'ün static SerializeObject'i kullanılmakta. Dosya oluşturma ve yazma işlemlerinde ise genellikle çıktının türüne göre(byte array veya string olabilir) hareket etmekteyiz. 

Pek tabii Benchmark koşucusunun devreye girmesi için BenchmarkRunner sınıfının statik Run metodunu Main metodunda ele almamız gerekiyor.

using System;
using BenchmarkDotNet.Running;

namespace SerializationPerformance
{
    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Serializers>();
        }
    }
}

Ölçümlemeleri hesaplatmak için uygulamayı release modda çalıştırmamız lazım. 

dotnet run -c release

Bir süre bekledikten sonra terminalde aşağıdakine benzer sonuçlarla karşılaşmalısınız. Ben yaklaşık olarak dört dakika kadar bekledim.

Burada üç ölçüm kriteri görülüyor. Herbirisi mikrosaniye cinsinden hesaplanmış durumda. Çalışma süresini Mean sütununda görebiliriz. Hata üretme değerleri Error kısmında yer alırken standart sapma verileri de StdDev sütununda bulunmakta. Kullandığımız sistemin donanımı da etkili tabii ama aslında kitap sayısı açısından bakarsak oransal anlamda tüm platformlarda benzer sonuçlar çıkacak. Elde edilen verilere göre serileşecek veri kümesinin küçük boyutlu olması halinde tüm ölçüm değerlerinin birbirlerine yakın çıktığını söyleyebiliriz. Ancak 1000, 10000 ve 100000 için farklılıklar daha da belirginleşmeye başlıyor. Newtonsoft aracılığıyla yapılan JSON serileştirme süreleri çok uzun. Hata payı ve standart sapma değerleri de oldukça yüksek. Binary MessagePack en iyi sonuçları üretmiş görünüyor. Hatta protobuf serileştirme süresinden de iyi bir performans sergilemiş diyebiliriz. 

Oluşan dosya boyutlarına baktığımızda da MessagePack serileştirmesinin(binay olan) diğerlerine göre en az yer kaplayan içeriği oluşturduğunu söyleyebiliriz(Protobuf serileştirme sonucu ortaya çıkan boyutta fena sayılmaz aslında) JSON çıktısı ise neredeyse iki katı. Tabii çok küçük veri kümelerinde boyutsal farklılıklar çok fark etmiyor.

BenchmarkDotnet ayrıca rapor çıktılarını da bir klasör altında topluyor(Proje klasöründeki BenchmarkDotNet.Artifacts altında) Aşağıdaki görsellerde örneğimize istinaden üretilen içeriklerden bir kaçını görebilirsiniz.

Raporun web tabanlı örnek görünümü;

Hepsi bu kadar :) MessagePack bu ölçümleme değerleri göz önüne alındığında özellikle gerçek zamanlı iletişim yapılan uygulamalarda değer kazanıyor. Gerçek zamanlı uygulamalar denince akla ilk gelen sanıyorum ki SignalR ve WebSockets. Örneğin Asp.Net Core tarafında geliştirilen bir chat uygulamasında mesajlaşma kısmı için MessagePack serileştirmeden yararlanılabilir. Ya da bir merkezden gelecek stok verisini, broadcast yayınla sunucuya bağlı olan tüm istemcilerin grafiklerinde güncelleyecek bir sistemde kullanılabilir. Nitekim kullanıcı sayısının ve gerçek zamanlı veri değiş tokuşunun arttığı senaryolarda, hat üzerinde yol alacak paket içeriklerinin boyutsal olarak minimize edilmesi her zaman için hız avantajı sağlayacaktır. Bu tip bir örneği sonraki makalelerimizde ele almaya çalışacağım. Ne de olsa MessagePack kullanmanın avantaj sağlayacağını görmüş olduk. Burada size düşen bir görev de var. Örneğimizde sadece serileştirme senaryoları ölçümlendi. Peki ya ters serileştirmeden(deserialization) ne haber? Ellerinizden öper :) Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

HTTP/2 Server Push Nasıl Bir Şeydir?

$
0
0

Bir türlü giriş hikayesini bulamıyordum. Takip ettiğim referansta geçenleri West World üzerinde kurgulayıp sonuçları görmüş ve anladığım haliyle yazıya dökmüştüm. Ama o klasik girizgah kısmına koymam gereken hikayeyi bulamıyordum. Ne hikmetse ilgi çekici olması için her fırsatta üzerinde titizlikle durduğum bu kısmın ilham perisi tatile çıkmış aklıma tek bir düşünce dahi gelmemişti. Sonuçta istediğim girizgahı yapamadım... Yine de başlayalım.

HTTP/2 protokolü ile gelen önemli özelliklerden birisi de, tek bir TCP/IP bağlantısında sunucudan istemciye birden fazla kaynağın(Resource) gönderilebilmesidir. HTTP/2 esas itibariyle 2015 yılından beri hayatımızda. Sevgili Recep Duman arkadaşımın konu ile ilgili şurada bir yazısı da bulunuyor.

Tabii olaya HTTP 1.1 ile HTTP/2 arasındaki farklılıkları göz önüne alarak bakmak gerekiyor. HTTP/2, 1.1 versiyonu gibi metin tabanlı değil, binary çalışan bir protokol. Bu nedenle ağda daha hızlı. HTTP/2 tek bir bağlantı üzerinden aynı anda n sayıda talebi karşılayabilecek ve bu tek bağlantı içerisinde istemci talep etmese bile onun için gerekli kaynakları kendisine gönderebilecek şekilde tasarlanmış. Dolayısıyla istemci index.html sayfasını talep ettiği zaman açılan bağlantı içerisinde, istemcinin index.html için gerek duyacağı ne kadar kaynak varsa sunucudan gönderilebilir(push işlemi olarak ifade edebiliriz) Header'lar konusunda da HTTP/2 oldukça yenilikçi. HPACK header sıkıştırma tekniğini kullanıyor ve bu sayede header boyutlarının azaltılmasın olanak tanıyor. Görüldüğü üzere HTTP/2'nin 1.1 sürümüne göre önemli performans artıları var (2015ten beri hayatımızda olan HTTP/2 ye ait endüstüriyel tanımlara IETF'nin şuradaki sayfasından ulaşabilirsiniz) 

Günümüzde pek çok web sitesi HTTP/2 protokolüne destek veriyor ve bir kaynağa bağlı içerikleri istemcinin talep etmesini beklemeden proaktif hareketle karşı tarafa gönderiyor. Örneğin Medium'un ana sayfasına gidelim. Google Chrome'da F12 ile açılan developer sekmesine bakılırsa, Network trafiğini izleyen kısımda h2 lakaplı izlere rastlanır. Bu izler HTTP/2 protokolüne aittir ve ilgili kaynakların istemci talep etmeden Initiator(bu örnek kapsamında index sayfası olarak görünüyor) için gönderildiğini ifade etmektedir. 

Örneğin m2.css, main-base.bundle.js dosyaları HTTP/2, p.js HTTP 1.1 ve son olarak analytics.js SPDY ile gelmekte(SPeeDY diye okunuyor ve Google'ın web'i daha hızlı hale getirmek için üzerinde çalıştığı deneysel bir protokol olduğu belirtiliyor. Henüz detaylarını öğrenemedim) Tabii Medium gibi içerik açısından zengin bir sayfanın ağ trafiğini takip ederken tekil bağlantıya karşılık n kaynağın tek seferde gönderildiğini görmek zor. 

Peki biz kendi sunucularımızda HTTP/2 protokolünü ve Server Push özelliğini nasıl kullanabiliriz? Bunu node.js kullanarak gerçekleştirmek mümkün kaynaklardaki örnekler oldukça açıklayıcı. Bir benzerini yapmaya çalışalım. Örneğimizde ilk olarak HTTP 1.1 tabanlı standart bir sunucu kodunu ele alacağız. İkinci aşamada ise HTTP/2 tabanlı çalışan versiyona bakacağız. Ben referanstakine benzer olarak aşağıdaki gibi bir yapı hazırladım.

sample
--- images
------ sample_1.jpg
------ sample_2.jpg
------ ve diğer bir kaç imaj
--- scripts
------ jquery.js 
--- style
------ style.css
appv1.js
appv2.js
package.json
simpleCert.pem
simpleKey.pem

images, scripts ve style klasörü içerisinde yer alan içerikleri (javascript betikleri, medya ve css gibi materyaller) index.html'e gelen isteğe ait oturumda henüz istemci talep etmeden karşı tarafa gönderildiklerini görmeyi umut ediyorum. Her iki örnek içinde test amaçlı sertifikalara ihtiyaç var. Self-Signed sertifikaları West-World(Ubuntu olduğunu ezberldiniz artık) ortamında openssl ile aşağıdaki şekilde üretebildim. 2048 bit RSA baz alarak hareket ediyoruz.

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' -keyout simpleKey.pem -out simpleCert.pem

index.html içeriği çok önemli değil ancak beraberinde gitmesini beklediğimiz kaynakları taşıması gerekiyor. 

<!DOCTYPE html><html><head><title>HTTP2 Server Push</title><link rel="stylesheet" href="style/style.css"><script src="scripts/jquery.js"></script></head><body><h1>Simple HTTP/2 Server Push Sample</h1><p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit
        , sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "</p><table><tr><td><img src="/images/sample_5.jpg" alt="sample_5" /></td><td><img src="/images/sample_3.jpg" alt="sample_3" /></td></tr><tr><td><img src="/images/sample_2.jpg" alt="sample_2" /></td><td><img src="/images/sample_4.jpg" alt="sample_4" /></td></tr><tr><td colspan="2"><img src="/images/sample_1.jpg" alt="sample_1" /></td></tr></table></body></html>

HTML içeriğinde referans olunan bazı kaynaklar var. jQuery.js, style.css ve örnek jpg uzantılı resim dosyaları bu sayfa ile alakalı. Normal şartlarda index.html sayfasını talep ettiğimizde tarayıcı ile sunucu arasındaki ağ trafiğinde nasıl hareketlilikler olur görmek için appv1.js dosyasında aşağıdaki kodlar kullanılıyor.

const fs = require("fs");
const mime = require("mime");
const https = require("https");

const securityOptions = {
    key: fs.readFileSync("simpleKey.pem"),
    cert: fs.readFileSync("simpleCert.pem")
};

const handler = (req, res) => {
    console.log("[Request]", req.url);
    if (req.url === "/favicon.ico") {
        res.writeHead(200);
        res.end();
        return;
    }
    const fileName = req.url === "/" ? "index.html" : __dirname + req.url;
    fs.readFile(fileName, (err, data) => {
        if (err) {
            res.writeHead(503);
            res.end("File read error", fileName);
            return;
        }
        res.writeHead(200, { "Content-Type": mime.getType(fileName) });
        res.end(data);
    });
};
https.createServer(securityOptions, handler)
    .listen(5047, () => console.log("Server listening at 5047"));

https modülünü kullanarak 5047 portundan dinleme yapacak bir nesne oluşturuluyor. createServer metoduna verilen parametrelerde sertifika ve talepleri ele alacak değişken bildirimlerine yer veriliyor. Handler içerisinde favicon için boş bir response dönüyor ve gelen talebe göre statik dosyanın yollanması sağlanıyor. Örneğe göre dosya adı belirtilmediği durumda otomatik olarak index.html dosyasının işlenmesi sağlanıyor. Eğer olmayan bir kaynak talep edilirse, kibarca 503 hatası basılıyor. Eğer appv1.js dosyası komut satırından

node appv1.js

ile çalıştırılıp, https://localhost:5047/ adresine gidilirse aşağıdaki ekran görüntüsünde yer alan hareketlilikleri görmemiz muhtemel.

Dikkat edileceği üzere jpg, jquery.js ve style.css kaynakları HTTP 1.1 protokolü nezninde değerlendirilmiştir. Ancak daha da önemlisi orta kısımda yer alan renkli çizgilerdir. Burada her kaynak için istemciden sunucuya gelindiği görülebilir(Sondaki kaynakça listesinde çok daha alofortanfaneli örnekler var bakın derim) Bu durumun HTTP/2 örneğinde değişmesini bekliyoruz. Hiç vakit kaybetmeden appv2.js içeriğine geçelim.

const http2 = require("http2");
const fs = require("fs");
const mime = require("mime");

const securityOptions = {
  key: fs.readFileSync("simpleKey.pem"),
  cert: fs.readFileSync("simpleCert.pem")
};

const sendResource = (stream, fileName) => {
  const fd = fs.openSync(fileName, "r");
  const stat = fs.fstatSync(fd);
  const headers = {
    "content-length": stat.size,
    "last-modified": stat.mtime.toUTCString(),
    "content-type": mime.getType(fileName)
  };
  stream.respondWithFD(fd, headers);  
  stream.end();
};

const pushResource = (stream, path, fileName) => {
  stream.pushStream({ ":path": path }, (err, pushStream) => {
    if (err) {
      throw err;
    }
    console.log("[Pushing]", fileName);
    sendResource(pushStream, fileName);
  });
};

const handler = (req, res) => {
  console.log("[Request]", req.url);

  if (req.url === "/") {
    pushResource(res.stream, "style/style.css", "style.css");
    pushResource(res.stream, "scripts/jquery.js", "jquery.js");

    const images = fs.readdirSync(__dirname + "/images");
    for (let i = 0; i < images.length; i++) {
      const fileName = __dirname + "/images/" + images[i];
      const path = "images/" + images[i];
      pushResource(res.stream, path, fileName);
    }

    sendResource(res.stream, "index.html");
  } else {
    if (req.url === "/favicon.ico") {
      res.stream.respond({ ":status": 200 });
      res.stream.end();
      return;
    }
    const fileName = __dirname + req.url;
    sendResource(res.stream, fileName);
  }
};

http2.createSecureServer(securityOptions, handler)
  .listen(5048, () => {
    console.log("Server is listening on 5048");
  });

Kodlar bir önceki kod parçasına göre biraz daha karışık ancak temel ilkeleri oldukça basit. Bu kez http2 modülünü kullanarak 5048 portu üzerinden hizmet veren bir sunucu söz konusu. Sertifikalar securityOptions değişkeni ile verilirken talepler yine handler fonksiyonu üzerinden ele alınıyor. Burada css, js ve jpg kaynakları için çağırılan sendResource metoduna odaklanmamız lazım. Duruma göre tek bir dosya veya klasör içindeki tüm jpg dosyaları için çağırılan sendResource'a gelinmekte. sendResource istemci ve sunucu arasındaki veri akışında rol oynanan stream nesnesini kullanıyor. İlk etapta talep edilen dosya ile ilgili bilgileri alıyor. Boyut, son değişiklik zamanı ve içerik tipi. Sonrasında açılan stream kapatılıyor.

Şimdi,

node appv2.js

ile sunucu çalıştırılıp https://localhost:5048/index.html adresine gidilirse bu kez bir öncekinden farklı olarak tek bir ağ çizgisinin oluştuğu görülebilir.

Dikkat edileceği üzere tek bir çizgi var. Bir başka deyişle sunucuya yapılan index.html talebi sonrası açılan tekil bağlantı(connection) için, stream süresince sunucudan gönderilen diğer kaynaklar da söz konusu. Kabaca aşağıdaki gibi bir durum var diyebiliriz.

Pek tabii bu çalışma mantığını daha otomatize etmenin bir yolu var mıdır henüz bilmiyorum. Nitekim kaynakları fazla olan sayfalar için sunucu tarafındaki kod karmaşıklığını arttırmamak bence önemli. Diğer yandan bu senaryoyu .Net Core için nasıl hazırlayabiliriz bir bakmakta yarar var. Araştırmak istediğim konulardan birisi de bu. 

Internet üzerinde hareket eden içeriklerin kalitesi, boyutu ve çeşitliliği arttıkça daha hızlı protokollere ihtiyacamız olacak gibi görünüyor. Etkileyici görünen bir web sayfasının ağ tarafındaki hareketlilik çoğu zaman inanılmaz boyutlarda. HTTP/2 şu anda iyi bir çözüm olarak görünse de SPDY ile birlikte neler olacağını da göreceğiz. Nitekim ihtiyaç olunmuş ki Google bunun üzerinde çalışmalara başlamış. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örnek kodunu github'dan indirebilirsiniz
Kaynak 1
Kaynak 2
Kaynak 3
Kaynak 4(Asıl izlediğim kaynak)

gRPC Nedir, Nasıl Uygulanır?

$
0
0

Oturduğum masanın hemen sağındaki pencerede ufak bir aralık kalmış olmalı ki "Vuuuu..." diye öten rüzgarın sesini fark ettim bir anda. Aslında işime konstantre olmuştum ama o sesle birlikte olduğum yerden uzaklaşmıştım. Dışarısı soğuktu. Havan kapalı ve biraz da kasvetliydi. Yağmur damlalarının cama vuruşunu fark ettim sonrasında. Gözlerim uzaklara daldı ve rüzgarın sesi ile birikte Lise yıllarında buldum kendimi. Doğal gaz İstanbul'a henüz gelmiş ve bizimki gibi kimi sobalı ev kalorifer petekleriyle tanışmıştı. Geçmiş yılların ardından bu yeni konfor sayesinde sabahları üşüyerek uyanmıyorduk artık. Benim en büyük keyiflerimden birisi olan öğle sonrası uykularım içinse yeni mekanımı bulmuştum bile. Okuldan her geldiğimde önce karnımı doyuruyor sonra o köşeye kuruluyordum. Minderler camın altındaki peteğin yanına seriliyor, üste hafif bir battaniye alınıyor, zaten sessiz olan sokağın sükunetini bozmak isteyen yağmur damlalarının cama vuruşu dinlenerek derin bir uykuya dalınıyor. Bazen de o sessizliğe eski sokak kapısının altındaki süngerden kaçan "Vuuuu" sesi ortak olurdu...

Kısa süreli bu seyahatin ardından tekrar bilgisayarıma döndüm ve araştırdığım konu üzerinde ilerlemeye başladım. Son zamanlarda micro service mimarisinin uygulanması ile ilgili bir çok doküman okumaktayım. Şirket bünyesinde bu tip bir oluşuma gidilmesi de araştırmalarımda beni motive etmekte. Tabii uygulama pratiklerine, çeşitli desenlerin kullanılışına baktıkça bilmediğim bir çok şey görüyorum. Bugüne kadar özellikle microservisler seviyesinde uçlar arasındaki iletişimde hep REST/HTTP1 tabanlı haberleşildiğini düşünüyordum. Oysa ki HTTP/2 ile stream desteği veren, TCP soket haberleşmesini benimseyen, binary serileştirmeyi kullanan ve REST/HTTP1 e göre 2.5 kat daha hızlı olduğu söylenen gRPC isimli bir çatı da varmış(Gerçi .Net'in en başından beri var olan arkadaşlarım, TCP, Binary serileştirme denilince .Net Remoting konusunu ve onun WCF içerisindeki evrimini gayet iyi hatırlayacaklardır) Bende bunu görünce gRPC'nin ne olduğunu araştırmaya başladım.

İşte bu yazımızda Google'ın protobuf protokolü üzerine kurguladığı ve Remote Procedure Call modelinde hizmet sunan gRPC isimli çatısını inclemeye çalışacağız. Özellikle dağıtık sistemlerde taraflar arası haberleşmede TCP bazlı binary serileştirme ilkelerine dayanan bu protokol REST'in standart iletişim teknikleri yerine daha çok tercih edilmeye(önerilmeye) başlanmış görünüyor. Google'ın bu çatıyı geliştirmekteki temel amacının, binary serileştirmenin performans avantajını kullanıp Remote Procedure Call tekniğini microservice sistemlerde uygulayabilmek olduğun düşünüyorum. 

İşin temelleri oldukça basit aslında. RPC'ye göre istemciler, uzak sunucudaki metodları sanki kendi ortamlarının birer parçasıymış gibi çağırabilirler. Taraflar farklı makinelerde dolayısıyla farklı domain sınıflarında olabilirler. Bu nedenle dağıtık sistem kurgularında ideal bir uygulama senaryosu olarak ele alınabileceğini ifade edebiliriz. Aşağıdaki şekille konuyu özetlemek mümkün. Node.js ile geliştirilmiş örnek bir gRPC sunucusu ile protoBuf bazlı mesajlarla anlaşan farklı dillerde istemcier veya servisler. Aslında oldukça bilindik ve tanıdık bir senaryo. 

Peki bu çatıyı sahada nasıl kullanabiliriz?

Öncelikle her iki taraf için ortak olan bir Proto dosyasına ihtiyacımız var(.Net Remoting zamanlarındaki Marshall By Value günlerimiz geldi aklıma) Bu dosya aslında bir servis sözleşmesi ve kullanılacak tiplere ait bilgileri içerecek. Buradaki sözleşme sunucu tarafında uygulanırken, istemci tarafından sadece uzak fonksiyon çağrısı yapabilmek amacıyla ele alınıyor. Gelin basit bir örnek ile konuyu anlamaya çalışalım. Kurgumuzda klasör yapısını aşağıdaki gibi oluşturabiliriz. Aynı makine üzerinden test yapacağız ancak rahatlıkla dağıtık senaryoları deneyebilirsiniz.

client(Folder)
   index.js
   product.proto
server(Folder)
   index.js
   Product.js
   product.proto
package.json
node_modules(Folder)

Bu arada node.js tarafında gRPC kullanımını kolaylaştırabilmek için grpc modülünün npm ile sisteme yüklenmesi lazım. İstemcinin tipine göre tabii farklı bir paket kullanmak gerekebilir. Örneğin .Net Core tarafında Grpc.Core isimli nuget paketini kullanmak gerekiyor. Gerekli modülü kurduktan sonra proto uzantılı servis sözleşmesini hazırlayarak devam edebiliriz.

syntax = "proto3"; //Specify proto3 version.

package products; //benzersiz bir paket ismi

//Service. GRPC sunucusunun istemci tarafına sundugu servis sözlesmesi
service ProductService {
  rpc List (Empty) returns (ProductList);
  rpc Insert (Product) returns (Empty);
  rpc Get (ProductId) returns (Product);  
}

// Serviste kullanilan mesaj tipi
message Product {
  int32 id = 1;
  string name = 2;
  double listPrice = 3;
}

// Ornek bir liste
message ProductList {
  repeated Product Product = 1;
}

message ProductId {
  int32 id = 1;
}

message Empty {}

product.proto isimli dosya içerisinde ProductService isimli bir arayüz tanımlandığını görebilirsiniz. Bu arayüz ile üç operasyon sunuyoruz. Her biri Remote Procedure Call tipinden. Yani uzaktan tetiklenebilecek operasyon bildirimleri. List fonksiyonu bir parametre almıyor(bunu Empty isimli message tipi ile tanımladık) ve ProductList türünden sonuç döndürüyor. ProductList bir message tipi ve içinde tekrarlı sayıda Product mesajı içerebiliyor. Product mesajı id, name ve listPrice gibi özellikler içeren bir tip olarak ifade edilmekte. Kısaca bu dosya içeriği ile bir servisin operasyonları ile birlikte kullandığı tipleri tanımlayabiliyoruz. Web servislerine ait Service Description Language(WSDL) dokümanlarına benzetebiliriz. Senaryomuz gereği bu dosyanın hem sunucu hem de istemci tarafında olması lazım. Bu pek tabii servis içeriğinin güncellenmesi halinde istemci tarafının ne yapacağı sorusunu da akla getiriyor(Doğru bir cevap verebimek için gRPC ile ilgili vakaları incelemeye devam ediyorum)

Sunucu tarafında bir ürünü bulunduğu programlama ortamında ifade edebilmek için Product isimli entity tipimiz de var. Hani makaledeki örneği uyguladıktan sonra belki içerideki ürünleri MongoDB gibi bir ortama almak veya oradan çekmek isterseniz sizlere kolaylık sağlasın diye :) 

let product = class Product {
	constructor(id, name, listPrice) {
		this._id = id;
		this._name = name;
		this._listPrice = listPrice;
	}
	get ProductId() {
		return this._id;
	}
	get Name() {
		return this._name;
	}
	get ListPrice() {
		return this._listPrice;
	}
	set Id(value) {
		this._Id = value;
	}
	set Name(value) {
		this._name = value;
	}
	set ListPrice(value) {
		this._listPrice = value;
	}
}

module.exports = product;

Ürün numarası, ismi ve liste fiyatını tanımlayan bir node.js sınıfı söz konusu. Gelelim en baba kodlarımızdan birisine. Sunucu tarafındaki index.js'i aşağıdaki gibi geliştirebiliriz.

const grpc = require('grpc');
const proto = grpc.load('./product.proto');
const server = new grpc.Server();
const product = require('./Product');

function allProducts() {
	console.log('[Server]:List all product');

	var products = [
		{ id: 1009, name: "lego mind storm", listPrice: 1499 },
		{ id: 1010, name: "star wars bardak altığı", listPrice: 35 },
		{ id: 1011, name: "ışıldak 40w", listPrice: 85.50 },
		{ id: 1012, name: "A4 X 100 adet", listPrice: 5 }
	];
	return products;
}
function singleProduct(productId) {
	console.log('[Server]:Get single product');
	console.log('[Server]:Incoming product id ' + productId);

	var product = {
		id: 1009,
		name: "lego mind storm",
		listPrice: 55
	};
	return product;
}
function addProduct(call) {
	console.log('[Server]:Insert new product');

	let p = new product(
		call.request.id,
		call.request.name,
		call.request.listPrice,
	);
	console.log(p);
}
function list(call, callback) {
	callback(null, allProducts());
}
function single(call, callback) {
	callback(null, singleProduct(call.request.id));
}

function insert(call, callback) {
	callback(null, addProduct(call));
}

server.addService(proto.products.ProductService.service, {
	List: list,
	Insert: insert,
	Get: single
});

server.bind('0.0.0.0:7500', grpc.ServerCredentials.createInsecure());
server.start();
console.log('grpc server is live', '0.0.0.0:7500');

Proto dosyasını ortama yüklemek için grpc modülünün load fonksiyonundan yararlanıyoruz. Kodun en önemli kısımlarından birisi server nesnesinin addService metoduna ait içerikte yer alıyor. İlk parametre ile grpc sunucusunun hangi servis sözleşmesini kullanacağını belirtiyoruz. İkinci parametrede yer alan eşleştirmelere dikkat etmemiz lazım. Sol taraf servis sözleşmesindeki metod adları ile aynı olmalı. Sağ taraftaki atamalarla, uzak çağrının yapıldığı servis operasyonu için, sunucu tarafında hangi metodun tetikleneceğini belirtiyoruz. Örneğin List operasyonuna yapılan çağrı list isimli metod tarafından ele alınıyor.

grpc sunucusunu 0.0.0.0:7500 portu üzerinden herhangi bir güvenlik protoklü uygulatmadan yayına alıyoruz. Bu işlem için bind ve start metodlarını kullanmaktayız. Kodun üst kısımlarında yer alan allProducts, singleProduct ve addProduct gibi metodlar basit işlemler gerçekleştirmekteler. Ancak dikkate değer kısımları var. İstemciye içerik döndüren operasyonlar hep JSON formatında veri üretiyorlar. Diğer yandan istemciden gelen payload içeriğini yakalamak için call.request değişkenini kullanıyoruz(addProduct metodunu inceleyin) 

Sunucu tarafındaki işlerimiz şimdilik bu kadar. Artık istemci tarafını geliştirmeye başlayabilriz. Client klasöründe de product.proto dosyasının olması gerektiğini önceden belirtmiştik. Ayrıca node.js tabanlı geliştirilen istemcinin de grpc modülüne ihtiyacı olacak. Kod tarafını aşağıdaki gibi yazabiliriz.

const grpc = require('grpc');
const proto = grpc.load('./product.proto');
const client = new proto.products.ProductService('localhost:7500', grpc.credentials.createInsecure());

client.List({}, (error, response) => {
	if (!error) {
		console.log("Response : ", response)
	}
	else {
		console.log("Error:", error.message);
	}
});

client.get({ id: 1001 }, (error, response) => {
	if (!error) {
		console.log("Response : ", response)
	}
	else {
		console.log("Error:", error.message);
	}
});

client.Insert({ id: 1001, name: "Scrum Post-It Kağıdı", listPrice: 5 }, (error, response) => {
	if (!error) {
		console.log("Response : ", response)
	}
	else {
		console.log("Error:", error.message);
	}
});

Burada öncelikle proto dosyasını yüklüyor ve sonrasında client isimli ProductService nesnesini örnekliyoruz. Sunucu tarafını aynı makinede 7500 nolu port üzerinden yayınladığımız için burada da aynı adres bilgisini kullanmamız gerekiyor. Sonrasında servis sözleşmesi ile sunulan operasyonları tek tek deniyoruz. client üzerinden List, get ve Insert isimli metodlara çağrı yapmaktayız. Basit olması açısından herhangi bir hata varsa bunu gösteriyor yoksa sunucudan döndürülen mesaj içeriğini ekrana bastırıyoruz.Tabii giden veya gelen mesajların JSON babında ele alınması gerektiğini bir kez daha hatırlatalım.

Yaptıklarımızı test etmek için önce sunucu tarafını, ardından istemci tarafını çalıştırmamız yeterli. Ben aşağıdaki ekran görüntüsünde yer alan sonuçları elde ettim.

Görüldüğü gibi istemci uygulama, uzak sunucu üzerinden sunulan servis operasyonlarını başarılı bir şekilde kullandı. Benim gRPC ile ilgili olarak ilk öğrendiklerim kısac bunlar. Aslında Microservice mimarisinde REST alternatifi bir iletişim tekniği olduğunu görmek benim için güzel oldu. Diğer yandan gRPC'nin yüksek performanslı, TCP soket haberleşmesi üzerinden binary serileştirme kullanan ve uygulanması kolay bir çatı olduğunu söyleyebiliriz. Performans konusunda bende Google'ın yalancısıyım dolayısıyla gerçek ölçümlemelere bakmakta yarar var. Bununla birlikte özellikle stream kullanımı örneğini de bir bakın derim ki burada güzel bir anlatımı var. gRPC'yi farklı dillerde uygulamak isterseniz google'ın bu adresteki pratiklerine bakabilirsiniz. Böylece geldik bir yazımızın daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Github'dan Örnek Kodları Alabilirsiniz

Apollo Server ile Bir GraphQL Sunucusu Yazmak

$
0
0

Merhaba Arkadaşlar,

James A. Lovell, John L. Swigert, ve Fred W. Haise. Bu isimleri düşününce belki de çoğumuzun aklına bir şey gelmiyordur. Peki ya, Amerikalı veya İngiliz oldukları düşünülen bu şahısların yerine şu isimleri söylersek. Tom Hanks, Bill Paxton ve Kevin Bacon. Hımm...Sanırım birilerinin zihninde bir şeyler canlandı. Evet, evet...Bunlar film yıldızları değil mi? Üçü bir arada hangi filmde oynamışlardı acaba? Hala anımsayamadıysanız işte bir ipucu daha. "Houston we've got a problem." Şimdi anımsadınız mı?

Başta söylediğimiz isimler, 11 Nisan 1970 tarihinde uzaya fırlatılan Apollo 13 mürettebatına ait. Apollo programınındaki bu uçuşun amacı aya insan götürmekti. Ne yazık ki mekik, uçuşunun ikinci gününde meydana gelen bir kaza sonrası acil olarak dünyaya dönüş yapmak zorunda kalmıştı. Yazılanlardan okuduğumuz ve filmden gördüğümüz kadarıyla astronotlar çok zor koşullara göğüs gererek mucizevi bir dönüş hikayesinin altına imza atmışlardı. Tesadüf bu ya, geçenlerde filmini tekrardan izlediğim gün Apollo isimli Framework GraphQL arayüzü ile cumartesi gecesi çalışmalarımı yürütüyordum. Örneği orada tamamladıktan uzun süre sonra sağını solunu biraz derleyip bloğuma kendime not olarak düşeyim dedim. Haydi başlayalım.

Bildiğiniz üzere Facebook menşeili GraphQL son yılların yükselen trendlerinden. Çalışmakta olduğum şirket dahil bir çok yerde mikro servisler söz konusu olduğunda REST API mi GraphQL mi sıklıkla karşılaştırılıyor. Ben henüz emekle aşamasında olduğum için GraphQL'i anlamaya çalıştığım bir dönemdeyim. Basit örnekler dışında bu seferki amacımsa stand alone olarak çalışabilen bir GraphQL sunucusu yazmak. Bu amaçla tavsiye edilen Apollo Server API arayüzünü kullanmaya karar verdim. Bu arada Apollo uzun zamandır kabul görmüş bir Framework olarak Thoughtworks teknoloji radarının merceğinde yer alıyor. 2018 Nisan ayında Trial, 2019 Mayısında ise Adopt kategorsinde değerlendiriliyor.

Apollo Server program arayüzü web, mobile gibi istemciler için GraphQL servisi sunan bir ürün olarak düşünülebilir. Otomatik API doküman desteği sunar ve herhangibir veri kaynağını kullanabilir. Yani bir veri tabanını veya bir mikroservisi ya da bir REST APIyi, GraphQL hizmeti verecek şekilde istemcilere açabilir. Tek başına sunucu gibi çalıştırılabilmektedir. Pek tabii Heroku gibi cloud ortamlar üzerinde Serverless modda da kullanılabilmekte. Takip ettiğim Apollo Server dokümanlarındaki çalışma modelini bende aşağıdaki gibi resmetmeye çalıştım. 

İstemciler kendilerine uygun Apollo Client paketlerini kullanarak sunucu tarafı ile kolayca haberleşebilirler. Benim bu çalışmadaki amacım stand alone çalışan bir Apollo sunucusu yazmak ve arka tarafta bir veri tabanını kullanarak (muhtemelen PostgreSQL) veriyi GraphQL üzerinden istemcilere açmak.

Başlangıç

Proje iskeletini aşağıdaki gibi oluşturabilir ve Node.js tarafı için gerekli paketleri yükleyebiliriz (Örneği her zaman olduğu gibi WestWorld-Ubuntu 18.04, 64bit- üzerinde denemekteyim)

mkdir project-server
cd project-server
npm init
npm install apollo-server graphql
touch server.js

Kodları node.js tarafında geliştireceğiz. Bu nedenle npm init ile işe başlıyoruz. Ardından gerekli paketleri yükleyip, server.js isimli dosyamızı oluşturuyoruz. Ben örnek kodları Visual Studio Code ile geliştiriyorum.

Birinci Sürüm (Dizi Kullanılan)

İlk sürümde veriyi bir diziyle beslemeye çalışacağız. İlk amacımız Apollo Server'ı ayağa kaldırabilmek. Kodları dikkatlice okumanızı öneririm. Gerekli açıklamalarla desteklemeye çalıştım.

const { ApolloServer, gql } = require('apollo-server');

const tasks = []; // İlk denemeler için veri kümesini dummy array olarak tasarlayabiliriz

/*
    Tip tanımlamalarını yaptığımız bu kısım iki önemli parçadan oluşuyor.

    Queries: istemciye sunduğumuz sorgu modelleri
    Schema : veri modelini belirlediğimiz parçalar (Task gibi)

    Task isimli bir veri modelimiz var.
    Ayrıca sundacağımız sorgu modellerini de Query tipinde belirtiyoruz.
    AllTasks tüm task içeriklerini geri döndürürken, TaskById ile Id bazlı olarak
    tek bir Task dönecek.

    Veri manipülasyonu için InputTask modeli tanımlanmış durumda.
    Bu modeli Create, Update, Delete işlemlerine ait Mutation tanımında kullanıyoruz.

    Int değişkeninin Task tipinin tanımlanması dışındaki yerlerde ! ile yazıldığına dikkat edelim.
*/
const typeDefs = gql`
    # Entity modelimiz olarak düşünebiliriz
    type Task{
        id:Int
        title:String
        description:String
        size:String
    }

    # Silme operasyonundan deneme mahiyetinde farklı bir tip döndük
    type DeleteResult{
        DeletedId:Int,
        Result:String
    }
    # Sunduğumuz sorgular
    type Query{
        AllTasks:[Task]
        TaskById(id:Int!): Task
    }
    # Insert ve Update operasyonlarında kullanacağımzı model
    input TaskInput {
        id:Int!
        title:String
        description:String
        size:String
    }
    # CUD operasyonlarına ait tanımlamalar
    # Burada kullanılan parametre adları, Mutation tarafında da aynen kullanılmalıdır
    type Mutation{
        Insert(payload:TaskInput) : Task
        Update(payload:TaskInput):Task
        Delete(id:Int!):DeleteResult
    }
`;

/*
    Asıl verini ele alındığı çözücü tanımı olarak düşünülebilir.
    CRUD operasyonlarının temel işleyişinin yer aldığı, iş kurallarının da
    konulabildiği kısımdır.
    İki alt parçadan oluşmakta. Select tarzı sorgular için bir kısım (Query)
    ve CUD operasyonları için diğer bir kısım (Mutation)
    Şimdilik Array kullanıyoruz ama bunu MongoDB'ye çekmek isterim.
*/
const resolvers = {
    Query: {
        AllTasks: () => tasks,
        TaskById: (root, { id }) => {
            return tasks.filter(t => {
                return t.id === id;
            })[0];
        }
    },
    Mutation: {
        Insert: (root, { payload }) => { // Yeni veri ekleme operasyonu
            //console.log(payload);
            tasks.push(payload);
            return payload;
        },
        Update: (root, { payload }) => { // Güncelleme operasyonu
            // Gelen payload içindeki id değerini kullanarak dizi indisini bul
            var index = tasks.findIndex(t => t.id === payload.id);
            // alanları gelen içerikle güncelle
            tasks[index].title = payload.title;
            tasks[index].description = payload.description;
            tasks[index].size = payload.size;
            // güncel task bilgisini geri döndür
            return tasks[index];
        },
        Delete: (root, { id }) => { // id üzerinde silme işlemi operasyonu
            tasks.splice(tasks.findIndex(t => t.id === id), 1);
            return { DeletedId: id, Result: "Silme işlemi başarılı" };
        }
    }
};

/*
    ApolloServer nesnesini örnekliyoruz.
    Bunu yaparken schema, query ve resolver bilgierini de veriyoruz.
    Ardından listen metodunu kullanarak sunucuyu etkinleştiriyoruz.
    Varsayılan olarak 4000 numaralı port üzerinde yayın yapar.
*/
const houston = new ApolloServer({ typeDefs, resolvers });
houston.listen({ port: 4444 }).then(({ url }) => {
    console.log(`Houston ${url} kanalı üzerinden dinlemede`);
});

Bu ilk sürümü ve sonradan yazacağımız yeni versiyonu çalıştırmak için terminalden

npm run serve

koutunu yazmamız yeterli (Tahmin edileceği gibi package.json içerisine eklediğimiz bir run komutu var) Bunun sonucu olarak http://localhost:4444 adresine gidebilir ve otomatik olarak açılan Playground arabirimi üzerinden denemelerimizi yapabiliriz. Array kullanan bu ilk sürümün çalışma zamanına ait örnek sorguları ile ekran görüntülerini aşağıdaki bulabilirsiniz.

# Yeni bir görev eklemek
mutation {
  Insert(
    payload: {
      id: 1
      title: "Günde 50 mekik"
      description: "Kocaman göbüşün oldu. Her gün düzenli olarak mekik çekmelisin."
      size: "S"
    }
  ) {
    id
    title
    description
    size
  }
}

# Tüm görevlerin listesi
{
  AllTasks {
    title
    description
    size
    id
  }
}

# Var olan bir satırı güncelleme
mutation {
  Update(
    payload: {
      id: 1
      title: "100 Mekik"
      description: "Göbek eritme operasyonu"
      size: "M"
    }
  ) {
    id
    title
    description
    size
  }
}

# Id değerine göre görev silinmesi
mutation {
  Delete(id: 1) {
    DeletedId
    Result
  }
}

İlk sürüm önceden de belirttiğimiz üzere Apollo Server'ı basitçe işin içerisine katmak ve nasıl çalıştığını anlamak içindi. Array içeriği kalıcı bir ortamda saklanmadığından uygulama sonlandırıldığında tüm görev listesi kaybolacaktır. Kalıcı bir depolama alanı için farklı bir alternatif düşünmeliyiz. CRUD operasyonlarını başka bir servise atayabilir veya bir veri tabanı kullanabiliriz.

İkinci Sürüm (PostgreSQL Kullanılan)

İkinci sürümde veriyi kalıcı olarak saklamak için PostgreSQL kullanıyoruz. Örneği çalışırken WestWorld'de PostgreSQL'in olmadığını fark ettim. PostgreSQL kurulumları ile ilgili olarak aşağıdaki terminal komutlarını işletmem yeterliydi.

sudo apt-get install postgresql

sudo su - postgres
psql

\l
\du
\conninfo

CREATE ROLE Scott WITH LOGIN PASSWORD 'Tiger';
ALTER ROLE Scott CREATEDB;

\q

İlk komut ile postgresql'i Linux ortamına kuruyoruz. Kurma işlemi sonrası ikinci ve üçüncü komutları kullanarak varsayılan kullanıcı bilgisi ile Postgresql ortmına giriyoruz. \l ile var olan veri tabanlarının listesini, \du ile kullanıcıları (rolleri ile birlikte), \conninfo ile de hangi veri tabanına hangi kullanıcı ile hangi porttan bağlandığımıza dair bilgileri elde ediyoruz. CREATE ROLE ile başlayan satırda Scott isimli yeni bir rol tanımladık. Sonrasında takip eden komutla bu role veri tabanı oluşturma yetkisi verdik. \q ile o an aktif olan oturumu kapatıyoruz. Şimdi scott rolünü kullanarak örnek veri tabanımızı ve tablolarını oluşturmaya çalışacağız.

psql -d postgres -U scott
CREATE DATABASE ThoughtWorld;

\list
\c thoughtworld

CREATE TABLE tasks (
  ID SERIAL PRIMARY KEY,
  title VARCHAR(50),
  description VARCHAR(250),
  size VARCHAR(2)
);

INSERT INTO tasks (title,description,size) VALUES ('Birinci Görev','Her sabah saat 06:00da kalk','L');

SELECT * FROM tasks;

İlk komut ile scott rolünde oturum açıyoruz. Sonrasında ThoughtWorld isimli bir veri tabanı oluşturuyoruz. \list ile var olan veri tabanlarına bakıyoruz ve \c komutuyla ThoughtWorld'e bağlanıyoruz. Ardından tasks isimli bir tablo oluşturuyor ve içerisine deneme amaçlı bir satır ekliyoruz. Son olarak basit bir Select işlemi icra etmekteyiz.

Artık PostgreSQL tarafı hazır. Şimdi veri tabanını Apollo suncusunda kullanmaya başlayabiliriz. Ancak öncesinde gerekli npm modülünü yüklemek lazım (Bir önceki senaryo ile kodların karışmaması adına pg-server.js isimli yeni bir dosya üzerinde çalışmaya karar verdim)

sudo npm install pg

pg-server.js isimli kod dosyamızın içeriği ise aşağıdaki gibi.

const { ApolloServer, gql } = require('apollo-server');

//  postgresql kullanabilmek için gerekli modülü ekledik
const db = require('pg').Pool;
// connection string tanımı gibi düşünebiliriz.
const mngr = new db({
    user: 'scott',
    host: 'localhost',
    database: 'thoughtworld',
    password: 'Tiger',
    port: 5432
});

const typeDefs = gql`
    type Task{
        id:Int
        title:String
        description:String
        size:String
    }
    type DeleteResult{
        DeletedId:Int,
        Result:String
    }
    type Query{
        AllTasks:[Task]
        TaskById(id:Int!): Task
    }
    input TaskInput {
        title:String
        description:String
        size:String
    }
    input UpdateInput {
        id:Int!
        title:String
        description:String
        size:String
    }
    type Mutation{
        Insert(payload:TaskInput) : Task
        Update(payload:UpdateInput):Task
        Delete(id:Int!):DeleteResult
    }
`;

/*
    sorguyu göndermek için query metodundan yararlanıyoruz.
    geriye rows nesnesini döndürmekteyiz.

    query metodunun dönüşünü resolvers'tan çıkartabilmek için senkronize etmem gerekti.
    Bu nedenle async-await desenini kullandım.
*/
const resolvers = {
    Query: {
        AllTasks: async () => {
            const res = await mngr.query("SELECT * FROM tasks ORDER BY ID;")
            // console.log(res);
            if (res)
                return res.rows;
        },
        TaskById: async (root, { id }) => {
            const res = await mngr.query("SELECT * FROM tasks WHERE id=$1", [id]);
            // console.log(res);
            return res.rows[0];
        }
    },
    Mutation: {
        /*
        Yeni bir görevi eklemek için kullandığımız operasyonu da 
        async await bünyesinde değerlendirdim.
        Sorguya dikkat edilecek olursa, Insert parametrelerini 
        $1, $2 benzeri placeholder'lar ile gönderiyoruz.
        Sorgu sonucu elde edilen id değerini payload'a yükleyip geri döndürüyoruz.

        Bazı sorgularda RETURNING * kullandım. 
        Bunu yapmadığım zaman sonuç değişkenleri boş verilerle dönüyordu.
        Sebebini öğrenene ve alternatif bir yol bulana kadar bu şekilde ele alacağım.
            
        */
        Insert: async (root, { payload }) => {
            const res = await mngr.query('INSERT INTO tasks (title,description,size) VALUES ($1,$2,$3) RETURNING *',
                [payload.title, payload.description, payload.size]);
            id = res.rows[0].id;
            payload.id = id;
            return payload;
        },
        Update: async (root, { payload }) => {
            const res = await mngr.query('UPDATE tasks SET title=$1,description=$2,size=$3 WHERE ID=$4 RETURNING *', [payload.title, payload.description, payload.size, payload.id]);
            // console.log(res);
            return res.rows[0];

        },
        Delete: async (root, { id }) => {
            const res = await mngr.query('DELETE FROM tasks WHERE ID=$1', [id]);
            // console.log(res);
            return { DeletedId: id, Result: "Silme işlemi başarılı" };
        }
    }
};

const houston = new ApolloServer({ typeDefs, resolvers });
houston.listen({ port: 4445 }).then(({ url }) => {
    console.log(`Houston ${url} kanalı üzerinden dinlemede`);
});

Birinci senaryodaki GraphQL sorguları benzer şekilde ikinci senaryo için de denenebilir. Bu arada Visual Studio Code üzerinde PostgreSQL tarafını kolayca görüntülemek için Chris Kolkman'nın PostgreSQL eklentisini kullandım.

İstemci Tarafı

throw new ToDoForYouException("Bu uygulamayı size bırakıyorum. Çünkü sonraki konuya geçmek istiyorum :|");

TODO (Eklenebilecek şeyler)

Pek tabii yapılabilecek bir kaç şey daha var. Benim aklıma gelenler şöyle;

  • Dependency Injection kurgusu ile Apollo Server'ın istenen veri sağlayıcısına enjekte edilmesi için uğraşılabilinir. Örneğin tasks tablosunu SQlite ile tutmak ya da bir NoSQL sistemi üzerinden kullanmak isteyebiliriz.
  • apollo-server-express modülünü kullanarak HTTPS desteğinin nasıl sağlanabileceğine bakabiliriz. Nitekim production ortamlarında HTTPS olmazsa olmazlardan.

Ben Neler Öğrendim?

Saturday-Night-Works çalışmalarım kapsamında denediğim bu örnekte de bir sürü şey öğrendim. Sanırım aşağıdaki maddeler halinde listeleyebilirim.

  • GraphQL'de tip tanımlaması (type definitions) ve çözücülerin (resolvers) ne anlama geldiğini ve neler barındırdığını
  • Apollo Server paketinin kullanımını
  • Insert, Update, Delete gibi operasyonların Mutation kavramı olarak ele alındığını
  • CRUD operasyonlarına ait iş mekaniklerinin resolvers içindeki Query ve Mutation segmentlerinde yürütüldüğünü
  • Veri kaynağı olarak farklı ortamların kullanılabileceğini (micro service, NoSQL, RDBMS, File System, REST API)
  • Int? ile Int tiplerinin yerine göre doğru kullanılmaları gerektiğini (bir kaç çalışma zamanı hatası sonrası fark ettim)
  • Ubuntu platformuna PostgreSQL'in kurulmasını, yeni rol oluşturulmasını, rol altında veri tabanı ve tablo açılmasını
  • Apollo metodlarında pg'nin query çağrısına ait sonuçları yakalayabilmek için async-await kullanılması gerektiğini
  • Visual Studio Code tarafında PostgreSQL için eklenti kullanımını

Böylece geldik bir maceramızın daha sonuna. Bu yazımızda Linux platformunda PostgreSQL kullanan stand alone çalışan bir Apollo GraphQL sunucusu yazmayı denedik. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örnek kodlarına Saturday-Night-Works 38 Numaradan erişebilirsiniz.


Python Loglamada ELK Kullanımı

$
0
0

Merhaba Arkadaşlar,

Laptop ekranına kitlenmiş error seviyesindeki logları inceliyordum. HTTP 400 en sevdiğim(yazar burada kendisiyle dalga geçiyor) ama çözmekte en çok zorlandıklarımdan birisiydi. Neyse ki monitör ettiğimiz araç bize güzel detaylar veriyordu. Pek tabii iş yoğunluğundan olsa gerek, üzerinde geliştirme yaptığımız ürünlerin bazı kurgularını inceleme fırsatı bulamıyordum. Lakin zaman zaman takım arkadaşlarımla veya mimari ekiptekilerle yaptığım konuşmalarda havada uçuşan, daha önceden duyduğum ama derinlemesine bilgi sahibi olmadığım kelimelere rastlıyordum.

ELK kısaltmasını ilk telafüz ettiklerinde zihnimde hiçbir şey canlanmamıştı. Bilmediğim ve öğrenmem gereken bir konu daha ortaya çıkmıştı işte. 2019 yılına girerken aldığım karar doğrultusunda öncelikle sağdan soldan adını duyduğum enstrümanları anlamaya çalışacak ve bunları saturday-night-works altında kabataslak notlarla deneyimleyecektim. Sanırım bu yıl için aldığım en doğru karardı diyebilirim. Şimdi bunun nimetlerini toplamak üzere bazı çalışmaları bloğumda kendime not olarak düşüyorum. Sırada ELK kelimesinin derin anlamını öğrenmek var. Tabii onu Kanada'nın simgelerinden biri olan 800 kiloluk koca bir geyik türü olarak düşünmüyoruz. En azından şimdilik...

ELK...Yani Elasticsearch, Logstash ve Kibana üçlüsü. Mikroservislerde log stratejisi olarak sıklıkla kullanılıyorlar. Tabii bazı ürünlerde çeşitli performans sıkıntıları nedeniyle tercih edilmediklerine dair yazılara da rastlamadım değil. Ancak şirketimizde de kullanılan bir düzenek olduğu için onu anlamamın en iyi yolu denemekten geçiyordu. Aslında düzenek son derece basit. İzlemek istediğimiz uygulamalara ait log bilgileri logstash tarafından dinlenip JSON formatına dönüştürülüyor ve Elasticsearch'e basılıyor. Elasticsearch'e alınan log kayıtları da Kibana arayüzü ile izleniyor.

Benim amacım ELK üçlüsünü WestWorld'de(Ubuntu 18.04, 64bit) deneyimlemek ve loglama işini yapan uygulama tarafında basit bir Python kodunu kullanmak. WestWorld'ün uzun denemeler sonrası bozulan ekosistemini daha da dağıtmak istemediğimden Elasticsearch ve Kibana tarafı için Docker Container'larını kullanacağım. Kabaca aşağıdaki gibi bir senaryonun söz konusu olduğunu ifade edebilirim.

Örnek Uygulama Kodları

Aşağıdaki içeriğe sahip ve izlemek için log üreten çok basit bir Python kod dosyamız var. Dosyanın başındaki import bildirimlerinden de görüleceği üzere logging, time ve random isimli modüller kullanılıyor. Built-in logging modülünden yararlanılarak appLogs isimli dosyaya appende modda log atılacağı belirtiliyor. Logun mesaj ve tarih formatları da basicConfig metodunun son parametreleri ile tanımlanıyor.

# Python tarafında logging sistemi built-in olarak gelmektedir.
import logging
import time
import random

logging.basicConfig(filename="appLogs.txt",
                    filemode='a',
                    format='%(asctime)s %(levelname)s-%(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')
logging.warning('Sistem açılışında tutarsızlık')

for i in range(0, 10):  # 10 tane log atıyoruz
    # zamanı 0 ile 4 arası rastgele sürelerde duraksatıp log attırıyoruz
    d = random.randint(0, 4)
    time.sleep(d)
    if d == 3:
        logging.exception('Fatal error oluştu')
    else:
        logging.warning('Sistemde yavaşlık var...')

logging.critical('Sistem kapatılamıyor')

Kurulumlar 

Şimdi bu kodun logları için gerekli ELK düzeneğini kuracağız. Gerekenleri aşağıdaki gibi maddeleştirmeye çalıştım.

Elasticsearch ve Kibana Tarafı

Elasticsearch'ün Docker kurulumu ve başlatılması için,

docker pull docker.elastic.co/elasticsearch/elasticsearch:6.6.0
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.6.0

Kibana'ya ait Docker imajı için,

docker pull docker.elastic.co/kibana/kibana:6.6.0
sudo docker run --net=host -e "ELASTICSEARCH_URL=http://localhost:9200" docker.elastic.co/kibana/kibana:6.6.0

terminal komutlarını çalıştırmak yeterli. Ben örneği denediğim dönemde stabil olan versiyonları kullandım ancak siz kendi denemelerinizi yaparken konuları google'layıp doğru sürümleri kullanmaya çalışın. Bu arada Elasticsearch ve Kibana container'ları çalıştıktan sonra aşağıdaki adreslere gidip aktif hale gelip gelmediklerini kontrol etmekte yarar var.

http://localhost:9200/ -> Elasticsearch
http://localhost:5601/status -> Kibana

Elastichsearch çalışır durumda.

Monitoring aracımız olan Kibana'da öyle.

Logstash Tarafı

Logstash tarafı için öncelikle şu adresten ilgili içeriği indirip kurmak gerekiyor (Docker imajı yerine neden bu yolu tercih ettim şu anda hatırlamıyorum) Bundan sonra python uygulamamızın ürettiği logları takip etmesi için aşağıdaki içeriğe sahip bir konfigurasyon dosyasına ihtiyacımız var. Dosyayı etc/logstash/conf.d altına oluşturuyoruz. Bu klasör içerisindeki conf uzantılı dosyalar logstash servisi tarafından takip edilmekte. Böylece logstash hangi logları takip edeceğini bilecek ve onları Kibana tarafına gönderecek.

Bu ve benzeri konfigurasyon dosyalarının logstash servisi tarafından otomatik olarak ele alınabilmesi için etc/logstash/conf.d klasörü altında konuşlandırılmaları önemli. Tabii Ubuntu için geçerli bir durum olduğunu ifade edelim.

logstash-python.conf

input{
 file{
 path => "/home/burakselyum/Development/saturday-night-works/No 20 - Python Logging with ELK/appLogs.txt"
 start_position => "beginning"
 }
}
filter
{
 grok{
 match => {"message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:log-level}-%{GREEDYDATA:message}"}
 }
    date {
    match => ["timestamp", "ISO8601"]
  }
}
output{
 elasticsearch{
 hosts => ["localhost:9200"]
 index => "index_name"}
stdout{codec => rubydebug}
}

Dosyanın üç ana kısımdan oluştuğunu söyleyebiliriz. Log dosya kaynağına ait bilgilerin olduğu input, dosya içinden bilginin nasıl alınacağına dair filter ve dönüşüm sonrası içeriğin nereye basılacağının belirtildiği output. path özelliğinin değeri logstash'in izleyeceği dosyayı ifade etmektedir. grok elementinin içeriği de önemlidir. Nitekim text dosyasına atılan standart log mesajlarını nasıl yakalayacağına dair bir desen tanımlamaktadır. Kısacası Grok filtreleme aracı ile text dosyaları gibi hedeflere atılan unstructured log bilgilerini parse etmenin oldukça kolaylaştığını ifade edebiliriz. Sistem logları, Apache logları, SQL logları vb bir çok enstrümanın loglama yapısı buradaki desenlere uygundur zaten. output kısmında dikkat edileceği üzere Elasticsearch'ün host bilgisi yer alıyor. 

Çalışma Zamanı

Tabii düzeneğin işlerliğini görebilmek adına en az bir kereliğine de olsa main.py dosyasını çalıştırmamız lazım. Bunu aşağıdaki terminal komutu ile yapabiliriz.

python3 main.py

Bu arada logstash servisinin aktif olduğundan emin olmak gerekiyor ki yazılan log'lar takip edilsin. Eğer çalışmıyorsa Logstash servisini başlatmak için terminalden

service logstash service

komutunu yürütmek yeterli.

Kibana Monitoring

logstash etkinleştirildikten sonra Kibana'ya gidip yeni bir index oluşturabiliriz. index_name* ve @timestamp field'ını seçerek ilerlediğimizde python uygulaması tarafından üretilen logların yakalandığını görürüz.

Visualize kısmını kurcalayarak çeşitli tipte grafikler hazırlayıp Dashboard'u etkili bir monitoring aracı haline dönüştürmemiz de mümkün.

Docker Tarafı

Testler sonrası Docker tarafında ihtiyaç duyabileceğimiz komutlar da olabilir. Söz gelimi Container'ların listesini görmek ve durdurmak için aşağıdaki komutlardan yararlanabiliriz ki çalışma sırasında benim çok işime yaradılar (Container ID'ler farklılık gösterecektir)

sudo docker ps -a
sudo docker stop 3402e6aaced3

Ben Neler Öğrendim

Cumartesi gecesi çalışmalarının 20nci bölümünü de bu şekilde tamamlamıştım. Bu macerada da öğrendiklerim oldu. 

  • ELK üçlemesinin nasıl bir çözüm sunduğunu
  • Mikro servis dünyasında nasıl kurgulanabileceklerini
  • Ubuntu platformunda Docker imajlarından nasıl yararlanılabileceğini
  • Python kodundan logging paketini kullanarak nasıl log atılabileceğini
  • Elastichsearch ve Kibana'nın docker imajları ile çalışmayı
  • logstash config dosyasında ne gibi tanımlamalara yer verildiğini

Böylece geldik bir maceramızın daha sonuna. Sizde merak ettiklerinizi öğrenmek için kendinize vakit ayırın. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

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.

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.

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.

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.

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.


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.

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.

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

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.

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 351 articles
Browse latest View live