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

Asp.Net Core Routing Mekanizmasını Kavramak

$
0
0

Merhaba Arkadaşlar,

Güzel otomobilleri hepimiz severiz. Özellikle spor olanlarını. Benim favori araçlarımdan birisi ise Audi RS8. 2017 model Türkiye satış fiyatı 430 bin Avro civarındaydı. Gerçekten çok yüksek bir rakam. Ne oldu da birden onunla yollarımız kesişti diye düşünebilirsiniz. Bir deneme sürüşüne çıktım demek isterdim ama... Aslında olay West-World üzerinde Asp.Net Core routing mekanizmasını incelerken meydana geldi. Bir şekilde dile benden ne dilersen tadındaki URL path'in çalışma zamanına Audi RS8 yazmış bulundum. Web sunucusunun bana verdiği cevapsa oldukça hoştu. "Oldu bil!" Onunla aramızda nasıl böyle bir muhabbet gerçekleşti merak ediyor olmalısınız. Gelin Asp.Net Core routing mekanizmasını yakından incelemeye çalışalım. 

Asp.Net dünyasında MVC zamanlarından beri kritik bir yere sahip olan talep yönlendirme mekanizması .Net Core için de etkili bir biçimde kullanılmakta. Farklı yöntemlerle web sunucusuna gelen taleplerin değerlendirilmesi mümkün. Bu senaryoların hali hazırdaki versiyonlarını zaten Web API ve MVC şablonlarında ele alıyoruz. Ancak mekanizmayı tanımak adında bir Console projesinden ayağa kaldırılacak Host örneğinde ne gibi operasyonları kullanabilirize bakmakta yarar var. Dilerseniz hiç vakit kaybetdemen bu varyasyonlardan bir kısmını basit örneklerle ele alalım.

İlk olarak bir Console uygulaması oluşturacağız. Ortamımız her zaman ki gibi Ubuntu ve kodlama için Visual Studio Code kullanıyoruz. Ancak aynı örnekleri MacOS'da ya da Windows'ta da yazıp çalıştırabilirsiniz. Denedim, oluyor. Büyüksün .Net Core!

dotnet new console -o RouterSamples

Yönlendirme, Kestrel sunucusunu ayağa kaldırma, HTML çıktıları üretme gibi operasyonlar için Asp.Net Core'un temel kütüphanelerini projeye eklememiz lazım. Tek tek uğraşabiliriz de ama ben Microsoft.AspNetCore.All paketini ekleyerek ilerlemeyi tercih ettim.

dotnet add package Microsoft.AspNetCore.All
dotnet restore

MapGet Örnekleri

İlk kod parçasında path bilgisine özel olarak gelen HTTP Get taleplerini nasıl ele alabileceğimize bakacağız. Programımıza aşağıdaki kod parçalarını ekleyerek devam edelim.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

namespace RouterSamples
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WebHostBuilder()
            .UseKestrel()
            .UseUrls("http://localhost:4001")
            .UseStartup<Booster>()
            .Build();

            host.Run();
        }
    }

    class Booster
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRouting();
        }

        public void Configure(IApplicationBuilder app)
        {
            var rootBuilder = new RouteBuilder(app);

            rootBuilder.MapGet("", (context) =>
            {
                context.Response.Headers.Add("Content-Type", "text/html; charset=utf-8");
                return context.Response.WriteAsync($"<h1><p style='color:orange'>Hoşgeldin Sahip</p></h1><i>Bugün nasılsın?</i>");
            }
            );

            rootBuilder.MapGet("green/mile", (context) =>
            {
                var routeData = context.GetRouteData();
                context.Response.Headers.Add("Content-Type", "text/html; charset=utf-8");
                return context.Response.WriteAsync($"Vayyy <b>Gizli yolu</b> buldun!<br/>Tebrikler.");
            }
            );

            rootBuilder.MapGet("{*urlPath}", (context) =>
            {
                var routeData = context.GetRouteData();
                return context.Response.WriteAsync($"Path bilgisi : {string.Join(",", routeData.Values)}");
            }
            );

            app.UseRouter(rootBuilder.Build());
        }
    }
}

Aslında web, web api veya mvc projesi açsak da benzer kurgu ile karşılaşacağız. Sonuçta belli bir adrese gelen HTTP taleplerini dinleyip bunlara karşılık cevap verecek olan bir web sunucusu yazıyoruz. Dolayısıyla işin başlangıç noktası WebHostBuilder sınıfı. Fluent yapısı sayesinde bir metod zinciri ile belirli özelliklerini etkinleştiriyoruz. Kestrel web motorunun kullanılacağını, localhost:4001 adresinden dinlemede kalınacağını, başlangıç ayarları için Booster sınıfına bakılacağını vs...En nihayetinde de Build ve Run çağrıları ile sunucunun ayağa kaldırılması. Gayet sade, yalın, anlaşılır. Çok sevdiğim bir yapı.

Tabii bizim odak noktamız daha çok Booster sınıfının içeriği. ConfigureServices metodunda  route tanımlamaları ile ilgilenecek servisi devreye alıyoruz. Configure fonkisyonunda ise yazımıza konu olan route mekanizmalarının ilk üç örneği bulunuyor. MapGet operasyonunun ilk kullanımında doğrudan http://localhost:4001 talebine karşılık vermekteyiz. İçerik tipinin HTML olacağını Header bilgisine eklerken takip eden WriteAsync çağrısında da örnek bir içerik basıyoruz. Bir sonraki MapGet kullanımında ise http://localhost:4001/green/mile adresine gelen talebi ele almaktayız. Bu sefer bir öncekinden farklı olarak ilk parametrede path bilgisi verildiğine dikkat edelim. Yine bir HTML içeriği basıyoruz. Son çağrıda ise * karakterinin kullanıldığı görülmekte. Yani ilk iki path bilgisinden farklı bir adresle talep gelirse ne yapılacağı ele alınıyor. Örneğin http://localhost:4001/nowhere adresine ait talep bu fonksiyonla karşılanacak.

MapGet fonksiyonunun ilk parametresi path bilgisini kullanırken ikinci parametre RequestDelegate tipinden bir temsilci. Bu temsilcinin aldığı parametreden yararlanarak Request ve Response nesnelerine müdahale etmemiz mümkün. Header bilgisine ilaveler yapmak/okumak, url parametrelerini değerlendirmek, içeriği değiştirmek bunlara örnek olarak verilebilir. Temel olarak Request, Response bloklarını HTTP Path bazında değerlendirdiğimizi ifade edebiliriz. 

Gelelim çalışma zamanı sonuçlarına. Eğer 4001 nolu porta doğrudan gidersek aşağıdaki sonucu elde ederiz.

Eğer green/mile path bilgisini kullanırsak da aşağıdaki sonuçla karşılaşırız.

Bu iki path dışında farklı bir path ile gelinirse, {*urlPath} bildirimi nedeniyle aşağıdakine benzer içeriklerle karşılaşırız. Sadece gelen path bilgisini ekrana bastırdığımıza dikkat edelim. Pekala ana sayfaya yönlendirme de yapabilir ya da HTTP 404 NotFound mesajı döndürebilirdik. Bu ikisini deneyin derim.

MapGet çağrılarında varsayılan değerleri ele almakta mümkün. Aynı kodun {*urlPath} operasyonunu yorum satırı haline getirip aşağıdaki ilaveyi yaptığımızı düşünelim.

rootBuilder.MapGet("whatyouwant/{wanted=1 Bitcoin please}", (context) =>
{
	var values = context.GetRouteData().Values;
	context.Response.Headers.Add("Content-Type", "text/html; charset=utf-8");
	return context.Response.WriteAsync($"İstediğin şey bu.<h2>{values["wanted"]}</h2>OLDU BİL :)");
});

Bu kez http://localhost:4001/whatyouwant/something benzeri talepleri karşılıyoruz. Dikkat edilmesi gereken husus {} içeriği. Burada wanted isimli bir değişken tanımladık. Aslında bu değişken içeriği GetRouteData().Values ile elde edilen listede yer alıyor. Bu nedenle HTML çıktısını üretirken ["wanted"] şeklinde erişerek kullanıcının path'e yazdığı değişken bilgisini yakalayabiliyoruz. = sonrası yapılan 1 Bitcoin please ataması ise varsayılan değer oluyor. Buna göre aşağıdaki iki farklı kullanım da geçerli. İlkinde kullanıcının path içerisine koyduğu örnek bir wanted değişkeni var.

Eğer parametre girmessek de aşağıdaki sonuçla karşılaşırız.

Varsayılan Handler'ı Kurcalamak

Şimdi ikinci örneğimize geçelim. Bu kez varsayılan HTTP Handler davranışına müdahale edeceğiz. Örnek olarak /products/books path'ine gelen taleplere karşılık çeşitli kitap bilgileri içeren bir JSON içeriği basacağız(Alın size REST bazlı Data Service yolu) Bunun dışındaki talepler içinde sıradan bir HTML sayfası göstereceğiz. İlk senaryo bir nevi Web API simülasyonu gibi olacak. Gelin vakit kaybetmeden kodlarımızı yazalım.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System;

namespace RouterSamples
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WebHostBuilder()
            .UseKestrel()
            .UseUrls("http://localhost:4001")
            .UseStartup<BoosterV2>()
            .Build();

            host.Run();
        }
    }

    class BoosterV2
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRouting();
        }

        public void Configure(IApplicationBuilder app)
        {
            var handler = new RouteHandler(context =>
            {
                var routeValues = context.GetRouteData().Values;
                var path = context.Request.Path;
                if (path == "/products/books")
                {
                    context.Response.Headers.Add("Content-Type", "application/json");
                    var books = File.ReadAllText("books.json");
                    return context.Response.WriteAsync(books);
                }

                context.Response.Headers.Add("Content-Type", "text/html;charset=utf-8");
                return context.Response.WriteAsync(
                    $@" 
                    <html><body><h2>Selam Patron! Bugün nasılsın?</h2>
					{DateTime.Now.ToString()}<ul><li><a href='/products/books'>Senin için bir kaç kitabım var. Haydi tıkla.</a></li><li><a href='https://github.com/buraksenyurt'>Bu ve diğer .Net Core örneklerine bakmak istersen Git!</a></li></ul>                     </body></html>
                    ");
            });
            app.UseRouter(handler);
        }
    }
}

BoosterV2 sınıfına ait Configure metoduna odaklanalım. Talep edilen path bilgisini ve değerleri aldıktan sonra bir kıyaslama yapılıyor. Eğer talep /products/books şeklinde gelmişse şuradaki github adresinden temin ettiğimörnek kitapları içeren books.json dosyasını istemciye gönderiyoruz. Tabii Content-Type değerini de application/json şeklinde set etmeyi ihmal etmiyoruz. Eğer farklı herhangibir talep gelirse de bir HTML şablonu yolluyoruz. Burada iki link yer almakta. Tüm bu yönetim operasyonu RouteHandler temsilcisi ile gerçekleştirilmekte. Onu devreye almak içinse, UseRouter metoduna parametre olarak geçmemiz gerekiyor. İşte çalışma zamanı çıktıları.

Varsayılan sayfamız.

ve kitaplarımız.

URL Dizilimini Kodla İnşa Etmek

Gelelim bu yazımızda ele alacağımız son örneğe. Bu kez URL dizilimini kod tarafında oluşturmaya çalışacağız. BoosterV3 sınıfının kodlarını programa aşağıdaki gibi entegre edebiliriz.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System.Linq;
using System;
using Microsoft.AspNetCore.Routing.Template;

namespace RouterSamples
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WebHostBuilder()
            .UseKestrel()
            .UseUrls("http://localhost:4001")
            .UseStartup<BoosterV3>()
            .Build();

            host.Run();
        }
    }

    class BoosterV3
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRouting();
        }

        public void Configure(IApplicationBuilder app)
        {
            var apiSegment = new TemplateSegment();
            apiSegment.Parts.Add(TemplatePart.CreateLiteral("api"));

            var serviceNameSegment = new TemplateSegment();
            serviceNameSegment.Parts.Add(
                TemplatePart.CreateParameter("serviceName",
                    isCatchAll: false,
                    isOptional: true,
                    defaultValue: null,
                    inlineConstraints: new InlineConstraint[] { })
            );

            var segments = new TemplateSegment[] {
                apiSegment,
                serviceNameSegment
            };

            var routeTemplate = new RouteTemplate("default", segments.ToList());
            var templateMatcher = new TemplateMatcher(routeTemplate, new RouteValueDictionary());

            app.Use(async (context, next) =>
            {
                context.Response.Headers.Add("Content-type", "text/html");
                var requestPath = context.Request.Path;
                var routeData = new RouteValueDictionary();
                var isMatch = templateMatcher.TryMatch(requestPath, routeData);
                await context.Response.WriteAsync($"Request Path is <i>{requestPath}</i><br/>Match state is <b>{isMatch}</b><br/>Requested service name is {routeData["serviceName"]}");
                await next.Invoke();
            });

            app.Run(async context =>
            {
                await context.Response.WriteAsync("");
            });
        }
    }
}

Configure metodu içerisinde bu kez farklı şeyler söz konusu. İlk olarak apiSegment ve serviceNameSegment isimli iki TemplateSegment örneği oluşturuluyor. İlki Literal tipindeyken ikincisi parametre türünden. Yani http://localhost:4001/api/collateral gibi bir path için api ifadesinin Literal olduğunu, collateral parçasının ise değişken türde parametre olduğunu ifade edebiliriz. serviceName isimli bu parametre'den sonra başka bir path içeriğinin geçerli olmayacağını isCatchAll'a atanan false değeri ile belirtiyoruz(true atayarak takip edecek path bildirimlerini uzatabilirsiniz) Ayrıca api ifadesinden sonra böyle bir değişken gelmek zorunda değil(isOptional=true nedeniyle) Varsayılan bir değeri de bulunmuyor.

Tanımlanan bu iki segmentin ardışıl olarak işe yaraması için bir dizide konuşlandırılması da gerekiyor. segments değişkeni burada devreye girmekte. Talebin karşılandığı yer Use fonksiyonu. Aslında UseMvc metodundan tanıdık gelmiş olabilir. Generic Func temisilcilerini ve Task tipini kullanan bu fonksiyon awaitable operasyonlar içerebilir. Bu nedenle istemciye cevaplar gönderilirken  ve Middleware'deki bir sonraki bloğa geçilirken await anahtar kelimesi kullanılmakta. İçeride TemplateMatcher nesne örneği kullanılarak talep olarak gelen adresin geçerli bir segment bileşimi olup olmadığına bakılıyor. Dolayısıyla burada gelen bilginin istenen şablona uygunluğuna göre bir içerik üretimi sağlanabilir. Biz örneğimizde sadece talebin şablona uygun olup olmadığına bakıyoruz. Çalışma zamanı sonuçları aşağıdaki gibi olacaktır.

http://localhost:4001/api adresine yapılan çağrı sonrası

Dikkat edileceği üzere karşılaştırma true dönmüştür. http://localhost:4001/api/wather gibi bir çağrı da geçerlidir. Nitekim belirtilen şablona uygundur. Ki bu sefer serviceName değişkeni de yakalanabilmiştir.

Ama tabii şablona uymayan bir path bilgisi için eşleşme false değer dönecektir. Söz gelimi http://localhost:4001/api/weather/v2/soap11 veya http://localhost:4001/rest/collateral için...

Bu yazımızda Asp.Net Core tarafındaki Routing mekanizmasının farklı kullanımlarını incelemeye çalıştık. Elbette daha fazlası vardır diye düşünüyorum. Şimdilik öğrenebildiklerim bunlar. Siz örnekleri geliştirmeye çalışarak ilerleyebilirsiniz. Özellikle ikinci örnek koddaki belli kategorideki ürünler mantığını ele alarak bir rest servisinin yazılmasında MapGet operasyonlarını ele alabilirsiniz. Gelen talebe göre çalışma zamanında dinamik web sayfası içeriklerini üretecek bir varsayılan handler da geliştirebilirsiniz. Hatta bir fotoğraf web sunucusu geliştirmeyi deneyebilirsiniz. Basılacak Content-Type bilgisini değiştirebildiğimize göre bu da mümkün. Elinizin altında tüm imkanlar mevcut. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.


http://www.buraksenyurt.com/post/core-web-api-icin-planlanmis-gorevlerCore Web API için Planlanmış Görevler

$
0
0

Merhaba Arkadaşlar,

Chuck Norris. Sanıyorum hayatımın bir bölümü onun televizyonda arka arkaya yayınlanan filmleri ile geçmiştir. Asıl adı Carlo Ray Norris'tir ve Chuck ismi 1958de O, hava kuvvetlerindeyken takma ad olarak ortaya çıkmıştır. Dövüş sanatları ustası olan Chuck'ın harika bir web sitesi var. Hayran kitlesi oldukça geniş. 1940 doğumlu olan film yıldızını Google aramalarında daha çok "Chuck Norris Facts" ile biliyoruz.

Hatta onun hayatına dair şakalar, olaylar, sözler o kadar popüler hale gelmiş ki, International Chuck Norris Database isimli gönüllülük esasına göre geliştirilmiş bir hizmet bile var. Üstelik http://api.icndb.com/jokes/random adresine gittiğinizde rastgele bir fıkrasını veya şakasını çekebileceğiniz JSON formatlı bir REST servisi de bulunuyor. İşin aslı .Net Core tarafında Hosted Service kavramını araştırırken örnek olarak kullanılan ve günün özlü sözünü sunan örnek bir REST servisten Chuck Norris REST API hizmetine kadar geldiğimi belirtmek isterim. Üstelik onu basit bir örnekte kullanmayı da başardım. West-World'ün yeni konusu Hosted Service.

Ağırlıklı olarak masaüstü uygulamalarından aşina olduğumuz arka plan işleri(Background Worker Process diyelim) pek çok alanda karşımıza çıkıyor. Bu arka plan işlerini o an çalışmakta olan uygulamanın ana Thread'inden bağımsız işleyen iş birimleri olarak düşünebiliriz. .Net dünyasında uzun zamandır paralel zamanlı çalışmalar için kullanılan Task odaklı bir çatı da mevcut. Bu tanımlamalar bir araya getirildiğinde belirli periyotlarda çalışan, çeşitli iş kurallarını işleten görev odaklı fonksiyonlar ortaya çıkıyor. .Net Core dünyasına baktığımızda ise, özellikle WebHost veya Host tarafı için kullanılan Hosted Service isimli bir kavram mevcut ve bahsettiğimiz planlı işler(Scheduled Jobs) ile yakından ilişkili.

Özellikle MicroService odaklı çözümlerde, servislerin yaşamı boyunca belli bir plana/takvime göre çalışan arka plan işleri için Hosted Service enstrümanının kullanılabileceği ifade ediliyor.

Kimi senaryolarda bir Web uygulamasının veya Web API hizmetinin çalıştığı süre boyunca arka planda işletilmesini istediğimiz planlanmış görevlere ihtiyacımız olabilir. Örneğin içeride biriken log'ların belirli periyotlarda Apache Kafka gibi bir kuyruk sistemine aktarılması, servis üzerinde işletilecek istemci talepleri sonrası veritabanında oluşan değişikliklerin aralıklarla dinlenip çeşitli görevlerin işletilmesi, ön belleğin zaman planlamasına göre temizlenmesi ve başka bir çok senaryo burada göz önüne alınabilir. Kısacası WebHost(bu makale özelinde) ayağa kalktıktan sonra yaşamı sonlanıncaya kadar geçen sürede belirli bir takvimlendirmeye göre çalıştırılmasını istediğimiz arka plan görevleri söz konusu ise Hosted Service'leri kullanabiliriz.

.Net Core 2.0 ile birlikte Hosted Service'lerin kolay bir şekilde uygulanabilmesini sağlamak amacıyla IHostedService(Microsoft.Extensions.Hosting isim alanında yer alıyor) isimli bir arayüz gelmiş. Hatta .Net Core 2.1 ile birlikte bu arayüzden türetilmiş ve implementasyonu da içeren BackgroundService isimli abstract bir sınıfta söz konusu. Bu sınıfın temel amacı CancellationToken mekanizmasının yönetiminin kolaylaştırılması(Kontrol edin. Adı değişmiş olabilir. West-World üzerinde halen .Net Core 2.0 var olduğundan okuduğum bloglardaki gibi açık kaynak olarak sunulan bir IHostedService implementasyonunu kullanmacağım)

IHostedService arayüzü iki operasyon tanımlamakta. StartAsync ve StopAsync isimli fonksiyonlar CancellationToken türünden parametre alıyorlar. StartAsync operasyonu ana uygulama(WebHost veya Host türevli olabilir) başlatıldığında devreye girerken tam tersine StopAsync uygulama kapanırken işletilmekte. IHostedService uyarlaması tipler Middleware tarafında Dependency Injection mekanizması kullanılarak çalışma zamanına monte edilebiliyorlar. Singleton tipinde n sayıda Hosted Service'in çalışma zamanına eklenmesi mümkün. Bir başka deyişle uygulamanın yaşam döngüsüne istediğimiz kadar planlanmış işi birer servis olarak ekleyebiliriz. Dilerseniz hiç vakit kaybetmeden örnek bir uygulama ile konuyu anlamaya çalışalım. Boş bir Web API projesi şu an için işimizi görecek. 

dotnet new webapi -o HowToHostedService

Sonrasında projemize HostedService isimli aşağıdaki sınıfı ekleyelim.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

public abstract class HostedService : IHostedService, IDisposable
{
    private Task currentTask;
    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken cToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        currentTask = ExecuteAsync(cancellationTokenSource.Token);

        if (currentTask.IsCompleted)
            return currentTask;

        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (currentTask == null)
            return;

        try
        {
            cancellationTokenSource.Cancel();
        }
        finally
        {
            await Task.WhenAny(currentTask, Task.Delay(Timeout.Infinite,cancellationToken));
        }
    }
    public virtual void Dispose()
    {
        cancellationTokenSource.Cancel();
    }
}

Dikkat edileceği üzere HostedService sınıf IHostedService ve IDisposable arayüzlerini(Interface) uygulamakta. Temel görevi Hosted Service'ler ile ilişkilendirilecek olan Task'ların iptal edilme mekanizmalarının kolayca yönetilebilmesi. Dispose edilebilir bir nesne olarak tanımlandığına da dikkat edelim. Ayrıca, HostedService abstract bir sınıf. Dolayısıyla kendisini örnekleyemeyiz. Bununla birlikte kendisini uygulayan sınıfların mutlaka ezmesi gereken ExecuteAsync isimli bir metod tanımı da sunuyor. Bu metodu alt tiplerde ezerken planlanmış görevin yapacağı çalışma için kodlamamız yeterli. Parametre olarak gelen CancellationTokenSource örneği ise StartAsync üzerinden devrediliyor. StartAsync metodu alt tipin uyguladığı ExecuteAsync operasyonunu çağırıp tamamlanıp tamamlanmadığına bakıyor. StopAsync operasynonu ise iptal işleminin yönetimini gerçekleştirmekte. Yukarıda da bahsettiğim gibi bu sınıf .Net Core 2.1 ile birlikte hazır olarak gelmesi beklenen bir tip(İçinde kocaman gülümseme olan şu makaleye göz gezdirebilirsiniz) 

Artık planlanmış görevleri içerecek örnek sınıfların yazılmasına başlanabilir. Ben sadece iki sınıfı sisteme dahil edeceğim. Her ikisi de belirli aralıklarla Console ekranına bir şeyler yazacaklar. Amacımız sadece Hosted Service mekanizmasının Web API tarafındaki tesisatının nasıl kurulması gerektiğini öğrenmek olduğundan bu basit yaklaşım yeterli olacaktır(Nitekim söz konusu arka plan servisleri gerçek hayat örneklerinde gerçek iş modellerini baz alarak kurgulanmalılar) RequestCollectorService ve ChuckFactService isimli arka plan servis sınıflarımızı aşağıdaki gibi geliştirebiliriz. 

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class ChuckFactService
: HostedService
{
    HttpClient restClient;
    string icndbUrl="http://api.icndb.com/jokes/random";
    public ChuckFactService()
    {
        restClient=new HttpClient();
    }
    protected override async Task ExecuteAsync(CancellationToken cToken)
    {
        while (!cToken.IsCancellationRequested)
        {   
            var response = await restClient.GetAsync(icndbUrl, cToken);
            if (response.IsSuccessStatusCode)
            {
                var fact = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"{DateTime.Now.ToString()}\n{fact}");
            }

            await Task.Delay(TimeSpan.FromSeconds(10), cToken);
        }
    }
}

Ezilen ExecuteAsync metodunda icndb adresine bir talepte bulunup, talep sonucu HTTP 200 ise elde edilen sonucu ekrana bastırıyoruz. REST talebini göndermek için HttpClient tipinden yararlanmaktayız. Bu sınıfın awaitable GetAsync fonkisyonunu kullanıyoruz. Fonkisyonda dikkat edileceği üzere ilgili Task için iptal talebi olup olmadığının sürekli olarak kontrol edildiği bir while döngüsü bulunuyor. Ayrıca söz konusu görevin 10 saniyede bir işletilmesini Task tipinin Delay metodu ile sağlamaktayız. Bir başka deyişle tekrarlı görevlerin zamanlamalarını bu teknikle ayarlayabiliriz. 

using System;
using System.Threading;
using System.Threading.Tasks;

public class RequestCollectorService
: HostedService
{
    protected override async Task ExecuteAsync(CancellationToken cToken)
    {
        while (!cToken.IsCancellationRequested)
        {
            Console.WriteLine($"{DateTime.Now.ToString()} Çalışma zamanı taleplerini topluyorum.");
            await Task.Delay(TimeSpan.FromSeconds(30), cToken);
        }
    }
}

RequestCollectorService sınıfında ise sadece ekrana bir mesaj bastırıyoruz. Çalışmasını 30 saniyede bir gerçekleştiren bir görevlendirme söz konusu. Tanımladığımız bu görev servislerini Host uygulamaya enjekte etmek için, Startup sınıfındaki ConfigureServices metodunu aşağıdaki gibi güncellememiz yeterli.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddSingleton<IHostedService,ChuckFactService>();
    services.AddSingleton<IHostedService,RequestCollectorService>();
}

Bu değişiklikle, AddSingleton metodunun generic versiyonunu kullanarak IHostedService uyarlamasını gerçekleştiren ChuckFactService ve RequestCollectorService sınıflarının arka plan hizmetlerine eklenmesini sağladık. Artık Web uygulaması çalışmaya başladığında bu sınıflar otomatik olarak devreye alınacak ve üzerlerindeki görevler belirlenen sürelerinde işletilecekler. Uygulamamızı çalıştırdıktan sonrasına ait örnek bir ekran görüntüsü aşağıdaki gibidir.

10 saniyede bir Chuck Norris'e ait servise bir çağrı ve 30 saniyede bir ortam verilerini toplama işlemi gerçekleşmektedir. Bu sırada Web API servisinin normal hizmetini sürdürdüğünü de ifade edelim. Yani gelen talepleri karşılar haldedir. Görüldüğü üzere arkaplan görevlerinin Web tabanlı uygulamalarda konuşlandırılması oldukça kolay. .Net Core tarafının Dependency Injection mekanizması da bu işi basitleştirmekte. Microservice odaklı çözümlerde bu teknikten yararlanılarak arka plan görevlerinin tesis edilmesi kolaylıkla sağlanabilir. Hosted Service tipleri Task'ların yürütüldüğü noktalarda asenkron çalışan dış sistemlerle entegre olabilirler(RabbitMQ, Kafka, MSMQ. Azure Service Bus, WSO2 vb) Benim için yine keşfedilmesi, çalışılması, uygulanması ve öğrenilmesi keyifli bir konuydu. Bir başka makalede görüşünceye dek hepinize mutlu günler dilerim.

Core Web API için Planlanmış Görevler

$
0
0

Merhaba Arkadaşlar,

Chuck Norris. Sanıyorum hayatımın bir bölümü onun televizyonda arka arkaya yayınlanan filmleri ile geçmiştir. Asıl adı Carlo Ray Norris'tir ve Chuck ismi 1958de O, hava kuvvetlerindeyken takma ad olarak ortaya çıkmıştır. Dövüş sanatları ustası olan Chuck'ın harika bir web sitesi var. Hayran kitlesi oldukça geniş. 1940 doğumlu olan film yıldızını Google aramalarında daha çok "Chuck Norris Facts" ile biliyoruz.

Hatta onun hayatına dair şakalar, olaylar, sözler o kadar popüler hale gelmiş ki, International Chuck Norris Database isimli gönüllülük esasına göre geliştirilmiş bir hizmet bile var. Üstelik http://api.icndb.com/jokes/random adresine gittiğinizde rastgele bir fıkrasını veya şakasını çekebileceğiniz JSON formatlı bir REST servisi de bulunuyor. İşin aslı .Net Core tarafında Hosted Service kavramını araştırırken örnek olarak kullanılan ve günün özlü sözünü sunan örnek bir REST servisten Chuck Norris REST API hizmetine kadar geldiğimi belirtmek isterim. Üstelik onu basit bir örnekte kullanmayı da başardım. West-World'ün yeni konusu Hosted Service.

Ağırlıklı olarak masaüstü uygulamalarından aşina olduğumuz arka plan işleri(Background Worker Process diyelim) pek çok alanda karşımıza çıkıyor. Bu arka plan işlerini o an çalışmakta olan uygulamanın ana Thread'inden bağımsız işleyen iş birimleri olarak düşünebiliriz. .Net dünyasında uzun zamandır paralel zamanlı çalışmalar için kullanılan Task odaklı bir çatı da mevcut. Bu tanımlamalar bir araya getirildiğinde belirli periyotlarda çalışan, çeşitli iş kurallarını işleten görev odaklı fonksiyonlar ortaya çıkıyor. .Net Core dünyasına baktığımızda ise, özellikle WebHost veya Host tarafı için kullanılan Hosted Service isimli bir kavram mevcut ve bahsettiğimiz planlı işler(Scheduled Jobs) ile yakından ilişkili.

Özellikle MicroService odaklı çözümlerde, servislerin yaşamı boyunca belli bir plana/takvime göre çalışan arka plan işleri için Hosted Service enstrümanının kullanılabileceği ifade ediliyor.

Kimi senaryolarda bir Web uygulamasının veya Web API hizmetinin çalıştığı süre boyunca arka planda işletilmesini istediğimiz planlanmış görevlere ihtiyacımız olabilir. Örneğin içeride biriken log'ların belirli periyotlarda Apache Kafka gibi bir kuyruk sistemine aktarılması, servis üzerinde işletilecek istemci talepleri sonrası veritabanında oluşan değişikliklerin aralıklarla dinlenip çeşitli görevlerin işletilmesi, ön belleğin zaman planlamasına göre temizlenmesi ve başka bir çok senaryo burada göz önüne alınabilir. Kısacası WebHost(bu makale özelinde) ayağa kalktıktan sonra yaşamı sonlanıncaya kadar geçen sürede belirli bir takvimlendirmeye göre çalıştırılmasını istediğimiz arka plan görevleri söz konusu ise Hosted Service'leri kullanabiliriz.

.Net Core 2.0 ile birlikte Hosted Service'lerin kolay bir şekilde uygulanabilmesini sağlamak amacıyla IHostedService(Microsoft.Extensions.Hosting isim alanında yer alıyor) isimli bir arayüz gelmiş. Hatta .Net Core 2.1 ile birlikte bu arayüzden türetilmiş ve implementasyonu da içeren BackgroundService isimli abstract bir sınıfta söz konusu. Bu sınıfın temel amacı CancellationToken mekanizmasının yönetiminin kolaylaştırılması(Kontrol edin. Adı değişmiş olabilir. West-World üzerinde halen .Net Core 2.0 var olduğundan okuduğum bloglardaki gibi açık kaynak olarak sunulan bir IHostedService implementasyonunu kullanmacağım)

IHostedService arayüzü iki operasyon tanımlamakta. StartAsync ve StopAsync isimli fonksiyonlar CancellationToken türünden parametre alıyorlar. StartAsync operasyonu ana uygulama(WebHost veya Host türevli olabilir) başlatıldığında devreye girerken tam tersine StopAsync uygulama kapanırken işletilmekte. IHostedService uyarlaması tipler Middleware tarafında Dependency Injection mekanizması kullanılarak çalışma zamanına monte edilebiliyorlar. Singleton tipinde n sayıda Hosted Service'in çalışma zamanına eklenmesi mümkün. Bir başka deyişle uygulamanın yaşam döngüsüne istediğimiz kadar planlanmış işi birer servis olarak ekleyebiliriz. Dilerseniz hiç vakit kaybetmeden örnek bir uygulama ile konuyu anlamaya çalışalım. Boş bir Web API projesi şu an için işimizi görecek. 

dotnet new webapi -o HowToHostedService

Sonrasında projemize HostedService isimli aşağıdaki sınıfı ekleyelim.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

public abstract class HostedService : IHostedService, IDisposable
{
    private Task currentTask;
    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken cToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        currentTask = ExecuteAsync(cancellationTokenSource.Token);

        if (currentTask.IsCompleted)
            return currentTask;

        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (currentTask == null)
            return;

        try
        {
            cancellationTokenSource.Cancel();
        }
        finally
        {
            await Task.WhenAny(currentTask, Task.Delay(Timeout.Infinite,cancellationToken));
        }
    }
    public virtual void Dispose()
    {
        cancellationTokenSource.Cancel();
    }
}

Dikkat edileceği üzere HostedService sınıf IHostedService ve IDisposable arayüzlerini(Interface) uygulamakta. Temel görevi Hosted Service'ler ile ilişkilendirilecek olan Task'ların iptal edilme mekanizmalarının kolayca yönetilebilmesi. Dispose edilebilir bir nesne olarak tanımlandığına da dikkat edelim. Ayrıca, HostedService abstract bir sınıf. Dolayısıyla kendisini örnekleyemeyiz. Bununla birlikte kendisini uygulayan sınıfların mutlaka ezmesi gereken ExecuteAsync isimli bir metod tanımı da sunuyor. Bu metodu alt tiplerde ezerken planlanmış görevin yapacağı çalışma için kodlamamız yeterli. Parametre olarak gelen CancellationTokenSource örneği ise StartAsync üzerinden devrediliyor. StartAsync metodu alt tipin uyguladığı ExecuteAsync operasyonunu çağırıp tamamlanıp tamamlanmadığına bakıyor. StopAsync operasynonu ise iptal işleminin yönetimini gerçekleştirmekte. Yukarıda da bahsettiğim gibi bu sınıf .Net Core 2.1 ile birlikte hazır olarak gelmesi beklenen bir tip(İçinde kocaman gülümseme olan şu makaleye göz gezdirebilirsiniz) 

Artık planlanmış görevleri içerecek örnek sınıfların yazılmasına başlanabilir. Ben sadece iki sınıfı sisteme dahil edeceğim. Her ikisi de belirli aralıklarla Console ekranına bir şeyler yazacaklar. Amacımız sadece Hosted Service mekanizmasının Web API tarafındaki tesisatının nasıl kurulması gerektiğini öğrenmek olduğundan bu basit yaklaşım yeterli olacaktır(Nitekim söz konusu arka plan servisleri gerçek hayat örneklerinde gerçek iş modellerini baz alarak kurgulanmalılar) RequestCollectorService ve ChuckFactService isimli arka plan servis sınıflarımızı aşağıdaki gibi geliştirebiliriz. 

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class ChuckFactService
: HostedService
{
    HttpClient restClient;
    string icndbUrl="http://api.icndb.com/jokes/random";
    public ChuckFactService()
    {
        restClient=new HttpClient();
    }
    protected override async Task ExecuteAsync(CancellationToken cToken)
    {
        while (!cToken.IsCancellationRequested)
        {   
            var response = await restClient.GetAsync(icndbUrl, cToken);
            if (response.IsSuccessStatusCode)
            {
                var fact = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"{DateTime.Now.ToString()}\n{fact}");
            }

            await Task.Delay(TimeSpan.FromSeconds(10), cToken);
        }
    }
}

Ezilen ExecuteAsync metodunda icndb adresine bir talepte bulunup, talep sonucu HTTP 200 ise elde edilen sonucu ekrana bastırıyoruz. REST talebini göndermek için HttpClient tipinden yararlanmaktayız. Bu sınıfın awaitable GetAsync fonkisyonunu kullanıyoruz. Fonkisyonda dikkat edileceği üzere ilgili Task için iptal talebi olup olmadığının sürekli olarak kontrol edildiği bir while döngüsü bulunuyor. Ayrıca söz konusu görevin 10 saniyede bir işletilmesini Task tipinin Delay metodu ile sağlamaktayız. Bir başka deyişle tekrarlı görevlerin zamanlamalarını bu teknikle ayarlayabiliriz. 

using System;
using System.Threading;
using System.Threading.Tasks;

public class RequestCollectorService
: HostedService
{
    protected override async Task ExecuteAsync(CancellationToken cToken)
    {
        while (!cToken.IsCancellationRequested)
        {
            Console.WriteLine($"{DateTime.Now.ToString()} Çalışma zamanı taleplerini topluyorum.");
            await Task.Delay(TimeSpan.FromSeconds(30), cToken);
        }
    }
}

RequestCollectorService sınıfında ise sadece ekrana bir mesaj bastırıyoruz. Çalışmasını 30 saniyede bir gerçekleştiren bir görevlendirme söz konusu. Tanımladığımız bu görev servislerini Host uygulamaya enjekte etmek için, Startup sınıfındaki ConfigureServices metodunu aşağıdaki gibi güncellememiz yeterli.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddSingleton<IHostedService,ChuckFactService>();
    services.AddSingleton<IHostedService,RequestCollectorService>();
}

Bu değişiklikle, AddSingleton metodunun generic versiyonunu kullanarak IHostedService uyarlamasını gerçekleştiren ChuckFactService ve RequestCollectorService sınıflarının arka plan hizmetlerine eklenmesini sağladık. Artık Web uygulaması çalışmaya başladığında bu sınıflar otomatik olarak devreye alınacak ve üzerlerindeki görevler belirlenen sürelerinde işletilecekler. Uygulamamızı çalıştırdıktan sonrasına ait örnek bir ekran görüntüsü aşağıdaki gibidir.

10 saniyede bir Chuck Norris'e ait servise bir çağrı ve 30 saniyede bir ortam verilerini toplama işlemi gerçekleşmektedir. Bu sırada Web API servisinin normal hizmetini sürdürdüğünü de ifade edelim. Yani gelen talepleri karşılar haldedir. Görüldüğü üzere arkaplan görevlerinin Web tabanlı uygulamalarda konuşlandırılması oldukça kolay. .Net Core tarafının Dependency Injection mekanizması da bu işi basitleştirmekte. Microservice odaklı çözümlerde bu teknikten yararlanılarak arka plan görevlerinin tesis edilmesi kolaylıkla sağlanabilir. Hosted Service tipleri Task'ların yürütüldüğü noktalarda asenkron çalışan dış sistemlerle entegre olabilirler(RabbitMQ, Kafka, MSMQ. Azure Service Bus, WSO2 vb) Benim için yine keşfedilmesi, çalışılması, uygulanması ve öğrenilmesi keyifli bir konuydu. Bir başka makalede görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/bir-aws-elastic-beanstalk-macerasiAWS Elastic Beanstalk Macerası

$
0
0

Merhaba Arkadaşlar,

Geçenlerde sıkıldığım bir ara kendimi  Google'da "How To Draw..." araması yaparken buldum. Bir internet sitesinde DC Comics'in Robin karakterini nasıl çizebileceğimizi anlatan içerik ilgimi çekmişti. Geometri bilgisini iyi kullandığı için anlaşılırdı. Tabii önemli bir eksiğim vardı...Yetenek. Sonuçları sizlerle paylaşmayı çok tercih etmiyorum ama yandaki Robin'in kafasının pek yakınlarından geçemediğimi gönül rahatlığıyla itiraf edebilirim. Dolayısıyla google aramasını ve internet sayfasını kapatıp tekrardan az buçuk anlamaya çalıştığım yazılım dünyasına döndüm.

Aslında bazen öğrenmek istediğimiz konuyu adım adım ve her adımında da tane tane anlatan bir dokümanı takip ederiz. Ama çalıştığımız ortamlar her zaman için bir yerlerde sorunlarla karşılaşmamıza neden olabilirler. Geçtiğimiz cumartesi günü de benzer sorunlarla karşılaştım. Amacım Amazon'un şu adreste yayınladığı dokümanı takip ederek Elastic Beanstalk üzerine Django ile oluşturulmuş bir web uygulamasını taşıyıp Doğu Amerika kıtasındaki herhangibir Elastic Compute Cloud(Amazon EC2) sistemi üzerinden canlı yayına almaktı. İlk başlarda kolay giden adımlar özellikle sonlara doğru çeşitli sürprizlerle karşılaşmama neden oldu.

Her şey o Cuma günü AWS'de açmış olduğum hesapla neler yapabileceğime bakarken başladı. Bir süre öncesinde Amazon Lambda hizmetini incelemiş ve .Net Core ile kullanabildiğimi gördükten sonra epey keyif almıştım. Şimdiki hedefim Elastic Beanstalk ürünüydü. Kısaca Platform as a Service gibi konumlanan bu ürün sayesinde, çeşitli platformları Amazon EC2 örnekleri ile çalışacak şekilde hazırlayabiliyoruz. Platform anlamında oldukça geniş bir ürün yelpazesi de söz konusu. Buradaki adresten detaylarını öğrenebileceğinize gibi .Net Core'dan Go'ya, Python'dan Java'ya, Ruby'den Php'ye, Node.js'ten Docker'a kadar pek çok uygulama için hazır ortamlar söz konusu.

Buradaki hazırlıklarda Infrastructure as a Service gibi konumlanan Amazon EC2 tarafını da pek düşünmemize gerek kalmıyor esasında. Bu noktada EB'nin başarılı bir Deployment aracı olduğunu da ifade edebiliriz. EB'yi kullanırken Amazon Web Console üzerinden bir kaç tıklama ile bir platformu ayağa kaldırıp yayına almamız mümkün. İşte o Cuma akşamı bunu denemiştim. Şekilden de görüleceği gibi üzerinde Python 3.6 ortamı kurulu olan 64bitlik bir Linux makine emrime amadeydi. 

Hatta hazır şablon olarak gelen bir giriş sayfası da bulunuyordu(What's Next? kısmı da oldukça dikkat çekiciydi. Django ve Flesk hemen dikkatimi çekmişlerdi)

Adres satırından da göreceğiniz gibi her şey Amazon'un doğu Amerika bölgesinde bir yerlerde gerçekleşmekteydi(Sanırım) Kuvvetle muhtemel domain'in arkasında o bölgeye ait bir Cloud Server deposu ve EC2 makine örnekleri yer almaktaydı. Yönetim panelini kullanarak bu platform üzerine dosya bırakma yoluyla da taşımalar yapılabiliyordu. Benim merak ettiğim konu ise kendi geliştirme ortamımda yazdığım bir uygulamayı(veya hazır şablondan üretilmiş bir tanesini) komut satırından Elastic Beanstalk ortamına nasıl aktarabileceğimdi. 

O zaman maceramıza başlayalım. Yapacaklarımız özetle oldukça basit. West-world üzerinde(ki artık 64bit çalışan Ubuntu 16.04 sistemi olduğunu biliyorsunuz) sanal bir çalışma ortamı hazırlayacağız. Ardından bu ortamda Django çatısını kullanarak hazır bir web şablonu üreteceğiz. Elastic Beanstalk için gerekli olan çevre değişkenlerini ayarladıktan sonra bir Local Repository üretip taşıma adımlarını işleteceğiz.

VirtualEnv Gerekli

VirtualEnv ile linux üzerinde sanal bir ortam hazırlamamız mümkün. Bunu daha çok sistemde var olan paketlerden daha eski veya yeni sürümlerini kullanmak istediğimiz uygulamalarda ele alabiliriz.

sudo apt-get install virtualenv

Sanal Ortamın Kurulumu

VirtualEnv aracı hazır olduğuna göre artık sanal ortamın kurulmasına başlanabilir.

virtualenv ~/beanstalk-virt

ile python odaklı sanal ortam kurulur. Oluşan klasör içeriğine bakıldığında Python ile hazırlanmış kod dosyaları ve ortam enstrümanları olduğu görülür. Sürüm olarak Python 2.7 söz konusudur ki Amazon Elastic Beanstalk'da bu Python sürümünü tavsiye etmektedir. Sanal ortamı etkinleştirmek içinse aşağıdaki komutu kullanabiliriz.

source ~/beanstalk-virt/bin/activate

Artık West-World'ün üzerinde tamamen izole bir çalışma sahası var. 

Sahne Django'nun

Python tarafında web uygulaması geliştirmek için kullanılan en popüler çatılardan birisi Django. Sanal ortama pip paket yöneticisi yardımıyla ilgili çatıyı kurmak gerekiyor.

pip install django==1.9.12

Amazon dokümantasyonuna göre desteklenen Django versiyonu önemli. Ben araştırmalarımı yaptığım sırada Amazon 1.9.12 sürümünün kullanılması tavsiye ediliyordu.

Kurulum başarılı bir şekilde tamamlandıktan sonra hemen hazır web şablonu kullanılarak bir proje oluşturabiliriz. Aşağıdaki komut bunun için yeterli.

django-admin startproject amznWebStore

Projenin adı amznWebStore. Klasör içeriğine bakıldığında Python kod dosyalarından oluşan basit bir içerik oluşturulduğu görülebilir. Projeyi bu haliyle local makine üzerinden çalıştırmak istersek manage.py kod dosyasının aşağıdaki gibi çağırılması yeterlidir.

python manage.py runserver

Eğer işler yolunda gittiyse varsayılan olarak localhost'un 8000 numaralı portu üzerinden web içeriğine ulaşabiliriz.

Uygulamanın Elastic Beanstalk'a Taşınması

Buraya kadarki işlemlerimizi şöyle bir hatırlayalım. İlk olarak West-World üzerinde sanal bir ortam açtık. Sanal ortam üzerinde Python yüklü olarak geliyordu. Oluşturulan ortamı etkinleştirdikten sonra basit bir Web uygulamasını ayağa kaldırmak için Django Framework'ü kurduk. Hiçbir kod değişikliği yapmadan manage.py dosyasından yararlanarak bu standart şablonun 127.0.0.1:8000 adresinden ayağa kaldırıldığını gördük. Sırada bu uygulamanın Elastic Beanstalk'a taşınması var. Önce bir kaç hazırlık yapmamız gerekiyor. İlk olarak ortam gereksinimlerin text dosyaya alıyoruz.

pip freeze > requriements.txt

Dosya içeriğinde aslında tek bir gereksinim var o da taşımanın yapılacağı ortamda Django'nun 1.9.12 sürümünün olmasını istiyor. Takip eden adımda amznWebStore kök dizini altında .ebextensions isimli yeni bir klasör oluşturup içerisine django.config isimli bir dosya atmamız gerekiyor. Bu dosyanın içeriği aşağıdaki gibi.

option_settings:
  aws:elasticbeanstalk:container:python:
    WSGIPath: amznWebStore/wsgi.py

Aslında Elastic Beanstalk ortamına söz konusu uygulamayı nereden başlatacağını söylemekteyiz ki bu senaryoda wsgi.py dosyası oluyor. Bu son iki adım bize şunu öğretmekte. Sanal ortamda geliştireceğimiz web uygulaması içerisinde neleri kullanırsak(söz gelimi SQLite gibi bir veritabanı, Flesk çatısı vb) bunların Elastic Beanstalk'a söylenmesi gerekmekte.

Sırada Command Languagen Interface'i kullanarak Local Repository'nin oluşturulması adımı var. Tabii bu aşamada benim gibi aşağıda görülen sorunlarla karşılaşabilirsiniz.

İlk olarak eb isimli CLI aracı sisteminizde yüklü olmayabilir. Python ile yazılmış olan bu aracı kullanabilmek için pip yöneticisi ile kurmak gerekiyor. Lakin bu kurulum sanal ortamda gerçekleştirilebilen bir kurulum da değil. Benim düştüğüm bir diğer hata da bu oldu. deactivate komutu ile beanstalk-virt isimli sanal ortamdan çıktıktan sonra gerekli kurulum işlemini yaptım. Sonrasında sanal ortamı etkinleştirip eb init operasyonunu bir kez daha denedim.

Bu sefer daha farklı bir durumla karşılaştım. İlk olarak hangi bölge üzerine taşıma yapmak istediğimi sorduğunu düşündüğüm bir liste ile karşılaştım. Amazon web sitesi üzerinden yaptığım örneği düşünerekten us-east-2(13ncü bölge) seçimini yaptım. Tabii sonrasında gelen hata mesajı gayet açıktı. Credential bilgilerimi bildirmediğim için yetki hatası almıştım. Hemen Amazon Web Console'a  geçerek ve AdministratorAccess Policy'sini kullanan westworld-buraksenyurt isimli bir kullanıcı oluşturdum. Access Key ID ve Secret Access Key değerlerini credential bilgilendirmesi için kullanarak adımı tamamlamayı başardım.

Sonunda uygulama oluşturuldu. Artık tek yapılması gereken ortamın EB üzerine taşınmasıydı. Aşağıdaki komut ile bunu denedim.

eb create my-eb-sample-env

Evdeki hesap çarşıya uymamış gibiydi.

Üstüne üstelik bir de şu vardı.

Ne yazık ki sonuç pek beklediğim gibi olmadı. Komut satırı hareketliliklerini izlerken Elastic Beanstalk üzerinde my-eb-sample-env isimli uygulamanın oluşturulduğunu gördüm ancak site üzerinden yaptığım uygulama gibi yeşil değil kırmızı renkteydi. Ayrıca komut satırına requriements.txt dosyasının geçersiz olduğuna dair hata mesajı düşmüştü. Requirements.txt dosyasının oluşturulduğu adıma geri dönerek sorunu anlamaya çalıştım. İlk iş içeriğini aşağıdaki hale getirdim.

Django==1.9.12

Başka bir şeye ihtiyacım yoktu çünkü. Tekrardan 

eb deploy

ile taşıma işlemini yaptım. Şaşılacak şekilde yeşil oku gördüm. Uygulama Elastic Beanstalk üzerinde sağlıklı bir şekilde ayağa kalkmış gibi duruyordu. Komut satırından

eb open

ile siteyi açtırmak istediğimdeyse yine hüsranla karşılaştım. HTTP_HOST header bilgisinin geçersiz olduğu söyleniyordu.

Bunun üzerine amznWebStore klasöründeki settings.py dosyasını açıp ALLOWED_HOSTS değerinin olduğu satırı bulup aşağıdaki hale getirdim.

ALLOWED_HOSTS = ['my-eb-sample-env.prii9kimtp.us-east-2.elasticbeanstalk.com']

Sonrasında tekrar deploy ve tekrar open.

eb deploy
eb open

Nihayet! Uygulama Elastic Beanstalk üzerine başarılı bir şekilde taşınmış ve ayağa kaldırılmıştı. 

Biraz yorucu bir çalışma olduğunu ifade edebilirim ancak West-World üzerinde kurgulanan senaryonun Amazon Elastic Beanstalk üzerinde konuşlanması tatmin ediciydi de diyebilirim. Bu çalışma bana bir çok şey kattı. Bir PaaS'in tam olarak ne işe yarayabileceğini görme fırsatı buldum. Amazon Console penceresini kullanırken her deployment denemesi sonrası web panelindeki anlık hareketleri inceleyip başarılı bir log sisteminin nasıl olabileceğini ve monitoring'in ne kadar önemli olduğunu anladım. Belki Django ile oluşturulan web uygulaması için hiç kod yazmadım ama taşıma öncesinde platformun hangi gereksinimlere ihtiyacı olduğunun nasıl söylenebileceğini gördüm. Her anlamda yararlı bir çalışma olduğunu ifade edebilirim. Umarım sizler için de bilgilendirici olmuştur. AWS tarafını incelemeye devam ediyorum. Yeni bir şeyler öğrendikçe paylaşmaya çalışacağım. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

AWS Elastic Beanstalk Macerası

$
0
0

Merhaba Arkadaşlar,

Geçenlerde sıkıldığım bir ara kendimi  Google'da "How To Draw..." araması yaparken buldum. Bir internet sitesinde DC Comics'in Robin karakterini nasıl çizebileceğimizi anlatan içerik ilgimi çekmişti. Geometri bilgisini iyi kullandığı için anlaşılırdı. Tabii önemli bir eksiğim vardı...Yetenek. Sonuçları sizlerle paylaşmayı çok tercih etmiyorum ama yandaki Robin'in kafasının pek yakınlarından geçemediğimi gönül rahatlığıyla itiraf edebilirim. Dolayısıyla google aramasını ve internet sayfasını kapatıp tekrardan az buçuk anlamaya çalıştığım yazılım dünyasına döndüm.

Aslında bazen öğrenmek istediğimiz konuyu adım adım ve her adımında da tane tane anlatan bir dokümanı takip ederiz. Ama çalıştığımız ortamlar her zaman için bir yerlerde sorunlarla karşılaşmamıza neden olabilirler. Geçtiğimiz cumartesi günü de benzer sorunlarla karşılaştım. Amacım Amazon'un şu adreste yayınladığı dokümanı takip ederek Elastic Beanstalk üzerine Django ile oluşturulmuş bir web uygulamasını taşıyıp Doğu Amerika kıtasındaki herhangibir Elastic Compute Cloud(Amazon EC2) sistemi üzerinden canlı yayına almaktı. İlk başlarda kolay giden adımlar özellikle sonlara doğru çeşitli sürprizlerle karşılaşmama neden oldu.

Her şey o Cuma günü AWS'de açmış olduğum hesapla neler yapabileceğime bakarken başladı. Bir süre öncesinde Amazon Lambda hizmetini incelemiş ve .Net Core ile kullanabildiğimi gördükten sonra epey keyif almıştım. Şimdiki hedefim Elastic Beanstalk ürünüydü. Kısaca Platform as a Service gibi konumlanan bu ürün sayesinde, çeşitli platformları Amazon EC2 örnekleri ile çalışacak şekilde hazırlayabiliyoruz. Platform anlamında oldukça geniş bir ürün yelpazesi de söz konusu. Buradaki adresten detaylarını öğrenebileceğinize gibi .Net Core'dan Go'ya, Python'dan Java'ya, Ruby'den Php'ye, Node.js'ten Docker'a kadar pek çok uygulama için hazır ortamlar söz konusu.

Buradaki hazırlıklarda Infrastructure as a Service gibi konumlanan Amazon EC2 tarafını da pek düşünmemize gerek kalmıyor esasında. Bu noktada EB'nin başarılı bir Deployment aracı olduğunu da ifade edebiliriz. EB'yi kullanırken Amazon Web Console üzerinden bir kaç tıklama ile bir platformu ayağa kaldırıp yayına almamız mümkün. İşte o Cuma akşamı bunu denemiştim. Şekilden de görüleceği gibi üzerinde Python 3.6 ortamı kurulu olan 64bitlik bir Linux makine emrime amadeydi. 

Hatta hazır şablon olarak gelen bir giriş sayfası da bulunuyordu(What's Next? kısmı da oldukça dikkat çekiciydi. Django ve Flesk hemen dikkatimi çekmişlerdi)

Adres satırından da göreceğiniz gibi her şey Amazon'un doğu Amerika bölgesinde bir yerlerde gerçekleşmekteydi(Sanırım) Kuvvetle muhtemel domain'in arkasında o bölgeye ait bir Cloud Server deposu ve EC2 makine örnekleri yer almaktaydı. Yönetim panelini kullanarak bu platform üzerine dosya bırakma yoluyla da taşımalar yapılabiliyordu. Benim merak ettiğim konu ise kendi geliştirme ortamımda yazdığım bir uygulamayı(veya hazır şablondan üretilmiş bir tanesini) komut satırından Elastic Beanstalk ortamına nasıl aktarabileceğimdi. 

O zaman maceramıza başlayalım. Yapacaklarımız özetle oldukça basit. West-world üzerinde(ki artık 64bit çalışan Ubuntu 16.04 sistemi olduğunu biliyorsunuz) sanal bir çalışma ortamı hazırlayacağız. Ardından bu ortamda Django çatısını kullanarak hazır bir web şablonu üreteceğiz. Elastic Beanstalk için gerekli olan çevre değişkenlerini ayarladıktan sonra bir Local Repository üretip taşıma adımlarını işleteceğiz.

VirtualEnv Gerekli

VirtualEnv ile linux üzerinde sanal bir ortam hazırlamamız mümkün. Bunu daha çok sistemde var olan paketlerden daha eski veya yeni sürümlerini kullanmak istediğimiz uygulamalarda ele alabiliriz.

sudo apt-get install virtualenv

Sanal Ortamın Kurulumu

VirtualEnv aracı hazır olduğuna göre artık sanal ortamın kurulmasına başlanabilir.

virtualenv ~/beanstalk-virt

ile python odaklı sanal ortam kurulur. Oluşan klasör içeriğine bakıldığında Python ile hazırlanmış kod dosyaları ve ortam enstrümanları olduğu görülür. Sürüm olarak Python 2.7 söz konusudur ki Amazon Elastic Beanstalk'da bu Python sürümünü tavsiye etmektedir. Sanal ortamı etkinleştirmek içinse aşağıdaki komutu kullanabiliriz.

source ~/beanstalk-virt/bin/activate

Artık West-World'ün üzerinde tamamen izole bir çalışma sahası var. 

Sahne Django'nun

Python tarafında web uygulaması geliştirmek için kullanılan en popüler çatılardan birisi Django. Sanal ortama pip paket yöneticisi yardımıyla ilgili çatıyı kurmak gerekiyor.

pip install django==1.9.12

Amazon dokümantasyonuna göre desteklenen Django versiyonu önemli. Ben araştırmalarımı yaptığım sırada Amazon 1.9.12 sürümünün kullanılması tavsiye ediliyordu.

Kurulum başarılı bir şekilde tamamlandıktan sonra hemen hazır web şablonu kullanılarak bir proje oluşturabiliriz. Aşağıdaki komut bunun için yeterli.

django-admin startproject amznWebStore

Projenin adı amznWebStore. Klasör içeriğine bakıldığında Python kod dosyalarından oluşan basit bir içerik oluşturulduğu görülebilir. Projeyi bu haliyle local makine üzerinden çalıştırmak istersek manage.py kod dosyasının aşağıdaki gibi çağırılması yeterlidir.

python manage.py runserver

Eğer işler yolunda gittiyse varsayılan olarak localhost'un 8000 numaralı portu üzerinden web içeriğine ulaşabiliriz.

Uygulamanın Elastic Beanstalk'a Taşınması

Buraya kadarki işlemlerimizi şöyle bir hatırlayalım. İlk olarak West-World üzerinde sanal bir ortam açtık. Sanal ortam üzerinde Python yüklü olarak geliyordu. Oluşturulan ortamı etkinleştirdikten sonra basit bir Web uygulamasını ayağa kaldırmak için Django Framework'ü kurduk. Hiçbir kod değişikliği yapmadan manage.py dosyasından yararlanarak bu standart şablonun 127.0.0.1:8000 adresinden ayağa kaldırıldığını gördük. Sırada bu uygulamanın Elastic Beanstalk'a taşınması var. Önce bir kaç hazırlık yapmamız gerekiyor. İlk olarak ortam gereksinimlerin text dosyaya alıyoruz.

pip freeze > requriements.txt

Dosya içeriğinde aslında tek bir gereksinim var o da taşımanın yapılacağı ortamda Django'nun 1.9.12 sürümünün olmasını istiyor. Takip eden adımda amznWebStore kök dizini altında .ebextensions isimli yeni bir klasör oluşturup içerisine django.config isimli bir dosya atmamız gerekiyor. Bu dosyanın içeriği aşağıdaki gibi.

option_settings:
  aws:elasticbeanstalk:container:python:
    WSGIPath: amznWebStore/wsgi.py

Aslında Elastic Beanstalk ortamına söz konusu uygulamayı nereden başlatacağını söylemekteyiz ki bu senaryoda wsgi.py dosyası oluyor. Bu son iki adım bize şunu öğretmekte. Sanal ortamda geliştireceğimiz web uygulaması içerisinde neleri kullanırsak(söz gelimi SQLite gibi bir veritabanı, Flesk çatısı vb) bunların Elastic Beanstalk'a söylenmesi gerekmekte.

Sırada Command Languagen Interface'i kullanarak Local Repository'nin oluşturulması adımı var. Tabii bu aşamada benim gibi aşağıda görülen sorunlarla karşılaşabilirsiniz.

İlk olarak eb isimli CLI aracı sisteminizde yüklü olmayabilir. Python ile yazılmış olan bu aracı kullanabilmek için pip yöneticisi ile kurmak gerekiyor. Lakin bu kurulum sanal ortamda gerçekleştirilebilen bir kurulum da değil. Benim düştüğüm bir diğer hata da bu oldu. deactivate komutu ile beanstalk-virt isimli sanal ortamdan çıktıktan sonra gerekli kurulum işlemini yaptım. Sonrasında sanal ortamı etkinleştirip eb init operasyonunu bir kez daha denedim.

Bu sefer daha farklı bir durumla karşılaştım. İlk olarak hangi bölge üzerine taşıma yapmak istediğimi sorduğunu düşündüğüm bir liste ile karşılaştım. Amazon web sitesi üzerinden yaptığım örneği düşünerekten us-east-2(13ncü bölge) seçimini yaptım. Tabii sonrasında gelen hata mesajı gayet açıktı. Credential bilgilerimi bildirmediğim için yetki hatası almıştım. Hemen Amazon Web Console'a  geçerek ve AdministratorAccess Policy'sini kullanan westworld-buraksenyurt isimli bir kullanıcı oluşturdum. Access Key ID ve Secret Access Key değerlerini credential bilgilendirmesi için kullanarak adımı tamamlamayı başardım.

Sonunda uygulama oluşturuldu. Artık tek yapılması gereken ortamın EB üzerine taşınmasıydı. Aşağıdaki komut ile bunu denedim.

eb create my-eb-sample-env

Evdeki hesap çarşıya uymamış gibiydi.

Üstüne üstelik bir de şu vardı.

Ne yazık ki sonuç pek beklediğim gibi olmadı. Komut satırı hareketliliklerini izlerken Elastic Beanstalk üzerinde my-eb-sample-env isimli uygulamanın oluşturulduğunu gördüm ancak site üzerinden yaptığım uygulama gibi yeşil değil kırmızı renkteydi. Ayrıca komut satırına requriements.txt dosyasının geçersiz olduğuna dair hata mesajı düşmüştü. Requirements.txt dosyasının oluşturulduğu adıma geri dönerek sorunu anlamaya çalıştım. İlk iş içeriğini aşağıdaki hale getirdim.

Django==1.9.12

Başka bir şeye ihtiyacım yoktu çünkü. Tekrardan 

eb deploy

ile taşıma işlemini yaptım. Şaşılacak şekilde yeşil oku gördüm. Uygulama Elastic Beanstalk üzerinde sağlıklı bir şekilde ayağa kalkmış gibi duruyordu. Komut satırından

eb open

ile siteyi açtırmak istediğimdeyse yine hüsranla karşılaştım. HTTP_HOST header bilgisinin geçersiz olduğu söyleniyordu.

Bunun üzerine amznWebStore klasöründeki settings.py dosyasını açıp ALLOWED_HOSTS değerinin olduğu satırı bulup aşağıdaki hale getirdim.

ALLOWED_HOSTS = ['my-eb-sample-env.prii9kimtp.us-east-2.elasticbeanstalk.com']

Sonrasında tekrar deploy ve tekrar open.

eb deploy
eb open

Nihayet! Uygulama Elastic Beanstalk üzerine başarılı bir şekilde taşınmış ve ayağa kaldırılmıştı. 

Biraz yorucu bir çalışma olduğunu ifade edebilirim ancak West-World üzerinde kurgulanan senaryonun Amazon Elastic Beanstalk üzerinde konuşlanması tatmin ediciydi de diyebilirim. Bu çalışma bana bir çok şey kattı. Bir PaaS'in tam olarak ne işe yarayabileceğini görme fırsatı buldum. Amazon Console penceresini kullanırken her deployment denemesi sonrası web panelindeki anlık hareketleri inceleyip başarılı bir log sisteminin nasıl olabileceğini ve monitoring'in ne kadar önemli olduğunu anladım. Belki Django ile oluşturulan web uygulaması için hiç kod yazmadım ama taşıma öncesinde platformun hangi gereksinimlere ihtiyacı olduğunun nasıl söylenebileceğini gördüm. Her anlamda yararlı bir çalışma olduğunu ifade edebilirim. Umarım sizler için de bilgilendirici olmuştur. AWS tarafını incelemeye devam ediyorum. Yeni bir şeyler öğrendikçe paylaşmaya çalışacağım. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/ef-core-ile-mariadb-kullanimiEF Core ile MariaDb Kullanımı

$
0
0

Merhaba Arkadaşlar,

Son bir kaç aydır Cumartesi gecelerimi bir şeyler yazmak veya araştırmak için değerlendirmekteyim. Bu tip çalışma disiplinlerini daha önceden de denemiş ve epeyce faydasını görmüştüm. Sonuçta üzerinde çalıştığımız yazılım platformları ve ürünler sürekli ve düzenli olarak değişim içerisindeler. Dolayısıyla yeniliklerin ucundan da olsa tutabilmek lazım. Bir anlamda şu meşhur Pomodoro çalışma tekniğini haftalık periyotlara böldüğümü ifade edebilirim.

Geçtiğimiz hafta içerisinde .Net Core tarafında nelere bakabilirim diye internette sörf yaparken eski dostumuz Entity Framework'e rastladım. Tabii oyun sahası benim için artık değişmişti. West-World, Microsoft Sql Server nedir pek bilmiyordu. Hatta IIS'e bile burun kıvırıp Nginx ya da Apache ile konuşuyordu. Elimde pek çok makalede kullanılan Visual Studio' da yoktu. Buna rağmen yetenekleri daha kısıtlı olan Visual Studio Code ile hayat oldukça güzeldi. Sonunda bir süredir merak ettiğim MariaDb'yi, Entity Framework Core ile basitçe nasıl konuşturabilleceğimi görmeye karar verdim. Öncelikle MariaDb'yi tanımlayalım...

MariaDB GNU lisansı ile sunulan, MySQL’in yaratıcısı Monty Widenius‘un kodlarını çatallayarak(fork işlemi) aynı komutları, arayüzleri ve API’leri destekleyecek şekilde geliştirmeye başlanmış bir ilişkisel veritabanı yönetim sistemi(Relational Database Management System) Her ne kadar sevimsiz bir tanımlama gibi dursa da logosu en az MySQL'in sevimli yunusu kadar dikkat çekici. Nihayetinde neden böyle bir oluşma gidildiğine dair internette pek çok tartışma var. Onları araştırmanızı öneririm. Benim dikkatimi çeken işin içerisine Oracle girdikten sonra inanılmaz derecede artan destek ücretleri(Sun'ın 2010 da satın alınması ve MySQL'in Oracle'e geçmesi sonrası destek ücretinin %600 arttığı söyleniyor) Bu düşünceler ışığında geçtim evdeki Ubuntu'nun başına.

MariaDb kurulumu

Tabii ilk olarak West-World'e MariaDb'yi kurmam lazım. Son zamanlarda bu dünya üzerinde çok fazla şey yüklenip kaldırıldı ama bana mısın demedi. Sanırım o da bu yeni deneyimlerden keyif alıyor. Kendisine sırasıyla aşağıdaki komutları gönderek MariaDb'yi yüklemesini ve gerekli servisleri çalıştırmasını söyledim. 

sudo apt-get update
sudo apt-get install mariadb-server mariadb-client
sudo mysql_secure_installation

Son komut sonrası bazı sorularla karşılaştım. İlk etapta root user şifresinin değiştirilmesinin yerinde olacağını belirteyim. Kurulum sorasında root kullanıcının şifresi boştur. Dolayısıyla Enter tuşuna basarak geçilebilir. Ardından bir şifre vermek gerekecektir. Şifre adımından sonra isimsiz kullanıcılara(anonymous user) izin verilip verilmeyeceği, root kullanıcı için uzaktan erişim sağlanıp sağlanmayacağı, test veritabanının kaldırılıp kaldırılmayacağı ve benzeri sorulara verilecek cevaplarla kurulum işlemi sonlandırılır. Kurulum sonrası aşağıdaki komutu kullanarak MariaDb ortamına giriş yapabiliyor olmamız gerekir.

sudo mysql -u root -p

Yukarıdaki ekran görüntüsünde show databases; komutu kullanılarak yüklü olan veritabanlarının listesinin elde edilmesi sağlanmışıtır. Burada basit SQL sorgu ifadeleri kullanarak bir takım işlemler yapabiliriz. Bir deneyin derim.

Bu arada eğer sudo kullanmadan giriş yapmak istersek yada ERROR 1698 (28000): Access denied for user 'burakselyum'@'localhost' benzeri bir hata alırsak, root user'ın mysql_native_password plug-in'inini kullanabilmesini söyleyerek çözüm üretebiliriz.

use mysql;
update user set plugin='mysql_native_password' where user='root';
flush privileges; 
exit;

sonrasında

service mysql restart 

ile mysql hizmetini yeniden başlatmak gerekir.

Console Uygulamasının Yazılması

Gelelim MariaDb ile konuşacak uygulama kodlarının yazımına. Konuya basit bir girizgah yapmak istediğim için her zaman ki gibi tercihim Console uygulaması geliştirmek. Ancak siz örneği denerken bir Web API servisi arkasınada da kullanmayı tercih edebilirsiniz.

dotnet new console -o MariaDbSample

Şimdi gerekli paketleri de eklemek lazım. MariaDb'yi Entity Framework Core ile kolayca kullanabilmek için Pomelo tarafından geliştirilmiş bir paket bulunmakta. Bu ve diğer EF paketlerini aşağıdaki komutları kullanarak projeye entegre edebiliriz.

dotnet add package Microsoft.EntityFrameworkCore.Tools -v:'2.0.0'
dotnet add package Microsoft.EntityFrameworkCore.Tools.DotNet -v:'2.0.0'
dotnet add package Pomelo.EntityFrameworkCore.MySql -v:'2.0.0.1'

Paketler yüklendikten sonra bir restore işlemi uygulamakta yarar var.

dotnet restore 

Program.cs içeriği ise aşağıdaki gibi. Kalabalık göründüğünü bakmayın. Büyük bir kısmı kitap ve kategori ekleme kodlarından oluşuyor.

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace MariaDbSample
{
    [Table("Book")]
    public class Book
    {
        public long BookID { get; set; }
        [Required]
        [MaxLength(50)]
        public string Title { get; set; }
        public bool InStock { get; set; }
        public double Price { get; set; }
        public virtual BookCategory Category { get; set; }
    }

    [Table("Category")]
    public class BookCategory
    {
        [Key]
        public long CategoryID { get; set; }
        [Required]
        [MaxLength(20)]
        public string Name { get; set; }
        public virtual ICollection<Book> Books { get; set; }
    }

    public partial class BeautyBooksContext : DbContext
    {
        public DbSet<Book> Books { get; set; }
        public DbSet<BookCategory> BookCategories { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseMySql(@"uid=root;pwd=rootşifre;Host=localhost;Database=BeautyBooks;");
        }
    }

    public class Program
    {
        public static void Main()
        {
            using (var context = new BeautyBooksContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();

                var scienceCategory = new BookCategory()
                {
                    Name = "Science"
                };
                var programmingCategory = new BookCategory()
                {
                    Name = "Programming"
                };
                var mathCategory = new BookCategory()
                {
                    Name = "Math"
                };
                context.BookCategories.Add(scienceCategory);
                context.BookCategories.Add(programmingCategory);

                context.Books.Add(new Book()
                {
                    Title = "2025 : Go to the MARS",
                    Price = 8.99,
                    InStock = true,
                    Category = scienceCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "The 9nth Element",
                    Price = 6.99,
                    InStock = false,
                    Category = scienceCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "Calculus - I",
                    Price = 19.99,
                    InStock = false,
                    Category = mathCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "Advanced Asp.Net Core 2.0",
                    Price = 38.10,
                    InStock = true,
                    Category = programmingCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "C# 7.0 Introduction",
                    Price = 15.33,
                    InStock = false,
                    Category = programmingCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "Vue.js for Dummies",
                    Price = 28.49,
                    InStock = false,
                    Category = programmingCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "GoLang - The New Era",
                    Price = 55.55,
                    InStock = false,
                    Category = programmingCategory
                });

                context.SaveChanges();

                Console.WriteLine("Book List\n");

                var query = context.Books.Include(p => p.Category)
                    .Where(p => p.Price < 30.0)
                    .ToList();

                Console.WriteLine("{0,-8} | {1,-50} | {2,-8} | {3}\n\n", "BookID", "Title", "Price", "Category");
                foreach (var book in query)
                    Console.WriteLine("{0,-8} | {1,-50} | {2,-8} | {3}", book.BookID, book.Title, book.Price, book.Category.Name);

                Console.WriteLine("Press any key to exit");
                Console.ReadKey();
            }
        }
    }
}

Aslında Entity Framework ile kod geliştirenlerin gayet aşina olduğu işlemler söz konusu. Senaryoda kitaplar ve kategorilerini tuttuğumuz tipler söz konusu. Book ve BookCategory tipleri için örnek olması açısından Table attribute'ları ile MariaDb tarafındaki tablo adları belirtiliyor. Normal şartlarda Primary Key olan alanın [TipAdı]ID olarak ifade edilmesi yeterli. Ancak BookCategory sınıfı için anahtar alanı işaret etmek üzere Key niteliğinden yararlanmaktayız. Bazı alanlar için maksimum alan uzunluğu ve gereklilik gibi kriterleri de yine nitelikler yardımıyla belirliyoruz(Required, MaxLength nitelikleri) İki sınıf arasında birer ilişki de söz konusu. Tek yönlü kurduğumuz ilişkide bir kategori altında birden fazla kitap olabileceğini ifade ediyoruz. Bu ilişki sağlanırken ICollection<Book> tipinden de yararlanıyoruz.

DbContext türevli BeautyBooksContext sınıfı içerisinde sunduğumuz DbSet'ler var(Books ve BookCategories isimli özellikler) OnConfiguring metodu eziliyor ve içerisinde UseMySql ile provider'ın değiştirilmesi sağlanıyor. Parametre, MariaDb için kullanılacak Connection String bilgisini içermekte. 

Main fonksiyonu içerisinde peş peşe iki Ensure operasyonunun çağırıldığı görülebilir. İlk olarak eğer BeautyBooks veritabanı varsa siliniyor ki bu şart değil. Sadece örnekte deneme olarak kullandım. EnsureCreated çağrısı ile de veritabanı ve ilgili nesnelerin(tabloların) olmaması halinde yaratılmaları sağlanıyor. Örnek ilk çalıştırıldığında aslında bu veritabanı ve tablolar sistemde yok. Bu yüzden önce oluşturulacaklar. 

İlerleyen kod satırlarında Context'e bir kaç veri girişi yapılıyor. SaveChanges ile de yapılan eklemelerin veritabanına yazılması sağlanıyor. Ekleme işlemleri sonrası bir LINQ sorgusu var. Fiyatı 30 birim altında olan ürünleri kategorileri ile birlikte çekip ve ekrana düzgün bir formatta basıyor. 

Artık uygulama çalışabilir. Console penceresine yansıyan çıktı aşağıdaki gibi olacaktır.

Diğer yandan MariaDb üzerindeki duruma da kendi arabiriminden bakılabilir. Ben aşağıdaki komutları denedim.

show databases;
use BeautyBooks;
show tables;
select * from Book;
select * from Category;

İlk komut ile veritabanlarını gösteriyoruz. Kodun çalışması sonrası oluşan BeautyBooks üzerinde işlemler yapmak istediğimiz için use komutunu kullanıyoruz. Veritabanı seçiminin ardından show tables ile tabloları gösteriyoruz ki Book ve Category nesnelerinin oluşturulduğunu görebiliriz. İki standart Select sorgusu ile de tablo içeriklerini göstermekteyiz(Bu arada MariaDb'nin terminal çıktıları çok hoşuma gitti. Bana üniversite yıllarımda DOS ekranında Pascal ile tablo çizip içini verilerle doldurmaya çalıştığım günleri anımsattı. Çok basit bir görünüm ama sade ve anlaşılır)

Log Eklemeyi Unutunca Ben

Aslında arka planda MariaDb üzerinde ne gibi SQL ifadelerinin çalıştırıldığını görebilsem hiç de fena olmazdı. Ben tabii bunu unuttum ve makaleyi tekrardan düzenlediğim bir ara fark ettim. Hemen Microsoft dokümanlarını kurcalayarak en azından Console'a log'ları nasıl atabilirimi araştırdım. Sonrasında BeautyBookContext içeriğini aşağıdaki hale getirdim.

public partial class BeautyBooksContext : DbContext
    {
        public static readonly LoggerFactory EFLoggerFactory
            = new LoggerFactory(new[] {new ConsoleLoggerProvider((cat, level) =>
            cat== DbLoggerCategory.Database.Command.Name && level==LogLevel.Information,true)});

        public DbSet<Book> Books { get; set; }
        public DbSet<BookCategory> BookCategories { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseLoggerFactory(EFLoggerFactory);
            optionsBuilder.UseMySql(@"uid=root;pwd=rootşifre;Host=localhost;Database=BeautyBooks;");
        }
    }

Aslında bu kod parçasında çok güzel bir ders var. Bir Log mekanizmasını EF çalışma motoruna enjekte ediyoruz.  EFLoggerFactory, LoggerFactory türünden üretilirken ilk parametrede ConsoleLoggerProvider örneği verilmekte. Farklı LoggerProvider'lar da var ve hatta biz de kendi LoggerProvider tipimizi buraya ekleyebiliriz. Yapıcı metodda verilen parametrelerle bir filtreleme yaparak hangi bilgilerin hangi seviyede kayıt altına alınacağını belirtiyoruz. İlgili LoggerFactory'nin kullanılabilmesi içinse UseLoggerFactory operasyonunu devreye alıyoruz. Sonuçta kodu çalıştırdığımızda arka planda hareket eden SQL ifadelerinin Console penceresine basıldığını görebiliriz.

Görüldüğü üzere Entity Framework ile MySQL türevli MariaDb'yi kullanmak oldukça basit. Elbette önemli olan konulardan birisi EF çalışma zamanına MariaDb provider'ının enjekte edilmesi. Yani DbContext türevli sınıfın ezilen OnConfiguring fonksiyonu içerisinde yapılan UseMySql çağrısı. Bu sebepten github'taki kod içeriğini incelemekte de oldukça büyük yarar olduğu kanısındayım. Nitekim kendi veritabanı sistemimizin Entity Framework Core tarafında kullanılmasını istediğimiz durumlarda benzer kod çalışmasını yapmamız gerekecektir. Böylece bir cumartesi gecesini daha eğlenceli şekilde bitiriyorum. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

EF Core ile MariaDb Kullanımı

$
0
0

Merhaba Arkadaşlar,

Son bir kaç aydır Cumartesi gecelerimi bir şeyler yazmak veya araştırmak için değerlendirmekteyim. Bu tip çalışma disiplinlerini daha önceden de denemiş ve epeyce faydasını görmüştüm. Sonuçta üzerinde çalıştığımız yazılım platformları ve ürünler sürekli ve düzenli olarak değişim içerisindeler. Dolayısıyla yeniliklerin ucundan da olsa tutabilmek lazım. Bir anlamda şu meşhur Pomodoro çalışma tekniğini haftalık periyotlara böldüğümü ifade edebilirim.

Geçtiğimiz hafta içerisinde .Net Core tarafında nelere bakabilirim diye internette sörf yaparken eski dostumuz Entity Framework'e rastladım. Tabii oyun sahası benim için artık değişmişti. West-World, Microsoft Sql Server nedir pek bilmiyordu. Hatta IIS'e bile burun kıvırıp Nginx ya da Apache ile konuşuyordu. Elimde pek çok makalede kullanılan Visual Studio' da yoktu. Buna rağmen yetenekleri daha kısıtlı olan Visual Studio Code ile hayat oldukça güzeldi. Sonunda bir süredir merak ettiğim MariaDb'yi, Entity Framework Core ile basitçe nasıl konuşturabilleceğimi görmeye karar verdim. Öncelikle MariaDb'yi tanımlayalım...

MariaDB GNU lisansı ile sunulan, MySQL’in yaratıcısı Monty Widenius‘un kodlarını çatallayarak(fork işlemi) aynı komutları, arayüzleri ve API’leri destekleyecek şekilde geliştirmeye başlanmış bir ilişkisel veritabanı yönetim sistemi(Relational Database Management System) Her ne kadar sevimsiz bir tanımlama gibi dursa da logosu en az MySQL'in sevimli yunusu kadar dikkat çekici. Nihayetinde neden böyle bir oluşma gidildiğine dair internette pek çok tartışma var. Onları araştırmanızı öneririm. Benim dikkatimi çeken işin içerisine Oracle girdikten sonra inanılmaz derecede artan destek ücretleri(Sun'ın 2010 da satın alınması ve MySQL'in Oracle'e geçmesi sonrası destek ücretinin %600 arttığı söyleniyor) Bu düşünceler ışığında geçtim evdeki Ubuntu'nun başına.

MariaDb kurulumu

Tabii ilk olarak West-World'e MariaDb'yi kurmam lazım. Son zamanlarda bu dünya üzerinde çok fazla şey yüklenip kaldırıldı ama bana mısın demedi. Sanırım o da bu yeni deneyimlerden keyif alıyor. Kendisine sırasıyla aşağıdaki komutları gönderek MariaDb'yi yüklemesini ve gerekli servisleri çalıştırmasını söyledim. 

sudo apt-get update
sudo apt-get install mariadb-server mariadb-client
sudo mysql_secure_installation

Son komut sonrası bazı sorularla karşılaştım. İlk etapta root user şifresinin değiştirilmesinin yerinde olacağını belirteyim. Kurulum sorasında root kullanıcının şifresi boştur. Dolayısıyla Enter tuşuna basarak geçilebilir. Ardından bir şifre vermek gerekecektir. Şifre adımından sonra isimsiz kullanıcılara(anonymous user) izin verilip verilmeyeceği, root kullanıcı için uzaktan erişim sağlanıp sağlanmayacağı, test veritabanının kaldırılıp kaldırılmayacağı ve benzeri sorulara verilecek cevaplarla kurulum işlemi sonlandırılır. Kurulum sonrası aşağıdaki komutu kullanarak MariaDb ortamına giriş yapabiliyor olmamız gerekir.

sudo mysql -u root -p

Yukarıdaki ekran görüntüsünde show databases; komutu kullanılarak yüklü olan veritabanlarının listesinin elde edilmesi sağlanmışıtır. Burada basit SQL sorgu ifadeleri kullanarak bir takım işlemler yapabiliriz. Bir deneyin derim.

Bu arada eğer sudo kullanmadan giriş yapmak istersek yada ERROR 1698 (28000): Access denied for user 'burakselyum'@'localhost' benzeri bir hata alırsak, root user'ın mysql_native_password plug-in'inini kullanabilmesini söyleyerek çözüm üretebiliriz.

use mysql;
update user set plugin='mysql_native_password' where user='root';
flush privileges; 
exit;

sonrasında

service mysql restart 

ile mysql hizmetini yeniden başlatmak gerekir.

Console Uygulamasının Yazılması

Gelelim MariaDb ile konuşacak uygulama kodlarının yazımına. Konuya basit bir girizgah yapmak istediğim için her zaman ki gibi tercihim Console uygulaması geliştirmek. Ancak siz örneği denerken bir Web API servisi arkasınada da kullanmayı tercih edebilirsiniz.

dotnet new console -o MariaDbSample

Şimdi gerekli paketleri de eklemek lazım. MariaDb'yi Entity Framework Core ile kolayca kullanabilmek için Pomelo tarafından geliştirilmiş bir paket bulunmakta. Bu ve diğer EF paketlerini aşağıdaki komutları kullanarak projeye entegre edebiliriz.

dotnet add package Microsoft.EntityFrameworkCore.Tools -v:'2.0.0'
dotnet add package Microsoft.EntityFrameworkCore.Tools.DotNet -v:'2.0.0'
dotnet add package Pomelo.EntityFrameworkCore.MySql -v:'2.0.0.1'

Paketler yüklendikten sonra bir restore işlemi uygulamakta yarar var.

dotnet restore 

Program.cs içeriği ise aşağıdaki gibi. Kalabalık göründüğünü bakmayın. Büyük bir kısmı kitap ve kategori ekleme kodlarından oluşuyor.

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace MariaDbSample
{
    [Table("Book")]
    public class Book
    {
        public long BookID { get; set; }
        [Required]
        [MaxLength(50)]
        public string Title { get; set; }
        public bool InStock { get; set; }
        public double Price { get; set; }
        public virtual BookCategory Category { get; set; }
    }

    [Table("Category")]
    public class BookCategory
    {
        [Key]
        public long CategoryID { get; set; }
        [Required]
        [MaxLength(20)]
        public string Name { get; set; }
        public virtual ICollection<Book> Books { get; set; }
    }

    public partial class BeautyBooksContext : DbContext
    {
        public DbSet<Book> Books { get; set; }
        public DbSet<BookCategory> BookCategories { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseMySql(@"uid=root;pwd=rootşifre;Host=localhost;Database=BeautyBooks;");
        }
    }

    public class Program
    {
        public static void Main()
        {
            using (var context = new BeautyBooksContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();

                var scienceCategory = new BookCategory()
                {
                    Name = "Science"
                };
                var programmingCategory = new BookCategory()
                {
                    Name = "Programming"
                };
                var mathCategory = new BookCategory()
                {
                    Name = "Math"
                };
                context.BookCategories.Add(scienceCategory);
                context.BookCategories.Add(programmingCategory);

                context.Books.Add(new Book()
                {
                    Title = "2025 : Go to the MARS",
                    Price = 8.99,
                    InStock = true,
                    Category = scienceCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "The 9nth Element",
                    Price = 6.99,
                    InStock = false,
                    Category = scienceCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "Calculus - I",
                    Price = 19.99,
                    InStock = false,
                    Category = mathCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "Advanced Asp.Net Core 2.0",
                    Price = 38.10,
                    InStock = true,
                    Category = programmingCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "C# 7.0 Introduction",
                    Price = 15.33,
                    InStock = false,
                    Category = programmingCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "Vue.js for Dummies",
                    Price = 28.49,
                    InStock = false,
                    Category = programmingCategory
                });
                context.Books.Add(new Book()
                {
                    Title = "GoLang - The New Era",
                    Price = 55.55,
                    InStock = false,
                    Category = programmingCategory
                });

                context.SaveChanges();

                Console.WriteLine("Book List\n");

                var query = context.Books.Include(p => p.Category)
                    .Where(p => p.Price < 30.0)
                    .ToList();

                Console.WriteLine("{0,-8} | {1,-50} | {2,-8} | {3}\n\n", "BookID", "Title", "Price", "Category");
                foreach (var book in query)
                    Console.WriteLine("{0,-8} | {1,-50} | {2,-8} | {3}", book.BookID, book.Title, book.Price, book.Category.Name);

                Console.WriteLine("Press any key to exit");
                Console.ReadKey();
            }
        }
    }
}

Aslında Entity Framework ile kod geliştirenlerin gayet aşina olduğu işlemler söz konusu. Senaryoda kitaplar ve kategorilerini tuttuğumuz tipler söz konusu. Book ve BookCategory tipleri için örnek olması açısından Table attribute'ları ile MariaDb tarafındaki tablo adları belirtiliyor. Normal şartlarda Primary Key olan alanın [TipAdı]ID olarak ifade edilmesi yeterli. Ancak BookCategory sınıfı için anahtar alanı işaret etmek üzere Key niteliğinden yararlanmaktayız. Bazı alanlar için maksimum alan uzunluğu ve gereklilik gibi kriterleri de yine nitelikler yardımıyla belirliyoruz(Required, MaxLength nitelikleri) İki sınıf arasında birer ilişki de söz konusu. Tek yönlü kurduğumuz ilişkide bir kategori altında birden fazla kitap olabileceğini ifade ediyoruz. Bu ilişki sağlanırken ICollection<Book> tipinden de yararlanıyoruz.

DbContext türevli BeautyBooksContext sınıfı içerisinde sunduğumuz DbSet'ler var(Books ve BookCategories isimli özellikler) OnConfiguring metodu eziliyor ve içerisinde UseMySql ile provider'ın değiştirilmesi sağlanıyor. Parametre, MariaDb için kullanılacak Connection String bilgisini içermekte. 

Main fonksiyonu içerisinde peş peşe iki Ensure operasyonunun çağırıldığı görülebilir. İlk olarak eğer BeautyBooks veritabanı varsa siliniyor ki bu şart değil. Sadece örnekte deneme olarak kullandım. EnsureCreated çağrısı ile de veritabanı ve ilgili nesnelerin(tabloların) olmaması halinde yaratılmaları sağlanıyor. Örnek ilk çalıştırıldığında aslında bu veritabanı ve tablolar sistemde yok. Bu yüzden önce oluşturulacaklar. 

İlerleyen kod satırlarında Context'e bir kaç veri girişi yapılıyor. SaveChanges ile de yapılan eklemelerin veritabanına yazılması sağlanıyor. Ekleme işlemleri sonrası bir LINQ sorgusu var. Fiyatı 30 birim altında olan ürünleri kategorileri ile birlikte çekip ve ekrana düzgün bir formatta basıyor. 

Artık uygulama çalışabilir. Console penceresine yansıyan çıktı aşağıdaki gibi olacaktır.

Diğer yandan MariaDb üzerindeki duruma da kendi arabiriminden bakılabilir. Ben aşağıdaki komutları denedim.

show databases;
use BeautyBooks;
show tables;
select * from Book;
select * from Category;

İlk komut ile veritabanlarını gösteriyoruz. Kodun çalışması sonrası oluşan BeautyBooks üzerinde işlemler yapmak istediğimiz için use komutunu kullanıyoruz. Veritabanı seçiminin ardından show tables ile tabloları gösteriyoruz ki Book ve Category nesnelerinin oluşturulduğunu görebiliriz. İki standart Select sorgusu ile de tablo içeriklerini göstermekteyiz(Bu arada MariaDb'nin terminal çıktıları çok hoşuma gitti. Bana üniversite yıllarımda DOS ekranında Pascal ile tablo çizip içini verilerle doldurmaya çalıştığım günleri anımsattı. Çok basit bir görünüm ama sade ve anlaşılır)

Log Eklemeyi Unutunca Ben

Aslında arka planda MariaDb üzerinde ne gibi SQL ifadelerinin çalıştırıldığını görebilsem hiç de fena olmazdı. Ben tabii bunu unuttum ve makaleyi tekrardan düzenlediğim bir ara fark ettim. Hemen Microsoft dokümanlarını kurcalayarak en azından Console'a log'ları nasıl atabilirimi araştırdım. Sonrasında BeautyBookContext içeriğini aşağıdaki hale getirdim.

public partial class BeautyBooksContext : DbContext
    {
        public static readonly LoggerFactory EFLoggerFactory
            = new LoggerFactory(new[] {new ConsoleLoggerProvider((cat, level) =>
            cat== DbLoggerCategory.Database.Command.Name && level==LogLevel.Information,true)});

        public DbSet<Book> Books { get; set; }
        public DbSet<BookCategory> BookCategories { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseLoggerFactory(EFLoggerFactory);
            optionsBuilder.UseMySql(@"uid=root;pwd=rootşifre;Host=localhost;Database=BeautyBooks;");
        }
    }

Aslında bu kod parçasında çok güzel bir ders var. Bir Log mekanizmasını EF çalışma motoruna enjekte ediyoruz.  EFLoggerFactory, LoggerFactory türünden üretilirken ilk parametrede ConsoleLoggerProvider örneği verilmekte. Farklı LoggerProvider'lar da var ve hatta biz de kendi LoggerProvider tipimizi buraya ekleyebiliriz. Yapıcı metodda verilen parametrelerle bir filtreleme yaparak hangi bilgilerin hangi seviyede kayıt altına alınacağını belirtiyoruz. İlgili LoggerFactory'nin kullanılabilmesi içinse UseLoggerFactory operasyonunu devreye alıyoruz. Sonuçta kodu çalıştırdığımızda arka planda hareket eden SQL ifadelerinin Console penceresine basıldığını görebiliriz.

Görüldüğü üzere Entity Framework ile MySQL türevli MariaDb'yi kullanmak oldukça basit. Elbette önemli olan konulardan birisi EF çalışma zamanına MariaDb provider'ının enjekte edilmesi. Yani DbContext türevli sınıfın ezilen OnConfiguring fonksiyonu içerisinde yapılan UseMySql çağrısı. Bu sebepten github'taki kod içeriğini incelemekte de oldukça büyük yarar olduğu kanısındayım. Nitekim kendi veritabanı sistemimizin Entity Framework Core tarafında kullanılmasını istediğimiz durumlarda benzer kod çalışmasını yapmamız gerekecektir. Böylece bir cumartesi gecesini daha eğlenceli şekilde bitiriyorum. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/net-core-configuration-management-uzerine.Net Core Konfigurasyon Yönetimi Üzerine

$
0
0

Merhaba Arkadaşlar,

West-World üzerinde bir şeyler araştırmak için vaktim olan bir haftayı geride bırakmak üzereyim. Internet üzerinde derya deniz içerik olsa da bazen ne araştıracağımı şaşırmıyor değilim. İşte bu anlarda MSDN dokümanları imdadıma yetişiyor. İlk zamanlarından beri oldukça verimli olduğunu düşündüğüm içerik son yıllarda çok daha profesyonelleşti(Tabii MSDN dokümanlarını CD veya DVD olarak edindiğimiz zamanları da hatırlıyorum)İşin aslı sadece MSDN değil, yazılım ürünü sahibi pek çok öncünün teknik destek dokümanları inanılmaz derecede doyurucu ve birbirleriyle yarışır durumdalar. Son zamanlarda uğradıklarım arasında Google Cloud Platform ve Amazon Web Services var. Bu rehberler ilk kaynak niteliğinde olduğu için bir şeyleri öğrenebilmemiz adına doğru adresler. Hatta yazılım geliştirici olarak ortalama bir seviyenin üstüne çıktıktan sonra bunlar gibi dokümantasyonlara uğramak, rastgele bir ürün seçip teknik dokümantasyonunu okumak, Get Started örneklerini yapıp bir şeylerin farkına varabilmek gerekiyor. Sözü fazla uzatmadan gerekli mesajları da verdiğimi düşünerek yazımıza başlayalım diyorum.

Çalışma zamanına bilgi taşımanın ve bazı ayarlamalar için gerekli değerleri okumanın en popüler yollarından birisi de bildiğiniz üzere konfigurasyon dosyalarından yararlanmak. Zaman içerisinde app.config, web.config gibi XML tabanlı konfigurasyon dosyalarına aşina olan bizler, .Net Core ile birlikte JSON formatlı içeriklerle çalışmaya başladık. .Net Core tarafında bu JSON içeriklerini yönetmek oldukça kolay. Farklı yöntemlerimiz var. Dahası Dependency Injection yeteneklerinden yararlanılabildiği için özel sekmelerin(section) sınıflara bağlanması da münkün(Hatta Interface Segregation Principle ve Seperation of Concerns ilkelerini kullanan Options deseni var ki ilk fırstatta inceleyip öğrenmek istiyorum. Farkında olmadan kullanıyoruz ama altındaki çalışma dinamiklerini öğrenmek çok yerinde olacaktır) Gelin bir kaç basit örnek ile konfigurasyon yönetimini nasıl yapabileceğimizi incelemeye çalışalım. Ağırlıklı olarak varsayılan konfigurasyon dosyaları haricinde kendi özel içeriklerimizle çalışacağız. Kodlarımızı Console tabanlı bir uygulamada deneyimleyeceğiz ama aynı teknikleri Web, Web API gibi diğer proje türlerinde de kullanabilirsiniz.

dotnet new console -o CustomConfig

Kendi JSON İçeriğimiz İle Çalışmak

İlk örnek için aşağıdaki içeriğe sahip olan aws.json isimli bir dosyadan yararlanacağız.

{
  "default_region": "east-2",
  "provider": "amazon",
  "region": {
    "name": "east-2",
    "address": "amazonda.bir.yer.east-2"
  },
  "services": [
    {
      "address": "products/get",
      "response_type": "json",
      "isPublic": "true"
    },
    {
      "address": "products/get/{categoryName}",
      "response_type": "json",
      "isPublic": "true"
    }
  ]
}

Tamamen hayal ürünü olan içerikte iç içe geçen alanlar da yer alıyor. default_region, provider, region ve services aynı seviyede olmakla birlikte, services içerisinde n sayıda eleman bulunabiliyor. Amacımız bu içeriği çalışma zamanında okuyabilmek. Özellikle JSON tabanlı çalışacağımız için bize gerekli fonksiyonellikleri sağlayacak iki pakete de ihtiyacımız bulunuyor. 

dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.CommandLine

Bu paketleri ekledikten sonra ilk örnek kodlarımızı aşağıdaki gibi yazabiliriz.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Configuration;

namespace CustomConfig
{
    class Program
    {
        static void Main(string[] args)
        {
            ConfigSupervisor rubio = new ConfigSupervisor();
            rubio.ExecuteJsonSample();
        }
    }

    public class ConfigSupervisor
    {
        public IConfigurationRoot ConfigurationManager { get; set; }

        public void ExecuteJsonSample()
        {
            var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("aws.json");

            ConfigurationManager = builder.Build();

            Console.WriteLine($"default_region: \t{ConfigurationManager["default_region"]}");
            Console.WriteLine($"provider: \t{ConfigurationManager["provider"]}");
            Console.WriteLine($"region name: \t{ConfigurationManager["region:name"]}");
            Console.WriteLine($"region address: \t{ConfigurationManager["region:address"]}");
            Console.WriteLine($"service[1] address : \t{ConfigurationManager["services:1:address"]}");
            Console.WriteLine($"service[1] type: \t{ConfigurationManager["services:1:response_type"]}");
            Console.WriteLine($"service[1] isPublic: \t{ConfigurationManager["services:1:isPublic"]}");

            var services = ConfigurationManager.GetSection("services").AsEnumerable();
            foreach (var service in services)
            {
                Console.WriteLine($"{service.Key}-{service.Value}");
            }
        }
    }
}

ConfigSupervisor sınıfı örneklere ait fonksiyonellikleri içeriyor. ExecuteJsonSample metodumuzun başında aws.json dosyasını ele alması için bir ConfigurationBuilder örneği oluşturuyoruz. Build çağrısı sonucu IConfigurationRoot arayüzü üzerinden taşınabilecek bir nesne örneği elde ediyoruz. Sonuç olarak indeksleyici operatörünü kullanarak konfigurasyon öğelerine erişim sağlıyoruz. root altında yer alan default_region ve provider alanlarının değerlerine erişmek oldukça kolay. region içerisindeki name niteliğine erişmek içinse : operatörünü kullanıyoruz(region:name şeklinde)

Bu notasyona göre services olarak isimlendirilmiş array içerisindeki bir elemana erişirken index değerini kullanarak ilerleyebiliyoruz. Söz gelimi services:1:isPublic ile 1 indisli elemanın isPublic niteliğinin değerine ulaşmış oluyoruz. Elbette services isimli dizinin elemanlarını bir döngü yardımıyla okuyabiliriz de. GetSection fonksiyonu ile konfigurasyon yöneticisinin okuduğu dosyadan ilgili sekmeyi almamız yeterli. AsEnumerable metodu ile üzerinde ileri yönlü hareket edilebilir hale getirdikten sonra Key ve Value değerlerine erişmemiz oldukça basit. Uygulamanın çalışma zamanı çıktısı aşağıdaki gibi olacaktır(services sekmesini daha iyi okumanın bir yolunu bul Burak! O ne öyle -, 1-,0- :D )

JSON İçeriğini Sınıflar ile İlişkilendirmek

Peki JSON dosyasının içeriğinde yer alan sekmeleri birer tiple ilişkilendirmek istersek? Ki zaten ezelden beridir konfigurasyon içerikleri .Net dünyasında sınıflarla ilişkilendirilip yönetimli kod tarafında kullanılabiliyorlar. Bunu .Net Core ortamında JSON içeriklerimiz için gerçekleştirmemiz de mümkün. İlk olarak aşağıdaki json içeriğini barındıracak gamesettings.json dosyasını projemize ekleyelim.

{
    "Game": {
        "Requirement": {
            "OS": "Ubuntu",
            "RAM": "8",
            "Region": "west-world",
            "Online": "true"
        },
        "Contacts": [
            {
                "Name": "tech support",
                "Email": "techsupport@west-world.bla.bla"
            },
            {
                "Name": "game master",
                "Email": "gamemaster@west-world.bla.bla"
            },
            {
                "Name": "help desk",
                "Email": "helpdesk@west-world.bla.bla"
            }
        ]
    }
}

Bu içeriğin kod tarafındaki karşılığı olacak sınıflarımızı ise aşağıdaki gibi yazalım. 

Tüm JSON içeriğini işaret edecek GameSetting sınıfı

using System.Collections.Generic;

public class GameSetting
{
    public Requirement Requirement { get; set; }
    public IEnumerable<Contact> Contacts{get;set;}
}

GameSettings sekmesinde yer alan Contacts bir dizi olduğu için, IEnumerable<Contact> tipinden Contacts isimli bir özellik söz konusu.

Requirement kısmını işaret eden Requirement sınıfı;

using System.Collections.Generic;

public class Requirement
{
    public string OS { get; set; }
    public int RAM { get; set; }
    public string Region { get; set; }
    public bool Online { get; set; }
}

ve son olarak Contacts sekmesi altındaki bağlantıların her birisini işaret edecek Contact sınıfı.

public class Contact
{
    public string Name { get; set; }
    public string Email { get; set; }
}

Sınıfımıza ekleyeceğimiz fonksiyonumuz ise şu şekilde yazılabilir.

public void ExecuteObjectGraphSample()
{
    var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("gamesettings.json");

    ConfigurationManager = builder.Build();

    var gameConfig = new GameSetting();
    ConfigurationManager.GetSection("Game").Bind(gameConfig);
    var requirement=gameConfig.Requirement;
    Console.WriteLine($"OS {requirement.OS} ({requirement.RAM} Ram)");            
    foreach(var contact in gameConfig.Contacts)
    {
        Console.WriteLine($"{contact.Name}({contact.Email})");
    }
}

Öncelikle gamesettings.json dosyasını ele alacak ConfigurationBuilder örneği oluşturulup Build operasyonu ile konfigurasyon yöneticisi üretiliyor. Sonraki kısım ise epey keyifli. GetSection ile yakalanacak olan Game içeriğini Bind metodundan yararlanarak GameSetting nesne örneği olan gameConfig'e bağlıyoruz. JSON konfigurasyonundaki isimlendirmelere göre Bind metodu doğru eşleştirmeleri bizim için otomatik olarak yapacak. Sonrasında örnek olması açısından OS ve RAM bilgileri ile firma kontaklarına ait Name ve Email değerlerini ekrana yazdırıyoruz. Dikkat edilmesi gereken nokta bir önceki örnekten farklı olarak tüm bu değerlerin Bind işlemi sonrası JSON İçeriğine bağlanan gameConfig nesnesi üzerinden yakalanabilmesi. Çalışma zamanı sonuçları aşağıdaki gibi olacaktır.

Bellekte Konuşlandırılmış Konfigurasyon İçeriği ile Çalışmak

MSDN dokümanlarından öğrendiğim ilginç örneklerden birisi de konfigurasyon bilgilerinin in-memory olarak tutulup yönetilebilmesi. Yeni fonksiyonumuz ExecuteInMemorySample'ı aşağıdaki gibi yazalım.

public void ExecuteInMemorySample()
{
    var builder = new ConfigurationBuilder();

    var parameters = new Dictionary<string, string>{
        {"Region:Name","east-us-2"},
        {"Region:BaseAddress","amazon.da.bir.yer/west-world/api"},
        {"Artifact:Service:Name","products"},
        {"Artifact:Service:MaxConcurrentCall","3500"},
        {"Artifact:Service:Type","json"},
        {"Artifact:Service:IsPublic","true"}
    };

    builder.AddInMemoryCollection(parameters);
    ConfigurationManager = builder.Build();
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:Name"]}");
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:Type"]}");
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:MaxConcurrentCall"]}");
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:IsPublic"]}");

    var service = new Service();
    ConfigurationManager.GetSection("Artifact:Service").Bind(service);
    Console.WriteLine($"{service.Name},{service.MaxConcurrentCall},{service.Type},{service.IsPublic}");
}

Kodun kilit noktası builder örneği üzerinden çağırılan AddInMemoryCollection metodu. Bu metoda parametre olarak parameters isimli Dictionary<string,string> tipinden bir koleksiyon verilmekte. Dictionary, key:value şeklindeki konfigurasyon mantığına uygun olduğu için biçilmiş kaftandır. Tabii alt elemanlar için yine : ayracına başvurulur. Örnek koleksiyonda Region ve Artifact aynı seviyede yer alan elemanlardır. Artifact altında Service ve onun altında da Name, MaxConcurrentCall, Type ve IsPublic isimli nitelikler yer almaktadır. 

Build çağrısı sonrası bu bilgilere ConfigurationManager isimli IConfigurationRoot arayüzü üzerinden erişilebilir. Dahası bellekte konuşlandırılan bu konfigurasyon içeriği herhangibir seviyesi için bir nesneye de bağlanabilir. Service isimli aşağıdaki sınıfı göz önüne aldığımızda,

public class Service
{
    public string Name { get; set; }
    public int MaxConcurrentCall { get; set; }
    public string Type { get; set; }
    public bool IsPublic { get; set; }
}

GetSection("Artifact:Service").Bind(service) çağrımı ile Artifact altındaki Service içeriğinin ilgili nesne örneğine bağlanması sağlanmış olur. Bu noktadan sonra MaxConcurrentCall, Name gibi özelliklere yönetimli kod üzerinden erişilebilinir. Fonksiyonun çalışma zamanı çıktısı aşağıdaki gibidir.

Konfigurasyon Parametrelerini Komut Satırından Göndermek

Bir önceki örnekte kullandığımız In-memory çözümünde, parametre değerlerinin komut satırından gönderilmesi de mümkündür. Bu güzel ve ilginç bir kullanım şekli olsa de pek çok durumda işimize yarayabilir. Peki nasıl yapabiliriz? Aşağıdaki metodu ConfigSupervisor sınıfımıza ekleyerek devam edelim(Başta eklediğimiz Microsoft.Extensions.Configuration.CommandLine paketi bu örnek için gerekli)

public void ExecuteCommandLineSample(string[] args=null)
{
    var builder=new ConfigurationBuilder();
    var connection=new Dictionary<string,string>{
        {"Connection:Value","data source=aws;provider:amazon;"},
        {"Connection:Name","aws"}
    };
    builder
    .AddInMemoryCollection(connection)
    .AddCommandLine(args);

    ConfigurationManager=builder.Build();
    Console.WriteLine($"Connection : {ConfigurationManager["Connection:Value"]}");
    Console.WriteLine($"Connection : {ConfigurationManager["Connection:Timeout"]}");
}

Yine bellekte tutulan bir konfigurasyon içeriği söz konusu. Bunun için generic Dictionary koleksiyonunu kullandık. builder üzerinden çağırdığımız AddCommandLine fonksiyonuna parametre olarak gelen args dizisinin içeriği tahmin edeceğiniz üzere komut satırından gelecek. Kodun ilerleyen satırlarında Connection:Value ve Connection:Name değerlerini ekrana bastrırıyoruz. Main kodunun içeriğini de aşağıdaki hale getirelim. Tek yaptığımız Main fonksiyonuna gelen args değişkenini ExecuteCommandLineSample çağrısına parametre olarak geçmek.

static void Main(string[] args)
{
    ConfigSupervisor rubio = new ConfigSupervisor();
    rubio.ExecuteCommandLineSample(args);
}

Eğer programımızı aşağıdaki gibi çalıştırırsak konfigurasyon içeriğinin bizim istediğimiz gibi değiştiğini görürüz.

dotnet run Connection:Value="Azure;timeout=1000;region=EU-1" Connection:Name="azure"

Tabii bu parametreyi vermeden uygulamayı çalıştırırsak varsayılan Connection:Value ve Connection:Name değerlerine ulaşırız. Bu arada tüm parametreleri detaylı olarak girmek zorunda değiliz. İsimle ulaştığımız için sadece değiştirmek istediklerimizi girebilir veya farklı sıralarda atamalar yapabiliriz. Aşağıdaki çalışma zamanı sonuçlarına bu anlamda bakabilirsiniz.

Konfigurasyon yönetimi ile ilgili daha pek çok şey var(Ben MSDN'in şu adresteki oldukça doyurucu dokümanını izleyerek öğrenmeye çalışıyorum)Örneğin özel bir Entity Framework provider'ının oluşturulması, komut satırı argümanlarında switch mapping tekniğinin kullanılması gibi konulara bu adresten bakılabilir. Şimdilik benden bu kadar. Tekrardan görüşünceye dek hepinize mutlu günler dilerim. 

Örnek kodlara Git üzerinden de erişebilirsiniz.


.Net Core Konfigurasyon Yönetimi Üzerine

$
0
0

Merhaba Arkadaşlar,

West-World üzerinde bir şeyler araştırmak için vaktim olan bir haftayı geride bırakmak üzereyim. Internet üzerinde derya deniz içerik olsa da bazen ne araştıracağımı şaşırmıyor değilim. İşte bu anlarda MSDN dokümanları imdadıma yetişiyor. İlk zamanlarından beri oldukça verimli olduğunu düşündüğüm içerik son yıllarda çok daha profesyonelleşti(Tabii MSDN dokümanlarını CD veya DVD olarak edindiğimiz zamanları da hatırlıyorum)İşin aslı sadece MSDN değil, yazılım ürünü sahibi pek çok öncünün teknik destek dokümanları inanılmaz derecede doyurucu ve birbirleriyle yarışır durumdalar. Son zamanlarda uğradıklarım arasında Google Cloud Platform ve Amazon Web Services var. Bu rehberler ilk kaynak niteliğinde olduğu için bir şeyleri öğrenebilmemiz adına doğru adresler. Hatta yazılım geliştirici olarak ortalama bir seviyenin üstüne çıktıktan sonra bunlar gibi dokümantasyonlara uğramak, rastgele bir ürün seçip teknik dokümantasyonunu okumak, Get Started örneklerini yapıp bir şeylerin farkına varabilmek gerekiyor. Sözü fazla uzatmadan gerekli mesajları da verdiğimi düşünerek yazımıza başlayalım diyorum.

Çalışma zamanına bilgi taşımanın ve bazı ayarlamalar için gerekli değerleri okumanın en popüler yollarından birisi de bildiğiniz üzere konfigurasyon dosyalarından yararlanmak. Zaman içerisinde app.config, web.config gibi XML tabanlı konfigurasyon dosyalarına aşina olan bizler, .Net Core ile birlikte JSON formatlı içeriklerle çalışmaya başladık. .Net Core tarafında bu JSON içeriklerini yönetmek oldukça kolay. Farklı yöntemlerimiz var. Dahası Dependency Injection yeteneklerinden yararlanılabildiği için özel sekmelerin(section) sınıflara bağlanması da münkün(Hatta Interface Segregation Principle ve Seperation of Concerns ilkelerini kullanan Options deseni var ki ilk fırstatta inceleyip öğrenmek istiyorum. Farkında olmadan kullanıyoruz ama altındaki çalışma dinamiklerini öğrenmek çok yerinde olacaktır) Gelin bir kaç basit örnek ile konfigurasyon yönetimini nasıl yapabileceğimizi incelemeye çalışalım. Ağırlıklı olarak varsayılan konfigurasyon dosyaları haricinde kendi özel içeriklerimizle çalışacağız. Kodlarımızı Console tabanlı bir uygulamada deneyimleyeceğiz ama aynı teknikleri Web, Web API gibi diğer proje türlerinde de kullanabilirsiniz.

dotnet new console -o CustomConfig

Kendi JSON İçeriğimiz İle Çalışmak

İlk örnek için aşağıdaki içeriğe sahip olan aws.json isimli bir dosyadan yararlanacağız.

{
  "default_region": "east-2",
  "provider": "amazon",
  "region": {
    "name": "east-2",
    "address": "amazonda.bir.yer.east-2"
  },
  "services": [
    {
      "address": "products/get",
      "response_type": "json",
      "isPublic": "true"
    },
    {
      "address": "products/get/{categoryName}",
      "response_type": "json",
      "isPublic": "true"
    }
  ]
}

Tamamen hayal ürünü olan içerikte iç içe geçen alanlar da yer alıyor. default_region, provider, region ve services aynı seviyede olmakla birlikte, services içerisinde n sayıda eleman bulunabiliyor. Amacımız bu içeriği çalışma zamanında okuyabilmek. Özellikle JSON tabanlı çalışacağımız için bize gerekli fonksiyonellikleri sağlayacak iki pakete de ihtiyacımız bulunuyor. 

dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.CommandLine

Bu paketleri ekledikten sonra ilk örnek kodlarımızı aşağıdaki gibi yazabiliriz.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Configuration;

namespace CustomConfig
{
    class Program
    {
        static void Main(string[] args)
        {
            ConfigSupervisor rubio = new ConfigSupervisor();
            rubio.ExecuteJsonSample();
        }
    }

    public class ConfigSupervisor
    {
        public IConfigurationRoot ConfigurationManager { get; set; }

        public void ExecuteJsonSample()
        {
            var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("aws.json");

            ConfigurationManager = builder.Build();

            Console.WriteLine($"default_region: \t{ConfigurationManager["default_region"]}");
            Console.WriteLine($"provider: \t{ConfigurationManager["provider"]}");
            Console.WriteLine($"region name: \t{ConfigurationManager["region:name"]}");
            Console.WriteLine($"region address: \t{ConfigurationManager["region:address"]}");
            Console.WriteLine($"service[1] address : \t{ConfigurationManager["services:1:address"]}");
            Console.WriteLine($"service[1] type: \t{ConfigurationManager["services:1:response_type"]}");
            Console.WriteLine($"service[1] isPublic: \t{ConfigurationManager["services:1:isPublic"]}");

            var services = ConfigurationManager.GetSection("services").AsEnumerable();
            foreach (var service in services)
            {
                Console.WriteLine($"{service.Key}-{service.Value}");
            }
        }
    }
}

ConfigSupervisor sınıfı örneklere ait fonksiyonellikleri içeriyor. ExecuteJsonSample metodumuzun başında aws.json dosyasını ele alması için bir ConfigurationBuilder örneği oluşturuyoruz. Build çağrısı sonucu IConfigurationRoot arayüzü üzerinden taşınabilecek bir nesne örneği elde ediyoruz. Sonuç olarak indeksleyici operatörünü kullanarak konfigurasyon öğelerine erişim sağlıyoruz. root altında yer alan default_region ve provider alanlarının değerlerine erişmek oldukça kolay. region içerisindeki name niteliğine erişmek içinse : operatörünü kullanıyoruz(region:name şeklinde)

Bu notasyona göre services olarak isimlendirilmiş array içerisindeki bir elemana erişirken index değerini kullanarak ilerleyebiliyoruz. Söz gelimi services:1:isPublic ile 1 indisli elemanın isPublic niteliğinin değerine ulaşmış oluyoruz. Elbette services isimli dizinin elemanlarını bir döngü yardımıyla okuyabiliriz de. GetSection fonksiyonu ile konfigurasyon yöneticisinin okuduğu dosyadan ilgili sekmeyi almamız yeterli. AsEnumerable metodu ile üzerinde ileri yönlü hareket edilebilir hale getirdikten sonra Key ve Value değerlerine erişmemiz oldukça basit. Uygulamanın çalışma zamanı çıktısı aşağıdaki gibi olacaktır(services sekmesini daha iyi okumanın bir yolunu bul Burak! O ne öyle -, 1-,0- :D )

JSON İçeriğini Sınıflar ile İlişkilendirmek

Peki JSON dosyasının içeriğinde yer alan sekmeleri birer tiple ilişkilendirmek istersek? Ki zaten ezelden beridir konfigurasyon içerikleri .Net dünyasında sınıflarla ilişkilendirilip yönetimli kod tarafında kullanılabiliyorlar. Bunu .Net Core ortamında JSON içeriklerimiz için gerçekleştirmemiz de mümkün. İlk olarak aşağıdaki json içeriğini barındıracak gamesettings.json dosyasını projemize ekleyelim.

{
    "Game": {
        "Requirement": {
            "OS": "Ubuntu",
            "RAM": "8",
            "Region": "west-world",
            "Online": "true"
        },
        "Contacts": [
            {
                "Name": "tech support",
                "Email": "techsupport@west-world.bla.bla"
            },
            {
                "Name": "game master",
                "Email": "gamemaster@west-world.bla.bla"
            },
            {
                "Name": "help desk",
                "Email": "helpdesk@west-world.bla.bla"
            }
        ]
    }
}

Bu içeriğin kod tarafındaki karşılığı olacak sınıflarımızı ise aşağıdaki gibi yazalım. 

Tüm JSON içeriğini işaret edecek GameSetting sınıfı

using System.Collections.Generic;

public class GameSetting
{
    public Requirement Requirement { get; set; }
    public IEnumerable<Contact> Contacts{get;set;}
}

GameSettings sekmesinde yer alan Contacts bir dizi olduğu için, IEnumerable<Contact> tipinden Contacts isimli bir özellik söz konusu.

Requirement kısmını işaret eden Requirement sınıfı;

using System.Collections.Generic;

public class Requirement
{
    public string OS { get; set; }
    public int RAM { get; set; }
    public string Region { get; set; }
    public bool Online { get; set; }
}

ve son olarak Contacts sekmesi altındaki bağlantıların her birisini işaret edecek Contact sınıfı.

public class Contact
{
    public string Name { get; set; }
    public string Email { get; set; }
}

Sınıfımıza ekleyeceğimiz fonksiyonumuz ise şu şekilde yazılabilir.

public void ExecuteObjectGraphSample()
{
    var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("gamesettings.json");

    ConfigurationManager = builder.Build();

    var gameConfig = new GameSetting();
    ConfigurationManager.GetSection("Game").Bind(gameConfig);
    var requirement=gameConfig.Requirement;
    Console.WriteLine($"OS {requirement.OS} ({requirement.RAM} Ram)");            
    foreach(var contact in gameConfig.Contacts)
    {
        Console.WriteLine($"{contact.Name}({contact.Email})");
    }
}

Öncelikle gamesettings.json dosyasını ele alacak ConfigurationBuilder örneği oluşturulup Build operasyonu ile konfigurasyon yöneticisi üretiliyor. Sonraki kısım ise epey keyifli. GetSection ile yakalanacak olan Game içeriğini Bind metodundan yararlanarak GameSetting nesne örneği olan gameConfig'e bağlıyoruz. JSON konfigurasyonundaki isimlendirmelere göre Bind metodu doğru eşleştirmeleri bizim için otomatik olarak yapacak. Sonrasında örnek olması açısından OS ve RAM bilgileri ile firma kontaklarına ait Name ve Email değerlerini ekrana yazdırıyoruz. Dikkat edilmesi gereken nokta bir önceki örnekten farklı olarak tüm bu değerlerin Bind işlemi sonrası JSON İçeriğine bağlanan gameConfig nesnesi üzerinden yakalanabilmesi. Çalışma zamanı sonuçları aşağıdaki gibi olacaktır.

Bellekte Konuşlandırılmış Konfigurasyon İçeriği ile Çalışmak

MSDN dokümanlarından öğrendiğim ilginç örneklerden birisi de konfigurasyon bilgilerinin in-memory olarak tutulup yönetilebilmesi. Yeni fonksiyonumuz ExecuteInMemorySample'ı aşağıdaki gibi yazalım.

public void ExecuteInMemorySample()
{
    var builder = new ConfigurationBuilder();

    var parameters = new Dictionary<string, string>{
        {"Region:Name","east-us-2"},
        {"Region:BaseAddress","amazon.da.bir.yer/west-world/api"},
        {"Artifact:Service:Name","products"},
        {"Artifact:Service:MaxConcurrentCall","3500"},
        {"Artifact:Service:Type","json"},
        {"Artifact:Service:IsPublic","true"}
    };

    builder.AddInMemoryCollection(parameters);
    ConfigurationManager = builder.Build();
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:Name"]}");
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:Type"]}");
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:MaxConcurrentCall"]}");
    Console.WriteLine($"{ConfigurationManager["Artifact:Service:IsPublic"]}");

    var service = new Service();
    ConfigurationManager.GetSection("Artifact:Service").Bind(service);
    Console.WriteLine($"{service.Name},{service.MaxConcurrentCall},{service.Type},{service.IsPublic}");
}

Kodun kilit noktası builder örneği üzerinden çağırılan AddInMemoryCollection metodu. Bu metoda parametre olarak parameters isimli Dictionary<string,string> tipinden bir koleksiyon verilmekte. Dictionary, key:value şeklindeki konfigurasyon mantığına uygun olduğu için biçilmiş kaftandır. Tabii alt elemanlar için yine : ayracına başvurulur. Örnek koleksiyonda Region ve Artifact aynı seviyede yer alan elemanlardır. Artifact altında Service ve onun altında da Name, MaxConcurrentCall, Type ve IsPublic isimli nitelikler yer almaktadır. 

Build çağrısı sonrası bu bilgilere ConfigurationManager isimli IConfigurationRoot arayüzü üzerinden erişilebilir. Dahası bellekte konuşlandırılan bu konfigurasyon içeriği herhangibir seviyesi için bir nesneye de bağlanabilir. Service isimli aşağıdaki sınıfı göz önüne aldığımızda,

public class Service
{
    public string Name { get; set; }
    public int MaxConcurrentCall { get; set; }
    public string Type { get; set; }
    public bool IsPublic { get; set; }
}

GetSection("Artifact:Service").Bind(service) çağrımı ile Artifact altındaki Service içeriğinin ilgili nesne örneğine bağlanması sağlanmış olur. Bu noktadan sonra MaxConcurrentCall, Name gibi özelliklere yönetimli kod üzerinden erişilebilinir. Fonksiyonun çalışma zamanı çıktısı aşağıdaki gibidir.

Konfigurasyon Parametrelerini Komut Satırından Göndermek

Bir önceki örnekte kullandığımız In-memory çözümünde, parametre değerlerinin komut satırından gönderilmesi de mümkündür. Bu güzel ve ilginç bir kullanım şekli olsa de pek çok durumda işimize yarayabilir. Peki nasıl yapabiliriz? Aşağıdaki metodu ConfigSupervisor sınıfımıza ekleyerek devam edelim(Başta eklediğimiz Microsoft.Extensions.Configuration.CommandLine paketi bu örnek için gerekli)

public void ExecuteCommandLineSample(string[] args=null)
{
    var builder=new ConfigurationBuilder();
    var connection=new Dictionary<string,string>{
        {"Connection:Value","data source=aws;provider:amazon;"},
        {"Connection:Name","aws"}
    };
    builder
    .AddInMemoryCollection(connection)
    .AddCommandLine(args);

    ConfigurationManager=builder.Build();
    Console.WriteLine($"Connection : {ConfigurationManager["Connection:Value"]}");
    Console.WriteLine($"Connection : {ConfigurationManager["Connection:Timeout"]}");
}

Yine bellekte tutulan bir konfigurasyon içeriği söz konusu. Bunun için generic Dictionary koleksiyonunu kullandık. builder üzerinden çağırdığımız AddCommandLine fonksiyonuna parametre olarak gelen args dizisinin içeriği tahmin edeceğiniz üzere komut satırından gelecek. Kodun ilerleyen satırlarında Connection:Value ve Connection:Name değerlerini ekrana bastrırıyoruz. Main kodunun içeriğini de aşağıdaki hale getirelim. Tek yaptığımız Main fonksiyonuna gelen args değişkenini ExecuteCommandLineSample çağrısına parametre olarak geçmek.

static void Main(string[] args)
{
    ConfigSupervisor rubio = new ConfigSupervisor();
    rubio.ExecuteCommandLineSample(args);
}

Eğer programımızı aşağıdaki gibi çalıştırırsak konfigurasyon içeriğinin bizim istediğimiz gibi değiştiğini görürüz.

dotnet run Connection:Value="Azure;timeout=1000;region=EU-1" Connection:Name="azure"

Tabii bu parametreyi vermeden uygulamayı çalıştırırsak varsayılan Connection:Value ve Connection:Name değerlerine ulaşırız. Bu arada tüm parametreleri detaylı olarak girmek zorunda değiliz. İsimle ulaştığımız için sadece değiştirmek istediklerimizi girebilir veya farklı sıralarda atamalar yapabiliriz. Aşağıdaki çalışma zamanı sonuçlarına bu anlamda bakabilirsiniz.

Konfigurasyon yönetimi ile ilgili daha pek çok şey var(Ben MSDN'in şu adresteki oldukça doyurucu dokümanını izleyerek öğrenmeye çalışıyorum)Örneğin özel bir Entity Framework provider'ının oluşturulması, komut satırı argümanlarında switch mapping tekniğinin kullanılması gibi konulara bu adresten bakılabilir. Şimdilik benden bu kadar. Tekrardan görüşünceye dek hepinize mutlu günler dilerim. 

Örnek kodlara Git üzerinden de erişebilirsiniz.

http://www.buraksenyurt.com/post/hackathon-desteginden-node-js-i-tanimayaHackathon'dan Node.js'i Tanımaya

$
0
0

Merhaba Arkadaşlar,

Javascript yüzyıllardır(abartmayı severim) front-end tarafında kullanılan en güçlü yazılım geliştirme dillerinden birisi. Bir web uygulamasını onsuz düşünmek neredeyse imkansız. Her ne kadar Typescript gibi oluşumlar söz konusu olsa da, Javascript'in yeri ayrı. Javascript dilini baz alan bir çok Framework(çatı) de uzun zamandır sektörümüzde yer almakta. Hatta bazıları tamamen sunucu bazlı çalışacak şekilde tasarlanmış durumdalar. Node.js bunlardan birisi.

Onunla kesişmem çalışmakta olduğum firmadaki bir kaç sevgili dostumun katılacağı  hackathon yarışması sayesinde oldu. Yarışmaya katılımın ön koşulu olarak bir problemin çözülmesi gerekiyordu. Katılımcılar isterlerse Node.js, MongoDb ve Heroku kullanılarak bu görevi gerçekleştirebilirlerdi. Kıt kanaat bilgi birikimimle hemen şu Node.js nedir, neler yapılabiliyordur diye bakınmaya başladım. Derken Cumartesi günü kendimi onu tanımaya çalışırken buldum. Şu an için iş yerindeki projelerimizde Node.js ile yürüyeceğimiz bir yol haritamız olmasa da, sunucu taraflı çalışan Javascript temelli bir çatı neymiş öğrenmek istedim. Örnekleri karıştırırken de benim için hızlı bir giriş niteliğinde olan aşağıdaki kod parçası ile işe başladım.

Kurulumlar

Malum aylardır West-World' de tatildeyim. Bu dünyada Ubuntu kuralları geçerli. Dolayısıyla öncelikli olarak buraya Node.js'in kurulması gerekiyor. Her zaman ki gibi paket listesini güncellemekle işe başlamak lazım. Sonrasında node.js yüklenebilir. npm(Node Packaged Modules) okuduğum kaynaklara göre dünyanın en büyük paket merkezi konumunda. Onu da bir takım paketlerin(mesela rest servisleri için kullanmayı düşündüğüm 'express') sisteme kolayca kurulumu için kullanmayı planladım. Kurulumları başarılı bir şekilde tamamladıktan sonra -v ile versiyon bilgisini de elde edebildiğimi de gördüm. Buraya kadar sizde beni takip ettiyseniz benzer bir görüntü ile karşılaşmanız lazım.

sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install npm
nodejs -v

Kod yazımı içinse Visual Studio Code'dan yararlandım. Zaten node kod dosyasını görür görmez bir kaç eklenti önerisinde de bulundu. I love Visual Studio Code!

Bu ara bilgilendirmeden sonra aşağıdaki bir kaç kod dosyası ile devam edebiliriz.

İlk Örnek

Visual Studio Code üzerinde aşağıdaki içeriklere sahip iki js dosyası oluşturarak ilerleyebiliriz. Öncelikle modül kavramını anlamak adına utility.js adından bir dosya oluşturalım. Tahmin edeceğiniz üzere bir modülü, içerisindeki fonksiyonellikleri dışarı açmak suretiyle ortak kütüphane olacak şekilde kullanabiliyoruz. Kaldıki npm tarafına baktığımızda bu şekilde kullanabileceğimiz yüzlerce kütüphane(ya da built-in module) olduğunu söyleyebiliriz.

exports.reverse=function(input){
    return input.split('').reverse().join('')
}

utility.js içerisinde sadece bir fonksiyon yer almakta. reverse isimli fonksiyonun görevi parametre olarak gelen metni ters çevirip geriye döndürmek. Asıl iş yapan helloworld.js dosya içeriği ise şöyle.

var http = require('http')
var url = require('url')
var fs=require('fs')
var utility = require('./utility')

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write("<h2>Wellcome to West-World</h2>");
    res.write("<b>" + Date() + "</b><br/>");
    res.write("<i>Request url " + req.url + "</i><br/>");
    var q = url.parse(req.url, true).query
    if (typeof q.nick == 'undefined') {
        console.log("[Error]:%s,URL string'de hata var",Date())
        res.write("<a href='http://localhost:5009/?nick=murdock&point=450'><p style='color:darkgreen'>Try this! ;)</p></a>")
    }
    else {
        res.write("<p style='color:darkblue'>your nickname is " + q.nick + "</p>");
        res.write("<p style='color:magenta;'>or " + utility.reverse(q.nick) + "</p>");
    }
    res.end();
}).listen(5002);

http.createServer(function (request, response) {    
    fs.readFile("intro.html", function (err, data) {
        if (err) {
            console.log("[Error]:%s,%s",Date(),err.message)
            res.status(404).send('Not found')
            response.end()
        }
        else{
            response.writeHead(200, { 'Content-Type': 'text/html' })
            console.log("[Request]:%s,intro.html",Date())
            response.write(data.toString());
            response.end();
        }
    });    
}).listen(5003);

Öncelikle burada neler olup bitti kısaca anlatmaya çalışayım. Kod temel olarak 5002 ve 5003 portlardan olacak şekilde iki farklı sunucu dinleme operasyonunu icra ediyor. Her iki operasyonda kullanıcıya HTML içerik döndürmekte. 5002 portu için querystring kullanımı ve HTML içeriğinin kod tarafında inşa edilişi söz konusu. response değişkeni üzerinden çeşitli fonksiyonları kullanarak bu yazma operasyonlarını icra ediyoruz. Querystring parametreleri ve utility sınıfındaki reverse fonksiyonu kullanılarak da sembolik bir içerik basılıyor. 5003 portuna gelen talepleri ise intro.html isimli statik bir HTML sayfası ile karşılamaktayız. Kodun başında diğer platformlardaki import, using gibi bildirimlerden aşina olduğumuz bir kaç tanımlama bulunuyor. Aslında kodda kullanılacak olan modüllerin bildirimi yapılıyor. Sunucu işlemleri için http, dosya okuma işlemi için fs, querystring parametreleri ile çalışmak için url ve son olarakta kendi modülümüzü kullanabilmek için utility modülleri için tanımlamalar söz konusu. Tabii örnekte güzel olan noktalardan birisi http.createServer metodlarının asenkron çalışma modelini desteklemeleri sayesinde programın aynı anda 5002 ve 5003 taleplerini işleyebilecek olması. Kodları geliştirirken bazı ifade sonlarına noktalı virgül koymadığımı mutlaka fark etmişssinizdir. Açıkçası yorumlayıcı bunu önemsemiyor. Bu arada intro.html içeriği de şu şekilde;

<html><head><title>Intro Page</title></head><body style="font-family:Cambria, Cochin, Georgia, Times, 'Times New Roman', serif"><h2>Node.js Introduction Tutorials</h2><p style="width:400px;background-color:khaki">At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti
        quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia
        deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.
        Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere
        possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis
        aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum
        rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus
        asperiores repellat.</p><p><a href="For">https://nodejs.org/en/">For more details...</a></p></body></html>

Lorem Ipsum sitesinden aldığım metinsel bir içeriğin tarayıcıda gösterimi söz konusu. Aslında burası tamamen önyüz. İstediğiniz gibi güçlendirdiğinizde node.js ile ayağa kaldırdığınız harika bir web uygulaması ortaya koymanız mümkün. Bu arada bir node.js dosyasını çalıştırmak için terminalden aşağıdaki gibi bir komut vermek yeterli.

node helloWorld.js

http://localhost:5003 talebi için sonuç

http://localhost:5002/?nick=murdock&point=450 için sonuç,

ve son olarak querystring bilgisi hatalı olan http://localhost:5002 için sonuç,

Elim Node.js ile bir şeyler yazmaya çabuk alıştı diyebilirim. Bunun en büyük sebebi geçen yıl boyunca farklı dilleri ve çatıları tanımaya ve özellikle Linux platformunda bir şeyleri yeniden keşfetmeye çalışmamdır. Sizlere de bu tip bir çalışma planını disiplinli bir şekilde uygulamanızı öneririm.

Onun Hakkında Neler Söylenebilir?

Yukarıdaki kod parçasından sonra bu çatının genel özelliklerini bilmekte de yarar olduğu kanısındayım. Her şeyden önce Javascript tabanlı bir çatı ya da geliştirme ortamı olduğunu ifade edebiliriz. Bu açıdan fonksiyonel programlama özelliklerini benimseyen ve modüler kod yazılmasını sağlayan bir ortam var. Google Chrome'un performansı oldukça yüksek olan V8 Javascript motoruüzerinde çalışacak şekilde tasarlanmış. En büyük amacı özellikle I/O(non blocking modeli kullanıyor) işlemlerinin çok sık yapıldığı yüksek performans isteyen web uygulamalarının basitçe geliştirilmesi. Single Page Application modeli, Video Streaming sunucuları bunlara örnek olarak verilmekte. Olay güdümlü(Event-Driven) yaklaşımı destekleyen bu çatının ölçeklenebilirliği de güçlü özelliklerinden. Bu özellikleri itibariyle gerçek zamanlı veri sunan uygulamalar için de biçilmiş kaftan olduğunu söyeleyebiliriz. Ryan Dahl tarafından 2009 yılında geliştirildiği ifade edilen çatının şurada güzel bir sunumu var. Tamamen açık kaynaklı olarak sunulan, OS X, Linux, Windows demeden platform bağımsız ele alınabilen, MIT lisanslama modelinde kullanılabilen bir çalışma zamanına sahip. Apple'dan IBM'e, Netflix'ten Paypal'a, Microsoft'tan benim çalışma odamdaki köhne West-World'e kadar pek çok kurum/kişi tarafından kullanılıyor. Tüm bunların yanından belki de en dikkat çekici yanı kendisinin tamamen asenkron programlamaya(Callback modelini mutlaka hatırlayınız) odaklanmış olmasıdır. Bunu daha net kavramak için öğretilerdeki örnek kod parçalarına baktım ve en sık kullanılan bir versiyonunu ele aldım.

var fs=require("fs");

var loremData=fs.readFileSync('loremipsum.txt');
console.log(loremData.toString()+"\n\n");
console.log("*** Bitmeyen kod yapmışlar ***\n");

Standart bir dosya okuma ve ekrana bastırma işlemi söz konusu. fs modülündeki readFileSync fonksiyonu yardımıyla okunan loremipsum.txt içeriği terminal penceresine basılıyor. Bu zaten yakından bildiğimiz senkron çalışma modelinin bir örneği. Aşağıdaki ekran çıktısından da durum anlaşılabiliyor. Diğer yandan readFile fonksiyonunun sonundaki Sync son eki mutlaka dikkatinizi çekmiş olmalı. Normalde fonksiyonlarımızı asenkron tasarladığımız hallerde sonuna Async takısı ekleriz. Node.js dünyası ise her fonksiyonunu asenkron olarak çalışacak şekilde modellemeye uğraşıyor. Bu nedenle senkron fonksiyonlar için ayrı bir isimlendirme standardı konulmuş.

Dikkat edileceği üzere önce dosyanın içeriği ekrana basıldı sonrasında ise kod kaldığı yerden çalışmasına devam ederek sonlandı. Oysaki Node.js tasarlanış amacı gereği aksi belirtilmedikçe fonksiyonelliklerini asenkron çalışacak şekilde sunuyor (Hatta tüm API'lerinin Callback modeline göre asenkron çalışma desteği sunduğu belirtilmekte) Aynı örneği aşağıdaki kod parçası ile denersek bunu daha net görebiliriz. 

var fs = require("fs");

fs.readFile('loremipsum.txt', function (err, data) {
    if (err) return console.error(err);
    console.log(data.toString() + "\n\n");
});
console.log("### Program sonu ###\n");

Dikkat edileceği üzere ilk olarak dosya okuma satırından sonraki kod parçası çalıştı. Tipik bir asenkron çalışma modeli yaklaşımı. Bu tip asenkron çalışan fonskiyonlarda Callback desteği olduğunu da belirtelim. Hatta bu geri bildirim fonksiyonu örneğimizde readFile metoduna ikinci parametre olarak verilmiş durumda. Yani dosya okuma işleyişi tamamlandığında devreye girecek fonksiyon parametre olarak tanımlanıyor. İstersek bu fonksiyonu dışarıda da tanımlayabiliriz. Aşağıdaki örnek kod parçası ile ne demek istediğimi sanırım daha iyi anlatabilirim.

var fs = require("fs");

var readCallback = function (err, content) {
    if (err) {
        console.log(err.message);
        return;
    }
    var lines = content.toString().split("\n");
    lines.forEach(l => {
        console.log(l);
    });
}

fs.readFile('cats.txt', readCallback);
console.log("### Program sonu ###\n");

readFile fonksiyonunun ikinci parametresi olan callback metodu readCallback isimli değişken olarak tanımlanmış durumda. cats.txt isimli içeriğin okunması tamamlandığında bu geri bildirim fonksiyonu devreye giriyor.

Terminal Penceresinden

Node.js'i öğrenmeye başlarken ille de js uzantılı dosyalar oluşturmaya gerek olmadığını da öğrendim. Meşhur REPL(Read Eval Print Loop) modelinin desteklendiği bir ortamdan söz konusu. Yani terminalden node arayüzü ile konuşmak mümkün. Sadece node demek yeterli. İşte bir kaç örnek kullanım.

var name='nodi'
console.log('merhaba %s',name)
4+5

for(var i=0;i<5;i++){
   console.log(i)
}

Date()
Math.random()

var sum=function(x,y){
   return x+y
}
sum(5,1.23)

.help
.exit

  • name isimli bir değişken kullanımı
  • terminale bir şeyler yazdırma
  • for döngüsü
  • anından matematiksel işlem yaptırma
  • günün tarihini yazdırma
  • rastgele sayı ürettirme
  • bir fonksiyon tanımlayıp onu çağırma

gibi işlemler söz konusu. Dolayısıyla dili tanımak için bu ortamı kullanabiliriz. Terminal'de açılan node ortamından çıkmak için .exit yazmak yeterli. Nokta ile başlayan farklı komutlar da var. Söz gelimi yazdığımız ve üç nokta ile devam eden bir ifadeden vazgeçmek istediğimizde .break yazabiliriz. Ya da kullanılabilecek kısayolları görmek için .help ifadesini kullanabiliriz. Bunları bir deneyin.

Express ile Sonlandıralım

Sonuç olarak Node.js'i SPA tipinden uygulamalarda, JSON bazlı REST API'lerinin geliştirilmesinde, veri-hassas ve gerçek zamanlı akış sunan programlarda tercih edebiliriz. Hatta ilk örnekten yola çıkarsak domain bazında bölünmüş REST servislerinin ayrı ayrı sunulmasında pekala rahatlıkla kullanılabilir. Söz gelimi müşteri modülünüzü ve alt modüllerini farklı portlardan tek bir node.js sunucusundan, muhasebe modülü ve alt modüllerini farklı port aralığından sunan bir diğer node.js sunucusundan vb... şeklinde bir kurgu pekala gerçeklenebilir. Bunun ölçeklenmesi de microservice yaklaşımında kendine yer edinmesi de oldukça mümkün. REST tarafı için önceden de belirttiğim üzere express paketinden yararlanmak mantıklı görünüyor. Hadi gelin yazımızı sonlandırmadan önce onunla ilgili çok basit bir örnek yapalım. Ön hazırlık olarak express ve body-parser paketlerini sisteme dahil etmek gerekiyor. Visual Studio Code ortamından çıkmadan kendi terminalini kullanarak bu kurulumlar kolayca yapılabilir.

npm install express
npm install body-parser

Ardından aşağıdaki kodlarla devam edebiliriz.

/*Ön gereksinimler
npm install express
npm install body-parser
*/

var express = require('express');
var app = express();
var bodyparser = require('body-parser');
var fs = require('fs');
app.use(bodyparser.json());

// HTTP Get
app.get('/api/jobs', function (request, response) {
    fs.readFile('jobs.json', 'utf8', function (err, data) {
        console.log('%s:%s', Date(), request.url);
        response.end(data);
    });
});

app.get('/api/jobs/:jobId', function (request, response) {
    console.log('%s:Requested job id %s',Date(),request.params.jobId);
    response.status(200);
    // Bu kısım sizde :)
    response.end();
});

// HTTP Post
app.post('/api/addJob', function (request, response) {
    console.log('%s:%s', Date(), request.url);
    console.log(request.body);
    response.status(200).send('Job has been added');
     // Bu kısım sizde :)
    response.end();  
});

// HTTP Delete Burası da sizde
// HTTP Update Burası da sizde

var server = app.listen(5006, function () {
    console.log('Sunucu dinlemde');
});

5006 numaralı port üzerinden gelen talepleri dinleyen bir servis sunucusu söz konusu. /api/jobs ve /api/jobs/3 gibi HTTP Get talepleri dışında yeni bir işin eklenmesi için HTTP Post talebini ele alan fonksiyonlar var. Hepsi app nesnesi üzerinden çağırılan asenkron metodlar. İstemcilere REST modelinde bir API sunulduğunu rahatlıkla görebilirsiniz. Tabii size düşen bir takım görevleri de yorum satırı olarak bulabilirsiniz(Onları ihmal etmeyin yapın)Örnekte job içeriklerinin tutulduğu bir json dosyası da var. O da şöyle bir içeriğe sahip.

{
    "job1": {
        "title": "just read",
        "duration": "4 books per month",
        "id": 12
    },
    "job2": {
        "title": "be smile",
        "duration": "to everyone",
        "id": 23
    },
    "job3": {
        "title": "learn node.js",
        "duration": "in 30 days",
        "id": 35
    },
    "job4": {
        "title": "play basketball",
        "duration": "2 times per week",
        "id": 35
    }
}

Ben Postman üzerinden yaptığım denemelerle aşağıdaki sonuçları elde etmeyi başardım.

http://localhost:5006/api/jobs talebi için tüm json içeriğini elde edebildim.

http://localhost:5006/api/addJob adresinden yaptığım HTTP Post talebi ile de ekleme işlemi için gerekli operasyonun tetiklendiğini gördüm.

Kodları dikkatlice incelemenizi öneririm. Örneğin api/jobs/3 teki 3 değerini nasıl alıyoruz, body-parser hangi aşamada devreye giriyor, fonksiyon parametresi olarak geçen fonksiyonlar bize nasıl bir avantaj sağlıyor, response.end'in tam olarak görevi ne ve kullanmak zorunda mıyız vb şekilde soru cevaplar ile konuyu pekiştirebileceğinizi düşünüyorum. Node.js dünyası takdirimi kazandı. Onu Docker ile bir arada kullanmak ya da Heroku üzerinde konuşlandırmak gibi araştırmalar yapmayı da planlıyorum. Hatta MongoDb ile nasıl kullanabiliyoruz bu da merak ettiğim konuların başında geliyor. Belki de şu az önce bahsettiğim ölçeklenebilir ayrık microservice'leri baz alan bir model üzerinde de çalışabilirim. Bir süre dinlendikten sonra. Şimdilik West-World'ten bu kadar. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örnek kod ve yardımcı dosyaları Github üzerinden alabilirsiniz

Kaynaklar

Node.js Official Site
W3 School
Code for Geek
Tutorials Point

Hackathon'dan Node.js'i Tanımaya

$
0
0

Merhaba Arkadaşlar,

Javascript yüzyıllardır(abartmayı severim) front-end tarafında kullanılan en güçlü yazılım geliştirme dillerinden birisi. Bir web uygulamasını onsuz düşünmek neredeyse imkansız. Her ne kadar Typescript gibi oluşumlar söz konusu olsa da, Javascript'in yeri ayrı. Javascript dilini baz alan bir çok Framework(çatı) de uzun zamandır sektörümüzde yer almakta. Hatta bazıları tamamen sunucu bazlı çalışacak şekilde tasarlanmış durumdalar. Node.js bunlardan birisi.

Onunla kesişmem çalışmakta olduğum firmadaki bir kaç sevgili dostumun katılacağı  hackathon yarışması sayesinde oldu. Yarışmaya katılımın ön koşulu olarak bir problemin çözülmesi gerekiyordu. Katılımcılar isterlerse Node.js, MongoDb ve Heroku kullanılarak bu görevi gerçekleştirebilirlerdi. Kıt kanaat bilgi birikimimle hemen şu Node.js nedir, neler yapılabiliyordur diye bakınmaya başladım. Derken Cumartesi günü kendimi onu tanımaya çalışırken buldum. Şu an için iş yerindeki projelerimizde Node.js ile yürüyeceğimiz bir yol haritamız olmasa da, sunucu taraflı çalışan Javascript temelli bir çatı neymiş öğrenmek istedim. Örnekleri karıştırırken de benim için hızlı bir giriş niteliğinde olan aşağıdaki kod parçası ile işe başladım.

Kurulumlar

Malum aylardır West-World' de tatildeyim. Bu dünyada Ubuntu kuralları geçerli. Dolayısıyla öncelikli olarak buraya Node.js'in kurulması gerekiyor. Her zaman ki gibi paket listesini güncellemekle işe başlamak lazım. Sonrasında node.js yüklenebilir. npm(Node Packaged Modules) okuduğum kaynaklara göre dünyanın en büyük paket merkezi konumunda. Onu da bir takım paketlerin(mesela rest servisleri için kullanmayı düşündüğüm 'express') sisteme kolayca kurulumu için kullanmayı planladım. Kurulumları başarılı bir şekilde tamamladıktan sonra -v ile versiyon bilgisini de elde edebildiğimi de gördüm. Buraya kadar sizde beni takip ettiyseniz benzer bir görüntü ile karşılaşmanız lazım.

sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install npm
nodejs -v

Kod yazımı içinse Visual Studio Code'dan yararlandım. Zaten node kod dosyasını görür görmez bir kaç eklenti önerisinde de bulundu. I love Visual Studio Code!

Bu ara bilgilendirmeden sonra aşağıdaki bir kaç kod dosyası ile devam edebiliriz.

İlk Örnek

Visual Studio Code üzerinde aşağıdaki içeriklere sahip iki js dosyası oluşturarak ilerleyebiliriz. Öncelikle modül kavramını anlamak adına utility.js adından bir dosya oluşturalım. Tahmin edeceğiniz üzere bir modülü, içerisindeki fonksiyonellikleri dışarı açmak suretiyle ortak kütüphane olacak şekilde kullanabiliyoruz. Kaldıki npm tarafına baktığımızda bu şekilde kullanabileceğimiz yüzlerce kütüphane(ya da built-in module) olduğunu söyleyebiliriz.

exports.reverse=function(input){
    return input.split('').reverse().join('')
}

utility.js içerisinde sadece bir fonksiyon yer almakta. reverse isimli fonksiyonun görevi parametre olarak gelen metni ters çevirip geriye döndürmek. Asıl iş yapan helloworld.js dosya içeriği ise şöyle.

var http = require('http')
var url = require('url')
var fs=require('fs')
var utility = require('./utility')

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write("<h2>Wellcome to West-World</h2>");
    res.write("<b>" + Date() + "</b><br/>");
    res.write("<i>Request url " + req.url + "</i><br/>");
    var q = url.parse(req.url, true).query
    if (typeof q.nick == 'undefined') {
        console.log("[Error]:%s,URL string'de hata var",Date())
        res.write("<a href='http://localhost:5009/?nick=murdock&point=450'><p style='color:darkgreen'>Try this! ;)</p></a>")
    }
    else {
        res.write("<p style='color:darkblue'>your nickname is " + q.nick + "</p>");
        res.write("<p style='color:magenta;'>or " + utility.reverse(q.nick) + "</p>");
    }
    res.end();
}).listen(5002);

http.createServer(function (request, response) {    
    fs.readFile("intro.html", function (err, data) {
        if (err) {
            console.log("[Error]:%s,%s",Date(),err.message)
            res.status(404).send('Not found')
            response.end()
        }
        else{
            response.writeHead(200, { 'Content-Type': 'text/html' })
            console.log("[Request]:%s,intro.html",Date())
            response.write(data.toString());
            response.end();
        }
    });    
}).listen(5003);

Öncelikle burada neler olup bitti kısaca anlatmaya çalışayım. Kod temel olarak 5002 ve 5003 portlardan olacak şekilde iki farklı sunucu dinleme operasyonunu icra ediyor. Her iki operasyonda kullanıcıya HTML içerik döndürmekte. 5002 portu için querystring kullanımı ve HTML içeriğinin kod tarafında inşa edilişi söz konusu. response değişkeni üzerinden çeşitli fonksiyonları kullanarak bu yazma operasyonlarını icra ediyoruz. Querystring parametreleri ve utility sınıfındaki reverse fonksiyonu kullanılarak da sembolik bir içerik basılıyor. 5003 portuna gelen talepleri ise intro.html isimli statik bir HTML sayfası ile karşılamaktayız. Kodun başında diğer platformlardaki import, using gibi bildirimlerden aşina olduğumuz bir kaç tanımlama bulunuyor. Aslında kodda kullanılacak olan modüllerin bildirimi yapılıyor. Sunucu işlemleri için http, dosya okuma işlemi için fs, querystring parametreleri ile çalışmak için url ve son olarakta kendi modülümüzü kullanabilmek için utility modülleri için tanımlamalar söz konusu. Tabii örnekte güzel olan noktalardan birisi http.createServer metodlarının asenkron çalışma modelini desteklemeleri sayesinde programın aynı anda 5002 ve 5003 taleplerini işleyebilecek olması. Kodları geliştirirken bazı ifade sonlarına noktalı virgül koymadığımı mutlaka fark etmişssinizdir. Açıkçası yorumlayıcı bunu önemsemiyor. Bu arada intro.html içeriği de şu şekilde;

<html><head><title>Intro Page</title></head><body style="font-family:Cambria, Cochin, Georgia, Times, 'Times New Roman', serif"><h2>Node.js Introduction Tutorials</h2><p style="width:400px;background-color:khaki">At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti
        quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia
        deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.
        Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere
        possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis
        aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum
        rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus
        asperiores repellat.</p><p><a href="For">https://nodejs.org/en/">For more details...</a></p></body></html>

Lorem Ipsum sitesinden aldığım metinsel bir içeriğin tarayıcıda gösterimi söz konusu. Aslında burası tamamen önyüz. İstediğiniz gibi güçlendirdiğinizde node.js ile ayağa kaldırdığınız harika bir web uygulaması ortaya koymanız mümkün. Bu arada bir node.js dosyasını çalıştırmak için terminalden aşağıdaki gibi bir komut vermek yeterli.

node helloWorld.js

http://localhost:5003 talebi için sonuç

http://localhost:5002/?nick=murdock&point=450 için sonuç,

ve son olarak querystring bilgisi hatalı olan http://localhost:5002 için sonuç,

Elim Node.js ile bir şeyler yazmaya çabuk alıştı diyebilirim. Bunun en büyük sebebi geçen yıl boyunca farklı dilleri ve çatıları tanımaya ve özellikle Linux platformunda bir şeyleri yeniden keşfetmeye çalışmamdır. Sizlere de bu tip bir çalışma planını disiplinli bir şekilde uygulamanızı öneririm.

Onun Hakkında Neler Söylenebilir?

Yukarıdaki kod parçasından sonra bu çatının genel özelliklerini bilmekte de yarar olduğu kanısındayım. Her şeyden önce Javascript tabanlı bir çatı ya da geliştirme ortamı olduğunu ifade edebiliriz. Bu açıdan fonksiyonel programlama özelliklerini benimseyen ve modüler kod yazılmasını sağlayan bir ortam var. Google Chrome'un performansı oldukça yüksek olan V8 Javascript motoruüzerinde çalışacak şekilde tasarlanmış. En büyük amacı özellikle I/O(non blocking modeli kullanıyor) işlemlerinin çok sık yapıldığı yüksek performans isteyen web uygulamalarının basitçe geliştirilmesi. Single Page Application modeli, Video Streaming sunucuları bunlara örnek olarak verilmekte. Olay güdümlü(Event-Driven) yaklaşımı destekleyen bu çatının ölçeklenebilirliği de güçlü özelliklerinden. Bu özellikleri itibariyle gerçek zamanlı veri sunan uygulamalar için de biçilmiş kaftan olduğunu söyeleyebiliriz. Ryan Dahl tarafından 2009 yılında geliştirildiği ifade edilen çatının şurada güzel bir sunumu var. Tamamen açık kaynaklı olarak sunulan, OS X, Linux, Windows demeden platform bağımsız ele alınabilen, MIT lisanslama modelinde kullanılabilen bir çalışma zamanına sahip. Apple'dan IBM'e, Netflix'ten Paypal'a, Microsoft'tan benim çalışma odamdaki köhne West-World'e kadar pek çok kurum/kişi tarafından kullanılıyor. Tüm bunların yanından belki de en dikkat çekici yanı kendisinin tamamen asenkron programlamaya(Callback modelini mutlaka hatırlayınız) odaklanmış olmasıdır. Bunu daha net kavramak için öğretilerdeki örnek kod parçalarına baktım ve en sık kullanılan bir versiyonunu ele aldım.

var fs=require("fs");

var loremData=fs.readFileSync('loremipsum.txt');
console.log(loremData.toString()+"\n\n");
console.log("*** Bitmeyen kod yapmışlar ***\n");

Standart bir dosya okuma ve ekrana bastırma işlemi söz konusu. fs modülündeki readFileSync fonksiyonu yardımıyla okunan loremipsum.txt içeriği terminal penceresine basılıyor. Bu zaten yakından bildiğimiz senkron çalışma modelinin bir örneği. Aşağıdaki ekran çıktısından da durum anlaşılabiliyor. Diğer yandan readFile fonksiyonunun sonundaki Sync son eki mutlaka dikkatinizi çekmiş olmalı. Normalde fonksiyonlarımızı asenkron tasarladığımız hallerde sonuna Async takısı ekleriz. Node.js dünyası ise her fonksiyonunu asenkron olarak çalışacak şekilde modellemeye uğraşıyor. Bu nedenle senkron fonksiyonlar için ayrı bir isimlendirme standardı konulmuş.

Dikkat edileceği üzere önce dosyanın içeriği ekrana basıldı sonrasında ise kod kaldığı yerden çalışmasına devam ederek sonlandı. Oysaki Node.js tasarlanış amacı gereği aksi belirtilmedikçe fonksiyonelliklerini asenkron çalışacak şekilde sunuyor (Hatta tüm API'lerinin Callback modeline göre asenkron çalışma desteği sunduğu belirtilmekte) Aynı örneği aşağıdaki kod parçası ile denersek bunu daha net görebiliriz. 

var fs = require("fs");

fs.readFile('loremipsum.txt', function (err, data) {
    if (err) return console.error(err);
    console.log(data.toString() + "\n\n");
});
console.log("### Program sonu ###\n");

Dikkat edileceği üzere ilk olarak dosya okuma satırından sonraki kod parçası çalıştı. Tipik bir asenkron çalışma modeli yaklaşımı. Bu tip asenkron çalışan fonskiyonlarda Callback desteği olduğunu da belirtelim. Hatta bu geri bildirim fonksiyonu örneğimizde readFile metoduna ikinci parametre olarak verilmiş durumda. Yani dosya okuma işleyişi tamamlandığında devreye girecek fonksiyon parametre olarak tanımlanıyor. İstersek bu fonksiyonu dışarıda da tanımlayabiliriz. Aşağıdaki örnek kod parçası ile ne demek istediğimi sanırım daha iyi anlatabilirim.

var fs = require("fs");

var readCallback = function (err, content) {
    if (err) {
        console.log(err.message);
        return;
    }
    var lines = content.toString().split("\n");
    lines.forEach(l => {
        console.log(l);
    });
}

fs.readFile('cats.txt', readCallback);
console.log("### Program sonu ###\n");

readFile fonksiyonunun ikinci parametresi olan callback metodu readCallback isimli değişken olarak tanımlanmış durumda. cats.txt isimli içeriğin okunması tamamlandığında bu geri bildirim fonksiyonu devreye giriyor.

Terminal Penceresinden

Node.js'i öğrenmeye başlarken ille de js uzantılı dosyalar oluşturmaya gerek olmadığını da öğrendim. Meşhur REPL(Read Eval Print Loop) modelinin desteklendiği bir ortamdan söz konusu. Yani terminalden node arayüzü ile konuşmak mümkün. Sadece node demek yeterli. İşte bir kaç örnek kullanım.

var name='nodi'
console.log('merhaba %s',name)
4+5

for(var i=0;i<5;i++){
   console.log(i)
}

Date()
Math.random()

var sum=function(x,y){
   return x+y
}
sum(5,1.23)

.help
.exit

  • name isimli bir değişken kullanımı
  • terminale bir şeyler yazdırma
  • for döngüsü
  • anından matematiksel işlem yaptırma
  • günün tarihini yazdırma
  • rastgele sayı ürettirme
  • bir fonksiyon tanımlayıp onu çağırma

gibi işlemler söz konusu. Dolayısıyla dili tanımak için bu ortamı kullanabiliriz. Terminal'de açılan node ortamından çıkmak için .exit yazmak yeterli. Nokta ile başlayan farklı komutlar da var. Söz gelimi yazdığımız ve üç nokta ile devam eden bir ifadeden vazgeçmek istediğimizde .break yazabiliriz. Ya da kullanılabilecek kısayolları görmek için .help ifadesini kullanabiliriz. Bunları bir deneyin.

Express ile Sonlandıralım

Sonuç olarak Node.js'i SPA tipinden uygulamalarda, JSON bazlı REST API'lerinin geliştirilmesinde, veri-hassas ve gerçek zamanlı akış sunan programlarda tercih edebiliriz. Hatta ilk örnekten yola çıkarsak domain bazında bölünmüş REST servislerinin ayrı ayrı sunulmasında pekala rahatlıkla kullanılabilir. Söz gelimi müşteri modülünüzü ve alt modüllerini farklı portlardan tek bir node.js sunucusundan, muhasebe modülü ve alt modüllerini farklı port aralığından sunan bir diğer node.js sunucusundan vb... şeklinde bir kurgu pekala gerçeklenebilir. Bunun ölçeklenmesi de microservice yaklaşımında kendine yer edinmesi de oldukça mümkün. REST tarafı için önceden de belirttiğim üzere express paketinden yararlanmak mantıklı görünüyor. Hadi gelin yazımızı sonlandırmadan önce onunla ilgili çok basit bir örnek yapalım. Ön hazırlık olarak express ve body-parser paketlerini sisteme dahil etmek gerekiyor. Visual Studio Code ortamından çıkmadan kendi terminalini kullanarak bu kurulumlar kolayca yapılabilir.

npm install express
npm install body-parser

Ardından aşağıdaki kodlarla devam edebiliriz.

/*Ön gereksinimler
npm install express
npm install body-parser
*/

var express = require('express');
var app = express();
var bodyparser = require('body-parser');
var fs = require('fs');
app.use(bodyparser.json());

// HTTP Get
app.get('/api/jobs', function (request, response) {
    fs.readFile('jobs.json', 'utf8', function (err, data) {
        console.log('%s:%s', Date(), request.url);
        response.end(data);
    });
});

app.get('/api/jobs/:jobId', function (request, response) {
    console.log('%s:Requested job id %s',Date(),request.params.jobId);
    response.status(200);
    // Bu kısım sizde :)
    response.end();
});

// HTTP Post
app.post('/api/addJob', function (request, response) {
    console.log('%s:%s', Date(), request.url);
    console.log(request.body);
    response.status(200).send('Job has been added');
     // Bu kısım sizde :)
    response.end();  
});

// HTTP Delete Burası da sizde
// HTTP Update Burası da sizde

var server = app.listen(5006, function () {
    console.log('Sunucu dinlemde');
});

5006 numaralı port üzerinden gelen talepleri dinleyen bir servis sunucusu söz konusu. /api/jobs ve /api/jobs/3 gibi HTTP Get talepleri dışında yeni bir işin eklenmesi için HTTP Post talebini ele alan fonksiyonlar var. Hepsi app nesnesi üzerinden çağırılan asenkron metodlar. İstemcilere REST modelinde bir API sunulduğunu rahatlıkla görebilirsiniz. Tabii size düşen bir takım görevleri de yorum satırı olarak bulabilirsiniz(Onları ihmal etmeyin yapın)Örnekte job içeriklerinin tutulduğu bir json dosyası da var. O da şöyle bir içeriğe sahip.

{
    "job1": {
        "title": "just read",
        "duration": "4 books per month",
        "id": 12
    },
    "job2": {
        "title": "be smile",
        "duration": "to everyone",
        "id": 23
    },
    "job3": {
        "title": "learn node.js",
        "duration": "in 30 days",
        "id": 35
    },
    "job4": {
        "title": "play basketball",
        "duration": "2 times per week",
        "id": 35
    }
}

Ben Postman üzerinden yaptığım denemelerle aşağıdaki sonuçları elde etmeyi başardım.

http://localhost:5006/api/jobs talebi için tüm json içeriğini elde edebildim.

http://localhost:5006/api/addJob adresinden yaptığım HTTP Post talebi ile de ekleme işlemi için gerekli operasyonun tetiklendiğini gördüm.

Kodları dikkatlice incelemenizi öneririm. Örneğin api/jobs/3 teki 3 değerini nasıl alıyoruz, body-parser hangi aşamada devreye giriyor, fonksiyon parametresi olarak geçen fonksiyonlar bize nasıl bir avantaj sağlıyor, response.end'in tam olarak görevi ne ve kullanmak zorunda mıyız vb şekilde soru cevaplar ile konuyu pekiştirebileceğinizi düşünüyorum. Node.js dünyası takdirimi kazandı. Onu Docker ile bir arada kullanmak ya da Heroku üzerinde konuşlandırmak gibi araştırmalar yapmayı da planlıyorum. Hatta MongoDb ile nasıl kullanabiliyoruz bu da merak ettiğim konuların başında geliyor. Belki de şu az önce bahsettiğim ölçeklenebilir ayrık microservice'leri baz alan bir model üzerinde de çalışabilirim. Bir süre dinlendikten sonra. Şimdilik West-World'ten bu kadar. Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örnek kod ve yardımcı dosyaları Github üzerinden alabilirsiniz

Kaynaklar

Node.js Official Site
W3 School
Code for Geek
Tutorials Point

http://www.buraksenyurt.com/post/apache-uzerinden-bir-web-api-hizmeti-sunmakApache Üzerinden Bir Web API Hizmeti Sunmak

$
0
0

Merhaba Arkadaşlar,

Küçük bir çocukken her pazar sabahı TRT ekranlarında kovboy(Western) filmleri olurdu. Rahmetli babam ile severek geçirdiğimiz nadide vakitlerdendi. Sanıyorum son yıllarda yine Pazar sabahları ekranlarımızı süslüyorlar(Bakamıyorum çünkü sabahlarımız basketbol antrenmanları ile geçiyor) Kızılderililer ile süvarilerin sıklıkla karşı karşıya geldiği, batının en hızlı silahşörlerinin yer aldığı filmlere ne hikmetse çok bağlanmıştım.

Oysa ki okuduğumuz tarih kitapları o zamanlardaki olayların pek de filmlerde gördüğümüz gibi olmadığını yazıyor. Özellikle de kızılderili yerlilerin durumu düşünüldüğünde. O zamanların pek çok yerli kabilesi günümüzün pek çok teknolojisine de isim kaynağı oldu aslında. Bunlardan en popüler olanlarından birisi de sanıyorum ki Apache'dir(Şu isimler de eminim ki çağrışım yapacaktır;Siu, Cheyenn, Comanche...)  Ünlü savaş helikopteri dışında hepimiz onun yazılım dünyasındaki ününü de duymuşuzdur. Gelelim bugün inceleyeceğimiz konuya.

Geçenlerde bir süredir West-World üzerinde denemek istediğim vakaların üstünden geçtim. Gerek iş değişikliği gerek Salı,Perşembe günleri veteranlar olarak yaptığımız basketbol maçlarının yoğunluğu sebebiyle kuyrukta baya bir araştırma konusu birikmiş durumda. Listeden sırayla geçerken kafada hemen bir ağırlıklandırma yaptım(Scrum içimize işlemiş) En yüksek öncelik puanı "Apache üzerinde bir .Net Core uygulamasının nasıl host ederim?" cümlesine aitti. Microsoft'un ilgili dokümanında CentOS tabanlı sitemler için bir anlatım bulunuyordu ki bu benim Ubuntu 16.04 dünyasında farklı şekilde ele alınabilirdi(ki öylede oldu) Gelin vakit kaybetmeden bu haftasonu West-World üzerinde neler olmuş bir bakalım. 

Öncelikle sisteme Apache kurulumunu gerçekleştirdim. Konum ile çok alakalı olmasa da Firewall ayarlarını yapıp, Apache'nin belirli komutlarını inceleyerek devam ettim. Ardından Apache üzerinde yönlendirme(Redirect) gerçekleştirilebilmesi için konfigurasyon ayarlamaları yaptım. Standard Web API servisini geliştirip aynen NGinX odaklı yaklaşımda olduğu gibi sisteme bir Service dosyası atıp testleri gerçekleştirdim. İlk adımla başlayalım.

Apache Kurulumu

Kurulum için standart olarak öncelikle bir sistem güncellemesi yapmak, ardından da apache2'yi yüklemek doğru olacaktı. West-World dünyasında her şeyin başı apt-get update idi.

sudo apt-get update
sudo apt-get install apache2

Yükleme sonrası apache'nin kullanılabileceği bir kaç profil, firewall listesine ilave edilmekte(edilirmiş) Benim makinemdeki UFW(Uncomplicated Firewall) listesini çektiğimde aşağıdaki sonuçlarla karşılaştım.

sudo ufw app list

Bu arada CUPS, Common Unix Printing Systems isimli bir servisin kısaltmasıymış. Bunu da öğrenmiş oldum.

Önceden NginX ile ilgili denemeler de yaptığım için onlar da listede kendine yer bulmuştu(West-World bir süre sonra çarpık kentleşme nedeniyle tekrardan yapılandırılmalı sanıyorum ki) Apache için üç farklı profil söz konusu. Apache isimli profil sadece 80 portunun açık olduğu ve şifresiz bir trafik imkanı sunmakta. Apache Full, Apache profiline ek olarak 443 nolu porttan şifrelenmiş web trafiğine imkan tanır(Yani TLS/SSL desteği verir) Apache Secure ise sadece 443 portu kullanılacak şekilde şifrelenmiş web trafiği sağlar. Benim örneğim için 80 portunu kullandırmak yeterliydi. Bu nedenle aşağıdaki komutu kullanarak gerekli etkinleştirmeyi yaptım.

sudo ufw allow 'Apache'

Güncel duruma baktığımda ilgili tanımın UFW listesine eklendiğini de gördüm.

sudo ufw status

Bu işlemler sonrasında yapmam gereken apache sunucusunun ayağa kalkıp kalkmadığını denetlemekti. Terminalden

sudo systemctl status apache2

komutunu kullandığımda servisin başarılı bir şekilde yüklendiğini ve hizmet vermeye başladığını gördüm.

active (running) yazısını görmek önemli ama yeterli değil. Hani hepimizin aşina olduğu o Apache'nin varsayılan giriş sayfası var ya...Onu görmek lazımdı. localhost'a talep gönderdiğimde o yalın sayfa karşımdaydı.

Birkaç Apache Komutu

Apache sunucusu ile çalışırken elbette bir takım yönetimsel işlemlere gereksinim duyulabilir. Sunucuyu durdurmak, yeniden başlatmak, konfigurasyon değişikliklerini tekrardan yüklemek ve diğerleri. Bu işlemler için aşağıdaki komutlardan yararlanılabilir.

Durdurmak için,

sudo systemctl stop apache2

Başlatmak için,

sudo systemctl start apache2

Servisi durdurup tekrar başlatmak için,

sudo systemctl restart apache2

Konfigurasyon değişikliklerini bağlantıyı kopartmadan yüklemek içinse

sudo systemctl reload apache2

ModProxy Özelliğini Etkinleştiriyoruz

Artık West-World'de gezinen bir Apache olduğuna göre basit bir .Net Core Web API hizmeti yazıp bunu apache üzerinden host etmeyi deneyebilirdim. Aslında olay NginX senaryosundakine çok benziyor. Apache'yi reverse proxy server rolünde çalışacak hale getirip localhost 80 portuna gelen taleplerin kestrel'e yönlendirilmesini sağlamak işin anafikri diyebiliriz(Tam tersi istikamette söz konusu tabii) İlk adım ise modproxy'yi etkinleştirmek. Bu sayede apache sunucumu HTTP taleplerini yönlendirecek kıvama getirebileceğim. Bunun için terminalden aşağıdaki komutu vermek gerekiyor.

sudo a2enmod proxy proxy_http proxy_html

Gerekli aktivasyonu sağladıktan sonra konfigurasyon değişikliği yapıp yönlendirme tanımlarını sisteme ilave ettim. Söz konusu konfigurasyon değişiklikleri için env/apache2/sites-enabled altındaki 000-default.conf dosyasının içeriğini düzenlemek gerekiyor. Bu varsayılan site dosyası. Aslında bu klasöre farklı sanal host tanımlama bilgileri içeren conf uzantılı dosyalar yükleyebiliyoruz. Bu dosyalar apache sunucusu tarafından otomatik olarak değerlendirilmekte. Örneğin merak ettiğim konulardan birisi localhost'un farklı bir portu için sanal host konfigurasyon dosyası tanımlamak ve yine yönlendirmeler yaparak Kestrel'i ayağa kaldırmak(Bunu bir araştırmam lazım)

<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

ProxyPreserveHost On
ProxyPass / http://localhost:5558/
ProxyPassReverse / http://localhost:5558/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

Tahmin edileceği üzere bir sanal host tanımı var. 80 portu ile Kestrel'in standart 5000 yerine bu örnek özelinde tercih ettiğim 5558 portu arasında reverse proxy kullanımı olacağı belirtiliyor. Bu değişiklikten sonra apache servisini tekrardan başlattım(restart) ve güncel durum bilgisini çektim.

sudo service apache2 restart
sudo service apache2 status

Şimdilik bir sorun görünmüyordu.

Web API ve Apache Service Dosyasının Oluşturulması

Tüm bunlar yeterli değil. Apache'nin bu konfigurasyon dosyası bilgisine göre, 80 portuna gelen talep için Kestrel çalışma zamanını da nasıl işleteceğini bilmesi lazım. Bir service dosyası oluşturmalı ve içerisinde gerekli ortam bilgilendirmelerini belirtmeliyiz. Ama her şeyden önce bir Web API projesi oluştursam hiç fena olmazdı. Bunun yolunu artık siz de benim kadar iyi biliyorsunuz.

dotnet new webapi -o apacheler

Kodda yaptığım tek değişiklik sunucu adresini ayarlamak oldu. Program.cs içerisindeki BuilWebHost fonksiyonunu aşağıdaki gibi düzenledim.

public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseUrls("http://localhost:5558")
.Build();

Kod düzenlemesi sonrası projeyi publish ettim.

dotnet publish -c Release

Apaçiler'in kültürel özelliklerine ait bir çok sırrı sunacağını hayal ettiğim web api servisimiz için /etc/systemd/system klasörü altında kestrel-apacheler.service isimli bir dosya oluşturup içeriğini aşağıdaki hale getirdim.

[Unit]
Description=Apaçiler hakkında gizemli bilgiler sunan web api

[Service]
WorkingDirectory=/home/burakselyum/dotnetcore/apacheler
ExecStart=/usr/share/dotnet/dotnet /home/burakselyum/dotnetcore/apacheler/bin/Release/netcoreapp2.0/publish/apacheler.dll
Restart=always
RestartSec=30
SyslogIdentifier=apachelerlog
User=root
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

Dosyanın üç parçası bulunuyor. Unit kısmında servise ait bir açıklamaya yer verilmekte. Servisin çalışacağı klasör WorkingDirectory bilgisi ile belirtilmekte. ExecStart, dotnet.exe'yi(West World için geçerli olan bu lokasyon sizin ortamınızda farklı olabilir) kullanarak publish edilen apacheler hizmetini başlatmakta. Bir nevi dotnet start işlemini gerçekleştirdiğini ifade edebiliriz. Hata olması halinde her zaman restart işlemi uygulanacağını da belirtiyoruz. Buradaki dayanma süremiz de 30 saniye. Servis hizmete girdikten sonra sistemden log'ları incelemek isteyebiliriz. SyslogIdentifier'a atanan apachelerlog kelimesi ile bu içerikleri daha kolay ayırt etme şansımız var. Kullanıcı olarak ben root'a yetki verdim ama bunu alana özel bir kullanıcı bazında kullandırmak daha doğru olabilir. 

Testler

Servis dosyası artık hazır. Şimdi bunu etkinleştirip devreye almak gerekiyor. Devreye aldıktan sonra da servis durumunu gözlemlemekte yarar var. Terminalden systemctl komutu kullanılarak bu işlemler gerçekleştirilebilir. Önce etkinleştir(enable), sonra başlat(start) ve güncel durumu izle(status).

sudo systemctl enable kestrel-apacheler.service
sudo systemctl start kestrel-apacheler.service
sudo systemctl status kestrel-apacheler.service

Gözlemlediğim kadarı ile kestrel-apacheler.service içeriği geçerliydi ve çalışır konumdaydı. Bunu gördükten sonra Firefox'tan http://localhost/api/values adresine gitmeyi denedim. Son ayların en popüler değer listesine erişebilmiştim. 

Gözlerime inanamıyordum. Emin olmak için onları bir kaç kez kırptım. Sonrasında daha gerçekçi olmaya karar verdim ve service dosyasını durdurup aynı talebi tekrar gönderdim.

sudo systemctl stop kestrel-apacheler.service

Bu çok sevindirici bir gelişmeydi(İnsanın Service Unavailable yazısını görünce sevinçten gözleri yaşarı mı?) West-World'de Apache'ler ile barış sağlandığına göre artık dinlenmeye çekilebilirdim. Tabii siz bu yazıdan ilham alarak konuyu geliştirmeyi deneyebilirsiniz. Söz gelimi 000-default.conf yerine aynı klasörde farklı bir conf dosyasını kullanarak ilgili yönlendirmenin nasıl yapılabileceğini araştırabilirsiniz. Özellikle 80 yerine farklı bir Apache portu kullandırtmayı deneyebilirsiniz. Böylece geldik bir makalemizin daha sonuna. Bu yazımızda Ubuntu 16.04 üzerinde kurduğumuz apache sunucusuna gelen talepleri, Kestrel tarafında host edilen bir Web API hizmetine yönlendirmeye çalıştık. Umarım yararlı bir makale olmuştur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Apache Üzerinden Bir Web API Hizmeti Sunmak

$
0
0

Merhaba Arkadaşlar,

Küçük bir çocukken her pazar sabahı TRT ekranlarında kovboy(Western) filmleri olurdu. Rahmetli babam ile severek geçirdiğimiz nadide vakitlerdendi. Sanıyorum son yıllarda yine Pazar sabahları ekranlarımızı süslüyorlar(Bakamıyorum çünkü sabahlarımız basketbol antrenmanları ile geçiyor) Kızılderililer ile süvarilerin sıklıkla karşı karşıya geldiği, batının en hızlı silahşörlerinin yer aldığı filmlere ne hikmetse çok bağlanmıştım.

Oysa ki okuduğumuz tarih kitapları o zamanlardaki olayların pek de filmlerde gördüğümüz gibi olmadığını yazıyor. Özellikle de kızılderili yerlilerin durumu düşünüldüğünde. O zamanların pek çok yerli kabilesi günümüzün pek çok teknolojisine de isim kaynağı oldu aslında. Bunlardan en popüler olanlarından birisi de sanıyorum ki Apache'dir(Şu isimler de eminim ki çağrışım yapacaktır;Siu, Cheyenn, Comanche...)  Ünlü savaş helikopteri dışında hepimiz onun yazılım dünyasındaki ününü de duymuşuzdur. Gelelim bugün inceleyeceğimiz konuya.

Geçenlerde bir süredir West-World üzerinde denemek istediğim vakaların üstünden geçtim. Gerek iş değişikliği gerek Salı,Perşembe günleri veteranlar olarak yaptığımız basketbol maçlarının yoğunluğu sebebiyle kuyrukta baya bir araştırma konusu birikmiş durumda. Listeden sırayla geçerken kafada hemen bir ağırlıklandırma yaptım(Scrum içimize işlemiş) En yüksek öncelik puanı "Apache üzerinde bir .Net Core uygulamasının nasıl host ederim?" cümlesine aitti. Microsoft'un ilgili dokümanında CentOS tabanlı sitemler için bir anlatım bulunuyordu ki bu benim Ubuntu 16.04 dünyasında farklı şekilde ele alınabilirdi(ki öylede oldu) Gelin vakit kaybetmeden bu haftasonu West-World üzerinde neler olmuş bir bakalım. 

Öncelikle sisteme Apache kurulumunu gerçekleştirdim. Konum ile çok alakalı olmasa da Firewall ayarlarını yapıp, Apache'nin belirli komutlarını inceleyerek devam ettim. Ardından Apache üzerinde yönlendirme(Redirect) gerçekleştirilebilmesi için konfigurasyon ayarlamaları yaptım. Standard Web API servisini geliştirip aynen NGinX odaklı yaklaşımda olduğu gibi sisteme bir Service dosyası atıp testleri gerçekleştirdim. İlk adımla başlayalım.

Apache Kurulumu

Kurulum için standart olarak öncelikle bir sistem güncellemesi yapmak, ardından da apache2'yi yüklemek doğru olacaktı. West-World dünyasında her şeyin başı apt-get update idi.

sudo apt-get update
sudo apt-get install apache2

Yükleme sonrası apache'nin kullanılabileceği bir kaç profil, firewall listesine ilave edilmekte(edilirmiş) Benim makinemdeki UFW(Uncomplicated Firewall) listesini çektiğimde aşağıdaki sonuçlarla karşılaştım.

sudo ufw app list

Bu arada CUPS, Common Unix Printing Systems isimli bir servisin kısaltmasıymış. Bunu da öğrenmiş oldum.

Önceden NginX ile ilgili denemeler de yaptığım için onlar da listede kendine yer bulmuştu(West-World bir süre sonra çarpık kentleşme nedeniyle tekrardan yapılandırılmalı sanıyorum ki) Apache için üç farklı profil söz konusu. Apache isimli profil sadece 80 portunun açık olduğu ve şifresiz bir trafik imkanı sunmakta. Apache Full, Apache profiline ek olarak 443 nolu porttan şifrelenmiş web trafiğine imkan tanır(Yani TLS/SSL desteği verir) Apache Secure ise sadece 443 portu kullanılacak şekilde şifrelenmiş web trafiği sağlar. Benim örneğim için 80 portunu kullandırmak yeterliydi. Bu nedenle aşağıdaki komutu kullanarak gerekli etkinleştirmeyi yaptım.

sudo ufw allow 'Apache'

Güncel duruma baktığımda ilgili tanımın UFW listesine eklendiğini de gördüm.

sudo ufw status

Bu işlemler sonrasında yapmam gereken apache sunucusunun ayağa kalkıp kalkmadığını denetlemekti. Terminalden

sudo systemctl status apache2

komutunu kullandığımda servisin başarılı bir şekilde yüklendiğini ve hizmet vermeye başladığını gördüm.

active (running) yazısını görmek önemli ama yeterli değil. Hani hepimizin aşina olduğu o Apache'nin varsayılan giriş sayfası var ya...Onu görmek lazımdı. localhost'a talep gönderdiğimde o yalın sayfa karşımdaydı.

Birkaç Apache Komutu

Apache sunucusu ile çalışırken elbette bir takım yönetimsel işlemlere gereksinim duyulabilir. Sunucuyu durdurmak, yeniden başlatmak, konfigurasyon değişikliklerini tekrardan yüklemek ve diğerleri. Bu işlemler için aşağıdaki komutlardan yararlanılabilir.

Durdurmak için,

sudo systemctl stop apache2

Başlatmak için,

sudo systemctl start apache2

Servisi durdurup tekrar başlatmak için,

sudo systemctl restart apache2

Konfigurasyon değişikliklerini bağlantıyı kopartmadan yüklemek içinse

sudo systemctl reload apache2

ModProxy Özelliğini Etkinleştiriyoruz

Artık West-World'de gezinen bir Apache olduğuna göre basit bir .Net Core Web API hizmeti yazıp bunu apache üzerinden host etmeyi deneyebilirdim. Aslında olay NginX senaryosundakine çok benziyor. Apache'yi reverse proxy server rolünde çalışacak hale getirip localhost 80 portuna gelen taleplerin kestrel'e yönlendirilmesini sağlamak işin anafikri diyebiliriz(Tam tersi istikamette söz konusu tabii) İlk adım ise modproxy'yi etkinleştirmek. Bu sayede apache sunucumu HTTP taleplerini yönlendirecek kıvama getirebileceğim. Bunun için terminalden aşağıdaki komutu vermek gerekiyor.

sudo a2enmod proxy proxy_http proxy_html

Gerekli aktivasyonu sağladıktan sonra konfigurasyon değişikliği yapıp yönlendirme tanımlarını sisteme ilave ettim. Söz konusu konfigurasyon değişiklikleri için env/apache2/sites-enabled altındaki 000-default.conf dosyasının içeriğini düzenlemek gerekiyor. Bu varsayılan site dosyası. Aslında bu klasöre farklı sanal host tanımlama bilgileri içeren conf uzantılı dosyalar yükleyebiliyoruz. Bu dosyalar apache sunucusu tarafından otomatik olarak değerlendirilmekte. Örneğin merak ettiğim konulardan birisi localhost'un farklı bir portu için sanal host konfigurasyon dosyası tanımlamak ve yine yönlendirmeler yaparak Kestrel'i ayağa kaldırmak(Bunu bir araştırmam lazım)

<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

ProxyPreserveHost On
ProxyPass / http://localhost:5558/
ProxyPassReverse / http://localhost:5558/
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

Tahmin edileceği üzere bir sanal host tanımı var. 80 portu ile Kestrel'in standart 5000 yerine bu örnek özelinde tercih ettiğim 5558 portu arasında reverse proxy kullanımı olacağı belirtiliyor. Bu değişiklikten sonra apache servisini tekrardan başlattım(restart) ve güncel durum bilgisini çektim.

sudo service apache2 restart
sudo service apache2 status

Şimdilik bir sorun görünmüyordu.

Web API ve Apache Service Dosyasının Oluşturulması

Tüm bunlar yeterli değil. Apache'nin bu konfigurasyon dosyası bilgisine göre, 80 portuna gelen talep için Kestrel çalışma zamanını da nasıl işleteceğini bilmesi lazım. Bir service dosyası oluşturmalı ve içerisinde gerekli ortam bilgilendirmelerini belirtmeliyiz. Ama her şeyden önce bir Web API projesi oluştursam hiç fena olmazdı. Bunun yolunu artık siz de benim kadar iyi biliyorsunuz.

dotnet new webapi -o apacheler

Kodda yaptığım tek değişiklik sunucu adresini ayarlamak oldu. Program.cs içerisindeki BuilWebHost fonksiyonunu aşağıdaki gibi düzenledim.

public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseUrls("http://localhost:5558")
.Build();

Kod düzenlemesi sonrası projeyi publish ettim.

dotnet publish -c Release

Apaçiler'in kültürel özelliklerine ait bir çok sırrı sunacağını hayal ettiğim web api servisimiz için /etc/systemd/system klasörü altında kestrel-apacheler.service isimli bir dosya oluşturup içeriğini aşağıdaki hale getirdim.

[Unit]
Description=Apaçiler hakkında gizemli bilgiler sunan web api

[Service]
WorkingDirectory=/home/burakselyum/dotnetcore/apacheler
ExecStart=/usr/share/dotnet/dotnet /home/burakselyum/dotnetcore/apacheler/bin/Release/netcoreapp2.0/publish/apacheler.dll
Restart=always
RestartSec=30
SyslogIdentifier=apachelerlog
User=root
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

Dosyanın üç parçası bulunuyor. Unit kısmında servise ait bir açıklamaya yer verilmekte. Servisin çalışacağı klasör WorkingDirectory bilgisi ile belirtilmekte. ExecStart, dotnet.exe'yi(West World için geçerli olan bu lokasyon sizin ortamınızda farklı olabilir) kullanarak publish edilen apacheler hizmetini başlatmakta. Bir nevi dotnet start işlemini gerçekleştirdiğini ifade edebiliriz. Hata olması halinde her zaman restart işlemi uygulanacağını da belirtiyoruz. Buradaki dayanma süremiz de 30 saniye. Servis hizmete girdikten sonra sistemden log'ları incelemek isteyebiliriz. SyslogIdentifier'a atanan apachelerlog kelimesi ile bu içerikleri daha kolay ayırt etme şansımız var. Kullanıcı olarak ben root'a yetki verdim ama bunu alana özel bir kullanıcı bazında kullandırmak daha doğru olabilir. 

Testler

Servis dosyası artık hazır. Şimdi bunu etkinleştirip devreye almak gerekiyor. Devreye aldıktan sonra da servis durumunu gözlemlemekte yarar var. Terminalden systemctl komutu kullanılarak bu işlemler gerçekleştirilebilir. Önce etkinleştir(enable), sonra başlat(start) ve güncel durumu izle(status).

sudo systemctl enable kestrel-apacheler.service
sudo systemctl start kestrel-apacheler.service
sudo systemctl status kestrel-apacheler.service

Gözlemlediğim kadarı ile kestrel-apacheler.service içeriği geçerliydi ve çalışır konumdaydı. Bunu gördükten sonra Firefox'tan http://localhost/api/values adresine gitmeyi denedim. Son ayların en popüler değer listesine erişebilmiştim. 

Gözlerime inanamıyordum. Emin olmak için onları bir kaç kez kırptım. Sonrasında daha gerçekçi olmaya karar verdim ve service dosyasını durdurup aynı talebi tekrar gönderdim.

sudo systemctl stop kestrel-apacheler.service

Bu çok sevindirici bir gelişmeydi(İnsanın Service Unavailable yazısını görünce sevinçten gözleri yaşarı mı?) West-World'de Apache'ler ile barış sağlandığına göre artık dinlenmeye çekilebilirdim. Tabii siz bu yazıdan ilham alarak konuyu geliştirmeyi deneyebilirsiniz. Söz gelimi 000-default.conf yerine aynı klasörde farklı bir conf dosyasını kullanarak ilgili yönlendirmenin nasıl yapılabileceğini araştırabilirsiniz. Özellikle 80 yerine farklı bir Apache portu kullandırtmayı deneyebilirsiniz. Böylece geldik bir makalemizin daha sonuna. Bu yazımızda Ubuntu 16.04 üzerinde kurduğumuz apache sunucusuna gelen talepleri, Kestrel tarafında host edilen bir Web API hizmetine yönlendirmeye çalıştık. Umarım yararlı bir makale olmuştur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

http://www.buraksenyurt.com/post/bir-web-api-servisini-github-hesabiyla-yetkilendirmekBir Web API Servisini Github Hesabıyla Yetkilendirmek

$
0
0

Merhaba Arkadaşlar,

Teknik konularda yazı yazmaya çalışmanın en zor yanlarından birisi de anlatımı basitleştirmek. Şüphesiz ki uğraştığımız konular bazen anlamakta güçlük çektiğimiz karmaşıklıkta olabiliyor. Böyle konuların bırakın anlatılması öğrenilmesi de güçleşebiliyor. Şahsen kendi adıma bazı konuları anlamak için epey çaba sarf ettiğimi söylesem yeridir. Üstelik bu konuları zaman içerisinde gerçek hayat senaryolarında kullanmaz veya üzerinde saatler geçirmessek unutuyoruz da. Oysaki bir konuyu basitçe anlatabiliyorsak hem iyi anlamışız demektir hem de verimli bir çalışma safhası geçirmişizdir. Ne demiş Austion Freeman;

"Simplicity is the soul of efficiency" - Austin Freeman

Bugün de benzer bir durumla karşı karşıyayım. Ne gecenin bu sessiz vaktinde o güzel kokusu ile odamı dolduran filtre kahvem ne de arka planda çalan piyano resitali aslında işimi kolaylaştırmıyor. Yine de West-World, odaklanmam için yeterli şartları sağlamış durumda. Bu yazımızda en azından benim için hep karmaşık olan OAuth tabanlı bir yetkilendirme sürecinin nasıl yapılabileceğini incelemeye çalışacağız. İlk önce ne yapacağımızı özetlemeye çalışalım.

Senaryomuzda basit bir Web API Servisi bulunuyor. .Net Core ile geliştirilen servisin bir Controller'ı için yetkilendirme(Authorization) sürecini uygulatmak istiyoruz. Burada OAuth 2 standardını ele almak, kullanıcı yetkilendirme yöneticisi ve bilet(Token) tedarikçisi olarak Github'dan yararlanmak istiyoruz. Tabii bu senaryonun gerçekleşmesi için bizim Github'a bir proje kaydettirmemiz(Pek çok platform için söz konusu olan Application Registration işlemi diyelim) ve özellike Redirect URI bilgisini Consumer rolündeki uygulamamıza bildirmemiz gerekiyor(Az sonra yapacağız)

Yaşam Döngüsü

OAuth 2 temelli sistemin çalışma prensibi basit(Aslında karmaşık ama iyice didikleyince gayet anlaşılır, mantıklı ve basit) Ortada üç aktör yer var. Senaryomuzu göz önüne alırsak bu aktörlerimiz Web API(Consumer), Github(Idendity and Token Service Provider) ve Web API servisini tüketmek isteyen kullanıcı(User) şeklinde ifade edilebilirler. Bu üç aktörün yaşam döngüsü içerisindeki iletişimi ise sırasyıla şöyle özetlenebilir.

  1. Kullanıcı yerel makinesindeki servise bir talepte bulunur(HTTP Get gibi) O anda elinde geçerli bir bilet olmadığını düşünelim.
  2. Bunun üzerine Web API uygulaması Github'dan bir kullanıcı doğrulaması ister.
  3. Github kullanıcıyı doğrulamak için Login sayfasına yönlendirme yapar.
  4. Kullanıcı doğrulanırsa, Github'ın bir sorusu ile karşılaşılır. Github'daki "bla bla" uygulamasının bilgilerinize erişmesine izin veriyor musunuz? gibi.
  5. Eğer kullanıcı bunu kabul ederse, Github tarafından Redirect URI ile belirtilen adrese yönlendirilir. Bu yönlendirmede geçici bir erişim kodu bulunur.
  6. Web API servisi aldığı kod ile Github'ın bilet sağlayan adresine(Token Endpoint) gider.
  7. Github bu talep üzerine daha kalıcı olan onaylanmış bir bilet hazırlayıp bunu Web API servisine verir.
  8. Web API servisi bu bilgiyi saklar(genelde bir son kullanma tarihi olur ama Github OAuth biletlerinde durum farklı) ve sonraki taleplerde bu bilet kullanılır.

Karışık değil mi? Hofff...Siz birde bana sorun. Eğer basit bir şekilde anlatamadıysam bu konuyu anlamamışım demektir. Diğer yandan adım adım örneği işlettiğimizde konuyu biraz daha pekiştirebileceğimizi düşünüyorum. Haydi başlayalım.

OAuth Uygulaması için Kayıt İşlemi

İlk olarak şu adrese giderek OAuth uygulamamızı Github'a kayıt etmemiz gerekiyor. "Register a new application" başlıklı düğmeye basarak işleme başlayabiliriz. Burada uygulamaya ait bazı bilgileri doldurmamız lazım.

vb bilgiler olabilir. Authorization Callback URL bilgisi dikkatiniz çekmiş olmalı. Senaryoya göre Service Provider rolü üstlenen Github, Consumer rolündeki yerel Web API servisi üzerinden gelen kullanıcıyı yetkilendirirse bu URL adresine doğru bir yönlendirme gerçekleştirilecek ki, bu yönlendirme sırasında Consumer'a birde geçici erişim kodu verilecek. Sonrasında Consumer(yani Web API hizmetimiz) bu geçici kod ile Github'ın Token Endpoint'ine gelerek daha kalıcı olan erişim biletini(Access Token) alacak.

"Register Application" başlıklı düğmeye basıldıktan sonra uygulamanın oluşturulduğu ve Web API servisimizde kullanılmak üzere Credential bilgisinin üretildiği görülebilir.

Buradaki Client ID ve Client Secret değerleri Web API servisimizin Github uygulamasını kullanabilmesi için gereklidir.

Web API Servisinin Geliştirilmesi

Sırada Web API servisinin oluşturulması adımı var. Bunun için aşağıdaki terminal komutunu kullanabiliriz.

dotnet new webapi -o MyQuoteService

İlk olarak Kestrel sunucusunu 5005 numaralı porta ayarlayalım(Bildiğiniz üzere varsayılan port 5000) Bunu Program.cs içerisinde yapabiliriz.

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseUrls("http://localhost:5005")
        .Build();

Uygulamanın en önemli değişiklikleri Startup sınıfında gerçekleştirilecek. Bu dosyanın içeriğini aşağıdaki hale getirelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;

namespace MyQuoteService
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = "GitHub";
            })
            .AddCookie()
            .AddOAuth("GitHub", options =>
            {
                options.ClientId = "sizinki";
                options.ClientSecret = "sizinki";
                options.CallbackPath = new PathString("/signin-github");

                options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
                options.TokenEndpoint = "https://github.com/login/oauth/access_token";
                options.UserInformationEndpoint = "https://api.github.com/user";

                options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
                options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
                options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
                options.ClaimActions.MapJsonKey("urn:github:blog", "blog");

                options.Events = new OAuthEvents
                {
                    OnCreatingTicket = async ctx =>
                    {
                        Console.WriteLine("OnCreatingTicket Event");

                        var request = new HttpRequestMessage(HttpMethod.Get, ctx.Options.UserInformationEndpoint);
                        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", ctx.AccessToken);

                        var response = await ctx.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx.HttpContext.RequestAborted);
                        response.EnsureSuccessStatusCode();

                        var userInfo = JObject.Parse(await response.Content.ReadAsStringAsync());
                        ctx.RunClaimActions(userInfo);
                        Console.WriteLine($"User Info:\n{userInfo.ToString()}");
                    }
                };
            });
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();
            app.UseMvc();
        }
    }
}

Sonuç itibariyle Middleware tarafına bir doğrulama katmanının eklenmesi söz konusu. Bunun hazırlığı için de Authentication, Cookie ve OAuth gibi servislerin devreye alınması gerekiyor. AddAuthentication metodu ile doğrulama servisini devreye alıyoruz. Buradaki ayarlar seçilen doğrulama servisine göre farklılık gösterebilir. Örnekte Cookie'lerden yaralanacağımızı belirtiyoruz. Buna göre çalışma zamanı kullanıcı doğrulamasını kontrol etmek için Cookie Authentication Handler'dan yararlanacak. Kullanıcı doğrulandığında da ise kullanıcı bilgisi Cookie içerisinde saklanacak(DefaultSignInScheme atamasına göre) Cookie Authentication Handler'ın devreye alınması işini AddCookie fonksiyon çağrısı ile bildiriyoruz. OAuth Handler'ını kayıt ederken Github tarafında oluşturduğumuz uygulama için üretilen Client ID, Client Secret değerleri ile bizim belirlediğimiz Callback adresini atıyoruz. Bu değerleri konfigurasyon dosyasından ya da daha güvenli bir ortamdan(Cyberark gibi) alabilirsiniz. Sonuçta hassas bilgiler.

Yetkilendirme, bilet alma ve kullanıcı bilgilerini çekme gibi operasyonlar, Github tarafında belirli adreslerden sunulan servisler tarafından karşılanmakta. Bu nedenle options parametresi üzerinden ilgili Endpoint adresleri belirtiliyor. Bu adreslerden sunulan servisler birer REST servis. Yani Postman, SOAPUI gibi araçları kullanarak da deneyebiliriz. ClaimsAction üzerinden çağırılan MapJsonKey metodu iki parametre ile çalışıyor. İlk parametre ile kullanıcı için Github tarafından gelen içerikteki Claim tipi, ikinci parametre ile de key bilgisi belirleniyor. Buradaki atamalara Controller tarafındaki User nesnesi üzerinden erişebileceğiz.

Kodun ilerleyen kısmında bir olay metodu da yer alıyor. OnCreatingTicket kullanıcı doğrulamasını takip eden süreçte bilet üretildikten sonra devreye giren bir olay olarak düşünülebilir. Bu olay metodu içerisinde Github'ın UserInformationEndpoint ile bildirilen adresine HTTP Get talebinde bulunuyoruz. Dikkat ederseniz bir Authentication Header bilgisi de veriyoruz ki bu bize Github tarafından verilen bilet(Bearer Token) SendAsync ile ilgili talep gerçekleştirildikten sonra kullanıcı bilgilerini elde etmiş oluyoruz. Bunları sadece örnekte görmek amacıyla ekrana bastırdık. Artık servisler devrede. Bu durumu Middleware tarafında etkinleştirmek içinse Configure fonksiyonunda UseAuthentication çağrısını yapmamız gerekli. Gelelim yetkilendirme sürecine dahil edeceğimiz Controller tipine. Örneğimizde WebApi şablonu ile gelen ValuesController yerine aşağıdaki içeriğe sahip QuotesController sınıfını kullanacağız.

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

namespace MyQuoteService.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class QuotesController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            var id = User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
            var userName = User.FindFirst(c => c.Type == ClaimTypes.Name)?.Value;
            var email = User.FindFirst(c => c.Type == ClaimTypes.Email)?.Value;
            var blog = User.FindFirst(c => c.Type == "urn:github:blog")?.Value;
            Console.WriteLine($"{DateTime.Now}\nCurrent user:{userName}({id})\n{email}\n{blog}");
            return new ObjectResult(_quotes);
        }

        List<Quote> _quotes = new List<Quote>{
                new Quote{Id=122548,Owner="Michael Jordan",Text="I have missed more than 9000 shots in my career. I have lost almost 300 games. 26 times, I have been trusted to take the game winning shot and missed. I have failed over and over and over again in my life. And that is why I succeed."},
                new Quote{Id=325440,Owner="Vince Lombardi",Text="We didn't lose the game; we just ran out of time"},
                new Quote{Id=150094,Owner="Randy Pausch",Text="We cannot change the cards we are dealt, just how we play the game"},
                new Quote{Id=167008,Owner="Johan Cruyff",Text="Football is a game of mistakes. Whoever makes the fewest mistakes wins."},
                new Quote{Id=650922,Owner="Gary Lineker",Text="Football is a simple game. Twenty-two men chase a ball for 90 minutes and at the end, the Germans always win."},
                new Quote{Id=682356,Owner="Paul Pierce",Text="The game isn't over till the clock says zero."},
                new Quote{Id=156480,Owner="Jose Mourinho",Text="Football is a game about feelings and intelligence."},
                new Quote{Id=777592,Owner="LeBron James",Text="You know, when I have a bad game, it continues to humble me and know that, you know, you still have work to do and you still have a lot of people to impress."},
                new Quote{Id=283941,Owner="Roman Abramovich",Text="I'm getting excited before every single game. The trophy at the end is less important than the process itself."},
                new Quote{Id=185674,Owner="Shaquille O'Neal",Text="I'm tired of hearing about money, money, money, money, money. I just want to play the game, drink Pepsi, wear Reebok."}
            };
    }

    class Quote
    {
        public int Id { get; set; }
        public string Owner { get; set; }
        public string Text { get; set; }
    }
}

Sadece özlü sözler listesini HTTP Get talebi karşılığında geriye döndüren bir operasyonumuz var. Get operasyonu içerisinde Github kullanıcısının doğrulanması sonrası çekilen ClaimSet içerisindeki bazı değerlere ulaşılmakta. Bu örnekte login olan Github kullanıcısına ait username,id,email ve blog bilgilerini Console ekranına bastırmaktayız. Bu bilgiler loglama amacıyla kullanılabilir. QuotesController sınıfının bir diğer önemli özelliği de Authorize niteliği ile işaretlenmiş olması. Buna göre tüm operasyonları için yetkilendirme sürecine dahil olacağız.

Testler

Geliştirme safhasını sonlandırdığımıza göre test sürüşüne çıkabiliriz. Uygulamayı

dotnet run

terminal komutu ile çalıştırdıktan ve tarayıcı üzerinden http://localhost:5005/api/quotes adresine gittikten sonra aşağıdaki ekran görüntüsü ile karşılaşırız.

Dikkat edileceği üzere Github login sayfasına yönlendirildik. Eğer Network hareketliliklerini izlersek aşağıdaki geçişlerin olduğunu fark edebiliriz.

  1. localhost:5005/api/quotes HTTP Get talebi HTTP 302 koduna çevrilerek Location header bilgisindeki github adresine yönlendirilir.
  2. Yönlendirildiğimiz https://github.com/login/oauth/authorize? client_id={client id bilgisi}&scope=&response_type=code&redirect_uri= http://localhost:5005/signin-github&state= {uzuuunnn bir state bilgisi var} adresinde Authorize kontrolünden geçeriz ki önceden login olmamışsak yeni bir adrese yönleniriz.
  3. HTTP Get ile https://github.com/login?client_id= {client id bilgisi}&return_to=/login/oauth/authorize?client_id= {client id bilgisi}&redirect_uri= http%3A%2F%2Flocalhost%3A5005%2Fsignin-github&response_type=code&scope=&state= {uzuuuuun state bilgisi} geldiğimiz bu adreste ise Login olmamız yeterli olacaktır.
  4. Sonrasında Github kullanıcısının söz konusu uygulama için yetki vermesini bekleyen bir onayı penceresi ile karşılaşabiliriz. 

Bu bir kereliğine sorulacaktır ancak Github üzerindeki uygulama ayarlarından Revoke All Users Tokens işlemini yaparsak tekrardan karşılaşabiliriz. Artık DailyQuoteService isimli uygulama için buraksenyurt kullanıcısı yetkilendirilmiş durumda. Dolayısıyla bir önceki taleple gelen Location header bilgisindeki URL adresine yönlendiriliriz ki bu da görmek istediğimiz özlü sözler operasyonudur.

Tabii Visual Studio Code arabirimine bakarsak Login olan kullanıcıya ait Github tarafından sunulan tüm ClaimSet değerlerinin JSON formatında geldiğini de görebiliriz. Ayrıca Get metodu içerisinden de oturum açan kullanıcının çeşitli bilgilerine erişebiliriz.

Servisimiz için Github tarafından sağlanan Token bilgisinin bir son kullanma tarihi bildiğim kadarı ile yok. Kullanıcının Token bilgisi sistemden düşmediği sürece servis yetkilendirme kontrolü yapma ihtiyacı duymadan çalışıyor olacak. Github'un ilgili servis adreslerine HTTP DELETE metoduyla ID bilgisiyle talepte bulunup düşürme işleminin bilinçli olarak uygulanabilineceği de ifade ediliyor. Bunu neden söylüyorum dersiniz? Uygulamayı denerken özlü sözler servisinin Authorization adımlarına takılmadan sürekli olarak çalıştığını gördüm. Bir yerlerde düşse de tekrar Login olmamı istese diye beklerken aslında kullanım amacının ne olduğunu hatırladım. Amaç bir uygulamanın Github üzerinden doğrulanmış kullanıcılar için OAuth protokolü üzerinden Bearer Token ile çalışmasıydı. Servisin çalıştığı sistem Github tarafından bir kere doğrulanıp ehliyet bilgisini aldıktan sonra hizmet verebilir konumda kalması yeterliydi. Bu arada pratik bir yol olarak tarayıcı çerezlerini temizlemeniz halinde tekrardan Login işlemine tabii tutulacağınızı söylemek isterim ;)

Böylece geldik bir makalemizin daha sonuna. Bu yazımızda bir Web API servisinin yetkilendirme sürecinde Github'ın OAuth hizmetinden nasıl yararlanabileceğimizi incelemeye çalıştık. Umarım faydalı olur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örneğe github adresi üzerinden erişebilirsiniz.

Bir Web API Servisini Github Hesabıyla Yetkilendirmek

$
0
0

Merhaba Arkadaşlar,

Teknik konularda yazı yazmaya çalışmanın en zor yanlarından birisi de anlatımı basitleştirmek. Şüphesiz ki uğraştığımız konular bazen anlamakta güçlük çektiğimiz karmaşıklıkta olabiliyor. Böyle konuların bırakın anlatılması öğrenilmesi de güçleşebiliyor. Şahsen kendi adıma bazı konuları anlamak için epey çaba sarf ettiğimi söylesem yeridir. Üstelik bu konuları zaman içerisinde gerçek hayat senaryolarında kullanmaz veya üzerinde saatler geçirmessek unutuyoruz da. Oysaki bir konuyu basitçe anlatabiliyorsak hem iyi anlamışız demektir hem de verimli bir çalışma safhası geçirmişizdir. Ne demiş Austion Freeman;

"Simplicity is the soul of efficiency" - Austin Freeman

Bugün de benzer bir durumla karşı karşıyayım. Ne gecenin bu sessiz vaktinde o güzel kokusu ile odamı dolduran filtre kahvem ne de arka planda çalan piyano resitali aslında işimi kolaylaştırmıyor. Yine de West-World, odaklanmam için yeterli şartları sağlamış durumda. Bu yazımızda en azından benim için hep karmaşık olan OAuth tabanlı bir yetkilendirme sürecinin nasıl yapılabileceğini incelemeye çalışacağız. İlk önce ne yapacağımızı özetlemeye çalışalım.

Senaryomuzda basit bir Web API Servisi bulunuyor. .Net Core ile geliştirilen servisin bir Controller'ı için yetkilendirme(Authorization) sürecini uygulatmak istiyoruz. Burada OAuth 2 standardını ele almak, kullanıcı yetkilendirme yöneticisi ve bilet(Token) tedarikçisi olarak Github'dan yararlanmak istiyoruz. Tabii bu senaryonun gerçekleşmesi için bizim Github'a bir proje kaydettirmemiz(Pek çok platform için söz konusu olan Application Registration işlemi diyelim) ve özellike Redirect URI bilgisini Consumer rolündeki uygulamamıza bildirmemiz gerekiyor(Az sonra yapacağız)

Yaşam Döngüsü

OAuth 2 temelli sistemin çalışma prensibi basit(Aslında karmaşık ama iyice didikleyince gayet anlaşılır, mantıklı ve basit) Ortada üç aktör yer var. Senaryomuzu göz önüne alırsak bu aktörlerimiz Web API(Consumer), Github(Idendity and Token Service Provider) ve Web API servisini tüketmek isteyen kullanıcı(User) şeklinde ifade edilebilirler. Bu üç aktörün yaşam döngüsü içerisindeki iletişimi ise sırasyıla şöyle özetlenebilir.

  1. Kullanıcı yerel makinesindeki servise bir talepte bulunur(HTTP Get gibi) O anda elinde geçerli bir bilet olmadığını düşünelim.
  2. Bunun üzerine Web API uygulaması Github'dan bir kullanıcı doğrulaması ister.
  3. Github kullanıcıyı doğrulamak için Login sayfasına yönlendirme yapar.
  4. Kullanıcı doğrulanırsa, Github'ın bir sorusu ile karşılaşılır. Github'daki "bla bla" uygulamasının bilgilerinize erişmesine izin veriyor musunuz? gibi.
  5. Eğer kullanıcı bunu kabul ederse, Github tarafından Redirect URI ile belirtilen adrese yönlendirilir. Bu yönlendirmede geçici bir erişim kodu bulunur.
  6. Web API servisi aldığı kod ile Github'ın bilet sağlayan adresine(Token Endpoint) gider.
  7. Github bu talep üzerine daha kalıcı olan onaylanmış bir bilet hazırlayıp bunu Web API servisine verir.
  8. Web API servisi bu bilgiyi saklar(genelde bir son kullanma tarihi olur ama Github OAuth biletlerinde durum farklı) ve sonraki taleplerde bu bilet kullanılır.

Karışık değil mi? Hofff...Siz birde bana sorun. Eğer basit bir şekilde anlatamadıysam bu konuyu anlamamışım demektir. Diğer yandan adım adım örneği işlettiğimizde konuyu biraz daha pekiştirebileceğimizi düşünüyorum. Haydi başlayalım.

OAuth Uygulaması için Kayıt İşlemi

İlk olarak şu adrese giderek OAuth uygulamamızı Github'a kayıt etmemiz gerekiyor. "Register a new application" başlıklı düğmeye basarak işleme başlayabiliriz. Burada uygulamaya ait bazı bilgileri doldurmamız lazım.

vb bilgiler olabilir. Authorization Callback URL bilgisi dikkatiniz çekmiş olmalı. Senaryoya göre Service Provider rolü üstlenen Github, Consumer rolündeki yerel Web API servisi üzerinden gelen kullanıcıyı yetkilendirirse bu URL adresine doğru bir yönlendirme gerçekleştirilecek ki, bu yönlendirme sırasında Consumer'a birde geçici erişim kodu verilecek. Sonrasında Consumer(yani Web API hizmetimiz) bu geçici kod ile Github'ın Token Endpoint'ine gelerek daha kalıcı olan erişim biletini(Access Token) alacak.

"Register Application" başlıklı düğmeye basıldıktan sonra uygulamanın oluşturulduğu ve Web API servisimizde kullanılmak üzere Credential bilgisinin üretildiği görülebilir.

Buradaki Client ID ve Client Secret değerleri Web API servisimizin Github uygulamasını kullanabilmesi için gereklidir.

Web API Servisinin Geliştirilmesi

Sırada Web API servisinin oluşturulması adımı var. Bunun için aşağıdaki terminal komutunu kullanabiliriz.

dotnet new webapi -o MyQuoteService

İlk olarak Kestrel sunucusunu 5005 numaralı porta ayarlayalım(Bildiğiniz üzere varsayılan port 5000) Bunu Program.cs içerisinde yapabiliriz.

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseUrls("http://localhost:5005")
        .Build();

Uygulamanın en önemli değişiklikleri Startup sınıfında gerçekleştirilecek. Bu dosyanın içeriğini aşağıdaki hale getirelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;

namespace MyQuoteService
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = "GitHub";
            })
            .AddCookie()
            .AddOAuth("GitHub", options =>
            {
                options.ClientId = "sizinki";
                options.ClientSecret = "sizinki";
                options.CallbackPath = new PathString("/signin-github");

                options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
                options.TokenEndpoint = "https://github.com/login/oauth/access_token";
                options.UserInformationEndpoint = "https://api.github.com/user";

                options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
                options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
                options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
                options.ClaimActions.MapJsonKey("urn:github:blog", "blog");

                options.Events = new OAuthEvents
                {
                    OnCreatingTicket = async ctx =>
                    {
                        Console.WriteLine("OnCreatingTicket Event");

                        var request = new HttpRequestMessage(HttpMethod.Get, ctx.Options.UserInformationEndpoint);
                        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", ctx.AccessToken);

                        var response = await ctx.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctx.HttpContext.RequestAborted);
                        response.EnsureSuccessStatusCode();

                        var userInfo = JObject.Parse(await response.Content.ReadAsStringAsync());
                        ctx.RunClaimActions(userInfo);
                        Console.WriteLine($"User Info:\n{userInfo.ToString()}");
                    }
                };
            });
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();
            app.UseMvc();
        }
    }
}

Sonuç itibariyle Middleware tarafına bir doğrulama katmanının eklenmesi söz konusu. Bunun hazırlığı için de Authentication, Cookie ve OAuth gibi servislerin devreye alınması gerekiyor. AddAuthentication metodu ile doğrulama servisini devreye alıyoruz. Buradaki ayarlar seçilen doğrulama servisine göre farklılık gösterebilir. Örnekte Cookie'lerden yaralanacağımızı belirtiyoruz. Buna göre çalışma zamanı kullanıcı doğrulamasını kontrol etmek için Cookie Authentication Handler'dan yararlanacak. Kullanıcı doğrulandığında da ise kullanıcı bilgisi Cookie içerisinde saklanacak(DefaultSignInScheme atamasına göre) Cookie Authentication Handler'ın devreye alınması işini AddCookie fonksiyon çağrısı ile bildiriyoruz. OAuth Handler'ını kayıt ederken Github tarafında oluşturduğumuz uygulama için üretilen Client ID, Client Secret değerleri ile bizim belirlediğimiz Callback adresini atıyoruz. Bu değerleri konfigurasyon dosyasından ya da daha güvenli bir ortamdan(Cyberark gibi) alabilirsiniz. Sonuçta hassas bilgiler.

Yetkilendirme, bilet alma ve kullanıcı bilgilerini çekme gibi operasyonlar, Github tarafında belirli adreslerden sunulan servisler tarafından karşılanmakta. Bu nedenle options parametresi üzerinden ilgili Endpoint adresleri belirtiliyor. Bu adreslerden sunulan servisler birer REST servis. Yani Postman, SOAPUI gibi araçları kullanarak da deneyebiliriz. ClaimsAction üzerinden çağırılan MapJsonKey metodu iki parametre ile çalışıyor. İlk parametre ile kullanıcı için Github tarafından gelen içerikteki Claim tipi, ikinci parametre ile de key bilgisi belirleniyor. Buradaki atamalara Controller tarafındaki User nesnesi üzerinden erişebileceğiz.

Kodun ilerleyen kısmında bir olay metodu da yer alıyor. OnCreatingTicket kullanıcı doğrulamasını takip eden süreçte bilet üretildikten sonra devreye giren bir olay olarak düşünülebilir. Bu olay metodu içerisinde Github'ın UserInformationEndpoint ile bildirilen adresine HTTP Get talebinde bulunuyoruz. Dikkat ederseniz bir Authentication Header bilgisi de veriyoruz ki bu bize Github tarafından verilen bilet(Bearer Token) SendAsync ile ilgili talep gerçekleştirildikten sonra kullanıcı bilgilerini elde etmiş oluyoruz. Bunları sadece örnekte görmek amacıyla ekrana bastırdık. Artık servisler devrede. Bu durumu Middleware tarafında etkinleştirmek içinse Configure fonksiyonunda UseAuthentication çağrısını yapmamız gerekli. Gelelim yetkilendirme sürecine dahil edeceğimiz Controller tipine. Örneğimizde WebApi şablonu ile gelen ValuesController yerine aşağıdaki içeriğe sahip QuotesController sınıfını kullanacağız.

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

namespace MyQuoteService.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class QuotesController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            var id = User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
            var userName = User.FindFirst(c => c.Type == ClaimTypes.Name)?.Value;
            var email = User.FindFirst(c => c.Type == ClaimTypes.Email)?.Value;
            var blog = User.FindFirst(c => c.Type == "urn:github:blog")?.Value;
            Console.WriteLine($"{DateTime.Now}\nCurrent user:{userName}({id})\n{email}\n{blog}");
            return new ObjectResult(_quotes);
        }

        List<Quote> _quotes = new List<Quote>{
                new Quote{Id=122548,Owner="Michael Jordan",Text="I have missed more than 9000 shots in my career. I have lost almost 300 games. 26 times, I have been trusted to take the game winning shot and missed. I have failed over and over and over again in my life. And that is why I succeed."},
                new Quote{Id=325440,Owner="Vince Lombardi",Text="We didn't lose the game; we just ran out of time"},
                new Quote{Id=150094,Owner="Randy Pausch",Text="We cannot change the cards we are dealt, just how we play the game"},
                new Quote{Id=167008,Owner="Johan Cruyff",Text="Football is a game of mistakes. Whoever makes the fewest mistakes wins."},
                new Quote{Id=650922,Owner="Gary Lineker",Text="Football is a simple game. Twenty-two men chase a ball for 90 minutes and at the end, the Germans always win."},
                new Quote{Id=682356,Owner="Paul Pierce",Text="The game isn't over till the clock says zero."},
                new Quote{Id=156480,Owner="Jose Mourinho",Text="Football is a game about feelings and intelligence."},
                new Quote{Id=777592,Owner="LeBron James",Text="You know, when I have a bad game, it continues to humble me and know that, you know, you still have work to do and you still have a lot of people to impress."},
                new Quote{Id=283941,Owner="Roman Abramovich",Text="I'm getting excited before every single game. The trophy at the end is less important than the process itself."},
                new Quote{Id=185674,Owner="Shaquille O'Neal",Text="I'm tired of hearing about money, money, money, money, money. I just want to play the game, drink Pepsi, wear Reebok."}
            };
    }

    class Quote
    {
        public int Id { get; set; }
        public string Owner { get; set; }
        public string Text { get; set; }
    }
}

Sadece özlü sözler listesini HTTP Get talebi karşılığında geriye döndüren bir operasyonumuz var. Get operasyonu içerisinde Github kullanıcısının doğrulanması sonrası çekilen ClaimSet içerisindeki bazı değerlere ulaşılmakta. Bu örnekte login olan Github kullanıcısına ait username,id,email ve blog bilgilerini Console ekranına bastırmaktayız. Bu bilgiler loglama amacıyla kullanılabilir. QuotesController sınıfının bir diğer önemli özelliği de Authorize niteliği ile işaretlenmiş olması. Buna göre tüm operasyonları için yetkilendirme sürecine dahil olacağız.

Testler

Geliştirme safhasını sonlandırdığımıza göre test sürüşüne çıkabiliriz. Uygulamayı

dotnet run

terminal komutu ile çalıştırdıktan ve tarayıcı üzerinden http://localhost:5005/api/quotes adresine gittikten sonra aşağıdaki ekran görüntüsü ile karşılaşırız.

Dikkat edileceği üzere Github login sayfasına yönlendirildik. Eğer Network hareketliliklerini izlersek aşağıdaki geçişlerin olduğunu fark edebiliriz.

  1. localhost:5005/api/quotes HTTP Get talebi HTTP 302 koduna çevrilerek Location header bilgisindeki github adresine yönlendirilir.
  2. Yönlendirildiğimiz https://github.com/login/oauth/authorize? client_id={client id bilgisi}&scope=&response_type=code&redirect_uri= http://localhost:5005/signin-github&state= {uzuuunnn bir state bilgisi var} adresinde Authorize kontrolünden geçeriz ki önceden login olmamışsak yeni bir adrese yönleniriz.
  3. HTTP Get ile https://github.com/login?client_id= {client id bilgisi}&return_to=/login/oauth/authorize?client_id= {client id bilgisi}&redirect_uri= http%3A%2F%2Flocalhost%3A5005%2Fsignin-github&response_type=code&scope=&state= {uzuuuuun state bilgisi} geldiğimiz bu adreste ise Login olmamız yeterli olacaktır.
  4. Sonrasında Github kullanıcısının söz konusu uygulama için yetki vermesini bekleyen bir onayı penceresi ile karşılaşabiliriz. 

Bu bir kereliğine sorulacaktır ancak Github üzerindeki uygulama ayarlarından Revoke All Users Tokens işlemini yaparsak tekrardan karşılaşabiliriz. Artık DailyQuoteService isimli uygulama için buraksenyurt kullanıcısı yetkilendirilmiş durumda. Dolayısıyla bir önceki taleple gelen Location header bilgisindeki URL adresine yönlendiriliriz ki bu da görmek istediğimiz özlü sözler operasyonudur.

Tabii Visual Studio Code arabirimine bakarsak Login olan kullanıcıya ait Github tarafından sunulan tüm ClaimSet değerlerinin JSON formatında geldiğini de görebiliriz. Ayrıca Get metodu içerisinden de oturum açan kullanıcının çeşitli bilgilerine erişebiliriz.

Servisimiz için Github tarafından sağlanan Token bilgisinin bir son kullanma tarihi bildiğim kadarı ile yok. Kullanıcının Token bilgisi sistemden düşmediği sürece servis yetkilendirme kontrolü yapma ihtiyacı duymadan çalışıyor olacak. Github'un ilgili servis adreslerine HTTP DELETE metoduyla ID bilgisiyle talepte bulunup düşürme işleminin bilinçli olarak uygulanabilineceği de ifade ediliyor. Bunu neden söylüyorum dersiniz? Uygulamayı denerken özlü sözler servisinin Authorization adımlarına takılmadan sürekli olarak çalıştığını gördüm. Bir yerlerde düşse de tekrar Login olmamı istese diye beklerken aslında kullanım amacının ne olduğunu hatırladım. Amaç bir uygulamanın Github üzerinden doğrulanmış kullanıcılar için OAuth protokolü üzerinden Bearer Token ile çalışmasıydı. Servisin çalıştığı sistem Github tarafından bir kere doğrulanıp ehliyet bilgisini aldıktan sonra hizmet verebilir konumda kalması yeterliydi. Bu arada pratik bir yol olarak tarayıcı çerezlerini temizlemeniz halinde tekrardan Login işlemine tabii tutulacağınızı söylemek isterim ;)

Böylece geldik bir makalemizin daha sonuna. Bu yazımızda bir Web API servisinin yetkilendirme sürecinde Github'ın OAuth hizmetinden nasıl yararlanabileceğimizi incelemeye çalıştık. Umarım faydalı olur. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Örneğe github adresi üzerinden erişebilirsiniz.


http://www.buraksenyurt.com/post/express-api-hizmetini-heroku-uzerine-tasimakExpress API Hizmetini Heroku Üzerine Taşımak

$
0
0

Merhaba Arkadaşlar,

Geçenlerde sevgili çalışma arkadaşlarımdan Atahan Ceylan ile Node.js ve MongoDb üzerine konuşurken bana Heroku diye bir şeyden bahsetti. Daha önceden duymamış olmamın verdiği etkiyle hemen nedir ne değildir diyerek kendisinden bilgi istedim. Sonunda konuyu pekiştirmek için ninjaları bana sevimli bir şekilde hatırlatan bu ürünü incelemeye karar verdim. 

Heroku'unun başlangıç rehberlerinden yararlanarak bir senaryo seçtim. Amacım Node.js'de çok ilkel(amaç taşıma işlemini adımlamak olduğu için çok karmaşık olmamasında yarar var) bir REST servisi yazıp bunu Heroku üzerine taşımak. Söz konusu servisi geliştirirken express paketinden de yararlanacağız. Bu sayede bağımlı bir paketin taşınması durumunu da ele almış olacağız. Tabii öncesinde Heroku hakkında kısa bir bilgi vermekte fayda var.

Heroku. Hiyaaaa!

Geliştiriciler için kullanımı kolay bir PaaS(Platform as a Service) ürünü desek sanıyorum yeridir. Container tabanlı olarak çalışan Heroku yazının ilerleyen safhalarında kısa bir atıfta bulunacağımız Dyno adı verilen Linux temelli sanal ortamlar kullanmakta. Bunlar başlangıç kapasiteleri itibariyle oldukça hafif ve hızlı taşıyıcılar. Geliştirdiğimiz uygulamaları Heroku üzerine taşıyıp yayınlayabiliyor, ölçeklenmeleri ile ilgili yönetimsel işlemleri basitçe gerçekleştirebiliyoruz. Çok doğal olarak diğer bulut bilişim hizmetlerinde olduğu gibi sistemsel bir takım yönetimsel işlemleri düşünmemize de gerek kalmıyor.

Heroku Node.js, Ruby, Python, Go, Java, Php, Scala ve Clojure dillerine doğrudan destek veriyor. Yani bu ortamların güncellemeleri takip edildiği için her an son sürümle çalışmamız mümkün. Yazıyı hazırladığım tarih itibariyle Heroku'ya abone olduktan sonra Free Plan'ı kullanmaya başladım. 512 mb ram barındıran, 1web/1worker tipinden çalışma modeli sunan, 30 dakika etkin olmadığından uyku moduna geçen sevimli minik bir dyno ortamım oldu. Gelin şimdi bu basit ortamı kullanarak senaryomuzu gerçekleştirmeye çalışalım. 

Heroku üzerinde kullanılabilecek bir çok ek hizmet(Add-On) bulunuyor. Şuradaki adresten de görebileceğiniz paketleri incelemenizi öneririm. Bu paketlerde Heroku'nun kendisi veya 3ncü parti ortakları tarafından geliştirilmiş çeşitli bileşenler, servisler ve altyapı araçları yer almakta. Belki de bir sonraki adımınız benim yapacağım gibi mLab MongoDb'yi kullanarak Node.js servisini veritabanı ile ilişkilendirmek olacaktır(Free Plan paketi bu tip araştırmalar için oldukça yeterli görünüyor)

Ön gereksinimler

Servis, West-World üzerinde geliştirilecek. Artık aşina olduğunuz üzere West-World, Ubuntu 16.04 temelli bir Linux çekirdeğine sahip. Platform bağımsız olarak sahip olmamız gereken başka enstrümanlar da var. Node(bu senaryo özelinde), npm ve git. West-World üzerinde bunların aşağıdaki sürümleri kullanılıyor.

node -v
npm -v
git --version

Taşınacak Servis Uygulamasının Geliştirilmesi

Ben Node.js projesini oluşturmak için Visual Studio Code arabirimini kullanıyorum. Servis tarafındaki işlerimizi kolaylaştırması açısından Express paketinden yararlanabiliriz. Paket izleyen terminal komutu ile yüklenebilir.

npm install express

personal_board.js olarak isimlendirdiğim kod dosyasının içeriği aşağıdaki gibi geliştirilebilir. 

var express = require('express');
var app = express();
var fs = require('fs');

// HTTP Get
app.get('/api/tasks', function (request, response) {
    fs.readFile('daily_task.json', 'utf8', function (err, data) {
        console.log('%s:%s', Date(), request.url);
        response.end(data);
    });
});

var server = app.listen(process.env.PORT || 8080, function () {
    console.log('Sunucu dinlemde');
});

Tipik olarak tek bir HTTP Get talebine(/api/tasks) hizmet verecek bir servis söz konusu. Buraya gelen taleplere karşılık olarak daily_task.json dosyasındaki içeriği istemci tarafına göndermekteyiz. Dosya okuma işlemi için fs modülünün readFile fonskiyonundan yararlanılıyor. Ayrıca console'a talebin yapıldığı tarih ve url bilgisini basıyoruz.

Dikkatinizi çeken bir kısım mutlaka olmuştur. app.listen fonksiyonunda 8080 veya process.env.PORT ile belirtilen bir portun kullanılacağı belirtilmekte. 8080i(başka bir port da olabilir) yerel testlerde kullanacağız. Diğer parametre ise servisi heroku üzerine taşıdığımızda işe yarayacak. Heroku dünyasında mevcutta kullanılabilir port bilgisi neyse, otomatik olarak atanacak. Bu işlemi yapmadığımızda "Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch"şeklinde bir hata mesajı ile karşılaşmamız söz konusu ki ben karşılaştım. Ah unutmadan. daily_taks.json dosyasını aşağıdaki haliyle kullanabilirsiniz.

{
    "task1": {
        "title": "Finish front-end login control test",
        "category": "Testing",
        "status": "todo",
        "id": 22325
    },
    "task2": {
        "title": "Back button issue",
        "category": "Bug Fix",
        "status": "in progress",
        "id": 12342
    },
    "task3": {
        "title": "Connect with SSO Service",
        "category": "Service Integration Development",
        "status": "epic",
        "id": 14345
    },
    "task4": {
        "title": "Design of web app's help page",
        "category": "Front-end Development",
        "status": "todo",
        "id": 18123
    },
    "task5": {
        "title": "Complete wheter api service",
        "category": "Service API Development",
        "status": "epic",
        "id": 48545
    }
}

Yerel Ortam Testleri

Kodlarımız hazır. İlk etapta söz konusu servisin West-World üzerinde çalışıp çalışmadığından emin olmakta yarar var. Siz de kendi kodunuzu test ederseniz iyi olacaktır. Test önemli bir şey :) 

node personal_board.js

Procfile Eklenmesi

Artık ufaktan Heroku ortamı için gerekli hazırlıkları yapmaya başlayacağız. Diğer pek çok bulut platformunda olduğu gibi, taşıma ortamına özel hazırlanan bazı dosyalara ihtiyacımız olacak. Nitekim hedef sunucunun hangi kodu nasıl çalıştıracağını, uygulama için gerekli harici paketler varsa bunların ne olduğunu ya da hangi sürümlerini yükleyeceğini bilmesi lazım. Bu genel geçer bir kural olarak karşımıza çıkıyor. Heroku için de Procfile isimli bir dosya ekleyerek devam edelim.

Procfile aslında Process anlamına gelen uzantısız bir dosya. Visual Studio Code üzerinde geliştirme yapıyorsanız dosyayı ekler eklemez Heroku logosunun dosya ile ilişkilendirileceğini de fark edeceksiniz. İçeriğini şu anda aşağıdaki tek satırla oluşturabiliriz.

web: node personal_board.js

Bu bildirim ile single process type tanımlıyoruz. web komutu gereği node personal_board.js şeklinde bir terminal komutu çalışacak ve sonrasında oluşacak process , Heroku'nun HTTP Routing kanalına bağlanacak. Böylece Heroku üzerindeki api/tasks ve benzeri talepler Node.js process'ine yönlendirilecek (İstenirse bu dosyada birden fazla process bildirimi de yapılabiliyormuş. Örneğin bir arkaplan process'i tanımlanıp planlanmış görevlerin işletilmesi sağlanabilirmiş. Denemedim ama Heroku diyorsa doğrudur)

Procfile dosyası aslında Heroku tarafındaki dyno ortamlarını ilgilendirir. Dyno'ların hafif birer taşıyıcı(lightweight container) olduğundan bahsetmiştik.

heroku ps:scale 

gibi terminal komutları yardımıyla Dyno'ların sayısını arttırabilir ve bu şekilde ölçeklendime işlemleri gerçekleştirilebilir.

Heroku CLI'ın Yüklenmesi ve Taşıma Operasyonu

Taşıma işlemlerini terminalden gerçekleştireceğiz. Bu nedenle bize bir Command Line Interface lazım. Tabii öncesinde eğer yoksa bir Heroku hesabının açılması da gerekiyor. Hesap açma işlemi oldukça basit. Bir email doğrulaması yeterli oluyor. Üstelik diğer bir çok bulut hizmet sağlayıcı gibi hemen kredi kartı bilgisi de istenmiyor(Hemen istemiyor en azından. Ama örneğin yazıdan sonra MongoDb için bir AddOn yüklemek istediğimde beni kredi kartı bilgimi doğrulatmam gerektiğine dair uyardı. Sandbox isimli free planı seçmiş olmama rağmen)

Hesap açma işlemini takiben terminal'den heroku ile ilgili işlemleri yaptırabilmek için Heroku CLI(Command Line Interface) arabiriminin yüklenmesi adımına geçebiliriz. Ubuntu için aşağıdaki komutlar yeterli. Pek tabii güncel komutlar için Heroku'nun resmi dokümanlarına bakmakta yarar var.

sudo add-apt-repository "deb https://cli-assets.heroku.com/branches/stable/apt ./"
curl -L https://cli-assets.heroku.com/apt/release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get install heroku

Eğer işler yolunda giderse versiyon numarasını alabiliyor olmamız gerekir. West-World şu anda Heroku CLI'ın 6.15.26-5726b6f versiyonuna sahip.

Artık sisteme Login olabilir ve Heroku üzerinde uygulamamızı(uygulamalarımızı) taşıyacağımız projeyi oluşturabiliriz. login ve create komutları sırasıyla bu işler için.

heroku login
heroku create

login sırasında, Heroku'ya kayıt olurken kullandığınız bilgiler geçerli olacaktır. Oluşan uygulama Heroku web control panel üzerinden de görülebilir. Benim yazıyı yazdığım tarih itibariyle bu oluşturduğum ikinci projeydi. İlki uyku moduna geçmiş durumda. Bunun sebebi de Freeplan'a göre söz konusu uygulamaya belli bir süre talep gelmeyince(bu zamanlar 30 dakika olarak belirlenmiş) uyku moduna geçmesi. West-World için Heroku'nun oluşturduğu projenin adı fierce-earth-61739. Siz dilerseniz kendi proje adınızı da kullanabilir veya sistemin vereceği bu tutarlı atışları değerlendirebilirsiniz.

Package.json Dosyasının Eklenmesi

Proje'nin package.json dosyasının içeriği Heroku açısından önemlidir. Örneğimizde dikkat edeceğiniz üzere express isimli harici bir paket kullanıldı. Heroku'nun bunu biliyor olması lazım. Üstelik uygulamamıza ait bir takım tanımlayıcı bilgileri(versiyon numarası, aramalarda değer kazanacak keyword'ler, açıklama, yazar, giriş sayfası vb) Heroku'ya söylememiz gerekiyor. Şu anda uygulamada package.json dosyası yok. Varsayılan haliyle oluşturmak için

npm init --yes

terminal komutundan yararlanılabilir. Sonrasında dosya içeriğini ben aşağıdaki gibi düzenledim.

{
  "name": "fierce-earth-61739",
  "version": "1.0.0",
  "description": "a simple rest api for homeworks",
  "main": "personal_board.js",
  "dependencies": {
    "express": "^4.16.2"
  },
  "devDependencies": {},
  "scripts": {
    "start": "node personal_board.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://git.heroku.com/fierce-earth-61739.git"
  },
  "keywords": [
    "daily",
    "tasks",
    "scrum",
    "board",
    "node.js",
    "express",
    "rest"
  ],
  "author": "burak selim senyurt",
  "license": "ISC"
}

Dikkat edileceği üzere git repository bilgisinden, yazara, script'in nasıl başlatılacağından bağımlılık oluşturan express paketi ve versiyonuna kadar gerekli ne kadar bilgi varsa burada yer alıyor. Bu arada projedeki git repo bilgisinin Heroku için üretilen adres neyse ona göre güncellenmesi de önemli.

Git Repo'sunun Oluşturulması,Commit ve Deploy

Artık git ile kaynak kod işlemlerine geçebiliriz. Öncelikle proje klasörüne gidilir ve yeni bir Git repository oluşturulur. 

git init
heroku git:remote -a fierce-earth-61739

Sonrasında klasör içeriği repo'ya eklenir ve commit işlemi icra edilir. Artık kodlar onaylandığına göre Heroku üzerine uygulama deploy edilebilir. Terminal komutlarımız aşağıdaki gibi.

git add .
git commit -am "first deploy of personal board api service"
git push heroku master

Örnek senaryomuzda CLI üzerinden Heroku Git kullanılarak bir taşıma işlemi yapılıyor. Lakin farklı metodolojileri de kullanabiliriz. Github'a bağlanarak, Dropbox'tan yararlanarak ve yine CLI üzerinden var olan bir docker imajını kullanarak söz konusu taşıma işlemleri yapılabilir.

Taşıma işlemleri sırasında olup biteni merak ediyorsanız gerek terminalden gerekse web arabiriminden sonuçları görmemiz mümkün. Söz gelimi web arabiriminde more->view logs kısmını kullandığımızda aşağıdakine benzer bir ekran görüntüsü ile karşılaşabiliriz. Eğer işler yolunda gittiyse uygulamanın da ayağa kalkmış olması beklenir.

"Sunucu dinlemede" yazan kısma dikkat ;)

Yine de taşımanın kontrol edilmesinde fayda var. İlk etapta dyno tarafı için bir ölçekleme işlemi yapılması öneriliyor. Sonrasında gelen open komutu ile local sistemde bir tarayıcı açılıyor.

heroku ps:scale web=1
heroku open

Heroku'da uygulamalar Dyno adı verilen Container'lara alınır. Dyno'lar birbirlerinden izole olacak şekilde çalışan Linux tabanlı sanal taşıyıclardır ve Heroku'nun kalbinde çok önemli bir yere sahiptir(Detaylı bir bilgi olduğu için yazının güncel konusu dışında kalıyor ama şu adresten daha fazlası öğrenilebilir)

open komutu doğrudan tarayıcı penceresini açacak ve bizi projenin giriş url'ine yönlendirecektir.

Tahmin edeceğiniz üzere bir sonuç gelmemesi normal. Çünkü doğru HTTP Get talebini yapmış olmak gerekiyor.

https://fierce-earth-61739.herokuapp.com/api/tasks

gibi. Volaaaa!!!

Bu esnada oluşan işlemleri canlı olarak takip etmek terminalde aşağıdaki komutu vererek izlemede kalabiliriz.

heroku logs --tail

Çok doğal olarak bu tip test uygulamalarını işlerimizi bitirdikten sonra silmekte yarar var. Projenin ayarlar kısmında bir Delete düğmesi bulunuyor. Bu iş için kullanabiliriz.

Küçük bir ipucu verelim. Heroku üzerinde host edilen bir servisi farklı bir domain'den çağıracağımız zaman CORS-Cross Origin Resource Sharing sorunu ile karşılaşabiliriz. Firefox özellikle bu konuda çok katı. Şu adreste .Net Core tarafında CORS konusunun ele alınışı var. Faydası olabilir tabii sizin kendi ortamınız için gerekli aksiyonu almanız lazım. 

Sonuç itibariyle geliştireceğimiz çeşitli tipteki web uygulamalarını Heroku üzerine almak görüldüğü üzere oldukça kolay. Heroku, bizlere geliştirici dostu bir PaaS ortamı ve kullanımı sunuyor. Desteklediği diller ve platformlar düşünüldüğünde aslında startup'lar ve özellikle hackathon tarzı yarışmalar için tercih edilmesi ideal gibi görünüyor.

Bu yazımızda Node.js ile yazılmış basit bir servisi heroku üzerine nasıl alabileceğimizi adımlamaya çalıştık. Pek tabii uygulamanın daha farklı şekilde geliştirilip çalışmanın heyecanlı hale getirilmesi sağlanabilir. Örneğin mongodb gibi bir veritabanı ile çalışacak şekilde tasarlanıp Post, Put, Delete, Get ve türevi HTTP taleplerine hizmet verecek hale getirilmesi ve sonrasında Heroku'ya taşınarak kullandırılması güzel bir haftasonu çalışması olabilir. Anladınız siz :)

Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Kaynaklar

Node-js Getting Started
Getting started with node.js(Introduction)
Dynos

Express API Hizmetini Heroku Üzerine Taşımak

$
0
0

Merhaba Arkadaşlar,

Geçenlerde sevgili çalışma arkadaşlarımdan Atahan Ceylan ile Node.js ve MongoDb üzerine konuşurken bana Heroku diye bir şeyden bahsetti. Daha önceden duymamış olmamın verdiği etkiyle hemen nedir ne değildir diyerek kendisinden bilgi istedim. Sonunda konuyu pekiştirmek için ninjaları bana sevimli bir şekilde hatırlatan bu ürünü incelemeye karar verdim. 

Heroku'unun başlangıç rehberlerinden yararlanarak bir senaryo seçtim. Amacım Node.js'de çok ilkel(amaç taşıma işlemini adımlamak olduğu için çok karmaşık olmamasında yarar var) bir REST servisi yazıp bunu Heroku üzerine taşımak. Söz konusu servisi geliştirirken express paketinden de yararlanacağız. Bu sayede bağımlı bir paketin taşınması durumunu da ele almış olacağız. Tabii öncesinde Heroku hakkında kısa bir bilgi vermekte fayda var.

Heroku. Hiyaaaa!

Geliştiriciler için kullanımı kolay bir PaaS(Platform as a Service) ürünü desek sanıyorum yeridir. Container tabanlı olarak çalışan Heroku yazının ilerleyen safhalarında kısa bir atıfta bulunacağımız Dyno adı verilen Linux temelli sanal ortamlar kullanmakta. Bunlar başlangıç kapasiteleri itibariyle oldukça hafif ve hızlı taşıyıcılar. Geliştirdiğimiz uygulamaları Heroku üzerine taşıyıp yayınlayabiliyor, ölçeklenmeleri ile ilgili yönetimsel işlemleri basitçe gerçekleştirebiliyoruz. Çok doğal olarak diğer bulut bilişim hizmetlerinde olduğu gibi sistemsel bir takım yönetimsel işlemleri düşünmemize de gerek kalmıyor.

Heroku Node.js, Ruby, Python, Go, Java, Php, Scala ve Clojure dillerine doğrudan destek veriyor. Yani bu ortamların güncellemeleri takip edildiği için her an son sürümle çalışmamız mümkün. Yazıyı hazırladığım tarih itibariyle Heroku'ya abone olduktan sonra Free Plan'ı kullanmaya başladım. 512 mb ram barındıran, 1web/1worker tipinden çalışma modeli sunan, 30 dakika etkin olmadığından uyku moduna geçen sevimli minik bir dyno ortamım oldu. Gelin şimdi bu basit ortamı kullanarak senaryomuzu gerçekleştirmeye çalışalım. 

Heroku üzerinde kullanılabilecek bir çok ek hizmet(Add-On) bulunuyor. Şuradaki adresten de görebileceğiniz paketleri incelemenizi öneririm. Bu paketlerde Heroku'nun kendisi veya 3ncü parti ortakları tarafından geliştirilmiş çeşitli bileşenler, servisler ve altyapı araçları yer almakta. Belki de bir sonraki adımınız benim yapacağım gibi mLab MongoDb'yi kullanarak Node.js servisini veritabanı ile ilişkilendirmek olacaktır(Free Plan paketi bu tip araştırmalar için oldukça yeterli görünüyor)

Ön gereksinimler

Servis, West-World üzerinde geliştirilecek. Artık aşina olduğunuz üzere West-World, Ubuntu 16.04 temelli bir Linux çekirdeğine sahip. Platform bağımsız olarak sahip olmamız gereken başka enstrümanlar da var. Node(bu senaryo özelinde), npm ve git. West-World üzerinde bunların aşağıdaki sürümleri kullanılıyor.

node -v
npm -v
git --version

Taşınacak Servis Uygulamasının Geliştirilmesi

Ben Node.js projesini oluşturmak için Visual Studio Code arabirimini kullanıyorum. Servis tarafındaki işlerimizi kolaylaştırması açısından Express paketinden yararlanabiliriz. Paket izleyen terminal komutu ile yüklenebilir.

npm install express

personal_board.js olarak isimlendirdiğim kod dosyasının içeriği aşağıdaki gibi geliştirilebilir. 

var express = require('express');
var app = express();
var fs = require('fs');

// HTTP Get
app.get('/api/tasks', function (request, response) {
    fs.readFile('daily_task.json', 'utf8', function (err, data) {
        console.log('%s:%s', Date(), request.url);
        response.end(data);
    });
});

var server = app.listen(process.env.PORT || 8080, function () {
    console.log('Sunucu dinlemde');
});

Tipik olarak tek bir HTTP Get talebine(/api/tasks) hizmet verecek bir servis söz konusu. Buraya gelen taleplere karşılık olarak daily_task.json dosyasındaki içeriği istemci tarafına göndermekteyiz. Dosya okuma işlemi için fs modülünün readFile fonskiyonundan yararlanılıyor. Ayrıca console'a talebin yapıldığı tarih ve url bilgisini basıyoruz.

Dikkatinizi çeken bir kısım mutlaka olmuştur. app.listen fonksiyonunda 8080 veya process.env.PORT ile belirtilen bir portun kullanılacağı belirtilmekte. 8080i(başka bir port da olabilir) yerel testlerde kullanacağız. Diğer parametre ise servisi heroku üzerine taşıdığımızda işe yarayacak. Heroku dünyasında mevcutta kullanılabilir port bilgisi neyse, otomatik olarak atanacak. Bu işlemi yapmadığımızda "Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch"şeklinde bir hata mesajı ile karşılaşmamız söz konusu ki ben karşılaştım. Ah unutmadan. daily_taks.json dosyasını aşağıdaki haliyle kullanabilirsiniz.

{
    "task1": {
        "title": "Finish front-end login control test",
        "category": "Testing",
        "status": "todo",
        "id": 22325
    },
    "task2": {
        "title": "Back button issue",
        "category": "Bug Fix",
        "status": "in progress",
        "id": 12342
    },
    "task3": {
        "title": "Connect with SSO Service",
        "category": "Service Integration Development",
        "status": "epic",
        "id": 14345
    },
    "task4": {
        "title": "Design of web app's help page",
        "category": "Front-end Development",
        "status": "todo",
        "id": 18123
    },
    "task5": {
        "title": "Complete wheter api service",
        "category": "Service API Development",
        "status": "epic",
        "id": 48545
    }
}

Yerel Ortam Testleri

Kodlarımız hazır. İlk etapta söz konusu servisin West-World üzerinde çalışıp çalışmadığından emin olmakta yarar var. Siz de kendi kodunuzu test ederseniz iyi olacaktır. Test önemli bir şey :) 

node personal_board.js

Procfile Eklenmesi

Artık ufaktan Heroku ortamı için gerekli hazırlıkları yapmaya başlayacağız. Diğer pek çok bulut platformunda olduğu gibi, taşıma ortamına özel hazırlanan bazı dosyalara ihtiyacımız olacak. Nitekim hedef sunucunun hangi kodu nasıl çalıştıracağını, uygulama için gerekli harici paketler varsa bunların ne olduğunu ya da hangi sürümlerini yükleyeceğini bilmesi lazım. Bu genel geçer bir kural olarak karşımıza çıkıyor. Heroku için de Procfile isimli bir dosya ekleyerek devam edelim.

Procfile aslında Process anlamına gelen uzantısız bir dosya. Visual Studio Code üzerinde geliştirme yapıyorsanız dosyayı ekler eklemez Heroku logosunun dosya ile ilişkilendirileceğini de fark edeceksiniz. İçeriğini şu anda aşağıdaki tek satırla oluşturabiliriz.

web: node personal_board.js

Bu bildirim ile single process type tanımlıyoruz. web komutu gereği node personal_board.js şeklinde bir terminal komutu çalışacak ve sonrasında oluşacak process , Heroku'nun HTTP Routing kanalına bağlanacak. Böylece Heroku üzerindeki api/tasks ve benzeri talepler Node.js process'ine yönlendirilecek (İstenirse bu dosyada birden fazla process bildirimi de yapılabiliyormuş. Örneğin bir arkaplan process'i tanımlanıp planlanmış görevlerin işletilmesi sağlanabilirmiş. Denemedim ama Heroku diyorsa doğrudur)

Procfile dosyası aslında Heroku tarafındaki dyno ortamlarını ilgilendirir. Dyno'ların hafif birer taşıyıcı(lightweight container) olduğundan bahsetmiştik.

heroku ps:scale 

gibi terminal komutları yardımıyla Dyno'ların sayısını arttırabilir ve bu şekilde ölçeklendime işlemleri gerçekleştirilebilir.

Heroku CLI'ın Yüklenmesi ve Taşıma Operasyonu

Taşıma işlemlerini terminalden gerçekleştireceğiz. Bu nedenle bize bir Command Line Interface lazım. Tabii öncesinde eğer yoksa bir Heroku hesabının açılması da gerekiyor. Hesap açma işlemi oldukça basit. Bir email doğrulaması yeterli oluyor. Üstelik diğer bir çok bulut hizmet sağlayıcı gibi hemen kredi kartı bilgisi de istenmiyor(Hemen istemiyor en azından. Ama örneğin yazıdan sonra MongoDb için bir AddOn yüklemek istediğimde beni kredi kartı bilgimi doğrulatmam gerektiğine dair uyardı. Sandbox isimli free planı seçmiş olmama rağmen)

Hesap açma işlemini takiben terminal'den heroku ile ilgili işlemleri yaptırabilmek için Heroku CLI(Command Line Interface) arabiriminin yüklenmesi adımına geçebiliriz. Ubuntu için aşağıdaki komutlar yeterli. Pek tabii güncel komutlar için Heroku'nun resmi dokümanlarına bakmakta yarar var.

sudo add-apt-repository "deb https://cli-assets.heroku.com/branches/stable/apt ./"
curl -L https://cli-assets.heroku.com/apt/release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get install heroku

Eğer işler yolunda giderse versiyon numarasını alabiliyor olmamız gerekir. West-World şu anda Heroku CLI'ın 6.15.26-5726b6f versiyonuna sahip.

Artık sisteme Login olabilir ve Heroku üzerinde uygulamamızı(uygulamalarımızı) taşıyacağımız projeyi oluşturabiliriz. login ve create komutları sırasıyla bu işler için.

heroku login
heroku create

login sırasında, Heroku'ya kayıt olurken kullandığınız bilgiler geçerli olacaktır. Oluşan uygulama Heroku web control panel üzerinden de görülebilir. Benim yazıyı yazdığım tarih itibariyle bu oluşturduğum ikinci projeydi. İlki uyku moduna geçmiş durumda. Bunun sebebi de Freeplan'a göre söz konusu uygulamaya belli bir süre talep gelmeyince(bu zamanlar 30 dakika olarak belirlenmiş) uyku moduna geçmesi. West-World için Heroku'nun oluşturduğu projenin adı fierce-earth-61739. Siz dilerseniz kendi proje adınızı da kullanabilir veya sistemin vereceği bu tutarlı atışları değerlendirebilirsiniz.

Package.json Dosyasının Eklenmesi

Proje'nin package.json dosyasının içeriği Heroku açısından önemlidir. Örneğimizde dikkat edeceğiniz üzere express isimli harici bir paket kullanıldı. Heroku'nun bunu biliyor olması lazım. Üstelik uygulamamıza ait bir takım tanımlayıcı bilgileri(versiyon numarası, aramalarda değer kazanacak keyword'ler, açıklama, yazar, giriş sayfası vb) Heroku'ya söylememiz gerekiyor. Şu anda uygulamada package.json dosyası yok. Varsayılan haliyle oluşturmak için

npm init --yes

terminal komutundan yararlanılabilir. Sonrasında dosya içeriğini ben aşağıdaki gibi düzenledim.

{
  "name": "fierce-earth-61739",
  "version": "1.0.0",
  "description": "a simple rest api for homeworks",
  "main": "personal_board.js",
  "dependencies": {
    "express": "^4.16.2"
  },
  "devDependencies": {},
  "scripts": {
    "start": "node personal_board.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://git.heroku.com/fierce-earth-61739.git"
  },
  "keywords": [
    "daily",
    "tasks",
    "scrum",
    "board",
    "node.js",
    "express",
    "rest"
  ],
  "author": "burak selim senyurt",
  "license": "ISC"
}

Dikkat edileceği üzere git repository bilgisinden, yazara, script'in nasıl başlatılacağından bağımlılık oluşturan express paketi ve versiyonuna kadar gerekli ne kadar bilgi varsa burada yer alıyor. Bu arada projedeki git repo bilgisinin Heroku için üretilen adres neyse ona göre güncellenmesi de önemli.

Git Repo'sunun Oluşturulması,Commit ve Deploy

Artık git ile kaynak kod işlemlerine geçebiliriz. Öncelikle proje klasörüne gidilir ve yeni bir Git repository oluşturulur. 

git init
heroku git:remote -a fierce-earth-61739

Sonrasında klasör içeriği repo'ya eklenir ve commit işlemi icra edilir. Artık kodlar onaylandığına göre Heroku üzerine uygulama deploy edilebilir. Terminal komutlarımız aşağıdaki gibi.

git add .
git commit -am "first deploy of personal board api service"
git push heroku master

Örnek senaryomuzda CLI üzerinden Heroku Git kullanılarak bir taşıma işlemi yapılıyor. Lakin farklı metodolojileri de kullanabiliriz. Github'a bağlanarak, Dropbox'tan yararlanarak ve yine CLI üzerinden var olan bir docker imajını kullanarak söz konusu taşıma işlemleri yapılabilir.

Taşıma işlemleri sırasında olup biteni merak ediyorsanız gerek terminalden gerekse web arabiriminden sonuçları görmemiz mümkün. Söz gelimi web arabiriminde more->view logs kısmını kullandığımızda aşağıdakine benzer bir ekran görüntüsü ile karşılaşabiliriz. Eğer işler yolunda gittiyse uygulamanın da ayağa kalkmış olması beklenir.

"Sunucu dinlemede" yazan kısma dikkat ;)

Yine de taşımanın kontrol edilmesinde fayda var. İlk etapta dyno tarafı için bir ölçekleme işlemi yapılması öneriliyor. Sonrasında gelen open komutu ile local sistemde bir tarayıcı açılıyor.

heroku ps:scale web=1
heroku open

Heroku'da uygulamalar Dyno adı verilen Container'lara alınır. Dyno'lar birbirlerinden izole olacak şekilde çalışan Linux tabanlı sanal taşıyıclardır ve Heroku'nun kalbinde çok önemli bir yere sahiptir(Detaylı bir bilgi olduğu için yazının güncel konusu dışında kalıyor ama şu adresten daha fazlası öğrenilebilir)

open komutu doğrudan tarayıcı penceresini açacak ve bizi projenin giriş url'ine yönlendirecektir.

Tahmin edeceğiniz üzere bir sonuç gelmemesi normal. Çünkü doğru HTTP Get talebini yapmış olmak gerekiyor.

https://fierce-earth-61739.herokuapp.com/api/tasks

gibi. Volaaaa!!!

Bu esnada oluşan işlemleri canlı olarak takip etmek terminalde aşağıdaki komutu vererek izlemede kalabiliriz.

heroku logs --tail

Çok doğal olarak bu tip test uygulamalarını işlerimizi bitirdikten sonra silmekte yarar var. Projenin ayarlar kısmında bir Delete düğmesi bulunuyor. Bu iş için kullanabiliriz.

Küçük bir ipucu verelim. Heroku üzerinde host edilen bir servisi farklı bir domain'den çağıracağımız zaman CORS-Cross Origin Resource Sharing sorunu ile karşılaşabiliriz. Firefox özellikle bu konuda çok katı. Şu adreste .Net Core tarafında CORS konusunun ele alınışı var. Faydası olabilir tabii sizin kendi ortamınız için gerekli aksiyonu almanız lazım. 

Sonuç itibariyle geliştireceğimiz çeşitli tipteki web uygulamalarını Heroku üzerine almak görüldüğü üzere oldukça kolay. Heroku, bizlere geliştirici dostu bir PaaS ortamı ve kullanımı sunuyor. Desteklediği diller ve platformlar düşünüldüğünde aslında startup'lar ve özellikle hackathon tarzı yarışmalar için tercih edilmesi ideal gibi görünüyor.

Bu yazımızda Node.js ile yazılmış basit bir servisi heroku üzerine nasıl alabileceğimizi adımlamaya çalıştık. Pek tabii uygulamanın daha farklı şekilde geliştirilip çalışmanın heyecanlı hale getirilmesi sağlanabilir. Örneğin mongodb gibi bir veritabanı ile çalışacak şekilde tasarlanıp Post, Put, Delete, Get ve türevi HTTP taleplerine hizmet verecek hale getirilmesi ve sonrasında Heroku'ya taşınarak kullandırılması güzel bir haftasonu çalışması olabilir. Anladınız siz :)

Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Kaynaklar

Node-js Getting Started
Getting started with node.js(Introduction)
Dynos

http://www.buraksenyurt.com/post/net-core-ile-amazon-dynamodb-yi-kullanmak.Net Core ile Amazon DynamoDB'yi Kullanmak

$
0
0

Merhaba Arkadaşlar,

Epey zamandır NoSQL veritabanı sistemlerini kurcalamıyordum. Ağırlıklı olarak .Net Framework tarafında nasıl kullanılabildiklerini incelediğimi hatırlıyorum. 2017nin son çeyreği ve 2018in tamamı için kendime koyduğum hedeflerden birisi ise .Net Core dünyasını daha yakından tanımaktı. Zaten Ubuntu üzerinde koşan West-World'ün kurulum amacı da buydu. Sonuç olarak Amazon'un DynamoDb'sini .Net Core tarafında nasıl kullanabileceğimi incelemeye karar verdim. Bir süredir AWS Console üzerinden bir şeyler araştırıyor ve Amazon Web Service ürünleri hakkında giriş niteliğinde bilgiler edinmeye çalışıyorum.

Amazon DynamoDb şemasız(schema-less) olarak kullanılabilen bir NoSQL veritabanı sistemi olarak karşımıza çıkıyor. Key-Value tipine göre çalışan(ama Document Store'a da benzeyen) bir model sunduğunu söyleyebiliriz. Belli koşullar gerçekleşinceye kadar tamamen ücretsiz kullanılabilen hızlı bir veri tabanı da aynı zamanda. Bununla birlikte veriyi hızlı SSD'ler üzerinde tuttuğuna dair bir bilgi de var. Bu açıdan bakıldığında veriyi bellekte konuşlandırmayı seçen Redis'ten ayrışıyor.

Elastic MapReduce ile entegre olabilen, otomatik olarak ölçeklenebilen, Backup sürecinde S3 hizmetlerini kullanan DynamoDb ile ilgili Bahadır Akın'un şu adreste oldukça güzel ve detaylı bir yazısı da bulunuyor. Okumanızı tavsiye ederim. Onunla ilgili çalışmalara bu adresten hızlıca başlayabiliriz.

Amazon Console Üzerinden Basit Bir Giriş

Amazon Console web arayüzü üzerinden DynamoDb ile ilgili pek çok işlem gerçekleştirilebilir. Bir kaç dakika içerisinde tablolar oluşturabilir, insert, update, delete gibi temel veri işlemlerini gerçekleştirebiliriz. Söz gelimi sevdiğimiz oyun karakterlerine ait sözleri tutan bir tablo tasarlayabiliriz. Bunun için Create Table bağlantısından hareket etmemiz ve sayfadaki ilgili alanları doldurmamız yeterli.

BEn bu deneme sonucunda aşağıdaki gibi bir ekranla karşılaştım. 

Quote isminde, Primary Key(Partition Key) olarak Game adında string tipte alan içeren bir tablo oluştu. Partition Key alanı özellikle veritabanının ölçeklendirilmesi noktasında önem arz eden bir konu. İkinci olarak eklediğimiz Sort Key alanı hızlı bir arama işlemi için ele alınacak ancak kullanılması elbette zorunlu değil. Game ve Character alanları bir arada yeni bir hash tanımının oluşmasına da neden olmakta. Tablonun tasarımını daha da detaylandırmamız mümkün. Şunu da unutmamak gerekiyor ki; iyi bir tablo tasarımı için partition key, sort key, global secondary index ve local secondary index kavramlarını iyi derecede kavramak lazım.

Oldukça detaylı ayarların yapılabildiği bir arabirim burası. Pek çok sekmeye yabancı olduğumu itiraf edebilirim. Yeni yeni keşfetmeye çalışıyorum. İlk olarak tabloya nasıl veri ekleyeceğimizi göstermeye çalışayım. Bunun için Items sekmesinden Create Item düğmesine basmak yeterli.

Tree olarak adlandırılan ve ağaç görünümü sunan arabirim kullanılabileceği gibi, Text moduna geçilerek JSON Formatındaki içeriğin elle yazılması da sağlanabilir. 

Tree modundaki görünüm;

Bunun text modundaki görünümü ise aşağıdaki gibiydi.

Bunun üzerine bir kaç öğe daha ekledim ve aşağıdaki içeriğin oluşmasını sağladım. Benim size önerim ücretsiz olarak sunulan REST tabanlı Quote hizmetlerinden yararlanarak veri girişi yapmanız. Bu güzel bir vaka çalışması da olabilir. Örneğin her gün bağlanıp o günün özlü sözünü aldığınız REST servis içeriğini, DynamoDB üzerindeki tablonuza aktarabilirsiniz ;)

Sonra bir filtreleme yapmaya çalıştım. Text alanı içerisinde "I" kelimesi geçenleri bulmayı denedim.

Ardından Halo 2 oyununda karakterin adı G harfi ile başlayanların sözlerini nasıl süzebileceğime bir baktım.

Tabii benim yaptığım sadece arabirimi tanımaya çalışmak. Tablo tasarımı aslına bakarsanız yanlış. Söz gelimi bir oyuna bir karakter için n sayıda söz ekleyemeyiz. Dolayısıyla tasarımı bir oyunun birbirinden farklı karakterlerine ait en iyi sözlerin tutulduğu bir depo gibi düşünebiliriz. Eğer aynı oyuna aynı karakterden bir söz daha girmeye çalışırsak şu uzun ifadeye benzer hata mesajı ile karşılaşmanız muhtemel.

"The conditional request failed (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException; Request ID: KP8D9MU06E36D9AH50IF9AV99RVV4KQNSO5AEMVJF66Q9ASUAAJG)

Tablo oluşturma, içine veri atma, veriyi sorgulama gibi basit işlemlerin Amazon Console arabirimi ile nasıl yapılabileceğini gördük. Ancak amacımız başta da belirttiğimiz gibi bir .Net Core uygulaması üzerinden Amazon DynamoDb'yi kullanmak.

.Net Core Tarafını Geliştiriyoruz

En büyük yardımcımız şu adresten sunulan DynamoDbv2 isimli NuGet paketimiz olacak. Tabii birde Amazon servisine erişirken kullanacağımız geçerli bir kimlik bilgimizin(Credential) olması lazım. Dolayısıyla IAM adresine giderek bu uygulama için bir kullanıcı oluşturabilirsiniz. Ben daha önceden oluşturduğum AdministratorAccess rolündeki westworld-buraksenyurt kullanıcısına ait Access Key ID ve Secret Access Key değerlerini kullanacağım. Bu değerler bildiğiniz üzere IAM üzerinden kullanıcı oluşturduğunuzda size verilmekte.

İşe bir Console projesi oluşturarak başlayabiliriz.

dotnet new console -o HowToDynamoDb

Bu işlemin ardından DynamoDb NuGet paketini projeye eklememiz gerekiyor.

dotnet add package AWSSDK.DynamoDBv2 --version 3.3.5

İlk olarak DynamoDb üzerindeki tablo listesini çekmeye çalışalım. Ben deneme olması için önce Amazon Console'dan 3 tablo oluşturdum ve Program dosyasına aşağıdaki kodları yazdım(Visual Studio Code kullanıyorum)

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon;
using Amazon.DynamoDBv2;
using Amazon.Runtime;

namespace HowToDynamoDb
{
    class Program
    {
        static void Main(string[] args)
        {
            var utl=new DynamoDBUtility();
            var tableNames = utl.GetTables();
            foreach (var tableName in tableNames)
            {
                Console.WriteLine($"{tableName}");
            }
            
        }
    }

    class DynamoDBUtility
    {
        AmazonDynamoDBClient aws;
        public DynamoDBUtility()
        {
            var myCredentials=new BasicAWSCredentials("Application Key ID","Secret Access Key");
            aws=new AmazonDynamoDBClient(myCredentials,RegionEndpoint.USEast2);
            
        }
        public List<string> GetTables()
        {
            var response = aws.ListTablesAsync();
            return response.Result.TableNames;
        }
    }
}

Öncelikle geçerli bir Credential bilgisi oluşturmalıyız. Bunun için BasicAWSCredentials sınıfını kullanabiliriz(Siz kendi oluşturduğunuz kullanıcıya ait Key bilgilerini girmelisiniz ki bu bilgiler konfigurasyondan da gelebilirler) Sonrasında AmazonDynamoDBClient türünden bir örnek oluşturuyoruz. Ben US-East-2 bölgesini kullandığım için ikinci parametrede RegionEndpoint.USEast2 değerini verdim. GetTables isimli metodun yaptığı işlem oldukça basit. ListTablesAsync metodu ile elde edilen tablo adlarını geriye döndürüyor. Sonuçlar aşağıdaki ekran görüntüsüne benzer olmalı (AmazonDynamoDBClient üzerindeki pek çok operasyon awaitable nitelikte. Dolayısıyla tamamen asenkron olarak kullanılabilirler de)

Peki kod ile sıfırdan bir tabloyu nasıl oluşturabiliriz? Kodu aşağıdaki gibi değiştirelim.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Amazon;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;

namespace HowToDynamoDb
{
    class Program
    {
        static void Main(string[] args)
        {
            var utl = new DynamoDBUtility();
            utl.CreateTable("GameQuotes","QuoteID", ScalarAttributeType.N);
            utl.CreateTable("Players","Nickname", ScalarAttributeType.S);

            var tableNames = utl.GetTables();
            foreach (var tableName in tableNames)
            {
                Console.WriteLine($"{tableName}");
            }
        }
    }

    class DynamoDBUtility
    {
        AmazonDynamoDBClient aws;
        public DynamoDBUtility()
        {
            var myCredentials=new BasicAWSCredentials("Application Key ID","Secret Access Key");
            aws = new AmazonDynamoDBClient(myCredentials, RegionEndpoint.USEast2);
        }
        public List<string> GetTables()
        {
            var response = aws.ListTablesAsync();
            return response.Result.TableNames;
        }

        public void CreateTable(string tableName,string partionKeyName,ScalarAttributeType partitionKeyType)
        {
            var tableResponse = GetTables();
            if (!tableResponse.Contains(tableName))
            {
                var response = aws.CreateTableAsync(new CreateTableRequest
                {
                    TableName = tableName,
                    KeySchema = new List<KeySchemaElement>
                    {
                        new KeySchemaElement
                        {
                            AttributeName = partionKeyName,
                            KeyType = KeyType.HASH
                        }
                    },
                    AttributeDefinitions = new List<AttributeDefinition>
                    {
                        new AttributeDefinition {
                            AttributeName = partionKeyName,
                            AttributeType=partitionKeyType
                        }
                    },
                    ProvisionedThroughput = new ProvisionedThroughput
                    {
                        ReadCapacityUnits = 3,
                        WriteCapacityUnits = 3
                    },
                });
                Console.WriteLine($"HTTP Response : {response.Result.HttpStatusCode}");
            }
            else
            {
                Console.WriteLine($"{tableName} isimli tablo zaten var");
            }
        }
    }
}

CreateTable isimli operayon DynamoDB üzerinde bir tablo oluşturmak için kullanılıyor. Üç parametresi var. Tablo ve Partition Key adları ile, Partition Key'in veri türünü alıyor. ScalarAttributeType üzerinden N(number), S(string) ve B(Binary) şeklinde anahtar türünün ne olacağını belirtebiliriz. Tablo oluşturma işlemini asenkron olarak kullanılabilen(ki burada senkron bir işleyiş var) CreateTableAsync fonksiyonu gerçekleştirmekte. Tabii tabloyu oluşturmadan önce var olup olmadığını da kontrol ediyoruz. Ben GameQuotes ve Players isimli iki tabloyu oluşturmayı denedim. İşlemler sorunsuz şekilde tamamlandıktan sonra AWS Console üzerinden de oluşturulan tabloları görebildim.

Şimdi tablolardan birisine veri eklemeye çalışalım. Henüz elimizde bir Entity örneği bulunmuyor. Öncelikle bu varlığa ait sınıfı tasarlamak lazım. GameQuotes tablomuz için aşağıdaki gibi bir sınıf yazabiliriz.

using Amazon.DynamoDBv2.DataModel;

namespace HowToDynamoDb
{
    [DynamoDBTable("GameQuotes")]
    public class Quote
    {
        [DynamoDBHashKey]
        public int QuoteID { get; set; }
        public QuoteInfo QuoteInfo { get; set; }
    }
    public class QuoteInfo
    {
        public string Character { get; set; }
        public int Like { get; set; }
        public string Game { get; set; }
        public string Text { get; set; }
    }
}

Quote isimli sınıfın başında DynamoDBTable isimli bir nitelik(attribite) yer alıyor. Bu nitelikteki isim biraz önce oluşturduğumuz tablo adı ile aynı. Ayrıca QuoteID isimli bir Parition Key tanımlamıştık. Bu anahtarı Entity tarafında DynamoDBHashKey niteliği yardımıyla işaretliyoruz. Quote sınıfı, QuoteInfo tipinden bir özellik de barındırıyor. Bu sınıfta oyunun adını, sözü söyleyen karakteri, beğeni sayısını ve tabii sözün kendisini tutuyoruz. Aslında QuoteID ile ilişkilendirdiğimiz bir içeriğin söz konusu olduğunu ifade edebiliriz. Tam bir key-value ilişkisi. JSONca düşündüğümüzde de içiçe bir tip yapısı söz konusu.

Bir Quote nesne örneğini DynamoDB üzerindeki ilgili tabloya yazmak için Utility sınıfına aşağıdaki metodu ekleyerek ilerleyelim.

public void InsertQuote(Quote quote)
{
    var context = new DynamoDBContext(aws);           
    context.SaveAsync<Quote>(quote).Wait();            
}

Entity Framework dünyasındaki Context kullanımına ne kadar benziyor değil mi? :) Tek yapmamız gereken AmazonDynamoDBClient nesnesi ile ilişkilendirilmiş DynamoDBContext örneği üzerinden SaveAsync metodunu çağırmak. Generic parametre olarak Quote tipini kullandığımıza dikkat edelim.

Artık bir deneme yapabiliriz. Main metodu içerisinde aşağıdaki gibi bir kod parçası işimizi görecektir.

var utl = new DynamoDBUtility();
Quote quote=new Quote{
QuoteID=1001,
QuoteInfo=new QuoteInfo{
    Character="Cortana",
    Game="Halo 2",
    Like=192834,
    Text="Child of my enemy, why have you come? I offer no forgiveness, a father's sins, passed to his son."
}
};
utl.InsertQuote(quote);

Ben Halo 2 oyunundan Cortana isimli karaktere ait bulduğum bir sözü girdim. Programı çalıştırdıktan sonra hemen Amazon Console'a gittim ve eklenen yeni satırın aşağıdaki ekran görüntüsünde olduğu gibi eklendiğini gördüm.

Peki kod tarafında bu içeriği nasıl çekebiliriz? Gelin aşağıdaki fonksiyonu Utility sınıfına dahil ederek devam edelim.

public Quote FindQuoteByID(int quoteID)
{
    var context = new DynamoDBContext(aws);  
    List<ScanCondition> queryConditions = new List<ScanCondition>();
    queryConditions.Add(new ScanCondition("QuoteID", ScanOperator.Equal, quoteID));
    var queryResult = context.ScanAsync<Quote>(queryConditions).GetRemainingAsync();
    return queryResult.Result.FirstOrDefault();
}

Aslında bir arama koşulu listesi oluşturmaktayız. Senaryomuzda sadece bir kriter var o da Parition Key alanı olarak belirlediğimiz QuoteID değerine göre arama yapmak. ScanCondition ile belirlenen kriterde karşılaştırma koşulu, kullanılacak alan ve aranan değer bilgileri parametre olarak verilmekte. Sonrasında DynamoDBContext nesne örneği üzerinden hareket ederek ScanAsync operasyonunu kullanıyor ve içeriğe ulaşmaya çalışıyoruz. Main metodunda bu fonkisyonu kullanarak az önce eklediğimiz 1001 numaralı Quote içeriğine ulaşabiliriz.

var utl = new DynamoDBUtility();
var findingQuote=utl.FindQuoteByID(1001);
Console.WriteLine($"{findingQuote.QuoteInfo.Character}\n{findingQuote.QuoteInfo.Text}");

Siz farklı arama ifadelerini bir araya getirerek değişik denemeler de yapabilirsiniz. Örneğin eklediğiniz n sayıda Quote içerisinden, belli bir oyuna ait olup beğeni değerleri 1000 ve üzerinde olanları çekmeyi deneyebilirsiniz.

Daha neler neler yapılabilir?

Örneğin oluşturduğumuz tabloların silinmesi, alan içeriklerinin güncellenmesi vb standart operasyonları deneyimleyebilirsiniz. Biraz fonksiyonellikleri araştırmanızda yarar var. NuGet paketinde gelen fonksiyonellikler Amazon DynamoDB'nin sunduğu imkanlara göre inşa edilmiş durumdalar. NuGet paketi üzerinden gelen operayonları asenkron olarak çalıştırmayı denemenizi öneririm. Benim örnek tamamen senkron çalışıyor(Rezalet!) Görsel arayüzü olan bir uygulamada awaitable operayonları dikkate alarak arayüzün donmasını engelleyecek şekilde değişiklikler yapmanız yerinde olur. Ah bir de tabii benim gibi God Object anti-pattern'inin yolunu açan, Single Responsibility ilkesini ihlal etmiş kirli bir Utility sınıfı kullanmayın :)

Böylece geldik bir makalemizin daha sonuna. Bu yazımızda Amazon'un NoSQL veritabanlarından olan key-value türevli DynamoDB'sini bir .Net Core uygulamasından nasıl kullanabileceğimizi incelemeye çalıştık. Fiyatlandırma kritlerini göz önüne alarak kendi ürünleriniz için kullanabileceğinizi düşünüyorum. Ben her ihtimale karşı onları sildim :) Geliştirmesi oldukça kolay. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Yazıdaki kodlara github adresimden de erişebilirsiniz.

.Net Core ile Amazon DynamoDB'yi Kullanmak

$
0
0

Merhaba Arkadaşlar,

Epey zamandır NoSQL veritabanı sistemlerini kurcalamıyordum. Ağırlıklı olarak .Net Framework tarafında nasıl kullanılabildiklerini incelediğimi hatırlıyorum. 2017nin son çeyreği ve 2018in tamamı için kendime koyduğum hedeflerden birisi ise .Net Core dünyasını daha yakından tanımaktı. Zaten Ubuntu üzerinde koşan West-World'ün kurulum amacı da buydu. Sonuç olarak Amazon'un DynamoDb'sini .Net Core tarafında nasıl kullanabileceğimi incelemeye karar verdim. Bir süredir AWS Console üzerinden bir şeyler araştırıyor ve Amazon Web Service ürünleri hakkında giriş niteliğinde bilgiler edinmeye çalışıyorum.

Amazon DynamoDb şemasız(schema-less) olarak kullanılabilen bir NoSQL veritabanı sistemi olarak karşımıza çıkıyor. Key-Value tipine göre çalışan(ama Document Store'a da benzeyen) bir model sunduğunu söyleyebiliriz. Belli koşullar gerçekleşinceye kadar tamamen ücretsiz kullanılabilen hızlı bir veri tabanı da aynı zamanda. Bununla birlikte veriyi hızlı SSD'ler üzerinde tuttuğuna dair bir bilgi de var. Bu açıdan bakıldığında veriyi bellekte konuşlandırmayı seçen Redis'ten ayrışıyor.

Elastic MapReduce ile entegre olabilen, otomatik olarak ölçeklenebilen, Backup sürecinde S3 hizmetlerini kullanan DynamoDb ile ilgili Bahadır Akın'un şu adreste oldukça güzel ve detaylı bir yazısı da bulunuyor. Okumanızı tavsiye ederim. Onunla ilgili çalışmalara bu adresten hızlıca başlayabiliriz.

Amazon Console Üzerinden Basit Bir Giriş

Amazon Console web arayüzü üzerinden DynamoDb ile ilgili pek çok işlem gerçekleştirilebilir. Bir kaç dakika içerisinde tablolar oluşturabilir, insert, update, delete gibi temel veri işlemlerini gerçekleştirebiliriz. Söz gelimi sevdiğimiz oyun karakterlerine ait sözleri tutan bir tablo tasarlayabiliriz. Bunun için Create Table bağlantısından hareket etmemiz ve sayfadaki ilgili alanları doldurmamız yeterli.

BEn bu deneme sonucunda aşağıdaki gibi bir ekranla karşılaştım. 

Quote isminde, Primary Key(Partition Key) olarak Game adında string tipte alan içeren bir tablo oluştu. Partition Key alanı özellikle veritabanının ölçeklendirilmesi noktasında önem arz eden bir konu. İkinci olarak eklediğimiz Sort Key alanı hızlı bir arama işlemi için ele alınacak ancak kullanılması elbette zorunlu değil. Game ve Character alanları bir arada yeni bir hash tanımının oluşmasına da neden olmakta. Tablonun tasarımını daha da detaylandırmamız mümkün. Şunu da unutmamak gerekiyor ki; iyi bir tablo tasarımı için partition key, sort key, global secondary index ve local secondary index kavramlarını iyi derecede kavramak lazım.

Oldukça detaylı ayarların yapılabildiği bir arabirim burası. Pek çok sekmeye yabancı olduğumu itiraf edebilirim. Yeni yeni keşfetmeye çalışıyorum. İlk olarak tabloya nasıl veri ekleyeceğimizi göstermeye çalışayım. Bunun için Items sekmesinden Create Item düğmesine basmak yeterli.

Tree olarak adlandırılan ve ağaç görünümü sunan arabirim kullanılabileceği gibi, Text moduna geçilerek JSON Formatındaki içeriğin elle yazılması da sağlanabilir. 

Tree modundaki görünüm;

Bunun text modundaki görünümü ise aşağıdaki gibiydi.

Bunun üzerine bir kaç öğe daha ekledim ve aşağıdaki içeriğin oluşmasını sağladım. Benim size önerim ücretsiz olarak sunulan REST tabanlı Quote hizmetlerinden yararlanarak veri girişi yapmanız. Bu güzel bir vaka çalışması da olabilir. Örneğin her gün bağlanıp o günün özlü sözünü aldığınız REST servis içeriğini, DynamoDB üzerindeki tablonuza aktarabilirsiniz ;)

Sonra bir filtreleme yapmaya çalıştım. Text alanı içerisinde "I" kelimesi geçenleri bulmayı denedim.

Ardından Halo 2 oyununda karakterin adı G harfi ile başlayanların sözlerini nasıl süzebileceğime bir baktım.

Tabii benim yaptığım sadece arabirimi tanımaya çalışmak. Tablo tasarımı aslına bakarsanız yanlış. Söz gelimi bir oyuna bir karakter için n sayıda söz ekleyemeyiz. Dolayısıyla tasarımı bir oyunun birbirinden farklı karakterlerine ait en iyi sözlerin tutulduğu bir depo gibi düşünebiliriz. Eğer aynı oyuna aynı karakterden bir söz daha girmeye çalışırsak şu uzun ifadeye benzer hata mesajı ile karşılaşmanız muhtemel.

"The conditional request failed (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ConditionalCheckFailedException; Request ID: KP8D9MU06E36D9AH50IF9AV99RVV4KQNSO5AEMVJF66Q9ASUAAJG)

Tablo oluşturma, içine veri atma, veriyi sorgulama gibi basit işlemlerin Amazon Console arabirimi ile nasıl yapılabileceğini gördük. Ancak amacımız başta da belirttiğimiz gibi bir .Net Core uygulaması üzerinden Amazon DynamoDb'yi kullanmak.

.Net Core Tarafını Geliştiriyoruz

En büyük yardımcımız şu adresten sunulan DynamoDbv2 isimli NuGet paketimiz olacak. Tabii birde Amazon servisine erişirken kullanacağımız geçerli bir kimlik bilgimizin(Credential) olması lazım. Dolayısıyla IAM adresine giderek bu uygulama için bir kullanıcı oluşturabilirsiniz. Ben daha önceden oluşturduğum AdministratorAccess rolündeki westworld-buraksenyurt kullanıcısına ait Access Key ID ve Secret Access Key değerlerini kullanacağım. Bu değerler bildiğiniz üzere IAM üzerinden kullanıcı oluşturduğunuzda size verilmekte.

İşe bir Console projesi oluşturarak başlayabiliriz.

dotnet new console -o HowToDynamoDb

Bu işlemin ardından DynamoDb NuGet paketini projeye eklememiz gerekiyor.

dotnet add package AWSSDK.DynamoDBv2 --version 3.3.5

İlk olarak DynamoDb üzerindeki tablo listesini çekmeye çalışalım. Ben deneme olması için önce Amazon Console'dan 3 tablo oluşturdum ve Program dosyasına aşağıdaki kodları yazdım(Visual Studio Code kullanıyorum)

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon;
using Amazon.DynamoDBv2;
using Amazon.Runtime;

namespace HowToDynamoDb
{
    class Program
    {
        static void Main(string[] args)
        {
            var utl=new DynamoDBUtility();
            var tableNames = utl.GetTables();
            foreach (var tableName in tableNames)
            {
                Console.WriteLine($"{tableName}");
            }
            
        }
    }

    class DynamoDBUtility
    {
        AmazonDynamoDBClient aws;
        public DynamoDBUtility()
        {
            var myCredentials=new BasicAWSCredentials("Application Key ID","Secret Access Key");
            aws=new AmazonDynamoDBClient(myCredentials,RegionEndpoint.USEast2);
            
        }
        public List<string> GetTables()
        {
            var response = aws.ListTablesAsync();
            return response.Result.TableNames;
        }
    }
}

Öncelikle geçerli bir Credential bilgisi oluşturmalıyız. Bunun için BasicAWSCredentials sınıfını kullanabiliriz(Siz kendi oluşturduğunuz kullanıcıya ait Key bilgilerini girmelisiniz ki bu bilgiler konfigurasyondan da gelebilirler) Sonrasında AmazonDynamoDBClient türünden bir örnek oluşturuyoruz. Ben US-East-2 bölgesini kullandığım için ikinci parametrede RegionEndpoint.USEast2 değerini verdim. GetTables isimli metodun yaptığı işlem oldukça basit. ListTablesAsync metodu ile elde edilen tablo adlarını geriye döndürüyor. Sonuçlar aşağıdaki ekran görüntüsüne benzer olmalı (AmazonDynamoDBClient üzerindeki pek çok operasyon awaitable nitelikte. Dolayısıyla tamamen asenkron olarak kullanılabilirler de)

Peki kod ile sıfırdan bir tabloyu nasıl oluşturabiliriz? Kodu aşağıdaki gibi değiştirelim.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Amazon;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;

namespace HowToDynamoDb
{
    class Program
    {
        static void Main(string[] args)
        {
            var utl = new DynamoDBUtility();
            utl.CreateTable("GameQuotes","QuoteID", ScalarAttributeType.N);
            utl.CreateTable("Players","Nickname", ScalarAttributeType.S);

            var tableNames = utl.GetTables();
            foreach (var tableName in tableNames)
            {
                Console.WriteLine($"{tableName}");
            }
        }
    }

    class DynamoDBUtility
    {
        AmazonDynamoDBClient aws;
        public DynamoDBUtility()
        {
            var myCredentials=new BasicAWSCredentials("Application Key ID","Secret Access Key");
            aws = new AmazonDynamoDBClient(myCredentials, RegionEndpoint.USEast2);
        }
        public List<string> GetTables()
        {
            var response = aws.ListTablesAsync();
            return response.Result.TableNames;
        }

        public void CreateTable(string tableName,string partionKeyName,ScalarAttributeType partitionKeyType)
        {
            var tableResponse = GetTables();
            if (!tableResponse.Contains(tableName))
            {
                var response = aws.CreateTableAsync(new CreateTableRequest
                {
                    TableName = tableName,
                    KeySchema = new List<KeySchemaElement>
                    {
                        new KeySchemaElement
                        {
                            AttributeName = partionKeyName,
                            KeyType = KeyType.HASH
                        }
                    },
                    AttributeDefinitions = new List<AttributeDefinition>
                    {
                        new AttributeDefinition {
                            AttributeName = partionKeyName,
                            AttributeType=partitionKeyType
                        }
                    },
                    ProvisionedThroughput = new ProvisionedThroughput
                    {
                        ReadCapacityUnits = 3,
                        WriteCapacityUnits = 3
                    },
                });
                Console.WriteLine($"HTTP Response : {response.Result.HttpStatusCode}");
            }
            else
            {
                Console.WriteLine($"{tableName} isimli tablo zaten var");
            }
        }
    }
}

CreateTable isimli operayon DynamoDB üzerinde bir tablo oluşturmak için kullanılıyor. Üç parametresi var. Tablo ve Partition Key adları ile, Partition Key'in veri türünü alıyor. ScalarAttributeType üzerinden N(number), S(string) ve B(Binary) şeklinde anahtar türünün ne olacağını belirtebiliriz. Tablo oluşturma işlemini asenkron olarak kullanılabilen(ki burada senkron bir işleyiş var) CreateTableAsync fonksiyonu gerçekleştirmekte. Tabii tabloyu oluşturmadan önce var olup olmadığını da kontrol ediyoruz. Ben GameQuotes ve Players isimli iki tabloyu oluşturmayı denedim. İşlemler sorunsuz şekilde tamamlandıktan sonra AWS Console üzerinden de oluşturulan tabloları görebildim.

Şimdi tablolardan birisine veri eklemeye çalışalım. Henüz elimizde bir Entity örneği bulunmuyor. Öncelikle bu varlığa ait sınıfı tasarlamak lazım. GameQuotes tablomuz için aşağıdaki gibi bir sınıf yazabiliriz.

using Amazon.DynamoDBv2.DataModel;

namespace HowToDynamoDb
{
    [DynamoDBTable("GameQuotes")]
    public class Quote
    {
        [DynamoDBHashKey]
        public int QuoteID { get; set; }
        public QuoteInfo QuoteInfo { get; set; }
    }
    public class QuoteInfo
    {
        public string Character { get; set; }
        public int Like { get; set; }
        public string Game { get; set; }
        public string Text { get; set; }
    }
}

Quote isimli sınıfın başında DynamoDBTable isimli bir nitelik(attribite) yer alıyor. Bu nitelikteki isim biraz önce oluşturduğumuz tablo adı ile aynı. Ayrıca QuoteID isimli bir Parition Key tanımlamıştık. Bu anahtarı Entity tarafında DynamoDBHashKey niteliği yardımıyla işaretliyoruz. Quote sınıfı, QuoteInfo tipinden bir özellik de barındırıyor. Bu sınıfta oyunun adını, sözü söyleyen karakteri, beğeni sayısını ve tabii sözün kendisini tutuyoruz. Aslında QuoteID ile ilişkilendirdiğimiz bir içeriğin söz konusu olduğunu ifade edebiliriz. Tam bir key-value ilişkisi. JSONca düşündüğümüzde de içiçe bir tip yapısı söz konusu.

Bir Quote nesne örneğini DynamoDB üzerindeki ilgili tabloya yazmak için Utility sınıfına aşağıdaki metodu ekleyerek ilerleyelim.

public void InsertQuote(Quote quote)
{
    var context = new DynamoDBContext(aws);           
    context.SaveAsync<Quote>(quote).Wait();            
}

Entity Framework dünyasındaki Context kullanımına ne kadar benziyor değil mi? :) Tek yapmamız gereken AmazonDynamoDBClient nesnesi ile ilişkilendirilmiş DynamoDBContext örneği üzerinden SaveAsync metodunu çağırmak. Generic parametre olarak Quote tipini kullandığımıza dikkat edelim.

Artık bir deneme yapabiliriz. Main metodu içerisinde aşağıdaki gibi bir kod parçası işimizi görecektir.

var utl = new DynamoDBUtility();
Quote quote=new Quote{
QuoteID=1001,
QuoteInfo=new QuoteInfo{
    Character="Cortana",
    Game="Halo 2",
    Like=192834,
    Text="Child of my enemy, why have you come? I offer no forgiveness, a father's sins, passed to his son."
}
};
utl.InsertQuote(quote);

Ben Halo 2 oyunundan Cortana isimli karaktere ait bulduğum bir sözü girdim. Programı çalıştırdıktan sonra hemen Amazon Console'a gittim ve eklenen yeni satırın aşağıdaki ekran görüntüsünde olduğu gibi eklendiğini gördüm.

Peki kod tarafında bu içeriği nasıl çekebiliriz? Gelin aşağıdaki fonksiyonu Utility sınıfına dahil ederek devam edelim.

public Quote FindQuoteByID(int quoteID)
{
    var context = new DynamoDBContext(aws);  
    List<ScanCondition> queryConditions = new List<ScanCondition>();
    queryConditions.Add(new ScanCondition("QuoteID", ScanOperator.Equal, quoteID));
    var queryResult = context.ScanAsync<Quote>(queryConditions).GetRemainingAsync();
    return queryResult.Result.FirstOrDefault();
}

Aslında bir arama koşulu listesi oluşturmaktayız. Senaryomuzda sadece bir kriter var o da Parition Key alanı olarak belirlediğimiz QuoteID değerine göre arama yapmak. ScanCondition ile belirlenen kriterde karşılaştırma koşulu, kullanılacak alan ve aranan değer bilgileri parametre olarak verilmekte. Sonrasında DynamoDBContext nesne örneği üzerinden hareket ederek ScanAsync operasyonunu kullanıyor ve içeriğe ulaşmaya çalışıyoruz. Main metodunda bu fonkisyonu kullanarak az önce eklediğimiz 1001 numaralı Quote içeriğine ulaşabiliriz.

var utl = new DynamoDBUtility();
var findingQuote=utl.FindQuoteByID(1001);
Console.WriteLine($"{findingQuote.QuoteInfo.Character}\n{findingQuote.QuoteInfo.Text}");

Siz farklı arama ifadelerini bir araya getirerek değişik denemeler de yapabilirsiniz. Örneğin eklediğiniz n sayıda Quote içerisinden, belli bir oyuna ait olup beğeni değerleri 1000 ve üzerinde olanları çekmeyi deneyebilirsiniz.

Daha neler neler yapılabilir?

Örneğin oluşturduğumuz tabloların silinmesi, alan içeriklerinin güncellenmesi vb standart operasyonları deneyimleyebilirsiniz. Biraz fonksiyonellikleri araştırmanızda yarar var. NuGet paketinde gelen fonksiyonellikler Amazon DynamoDB'nin sunduğu imkanlara göre inşa edilmiş durumdalar. NuGet paketi üzerinden gelen operayonları asenkron olarak çalıştırmayı denemenizi öneririm. Benim örnek tamamen senkron çalışıyor(Rezalet!) Görsel arayüzü olan bir uygulamada awaitable operayonları dikkate alarak arayüzün donmasını engelleyecek şekilde değişiklikler yapmanız yerinde olur. Ah bir de tabii benim gibi God Object anti-pattern'inin yolunu açan, Single Responsibility ilkesini ihlal etmiş kirli bir Utility sınıfı kullanmayın :)

Böylece geldik bir makalemizin daha sonuna. Bu yazımızda Amazon'un NoSQL veritabanlarından olan key-value türevli DynamoDB'sini bir .Net Core uygulamasından nasıl kullanabileceğimizi incelemeye çalıştık. Fiyatlandırma kritlerini göz önüne alarak kendi ürünleriniz için kullanabileceğinizi düşünüyorum. Ben her ihtimale karşı onları sildim :) Geliştirmesi oldukça kolay. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Yazıdaki kodlara github adresimden de erişebilirsiniz.

http://www.buraksenyurt.com/post/atlas-ile-node-js-uzerinden-haberlesmekAtlas ile Node.js Üzerinden Haberleşmek

$
0
0

Merhaba Arkadaşlar,

Atlas denilince aklımıza gelen çoğunlukla coğrafya dersleridir. Hatta bu isimde coğrafya dergisi bile var. İşin köküne indiğimizde ise aslında bu ismin Yunan mitolojisinden geldiğini görmekteyiz. Yunan mitolojisine göre Atlas, Lapetos ve Klymene'nin 13 çocuğundan en güçlü olanıdır. Hatta o kadar güçlüdür ki Olympos'a saldırmış ve bu sebepten Zeus tarafından gök kubbeyi omuzlarından taşımakla cezalandırılmıştır. Hatta bu omuzlarda taşıma olayı bizleri o kadar etkilemiştir ki, kafatasını taşıyan ilk omura da tıp dünyası tarafından Atlas adı verilmiştir. Sanırım şimdi yandaki fotoğrafın ne anlama geldiğini daha net anladınız. Oysa ki Atlas'ın taşıdığı tek yük dünya ya da kafataslarımız değil. O, Big Data konusunda da büyük bir yükün altına girmiş durumda. İşte bugün inceleyeceğimiz konumuz. MongoDB'nin bulut çözümü hizmeti olan Atlas...

Atlas'a giriş mevzum yeni iş yerimdeki genç meslektaşlarım sayesinde gerçekleşti. Beni bu tip araştırmalara ittikleri için çok memnunum. Bir kaç gün öncesinde katıldıkları Hackathon'la ilgili izlenimlerini paylaşan arkadaşlarıma ne kadar teşekkür etsem azdır. Hemen nasıl bir şeydir araştırmaya başladım ve sonunda aşağıdaki çizimde yer alan senaryonun gerçekleştirilebileceğini öğrendim. Burada Node.js istemcisinin yerine farklı platformlar da gelebiliyor hemen söyleyeyim. Dilerseniz Ruby, Python gibi dilleri de katabilirsiniz. Ben son günlerin popüler konuşmaları özelinde Node.js tipinde bir istemciyi tercih ettim.

MongoDB aşina olduğumuz üzere oldukça başarılı ve popüler olan doküman bazlı NoSQL veritabanı sistemlerinden birisi. Atlas' da onun bulut tabanlı versiyonu olarak düşünülebilir. Bu şu anlama geliyor. Dilersek MongoDB veritabanımızı Cloud üzerinde konuşlandırabiliriz. Bu sayede sunucu ve yönetim maliyetlerini azaltmış, ölçeklenebilirlik gibi işleri de kolaylaştırmış oluruz. Atlas, esas itibariyle AWS, GCP ve Azure sunucu merkezlerini kullanmakta. Normalde bulut bilişim platformları kendi bünyelerinde çeşitli veritabanlarına hizmet sunarlarken Atlas bu işi belirli bir veritabanını ele alarak yapmakta desek sanırım yeridir. MongoDB kullanacağımız belli ancak hangi platform üzerinde konuşlandıracağımızı Atlas üzerinden şekillendirebiliyoruz.

Ne demek istediğimi anlamanızın en güzel yolu şu adrese girip bir hesap açmakla olacak. Sonrasında bizi bekleyen şey proje açılması ve bir Cluster oluşturulması. Bu adımda kullanmak istediğiniz sunucu merkezini seçebiliyorsunuz. Ben 0$ maliyeti olan Free plan'ı seçerek Freelancer altında Project Zero isimli bir proje oluşturmakla işe başladım. Sandbox olarak geçen ürün içerisinde hemen varsayılan bir Cluster bir kaç dakika içerisinde oluşturuldu. Aşağıdaki ekran görüntüsünden görebileceğiniz üzere Frankfurt'ta ki AWS sunucularında 3 node'dan oluşan ve MongoDB'nin 3.4.13 sürümünü kullanan bir Cluster' ım var.

Projeyi seçip ilerledikten ve bazı basit operasyonları icra ettiktsen sonra Cluster içerisindeki node'lar da görebildim. İki secondary ve bir primary node söz konusuydu.

Benim amacım yazının başında da belirttiğimi üzere Atlas üzerinde oluşturacağım bir MongoDB örneğine, West-World'teki Node.js kodlarını kullanarak erişebilmek. Tabii bu tip bir iş için bazı ön gereklilikleri yerine getirmek lazım. Atlas üzerinde proje oluşturup Cluster yaratmak sadece bir başlangıçtı. Bunun dışında veritabanı ile ilişkili bir kullanıcının oluşturulması da gerekiyor. Ben örnek olarak scothy isimli bir kullanıcı oluşturdum ve bonkörlük edip ona DBAdmin rolünü verdim. 

Kullanıcı oluşturmakta yeterli değil. Atlas üzerinde konuşlanacak olan MongoDB örneğine nereden erişilecek? Eğer herkesin kullanımına sunulacak bir veritabanı söz konusu ise Public erişim hakkı verebiliriz. Ben örnekten sadece kendi bilgisayarımdan erişim sağlanması için var olan o anki IP adresimi IP Whitelist adı verilen bölüme eklettim. Dolayısıyla veritabanına sadece West-World üzerinden erişilebilecek şekilde bir ayarlama yaptım. Tabii bu senaryo için işin can sıkıcı tarafı statik bir IP olmadığında yaşanıyor. Bu sebepten örnekleri hazırlarken zaman zaman güncel IP bilgisini yeniden eklemek durumunda kaldığım da oldu.

Bu adımları tamamladıktan sonra artık elimde Atlas üzerinde konuşlandırılmış bir MongoDB veritabanı olduğunu söyleyebilirim. Ama tabii ki bu da yeterli değil. Bir şekilde söz konusu veritabanına bilgi atabilmeli ve en azından bunu sorgulayabilmeliyim.

Atlas üzerindeki veritabanı ve içeriğine erişmek için bir kaç alternatif yol var. Kod tarafını denemeden önce MongoDB'nin Compass isimli ürününü kullandım. Aslında beni buraya götüren yol Atlas arabirimindeki Connect düğmesi oldu. Seçenekler arasında yer alan Compass'a bakmaya karar verdim(Bunun dışında uygulama ve Shell seçenekleri de bulunuyor)Öncelikle West-World'ün Ubuntu 64bit versiyonu için gerekli deb uzantılı paketi belirtilen adresten indirdim. Kurulum işini gerçekleştirmek işin en basit kısmıydı.

Bu arada fotoğrafın altındaki bilgiye de dikkatinizi çekerim.

Tahmin edeceğiniz gibi sonrasında Atlas'a bağlanmak gerekiyor. Atlas şu an için West-World'ün oldukça uzağında taaaa Frankfurt'ta ikamet ediyor. Bu nedenle bağlantı bilgileri de önemli. Yukarıdaki ekran görüntüsünde bir de ConnectionString bilgisi olduğunu fark etmişsinizdir. Bu bilgiyi panoya kopyalamak aslında yeterli. Compass'i açtığımızda otomatik olarak host, port, username, gibi bilgiler doldurulmakta. Ama dolmayabilir de. Ki ben denemelerimi yaparken eksik olarak eklendiler. Temel olarak aşağıdaki ekran görüntüsünde yer alan bilgilerin doldurulması gerekiyor. Buradaki Hostname, Replica Set Name bilgileri önemli. Port aksi belirtilmedikçe 27017 olarak veriliyor. Authentication modunda kullanıcı adı ve şifre kullanacağımızı belirtmemiz lazım. Bu, Atlas üzerinde oluşturduğumuz kullanıcının bilgileri.

Ben bu bilgileri doldurduktan sonra Project Zero'da yer alan Cluster0'a bağlanmayı başarabildim. Sonrasında ilk işim remote isimli bir veritabanı oluşturmak ve içerisine gamers isimli bir koleksiyon(collection) koymak oldu. Hatta aşağıdaki JSON içeriğini alıp, 

{
  "primary_info": {
     "weapon": "lazer cannon",     
     "motto": "go go gooo",
     "movie": "star wars - Jedi the last"
  },
  "max_point": 385,
  "min_point": 35,
  "matches": [
     { "game_day": 1, "vs": "lord rikken", "player_score": 200, "opponent_score":145 },
     { "game_day": 2, "vs": "evelyn", "player_score": 189, "opponent_score":485 },
     { "game_day": 3, "vs": "moka pinku", "player_score": 21, "opponent_score":18 },
     { "game_day": 4, "vs": "draymmeor", "player_score": 53, "opponent_score":56 }
  ],
  "current_rank":3,
  "nickname": "lord fungus mungus",
  "player_id": "21454834"
}

gamers koleksiyonuna yeni bir doküman olarak da ekledim. Sonuç şöyleydi...

Pek tabii hedefim bir kod parçasından faydalanarak Atlas ile haberleşmekti. Hali hazırda basit bir veri içeriği de eklediğime göre en azından Atlas'a bağlanıp bu içeriği çekmeyi deneyebilirdim. Pek çok programlama ortamı için yardımcı paketler mevcut. Ben son ay içerisinde haşırneşir olduğum Node.js tarafında ilerlemek istedim. Bu nedenle West-World'de Visual Studio Code'u kullanarak aşağıdaki kodları içeren basit bir js dosyasyı hazırladım. 

var MongoClient = require('mongodb').MongoClient;

function getAllGamers(url) {
    console.log("processing...");
    return new Promise(function (resolve, reject) {

        console.log("connecting...");

        MongoClient.connect(url, function (err, db) {
            var dbo = db.db("remote");
            var query = {};
            console.log("fetching...");
            dbo.collection("gamers")
                .find(query)
                .toArray(function (err, result) {
                    if (err) {
                        reject(err);
                    } else {
                        console.log("we have gamers now...")
                        resolve(result)
                    }
                    db.close();
                });
        });
    })
};

function main() {
    url = "mongodb://scothy:tiGeR@cluster0-shard-00-00-m2yq0.mongodb.net:27017/admin?replicaSet=Cluster0-shard-0&ssl=true"
    getAllGamers(url).then(function (result) {
        console.log(result);
    }, function (err) {
        console.log(err);
    });
    console.log("mongodb examples...")
};

main();

İlerlemeden önce kodda neler olup bitttiğini anlatsam iyi olacak. Temel olarak iki fonksiyonumuz bulunuyor. main ve getAllGamers. getAllGamers Atlas üzerinde konuşlandırdığımız koleksiyonun tüm içeriğini sorgulamak için kullanılmakta(Select * from'un hallicesinden) Tabii node.js'in npm paketini kullandığımızı da fark etmişsinizdir. Bu da

npm install mongodb

terminal komutunun gerekli olabileceği anlamına geliyor. connect fonksiyonu içerisindeki ilgili metodlarla, remote veritabanındaki gamers koleksiyonuna kadar gidiyor ve tüm içeriği bir array'e alıyoruz. Tabii kod tarafında dikkatinizi çeken bir şey de olmuştur. getAllGamers içerisinde promise isimli bir nesne örneği döndürülmekte. Bu benim yeni duyduğum ve henüz idrak etmeye çalıştığım farklı bir asenkron uygulama yaklaşımı. Normalde node.js asenkron çalışma prensiplerini benimsiyor ancak callback tarzı kullanımların bir takım sıkıntıları olduğu dillendirilmekte. Callback fonksiyonunun hiç çağırılmaması, az ya da çok çağırılması, parametrelerinin doğru alınamaması, hataların kaybolması gibi sorunlardan bahsedilmekte. Promise adı verilen yapıda ise belli başlı avantajlar var. Örneğin istenilen görev tamamlandığında promise değişikliğe uğramıyor. Immutable bir tip olduğunu ve kararlı bir nesne yapısı bahşettiğini anlayabiliriz. İşlem sonucu sadece bir kereliğine başarıya ulaşabilir veya ulaşamaz. Bu, resolved ve rejected durumları ile ele alınmakta. Öngörülemeyen hatalarda promise otomatikman rejected moduna geçiyor. 

Koddan da fark edeceğiniz üzere toArray fonksiyonunun parametresindeki isimsiz metodda reject ve resolve çağrıları söz konusu. Veri sonucu alındığında tetiklenen resolve fonksiyonu sonucun getAllGamers.then metodundan ele alınabiliyor olmasını sağlamakta. Tabii bir hata olması halinde reject operasyonunun tetikleneceği de aşikar. Biraz kafa karıştırıcı değil mi? En azından benim için öyle. Oysaki promise kullanımını hamburger siparişi ve siparişin hazır olması sonrası çalan masa zili ile gayet güzel bir tasvirleyerek anlatan esaslı bir yazı var. Özgün Bal'ın şu yazısını mutlaka okumanızıöneririm.

Sonuçta uygulamayı çalıştırdığımda gamers koleksiyonunun içeriğini çekebildiğimi fark ettim.

Tüm bu işlemler olurken Atlas üzerinde de veri hareketlilikleri olmakta tabii ki. Onları izlemek mümkün, sonuçlara göre ölçeklendirme ile ilgili bir takım yönetimsel işlemleri yapmak mümkün. Eğer kullanılan cluster'lara bakılırsa aşağıdaki ekran görüntüsünde olduğu gibi gayet güzel grafikler sunuluyor. 

Bu güzel gelişmelerden sonra kod tarafını biraz daha kurcalamak istedim. İlk olarak yeni bir koleksiyonu nasıl oluşturabilirim buna baktım. Promise konusunu şimdilik işe katmadan(ki kullanımına alışana kadar kafamı karıştırsın istemiyorum) koda aşağıdaki createCollection fonksiyonunu ekledim.

function createCollection(url, name) {
    console.log("db create process...");
    MongoClient.connect(url, function (err, db) {
        if (err) throw err;
        var dbOwner = db.db("remote");
        dbOwner.createCollection(name, function (err, res) {
            if (err) throw err;
            console.log("collection created!");
            db.close();
        });
    });
};

url ve name isimli iki parametre alan createCollection fonksiyonu MongoClient.connect ile Atlas'a bağlantı sağladıktan sonra üzerinde işlem yapabilmek için remote veritabanını işaret eden bir db nesnesi örnekliyor. Sonrasında dbOwner isimli nesne örneği üzerinden createCollection metodu yardımıyla ilgili koleksiyonun oluşturulması sağlanıyor. Kodun main fonksiyonu içeriği ise şu şekilde.

function main() {
    url = "mongodb://scothy:tiGeR@cluster0-shard-00-00-m2yq0.mongodb.net:27017/admin?replicaSet=Cluster0-shard-0&ssl=true"
    createCollection(url,"designers");
    console.log("mongodb examples...")
};

main();

Çalışma zamanı çıktısı tam da istediğim gibiydi. gamers dışında designers isimli yeni bir koleksiyon daha oluşmuştu.

E o zaman birde bu yeni koleksiyona doküman eklemeyi denesek mi? İşte bu işi üstlenecek insertDesigner isimli fonksiyonumuz.

function insertDesigner(url,content){
    console.log("inserting...");
    MongoClient.connect(url,function(err,db){
        if(err) throw err;
        var dbOwner=db.db("remote");
        dbOwner.collection("designers").insertOne(content,function(err,res){
            if(err)throw err;
            console.log("a new designer inserted");
            console.log("ID : %s",res.insertedId);
            db.close();
        });
    });
};

function main() {
    url = "mongodb://scothy:tiGeR@cluster0-shard-00-00-m2yq0.mongodb.net:27017/admin?replicaSet=Cluster0-shard-0&ssl=true"
    var vanDyk={fullName:"Yurri van dayk de la rossa",country:"green age",system:"Tatuin",expLevel:980};
    insertDesigner(url,vanDyk);
    console.log("mongodb examples...")
};

main();

Bu sefer collection fonksiyonu ile yakaladığımız koleksiyon için insertOne metodu yardımıyla yeni bir JSON nesnesini göndermekteyiz. Kodun West-World tarafındaki çalışma zamanı sonuçları aşağıdaki gibi.

Dikkat edileceği üzere insertOne metodunun ikinci parametresi olarak kullanılan fonksiyondaki res değişkeni üzerinden, henüz eklenen dokümanın MongoDB tarafında üretilen objectId bilgisine ulaşabildik.

Örnekte yer alan insertOne metodu dışında kullanabileceğimiz bir diğer alternatif de insertMany. Adından da anlaşılacağı üzere koleksiyona birden fazla doküman eklemek istediğimiz durumlarda kullanılabilir. remote veritabanındaki designers koleksiyonu için ben şöyle bir deneme yaptım.

function insertDesigners(url,content){
    console.log("inserting...");
    MongoClient.connect(url,function(err,db){
        if(err) throw err;
        var dbOwner=db.db("remote");
        dbOwner.collection("designers").insertMany(content,function(err,res){
            if(err)throw err;
            console.log("%i documents inserted",res.insertedCount);
            db.close();
        });
    });
};

function main() {
    url = "mongodb://scothy:tiGeR@cluster0-shard-00-00-m2yq0.mongodb.net:27017/admin?replicaSet=Cluster0-shard-0&ssl=true"
    var designers=[
        {fullName:"blue man",country:"ingland",system:"oceanic continent",expLevel:256},
        {fullName:"Reddick",country:"red sun",system:"world",expLevel:128},
        {fullName:"mari dö marş",country:"pari",system:"moon",expLevel:45}
    ];
    insertDesigners(url,designers);

    console.log("mongodb examples...")
};

main();

Bu sefer bir JSON nesne dizisini parametre olarak kullanmaktayız. Her birisi ayrı birer doküman olarak değerlendirilecek. İşte benim elde ettiğim sonuçlar,

Örnekler çoğaltılabilir. Doküman sorgulanması, veritabanı üretilmesi, çeşitli kriterlere göre sorgulamalar yapılması gibi temel pek çok operasyon kolaylıkla gerçekleştirilebilir. Söz konusu kodlar birer WebAPI servisi gibi tasarlanabilir de. Siz görevinizi anladınız değil mi?

Eğer konusunda veri kullanılması gereken bir startup denemesi veya bir proje ya da tez ödevi olsa sanırım Atlas gibi platformların Free Plan statüsündeki versiyonlarını kullanmayı tercih edebiliriz. Maliyeti sadece internet ücreti ile sınırlı olacaktır. Üstelik istemci tarafını geliştirmekte oldukça basit. Senaryolar çokça çeşitlendirilebilir. Node.js uygulamasının yine bulut üzerinde bir yerlde servisleştirilmesi ve bu servisin Atlas ile konuşarak MongoDB tabanlı veri yapılarını kullanması pekala mümkün. Günümüzde birbirine bağlanamayan sistem neredeyse kalmadı gibi ne dersiniz :) Böylece geldik bir makalemizin daha sonuna. Tekrardan görüşünceye dek hepinize mutlu günler dilerim.

Viewing all 525 articles
Browse latest View live